From 29d81c4d8023f5c0d9c883a8b73b94ad393c8d44 Mon Sep 17 00:00:00 2001 From: Nabeel Parkar Date: Sat, 23 Sep 2023 00:47:00 +0530 Subject: [PATCH] feat(firestore): V9 modular APIs (#7235) --- packages/firestore/e2e/Aggregate/count.e2e.js | 159 +- .../firestore/e2e/Bundle/loadBundle.e2e.js | 76 +- .../firestore/e2e/Bundle/namedQuery.e2e.js | 207 +- packages/firestore/e2e/Bytes.e2e.js | 111 + .../e2e/CollectionReference/add.e2e.js | 61 +- .../e2e/CollectionReference/doc.e2e.js | 58 +- .../e2e/CollectionReference/properties.e2e.js | 82 +- packages/firestore/e2e/DocumentChange.e2e.js | 245 +- .../e2e/DocumentReference/collection.e2e.js | 118 +- .../e2e/DocumentReference/delete.e2e.js | 39 +- .../e2e/DocumentReference/get.e2e.js | 128 +- .../e2e/DocumentReference/isEqual.e2e.js | 88 +- .../e2e/DocumentReference/onSnapshot.e2e.js | 739 +++-- .../e2e/DocumentReference/properties.e2e.js | 63 +- .../e2e/DocumentReference/set.e2e.js | 603 ++-- .../e2e/DocumentReference/update.e2e.js | 190 +- .../e2e/DocumentSnapshot/data.e2e.js | 336 +- .../firestore/e2e/DocumentSnapshot/get.e2e.js | 411 ++- .../e2e/DocumentSnapshot/isEqual.e2e.js | 102 +- .../e2e/DocumentSnapshot/properties.e2e.js | 124 +- packages/firestore/e2e/FieldPath.e2e.js | 266 +- packages/firestore/e2e/FieldValue.e2e.js | 660 +++- .../firestore/e2e/FirestoreStatics.e2e.js | 53 +- packages/firestore/e2e/GeoPoint.e2e.js | 311 +- packages/firestore/e2e/Query/endAt.e2e.js | 338 +- packages/firestore/e2e/Query/endBefore.e2e.js | 324 +- packages/firestore/e2e/Query/get.e2e.js | 120 +- packages/firestore/e2e/Query/isEqual.e2e.js | 334 +- packages/firestore/e2e/Query/limit.e2e.js | 76 +- .../firestore/e2e/Query/limitToLast.e2e.js | 199 +- .../firestore/e2e/Query/onSnapshot.e2e.js | 768 +++-- packages/firestore/e2e/Query/orderBy.e2e.js | 304 +- packages/firestore/e2e/Query/query.e2e.js | 159 +- .../firestore/e2e/Query/startAfter.e2e.js | 383 ++- packages/firestore/e2e/Query/startAt.e2e.js | 335 +- .../e2e/Query/where.and.filter.e2e.js | 1759 +++++++--- packages/firestore/e2e/Query/where.e2e.js | 1497 ++++++--- .../e2e/Query/where.or.filter.e2e.js | 2852 ++++++++++++----- packages/firestore/e2e/QuerySnapshot.e2e.js | 792 +++-- .../firestore/e2e/SnapshotMetadata.e2e.js | 140 +- packages/firestore/e2e/Timestamp.e2e.js | 414 ++- packages/firestore/e2e/Transaction.e2e.js | 908 ++++-- .../firestore/e2e/WriteBatch/commit.e2e.js | 498 ++- .../firestore/e2e/WriteBatch/delete.e2e.js | 106 +- packages/firestore/e2e/WriteBatch/set.e2e.js | 458 ++- .../firestore/e2e/WriteBatch/update.e2e.js | 236 +- packages/firestore/e2e/firestore.e2e.js | 800 +++-- packages/firestore/e2e/issues.e2e.js | 386 ++- packages/firestore/lib/index.js | 2 + packages/firestore/lib/modular/Bytes.d.ts | 11 + packages/firestore/lib/modular/Bytes.js | 59 + packages/firestore/lib/modular/FieldPath.d.ts | 13 + packages/firestore/lib/modular/FieldPath.js | 3 + .../firestore/lib/modular/FieldValue.d.ts | 67 + packages/firestore/lib/modular/FieldValue.js | 41 + packages/firestore/lib/modular/GeoPoint.d.ts | 17 + packages/firestore/lib/modular/GeoPoint.js | 3 + packages/firestore/lib/modular/Timestamp.d.ts | 85 + packages/firestore/lib/modular/Timestamp.js | 3 + packages/firestore/lib/modular/index.d.ts | 535 ++++ packages/firestore/lib/modular/index.js | 218 ++ packages/firestore/lib/modular/query.d.ts | 344 ++ packages/firestore/lib/modular/query.js | 202 ++ packages/firestore/lib/modular/snapshot.d.ts | 203 ++ packages/firestore/lib/modular/snapshot.js | 14 + .../firestore/lib/modular/utils/observer.js | 16 + tests/app.js | 2 + tests/e2e/globals.js | 6 + 68 files changed, 15654 insertions(+), 5606 deletions(-) create mode 100644 packages/firestore/e2e/Bytes.e2e.js create mode 100644 packages/firestore/lib/modular/Bytes.d.ts create mode 100644 packages/firestore/lib/modular/Bytes.js create mode 100644 packages/firestore/lib/modular/FieldPath.d.ts create mode 100644 packages/firestore/lib/modular/FieldPath.js create mode 100644 packages/firestore/lib/modular/FieldValue.d.ts create mode 100644 packages/firestore/lib/modular/FieldValue.js create mode 100644 packages/firestore/lib/modular/GeoPoint.d.ts create mode 100644 packages/firestore/lib/modular/GeoPoint.js create mode 100644 packages/firestore/lib/modular/Timestamp.d.ts create mode 100644 packages/firestore/lib/modular/Timestamp.js create mode 100644 packages/firestore/lib/modular/index.d.ts create mode 100644 packages/firestore/lib/modular/index.js create mode 100644 packages/firestore/lib/modular/query.d.ts create mode 100644 packages/firestore/lib/modular/query.js create mode 100644 packages/firestore/lib/modular/snapshot.d.ts create mode 100644 packages/firestore/lib/modular/snapshot.js create mode 100644 packages/firestore/lib/modular/utils/observer.js diff --git a/packages/firestore/e2e/Aggregate/count.e2e.js b/packages/firestore/e2e/Aggregate/count.e2e.js index 6755fb36f9..a94a5968dc 100644 --- a/packages/firestore/e2e/Aggregate/count.e2e.js +++ b/packages/firestore/e2e/Aggregate/count.e2e.js @@ -21,62 +21,117 @@ describe('firestore().collection().count()', function () { return wipe(); }); - it('throws if no argument provided', function () { - try { - firebase.firestore().collection(COLLECTION).startAt(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Expected a DocumentSnapshot or list of field values but got undefined', - ); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if no argument provided', function () { + try { + firebase.firestore().collection(COLLECTION).startAt(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'Expected a DocumentSnapshot or list of field values but got undefined', + ); + return Promise.resolve(); + } + }); - it('gets count of collection reference - unfiltered', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`); + it('gets count of collection reference - unfiltered', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 1 } }), + doc2.set({ foo: 2, bar: { value: 2 } }), + doc3.set({ foo: 3, bar: { value: 3 } }), + ]); - const qs = await colRef.count().get(); - qs.data().count.should.eql(3); - }); - it('gets countFromServer of collection reference - unfiltered', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`); - - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); - - const qs = await colRef.countFromServer().get(); - qs.data().count.should.eql(3); + const qs = await colRef.count().get(); + qs.data().count.should.eql(3); + }); + it('gets countFromServer of collection reference - unfiltered', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`); + + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 1 } }), + doc2.set({ foo: 2, bar: { value: 2 } }), + doc3.set({ foo: 3, bar: { value: 3 } }), + ]); + + const qs = await colRef.countFromServer().get(); + qs.data().count.should.eql(3); + }); + it('gets correct count of collection reference - where equal', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`); + + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 1 } }), + doc2.set({ foo: 2, bar: { value: 2 } }), + doc3.set({ foo: 3, bar: { value: 3 } }), + ]); + + const qs = await colRef.where('foo', '==', 3).count().get(); + qs.data().count.should.eql(1); + }); }); - it('gets correct count of collection reference - where equal', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`); - - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); - - const qs = await colRef.where('foo', '==', 3).count().get(); - qs.data().count.should.eql(1); + + describe('modular', function () { + it('throws if no argument provided', function () { + const { getFirestore, collection, startAt, query } = firestoreModular; + + try { + query(collection(getFirestore(), COLLECTION), startAt()); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'Expected a DocumentSnapshot or list of field values but got undefined', + ); + return Promise.resolve(); + } + }); + + it('gets countFromServer of collection reference - unfiltered', async function () { + const { getFirestore, collection, doc, setDoc, getCountFromServer } = firestoreModular; + + const colRef = collection(getFirestore(), `${COLLECTION}/count/collection`); + + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + await Promise.all([ + setDoc(doc1, { foo: 1, bar: { value: 1 } }), + setDoc(doc2, { foo: 2, bar: { value: 2 } }), + setDoc(doc3, { foo: 3, bar: { value: 3 } }), + ]); + + const qs = await getCountFromServer(colRef); + qs.data().count.should.eql(3); + }); + + it('gets correct count of collection reference - where equal', async function () { + const { getFirestore, collection, doc, setDoc, query, where, getCountFromServer } = + firestoreModular; + + const colRef = collection(getFirestore(), `${COLLECTION}/count/collection`); + + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + await Promise.all([ + setDoc(doc1, { foo: 1, bar: { value: 1 } }), + setDoc(doc2, { foo: 2, bar: { value: 2 } }), + setDoc(doc3, { foo: 3, bar: { value: 3 } }), + ]); + + const qs = await getCountFromServer(query(colRef, where('foo', '==', 3))); + qs.data().count.should.eql(1); + }); }); // TODO diff --git a/packages/firestore/e2e/Bundle/loadBundle.e2e.js b/packages/firestore/e2e/Bundle/loadBundle.e2e.js index 81f7290026..bbe16c7f4c 100644 --- a/packages/firestore/e2e/Bundle/loadBundle.e2e.js +++ b/packages/firestore/e2e/Bundle/loadBundle.e2e.js @@ -21,28 +21,62 @@ describe('firestore().loadBundle()', function () { return await wipe(); }); - it('loads the bundle contents', async function () { - const bundle = getBundle(); - const progress = await firebase.firestore().loadBundle(bundle); - const query = firebase.firestore().collection(BUNDLE_COLLECTION); - const snapshot = await query.get({ source: 'cache' }); - - progress.taskState.should.eql('Success'); - progress.documentsLoaded.should.eql(6); - snapshot.size.should.eql(6); + describe('v8 compatibility', function () { + it('loads the bundle contents', async function () { + const bundle = getBundle(); + const progress = await firebase.firestore().loadBundle(bundle); + const query = firebase.firestore().collection(BUNDLE_COLLECTION); + const snapshot = await query.get({ source: 'cache' }); + + progress.taskState.should.eql('Success'); + progress.documentsLoaded.should.eql(6); + snapshot.size.should.eql(6); + }); + + it('throws if invalid bundle', async function () { + try { + await firebase.firestore().loadBundle('not-a-bundle'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + /* + * Due to inconsistent error throws between Android and iOS Firebase SDK, + * it is not able to test a specific error message. + * Android SDK throws 'invalid-arguments', while iOS SDK throws 'unknown' + */ + return Promise.resolve(); + } + }); }); - it('throws if invalid bundle', async function () { - try { - await firebase.firestore().loadBundle('not-a-bundle'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - /* - * Due to inconsistent error throws between Android and iOS Firebase SDK, - * it is not able to test a specific error message. - * Android SDK throws 'invalid-arguments', while iOS SDK throws 'unknown' - */ - return Promise.resolve(); - } + describe('modular', function () { + it('loads the bundle contents', async function () { + const { getFirestore, loadBundle, collection, getDocsFromCache } = firestoreModular; + const db = getFirestore(); + + const bundle = getBundle(); + const progress = await loadBundle(db, bundle); + const query = collection(db, BUNDLE_COLLECTION); + const snapshot = await getDocsFromCache(query); + + progress.taskState.should.eql('Success'); + progress.documentsLoaded.should.eql(6); + snapshot.size.should.eql(6); + }); + + it('throws if invalid bundle', async function () { + const { getFirestore, loadBundle } = firestoreModular; + + try { + await loadBundle(getFirestore(), 'not-a-bundle'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + /* + * Due to inconsistent error throws between Android and iOS Firebase SDK, + * it is not able to test a specific error message. + * Android SDK throws 'invalid-arguments', while iOS SDK throws 'unknown' + */ + return Promise.resolve(); + } + }); }); }); diff --git a/packages/firestore/e2e/Bundle/namedQuery.e2e.js b/packages/firestore/e2e/Bundle/namedQuery.e2e.js index 5199e8fe45..e02123d0f5 100644 --- a/packages/firestore/e2e/Bundle/namedQuery.e2e.js +++ b/packages/firestore/e2e/Bundle/namedQuery.e2e.js @@ -22,77 +22,170 @@ describe('firestore().namedQuery()', function () { return await firebase.firestore().loadBundle(getBundle()); }); - it('returns bundled QuerySnapshot', async function () { - const query = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME); - const snapshot = await query.get({ source: 'cache' }); - - snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); - snapshot.docs.forEach(doc => { - doc.data().number.should.equalOneOf(1, 2, 3); - doc.metadata.fromCache.should.eql(true); + describe('v8 compatibility', function () { + it('returns bundled QuerySnapshot', async function () { + const query = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME); + const snapshot = await query.get({ source: 'cache' }); + + snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); + snapshot.docs.forEach(doc => { + doc.data().number.should.equalOneOf(1, 2, 3); + doc.metadata.fromCache.should.eql(true); + }); }); - }); - it('limits the number of documents in bundled QuerySnapshot', async function () { - const query = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME); - const snapshot = await query.limit(1).get({ source: 'cache' }); + it('limits the number of documents in bundled QuerySnapshot', async function () { + const query = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME); + const snapshot = await query.limit(1).get({ source: 'cache' }); - snapshot.size.should.equal(1); - snapshot.docs[0].metadata.fromCache.should.eql(true); - }); + snapshot.size.should.equal(1); + snapshot.docs[0].metadata.fromCache.should.eql(true); + }); - // TODO: log upstream issue - this broke with BoM >= 32.0.0, source always appears to be cache now - xit('returns QuerySnapshot from firestore backend when omitting "source: cache"', async function () { - const docRef = firebase.firestore().collection(BUNDLE_COLLECTION).doc(); - await docRef.set({ number: 4 }); + // TODO: log upstream issue - this broke with BoM >= 32.0.0, source always appears to be cache now + xit('returns QuerySnapshot from firestore backend when omitting "source: cache"', async function () { + const docRef = firebase.firestore().collection(BUNDLE_COLLECTION).doc(); + await docRef.set({ number: 4 }); - const query = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME); - const snapshot = await query.get(); + const query = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME); + const snapshot = await query.get(); - snapshot.size.should.equal(1); - snapshot.docs[0].data().number.should.eql(4); - snapshot.docs[0].metadata.fromCache.should.eql(false); - }); + snapshot.size.should.equal(1); + snapshot.docs[0].data().number.should.eql(4); + snapshot.docs[0].metadata.fromCache.should.eql(false); + }); - it('calls onNext with QuerySnapshot from firestore backend', async function () { - const docRef = firebase.firestore().collection(BUNDLE_COLLECTION).doc(); - await docRef.set({ number: 5 }); + it('calls onNext with QuerySnapshot from firestore backend', async function () { + const docRef = firebase.firestore().collection(BUNDLE_COLLECTION).doc(); + await docRef.set({ number: 5 }); - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME).onSnapshot(onNext, onError); + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME).onSnapshot(onNext, onError); - await Utils.spyToBeCalledOnceAsync(onNext); + await Utils.spyToBeCalledOnceAsync(onNext); - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - // FIXME not stable on tests::test-reuse - // 5 on first run, 4 on reuse - // onNext.args[0][0].docs[0].data().number.should.eql(4); - unsub(); - }); + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + // FIXME not stable on tests::test-reuse + // 5 on first run, 4 on reuse + // onNext.args[0][0].docs[0].data().number.should.eql(4); + unsub(); + }); + + it('throws if invalid query name', async function () { + const query = firebase.firestore().namedQuery('invalid-query'); + try { + await query.get({ source: 'cache' }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('unknown'); + return Promise.resolve(); + } + }); - it('throws if invalid query name', async function () { - const query = firebase.firestore().namedQuery('invalid-query'); - try { - await query.get({ source: 'cache' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('unknown'); - return Promise.resolve(); - } + it('calls onError if invalid query name', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().namedQuery('invalid-query').onSnapshot(onNext, onError); + + await Utils.spyToBeCalledOnceAsync(onError); + + onNext.should.be.callCount(0); + onError.should.be.calledOnce(); + onError.args[0][0].message.should.containEql('unknown'); + unsub(); + }); }); - it('calls onError if invalid query name', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().namedQuery('invalid-query').onSnapshot(onNext, onError); + describe('modular', function () { + // FIXME: works in isolation, but not in suite + xit('returns bundled QuerySnapshot', async function () { + const { getFirestore, namedQuery, getDocsFromCache } = firestoreModular; - await Utils.spyToBeCalledOnceAsync(onError); + const query = namedQuery(getFirestore(), BUNDLE_QUERY_NAME); + const snapshot = await getDocsFromCache(query); + + snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); + snapshot.docs.forEach(doc => { + doc.data().number.should.equalOneOf(1, 2, 3); + doc.metadata.fromCache.should.eql(true); + }); + }); + + it('limits the number of documents in bundled QuerySnapshot', async function () { + const { getFirestore, namedQuery, getDocsFromCache, query, limit } = firestoreModular; + + const q = namedQuery(getFirestore(), BUNDLE_QUERY_NAME); + const snapshot = await getDocsFromCache(query(q, limit(1))); + + snapshot.size.should.equal(1); + snapshot.docs[0].metadata.fromCache.should.eql(true); + }); - onNext.should.be.callCount(0); - onError.should.be.calledOnce(); - onError.args[0][0].message.should.containEql('unknown'); - unsub(); + // TODO: log upstream issue - this broke with BoM >= 32.0.0, source always appears to be cache now + xit('returns QuerySnapshot from firestore backend when omitting "source: cache"', async function () { + const { getFirestore, namedQuery, getDocs, setDoc, collection, doc } = firestoreModular; + const db = getFirestore(); + + const docRef = doc(collection(db, BUNDLE_COLLECTION)); + await setDoc(docRef, { number: 4 }); + + const query = namedQuery(db, BUNDLE_QUERY_NAME); + const snapshot = await getDocs(query); + + snapshot.size.should.equal(1); + snapshot.docs[0].data().number.should.eql(4); + snapshot.docs[0].metadata.fromCache.should.eql(false); + }); + + it('calls onNext with QuerySnapshot from firestore backend', async function () { + const { getFirestore, collection, doc, setDoc, namedQuery, onSnapshot } = firestoreModular; + const db = getFirestore(); + + const docRef = doc(collection(db, BUNDLE_COLLECTION)); + await setDoc(docRef, { number: 5 }); + + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(namedQuery(db, BUNDLE_QUERY_NAME), onNext, onError); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + // FIXME not stable on tests::test-reuse + // 5 on first run, 4 on reuse + // onNext.args[0][0].docs[0].data().number.should.eql(4); + unsub(); + }); + + it('throws if invalid query name', async function () { + const { getFirestore, namedQuery, getDocsFromCache } = firestoreModular; + + const query = namedQuery(getFirestore(), 'invalid-query'); + try { + await getDocsFromCache(query); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('unknown'); + return Promise.resolve(); + } + }); + + it('calls onError if invalid query name', async function () { + const { getFirestore, namedQuery, onSnapshot } = firestoreModular; + + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(namedQuery(getFirestore(), 'invalid-query'), onNext, onError); + + await Utils.spyToBeCalledOnceAsync(onError); + + onNext.should.be.callCount(0); + onError.should.be.calledOnce(); + onError.args[0][0].message.should.containEql('unknown'); + unsub(); + }); }); }); diff --git a/packages/firestore/e2e/Bytes.e2e.js b/packages/firestore/e2e/Bytes.e2e.js new file mode 100644 index 0000000000..c18a45ab27 --- /dev/null +++ b/packages/firestore/e2e/Bytes.e2e.js @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const testObject = { hello: 'world' }; +const testString = JSON.stringify(testObject); +const testBuffer = Buffer.from(testString); +const testBase64 = testBuffer.toString('base64'); + +const testObjectLarge = new Array(5000).fill(testObject); +const testStringLarge = JSON.stringify(testObjectLarge); +const testBufferLarge = Buffer.from(testStringLarge); +const testBase64Large = testBufferLarge.toString('base64'); + +describe('Bytes modular', function () { + it('.fromBase64String() -> returns new instance of Bytes', async function () { + const { Bytes } = firestoreModular; + const myBytes = Bytes.fromBase64String(testBase64); + myBytes.should.be.instanceOf(Bytes); + myBytes._blob.should.be.instanceOf(firebase.firestore.Blob); + myBytes._blob._binaryString.should.equal(testString); + should.deepEqual( + JSON.parse(myBytes._blob._binaryString), + testObject, + 'Expected Blob _binaryString internals to serialize to json and match test object', + ); + }); + + it('.fromBase64String() -> throws if arg not typeof string and length > 0', async function () { + const { Bytes } = firestoreModular; + const myBytes = Bytes.fromBase64String(testBase64); + myBytes.should.be.instanceOf(Bytes); + (() => Bytes.fromBase64String(1234)).should.throwError(); + (() => Bytes.fromBase64String('')).should.throwError(); + }); + + it('.fromUint8Array() -> returns new instance of Bytes', async function () { + const testUInt8Array = new jet.context.window.Uint8Array(testBuffer); + const { Bytes } = firestoreModular; + const myBytes = Bytes.fromUint8Array(testUInt8Array); + myBytes.should.be.instanceOf(Bytes); + const json = JSON.parse(myBytes._blob._binaryString); + json.hello.should.equal('world'); + }); + + it('.fromUint8Array() -> throws if arg not instanceof Uint8Array', async function () { + const testUInt8Array = new jet.context.window.Uint8Array(testBuffer); + const { Bytes } = firestoreModular; + const myBytes = Bytes.fromUint8Array(testUInt8Array); + myBytes.should.be.instanceOf(Bytes); + (() => Bytes.fromUint8Array('derp')).should.throwError(); + }); + + it('.toString() -> returns string representation of bytes instance', async function () { + const { Bytes } = firestoreModular; + const myBytes = Bytes.fromBase64String(testBase64); + myBytes.should.be.instanceOf(Bytes); + should.equal( + myBytes.toString().includes(testBase64), + true, + 'toString() should return a string that includes the base64', + ); + }); + + it('.isEqual() -> returns true or false', async function () { + const { Bytes } = firestoreModular; + const myBytes = Bytes.fromBase64String(testBase64); + const myBytes2 = Bytes.fromBase64String(testBase64Large); + myBytes.isEqual(myBytes).should.equal(true); + myBytes2.isEqual(myBytes).should.equal(false); + }); + + it('.isEqual() -> throws if arg not instanceof Bytes', async function () { + const { Bytes } = firestoreModular; + const myBytes = Bytes.fromBase64String(testBase64); + const myBytes2 = Bytes.fromBase64String(testBase64Large); + myBytes.isEqual(myBytes).should.equal(true); + (() => myBytes2.isEqual('derp')).should.throwError(); + }); + + it('.toBase64() -> returns base64 string', async function () { + const { Bytes } = firestoreModular; + const myBytes = Bytes.fromBase64String(testBase64); + myBytes.should.be.instanceOf(Bytes); + myBytes.toBase64().should.equal(testBase64); + }); + + it('.toUint8Array() -> returns Uint8Array', async function () { + const { Bytes } = firestoreModular; + const myBytes = Bytes.fromBase64String(testBase64); + const testUInt8Array = new jet.context.window.Uint8Array(testBuffer); + const testUInt8Array2 = new jet.context.window.Uint8Array(); + + myBytes.should.be.instanceOf(Bytes); + should.deepEqual(myBytes.toUint8Array(), testUInt8Array); + should.notDeepEqual(myBytes.toUint8Array(), testUInt8Array2); + }); +}); diff --git a/packages/firestore/e2e/CollectionReference/add.e2e.js b/packages/firestore/e2e/CollectionReference/add.e2e.js index 6acfde75e0..19e7857ded 100644 --- a/packages/firestore/e2e/CollectionReference/add.e2e.js +++ b/packages/firestore/e2e/CollectionReference/add.e2e.js @@ -21,23 +21,52 @@ describe('firestore.collection().add()', function () { before(function () { return wipe(); }); - it('throws if data is not an object', function () { - try { - firebase.firestore().collection(COLLECTION).add(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'data' must be an object"); - return Promise.resolve(); - } + + describe('v8 compatibility', function () { + it('throws if data is not an object', function () { + try { + firebase.firestore().collection(COLLECTION).add(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'data' must be an object"); + return Promise.resolve(); + } + }); + + it('adds a new document', async function () { + const data = { foo: 'bar' }; + const docRef = await firebase.firestore().collection(COLLECTION).add(data); + should.equal(docRef.constructor.name, 'FirestoreDocumentReference'); + const docSnap = await docRef.get(); + docSnap.data().should.eql(jet.contextify(data)); + docSnap.exists.should.eql(true); + await docRef.delete(); + }); }); - it('adds a new document', async function () { - const data = { foo: 'bar' }; - const docRef = await firebase.firestore().collection(COLLECTION).add(data); - should.equal(docRef.constructor.name, 'FirestoreDocumentReference'); - const docSnap = await docRef.get(); - docSnap.data().should.eql(jet.contextify(data)); - docSnap.exists.should.eql(true); - await docRef.delete(); + describe('modular', function () { + it('throws if data is not an object', function () { + const { getFirestore, collection, addDoc } = firestoreModular; + + try { + addDoc(collection(getFirestore(), COLLECTION), 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'data' must be an object"); + return Promise.resolve(); + } + }); + + it('adds a new document', async function () { + const { getFirestore, collection, addDoc, getDocs, deleteDoc } = firestoreModular; + + const data = { foo: 'bar' }; + const docRef = await addDoc(collection(getFirestore(), COLLECTION), data); + should.equal(docRef.constructor.name, 'FirestoreDocumentReference'); + const docSnap = await getDocs(docRef); + docSnap.data().should.eql(jet.contextify(data)); + docSnap.exists.should.eql(true); + await deleteDoc(docRef); + }); }); }); diff --git a/packages/firestore/e2e/CollectionReference/doc.e2e.js b/packages/firestore/e2e/CollectionReference/doc.e2e.js index 9be4063783..9694a4c5bf 100644 --- a/packages/firestore/e2e/CollectionReference/doc.e2e.js +++ b/packages/firestore/e2e/CollectionReference/doc.e2e.js @@ -21,23 +21,51 @@ describe('firestore.collection().doc()', function () { before(function () { return wipe(); }); - it('throws if path is not a document', function () { - try { - firebase.firestore().collection(COLLECTION).doc('bar/baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentPath' must point to a document"); - return Promise.resolve(); - } - }); - it('generates an ID if no path is provided', function () { - const instance = firebase.firestore().collection(COLLECTION).doc(); - should.equal(20, instance.id.length); + describe('v8 compatibility', function () { + it('throws if path is not a document', function () { + try { + firebase.firestore().collection(COLLECTION).doc('bar/baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentPath' must point to a document"); + return Promise.resolve(); + } + }); + + it('generates an ID if no path is provided', function () { + const instance = firebase.firestore().collection(COLLECTION).doc(); + should.equal(20, instance.id.length); + }); + + it('uses path if provided', function () { + const instance = firebase.firestore().collection(COLLECTION).doc('bar'); + instance.id.should.eql('bar'); + }); }); - it('uses path if provided', function () { - const instance = firebase.firestore().collection(COLLECTION).doc('bar'); - instance.id.should.eql('bar'); + describe('modular', function () { + it('throws if path is not a document', function () { + const { getFirestore, collection, doc } = firestoreModular; + try { + doc(collection(getFirestore(), COLLECTION), 'bar/baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentPath' must point to a document"); + return Promise.resolve(); + } + }); + + it('generates an ID if no path is provided', function () { + const { getFirestore, collection, doc } = firestoreModular; + const instance = doc(collection(getFirestore(), COLLECTION)); + should.equal(20, instance.id.length); + }); + + it('uses path if provided', function () { + const { getFirestore, collection, doc } = firestoreModular; + const instance = doc(collection(getFirestore(), COLLECTION), 'bar'); + instance.id.should.eql('bar'); + }); }); }); diff --git a/packages/firestore/e2e/CollectionReference/properties.e2e.js b/packages/firestore/e2e/CollectionReference/properties.e2e.js index a0aa3e63dc..c5429a5148 100644 --- a/packages/firestore/e2e/CollectionReference/properties.e2e.js +++ b/packages/firestore/e2e/CollectionReference/properties.e2e.js @@ -21,29 +21,71 @@ describe('firestore.collection()', function () { before(function () { return wipe(); }); - it('returns the firestore instance', function () { - const instance = firebase.firestore().collection(COLLECTION); - instance.firestore.app.name.should.eql('[DEFAULT]'); - }); - it('returns the collection id', function () { - const instance1 = firebase.firestore().collection(COLLECTION); - const instance2 = firebase.firestore().collection(`${COLLECTION}/bar/baz`); - instance1.id.should.eql(COLLECTION); - instance2.id.should.eql('baz'); - }); + describe('v8 compatibility', function () { + it('returns the firestore instance', function () { + const instance = firebase.firestore().collection(COLLECTION); + instance.firestore.app.name.should.eql('[DEFAULT]'); + }); + + it('returns the collection id', function () { + const instance1 = firebase.firestore().collection(COLLECTION); + const instance2 = firebase.firestore().collection(`${COLLECTION}/bar/baz`); + instance1.id.should.eql(COLLECTION); + instance2.id.should.eql('baz'); + }); + + it('returns the collection parent', function () { + const instance1 = firebase.firestore().collection(COLLECTION); + should.equal(instance1.parent, null); + const instance2 = firebase.firestore().collection('foo').doc('bar').collection('baz'); + should.equal(instance2.parent.id, 'bar'); + }); - it('returns the collection parent', function () { - const instance1 = firebase.firestore().collection(COLLECTION); - should.equal(instance1.parent, null); - const instance2 = firebase.firestore().collection('foo').doc('bar').collection('baz'); - should.equal(instance2.parent.id, 'bar'); + it('returns the firestore path', function () { + const instance1 = firebase.firestore().collection(COLLECTION); + instance1.path.should.eql(COLLECTION); + const instance2 = firebase + .firestore() + .collection(COLLECTION) + .doc('bar') + .collection(COLLECTION); + instance2.path.should.eql(`${COLLECTION}/bar/${COLLECTION}`); + }); }); - it('returns the firestore path', function () { - const instance1 = firebase.firestore().collection(COLLECTION); - instance1.path.should.eql(COLLECTION); - const instance2 = firebase.firestore().collection(COLLECTION).doc('bar').collection(COLLECTION); - instance2.path.should.eql(`${COLLECTION}/bar/${COLLECTION}`); + describe('modular', function () { + it('returns the firestore instance', function () { + const { getFirestore, collection } = firestoreModular; + const instance = collection(getFirestore(), COLLECTION); + instance.firestore.app.name.should.eql('[DEFAULT]'); + }); + + it('returns the collection id', function () { + const { getFirestore, collection } = firestoreModular; + const db = getFirestore(); + const instance1 = collection(db, COLLECTION); + const instance2 = collection(db, `${COLLECTION}/bar/baz`); + instance1.id.should.eql(COLLECTION); + instance2.id.should.eql('baz'); + }); + + it('returns the collection parent', function () { + const { getFirestore, collection, doc } = firestoreModular; + const db = getFirestore(); + const instance1 = collection(db, COLLECTION); + should.equal(instance1.parent, null); + const instance2 = collection(doc(collection(db, 'foo'), 'bar'), 'baz'); + should.equal(instance2.parent.id, 'bar'); + }); + + it('returns the firestore path', function () { + const { getFirestore, collection, doc } = firestoreModular; + const db = getFirestore(); + const instance1 = collection(db, COLLECTION); + instance1.path.should.eql(COLLECTION); + const instance2 = collection(doc(collection(db, COLLECTION), 'bar'), COLLECTION); + instance2.path.should.eql(`${COLLECTION}/bar/${COLLECTION}`); + }); }); }); diff --git a/packages/firestore/e2e/DocumentChange.e2e.js b/packages/firestore/e2e/DocumentChange.e2e.js index 17015f3b71..b3f36c854b 100644 --- a/packages/firestore/e2e/DocumentChange.e2e.js +++ b/packages/firestore/e2e/DocumentChange.e2e.js @@ -21,95 +21,208 @@ describe('firestore.DocumentChange', function () { before(function () { return wipe(); }); - it('.doc -> returns a DocumentSnapshot', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - await colRef.add({}); - const snapshot = await colRef.limit(1).get(); - const changes = snapshot.docChanges(); - const docChange = changes[0]; + describe('v8 compatibility', function () { + it('.doc -> returns a DocumentSnapshot', async function () { + const colRef = firebase.firestore().collection(COLLECTION); + await colRef.add({}); + const snapshot = await colRef.limit(1).get(); + const changes = snapshot.docChanges(); - docChange.doc.constructor.name.should.eql('FirestoreDocumentSnapshot'); - }); + const docChange = changes[0]; + + docChange.doc.constructor.name.should.eql('FirestoreDocumentSnapshot'); + }); + + it('returns the correct metadata when adding and removing', async function () { + const colRef = firebase + .firestore() + .collection(`${COLLECTION}/docChanges/docChangesCollection`); + const doc1 = firebase.firestore().doc(`${COLLECTION}/docChanges/docChangesCollection/doc1`); + + // Set something in the database + await doc1.set({ name: 'doc1' }); + + // Subscribe to changes + const callback = sinon.spy(); + const unsub = colRef.onSnapshot(callback); + await Utils.spyToBeCalledOnceAsync(callback); + + // Validate docChange item exists + callback.should.be.calledOnce(); + const changes1 = callback.args[0][0].docChanges(); + changes1.length.should.eql(1); + changes1[0].newIndex.should.eql(0); + changes1[0].oldIndex.should.eql(-1); + changes1[0].type.should.eql('added'); + changes1[0].doc.data().name.should.eql('doc1'); + + // Delete the document + await doc1.delete(); + await Utils.sleep(800); + + // The QuerySnapshot should be empty + callback.args[1][0].size.should.eql(0); + + // The docChanges should keep removed doc + const changes2 = callback.args[1][0].docChanges(); + changes2.length.should.eql(1); + + changes2[0].doc.data().name.should.eql('doc1'); + changes2[0].type.should.eql('removed'); + changes2[0].newIndex.should.eql(-1); + changes2[0].oldIndex.should.eql(0); + + unsub(); + }); + + it('returns the correct metadata when modifying documents', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/docChanges/docMovedCollection`); + + const doc1 = firebase.firestore().doc(`${COLLECTION}/docChanges/docMovedCollection/doc1`); + const doc2 = firebase.firestore().doc(`${COLLECTION}/docChanges/docMovedCollection/doc2`); + const doc3 = firebase.firestore().doc(`${COLLECTION}/docChanges/docMovedCollection/doc3`); - it('returns the correct metadata when adding and removing', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/docChanges/docChangesCollection`); - const doc1 = firebase.firestore().doc(`${COLLECTION}/docChanges/docChangesCollection/doc1`); + await Promise.all([doc1.set({ value: 1 }), doc2.set({ value: 2 }), doc3.set({ value: 3 })]); - // Set something in the database - await doc1.set({ name: 'doc1' }); + // Subscribe to changes + const callback = sinon.spy(); + const unsub = colRef.orderBy('value').onSnapshot(callback); + await Utils.spyToBeCalledOnceAsync(callback); - // Subscribe to changes - const callback = sinon.spy(); - const unsub = colRef.onSnapshot(callback); - await Utils.spyToBeCalledOnceAsync(callback); + // Validate docChange item exists + callback.should.be.calledOnce(); + const changes1 = callback.args[0][0].docChanges(); + changes1.length.should.eql(3); - // Validate docChange item exists - callback.should.be.calledOnce(); - const changes1 = callback.args[0][0].docChanges(); - changes1.length.should.eql(1); - changes1[0].newIndex.should.eql(0); - changes1[0].oldIndex.should.eql(-1); - changes1[0].type.should.eql('added'); - changes1[0].doc.data().name.should.eql('doc1'); + changes1.forEach((dc, i) => { + dc.oldIndex.should.eql(-1); + dc.newIndex.should.eql(i); + dc.doc.data().value.should.eql(i + 1); + }); - // Delete the document - await doc1.delete(); - await Utils.sleep(800); + // Update a document + await doc1.update({ value: 4 }); - // The QuerySnapshot should be empty - callback.args[1][0].size.should.eql(0); + await Utils.sleep(800); - // The docChanges should keep removed doc - const changes2 = callback.args[1][0].docChanges(); - changes2.length.should.eql(1); + const changes2 = callback.args[1][0].docChanges(); + changes2.length.should.eql(1); - changes2[0].doc.data().name.should.eql('doc1'); - changes2[0].type.should.eql('removed'); - changes2[0].newIndex.should.eql(-1); - changes2[0].oldIndex.should.eql(0); + const dc = changes2[0]; + dc.type.should.eql('modified'); + dc.oldIndex.should.eql(0); + dc.newIndex.should.eql(2); - unsub(); + unsub(); + }); }); - it('returns the correct metadata when modifying documents', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/docChanges/docMovedCollection`); + describe('modular', function () { + it('.doc -> returns a DocumentSnapshot', async function () { + const { getFirestore, collection, addDoc, limit, getDocs, query } = firestoreModular; + const db = getFirestore(); + + const colRef = collection(db, COLLECTION); + await addDoc(colRef, {}); + const snapshot = await getDocs(query(colRef, limit(1))); + const changes = snapshot.docChanges(); + + const docChange = changes[0]; + + docChange.doc.constructor.name.should.eql('FirestoreDocumentSnapshot'); + }); + + it('returns the correct metadata when adding and removing', async function () { + const { getFirestore, collection, doc, setDoc, onSnapshot, deleteDoc } = firestoreModular; + const db = getFirestore(); - const doc1 = firebase.firestore().doc(`${COLLECTION}/docChanges/docMovedCollection/doc1`); - const doc2 = firebase.firestore().doc(`${COLLECTION}/docChanges/docMovedCollection/doc2`); - const doc3 = firebase.firestore().doc(`${COLLECTION}/docChanges/docMovedCollection/doc3`); + const colRef = collection(db, `${COLLECTION}/docChanges/docChangesCollection`); + const doc1 = doc(db, `${COLLECTION}/docChanges/docChangesCollection/doc1`); - await Promise.all([doc1.set({ value: 1 }), doc2.set({ value: 2 }), doc3.set({ value: 3 })]); + // Set something in the database + await setDoc(doc1, { name: 'doc1' }); - // Subscribe to changes - const callback = sinon.spy(); - const unsub = colRef.orderBy('value').onSnapshot(callback); - await Utils.spyToBeCalledOnceAsync(callback); + // Subscribe to changes + const callback = sinon.spy(); + const unsub = onSnapshot(colRef, callback); + await Utils.spyToBeCalledOnceAsync(callback); - // Validate docChange item exists - callback.should.be.calledOnce(); - const changes1 = callback.args[0][0].docChanges(); - changes1.length.should.eql(3); + // Validate docChange item exists + callback.should.be.calledOnce(); + const changes1 = callback.args[0][0].docChanges(); + changes1.length.should.eql(1); + changes1[0].newIndex.should.eql(0); + changes1[0].oldIndex.should.eql(-1); + changes1[0].type.should.eql('added'); + changes1[0].doc.data().name.should.eql('doc1'); - changes1.forEach((dc, i) => { - dc.oldIndex.should.eql(-1); - dc.newIndex.should.eql(i); - dc.doc.data().value.should.eql(i + 1); + // Delete the document + await deleteDoc(doc1); + await Utils.sleep(800); + + // The QuerySnapshot should be empty + callback.args[1][0].size.should.eql(0); + + // The docChanges should keep removed doc + const changes2 = callback.args[1][0].docChanges(); + changes2.length.should.eql(1); + + changes2[0].doc.data().name.should.eql('doc1'); + changes2[0].type.should.eql('removed'); + changes2[0].newIndex.should.eql(-1); + changes2[0].oldIndex.should.eql(0); + + unsub(); }); - // Update a document - await doc1.update({ value: 4 }); + it('returns the correct metadata when modifying documents', async function () { + const { getFirestore, collection, doc, setDoc, orderBy, query, onSnapshot, updateDoc } = + firestoreModular; + const db = getFirestore(); + + const colRef = collection(db, `${COLLECTION}/docChanges/docMovedCollection`); + + const doc1 = doc(db, `${COLLECTION}/docChanges/docMovedCollection/doc1`); + const doc2 = doc(db, `${COLLECTION}/docChanges/docMovedCollection/doc2`); + const doc3 = doc(db, `${COLLECTION}/docChanges/docMovedCollection/doc3`); - await Utils.sleep(800); + await Promise.all([ + setDoc(doc1, { value: 1 }), + setDoc(doc2, { value: 2 }), + setDoc(doc3, { value: 3 }), + ]); - const changes2 = callback.args[1][0].docChanges(); - changes2.length.should.eql(1); + // Subscribe to changes + const callback = sinon.spy(); + const unsub = onSnapshot(query(colRef, orderBy('value')), callback); + await Utils.spyToBeCalledOnceAsync(callback); - const dc = changes2[0]; - dc.type.should.eql('modified'); - dc.oldIndex.should.eql(0); - dc.newIndex.should.eql(2); + // Validate docChange item exists + callback.should.be.calledOnce(); + const changes1 = callback.args[0][0].docChanges(); + changes1.length.should.eql(3); - unsub(); + changes1.forEach((dc, i) => { + dc.oldIndex.should.eql(-1); + dc.newIndex.should.eql(i); + dc.doc.data().value.should.eql(i + 1); + }); + + // Update a document + await updateDoc(doc1, { value: 4 }); + + await Utils.sleep(800); + + const changes2 = callback.args[1][0].docChanges(); + changes2.length.should.eql(1); + + const dc = changes2[0]; + dc.type.should.eql('modified'); + dc.oldIndex.should.eql(0); + dc.newIndex.should.eql(2); + + unsub(); + }); }); }); diff --git a/packages/firestore/e2e/DocumentReference/collection.e2e.js b/packages/firestore/e2e/DocumentReference/collection.e2e.js index 1cb8a05f42..2bf31fd33e 100644 --- a/packages/firestore/e2e/DocumentReference/collection.e2e.js +++ b/packages/firestore/e2e/DocumentReference/collection.e2e.js @@ -18,42 +18,94 @@ const COLLECTION = 'firestore'; describe('firestore.doc().collection()', function () { - it('throws if path is not a string', function () { - try { - firebase.firestore().doc('bar/baz').collection(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'collectionPath' must be a string value"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if path is not a string', function () { + try { + firebase.firestore().doc('bar/baz').collection(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'collectionPath' must be a string value"); + return Promise.resolve(); + } + }); - it('throws if path empty', function () { - try { - firebase.firestore().doc('bar/baz').collection(''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'collectionPath' must be a non-empty string"); - return Promise.resolve(); - } - }); + it('throws if path empty', function () { + try { + firebase.firestore().doc('bar/baz').collection(''); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'collectionPath' must be a non-empty string"); + return Promise.resolve(); + } + }); + + it('throws if path does not point to a collection', function () { + try { + firebase.firestore().doc('bar/baz').collection(`${COLLECTION}/bar`); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'collectionPath' must point to a collection"); + return Promise.resolve(); + } + }); - it('throws if path does not point to a collection', function () { - try { - firebase.firestore().doc('bar/baz').collection(`${COLLECTION}/bar`); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'collectionPath' must point to a collection"); - return Promise.resolve(); - } + it('returns a collection instance', function () { + const instance1 = firebase.firestore().doc(`${COLLECTION}/bar`).collection(COLLECTION); + const instance2 = firebase + .firestore() + .collection(COLLECTION) + .doc('bar') + .collection(COLLECTION); + should.equal(instance1.constructor.name, 'FirestoreCollectionReference'); + should.equal(instance2.constructor.name, 'FirestoreCollectionReference'); + instance1.id.should.equal(COLLECTION); + instance2.id.should.equal(COLLECTION); + }); }); - it('returns a collection instance', function () { - const instance1 = firebase.firestore().doc(`${COLLECTION}/bar`).collection(COLLECTION); - const instance2 = firebase.firestore().collection(COLLECTION).doc('bar').collection(COLLECTION); - should.equal(instance1.constructor.name, 'FirestoreCollectionReference'); - should.equal(instance2.constructor.name, 'FirestoreCollectionReference'); - instance1.id.should.equal(COLLECTION); - instance2.id.should.equal(COLLECTION); + describe('modular', function () { + it('throws if path is not a string', function () { + const { getFirestore, doc, collection } = firestoreModular; + try { + collection(doc(getFirestore(), 'bar/baz'), 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'collectionPath' must be a string value"); + return Promise.resolve(); + } + }); + + it('throws if path empty', function () { + const { getFirestore, doc, collection } = firestoreModular; + try { + collection(doc(getFirestore(), 'bar/baz'), ''); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'collectionPath' must be a non-empty string"); + return Promise.resolve(); + } + }); + + it('throws if path does not point to a collection', function () { + const { getFirestore, doc, collection } = firestoreModular; + try { + collection(doc(getFirestore(), 'bar/baz'), `${COLLECTION}/bar`); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'collectionPath' must point to a collection"); + return Promise.resolve(); + } + }); + + it('returns a collection instance', function () { + const { getFirestore, doc, collection } = firestoreModular; + const db = getFirestore(); + const instance1 = collection(doc(db, `${COLLECTION}/bar`), COLLECTION); + const instance2 = collection(doc(collection(db, COLLECTION), 'bar'), COLLECTION); + should.equal(instance1.constructor.name, 'FirestoreCollectionReference'); + should.equal(instance2.constructor.name, 'FirestoreCollectionReference'); + instance1.id.should.equal(COLLECTION); + instance2.id.should.equal(COLLECTION); + }); }); }); diff --git a/packages/firestore/e2e/DocumentReference/delete.e2e.js b/packages/firestore/e2e/DocumentReference/delete.e2e.js index c9fa45e62f..3e91932755 100644 --- a/packages/firestore/e2e/DocumentReference/delete.e2e.js +++ b/packages/firestore/e2e/DocumentReference/delete.e2e.js @@ -21,15 +21,34 @@ describe('firestore.doc().delete()', function () { before(function () { return wipe(); }); - it('deletes a document', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/deleteme`); - await ref.set({ foo: 'bar' }); - const snapshot1 = await ref.get(); - snapshot1.id.should.equal('deleteme'); - snapshot1.exists.should.equal(true); - await ref.delete(); - const snapshot2 = await ref.get(); - snapshot2.id.should.equal('deleteme'); - snapshot2.exists.should.equal(false); + + describe('v8 compatibility', function () { + it('deletes a document', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/deleteme`); + await ref.set({ foo: 'bar' }); + const snapshot1 = await ref.get(); + snapshot1.id.should.equal('deleteme'); + snapshot1.exists.should.equal(true); + await ref.delete(); + const snapshot2 = await ref.get(); + snapshot2.id.should.equal('deleteme'); + snapshot2.exists.should.equal(false); + }); + }); + + describe('modular', function () { + it('deletes a document', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + + const ref = doc(getFirestore(), `${COLLECTION}/deleteme`); + await setDoc(ref, { foo: 'bar' }); + const snapshot1 = await getDocs(ref); + snapshot1.id.should.equal('deleteme'); + snapshot1.exists.should.equal(true); + await deleteDoc(ref); + const snapshot2 = await getDocs(ref); + snapshot2.id.should.equal('deleteme'); + snapshot2.exists.should.equal(false); + }); }); }); diff --git a/packages/firestore/e2e/DocumentReference/get.e2e.js b/packages/firestore/e2e/DocumentReference/get.e2e.js index b4ddd576e8..76eae852c0 100644 --- a/packages/firestore/e2e/DocumentReference/get.e2e.js +++ b/packages/firestore/e2e/DocumentReference/get.e2e.js @@ -21,54 +21,94 @@ describe('firestore.doc().get()', function () { before(function () { return wipe(); }); - it('throws if get options are not an object', function () { - try { - firebase.firestore().doc('bar/baz').get('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must be an object is provided"); - return Promise.resolve(); - } - }); - it('throws if get options are invalid', function () { - try { - firebase.firestore().doc('bar/baz').get({ source: 'nope' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' GetOptions.source must be one of 'default', 'server' or 'cache'", - ); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if get options are not an object', function () { + try { + firebase.firestore().doc('bar/baz').get('foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' must be an object is provided"); + return Promise.resolve(); + } + }); - it('gets data from default source', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/get`); - const data = { foo: 'bar', bar: 123 }; - await ref.set(data); - const snapshot = await ref.get(); - snapshot.data().should.eql(jet.contextify(data)); - await ref.delete(); - }); + it('throws if get options are invalid', function () { + try { + firebase.firestore().doc('bar/baz').get({ source: 'nope' }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' GetOptions.source must be one of 'default', 'server' or 'cache'", + ); + return Promise.resolve(); + } + }); + + it('gets data from default source', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/get`); + const data = { foo: 'bar', bar: 123 }; + await ref.set(data); + const snapshot = await ref.get(); + snapshot.data().should.eql(jet.contextify(data)); + await ref.delete(); + }); + + it('gets data from the server', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/get`); + const data = { foo: 'bar', bar: 123 }; + await ref.set(data); + const snapshot = await ref.get({ source: 'server' }); + snapshot.data().should.eql(jet.contextify(data)); + snapshot.metadata.fromCache.should.equal(false); + await ref.delete(); + }); - it('gets data from the server', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/get`); - const data = { foo: 'bar', bar: 123 }; - await ref.set(data); - const snapshot = await ref.get({ source: 'server' }); - snapshot.data().should.eql(jet.contextify(data)); - snapshot.metadata.fromCache.should.equal(false); - await ref.delete(); + it('gets data from cache', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/get`); + const data = { foo: 'bar', bar: 123 }; + await ref.set(data); + const snapshot = await ref.get({ source: 'cache' }); + snapshot.data().should.eql(jet.contextify(data)); + snapshot.metadata.fromCache.should.equal(true); + await ref.delete(); + }); }); - it('gets data from cache', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/get`); - const data = { foo: 'bar', bar: 123 }; - await ref.set(data); - const snapshot = await ref.get({ source: 'cache' }); - snapshot.data().should.eql(jet.contextify(data)); - snapshot.metadata.fromCache.should.equal(true); - await ref.delete(); + describe('modular', function () { + it('gets data from default source', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + + const ref = doc(getFirestore(), `${COLLECTION}/get`); + const data = { foo: 'bar', bar: 123 }; + await setDoc(ref, data); + const snapshot = await getDocs(ref); + snapshot.data().should.eql(jet.contextify(data)); + await deleteDoc(ref); + }); + + it('gets data from the server', async function () { + const { getFirestore, doc, setDoc, getDocsFromServer, deleteDoc } = firestoreModular; + + const ref = doc(getFirestore(), `${COLLECTION}/get`); + const data = { foo: 'bar', bar: 123 }; + await setDoc(ref, data); + const snapshot = await getDocsFromServer(ref); + snapshot.data().should.eql(jet.contextify(data)); + snapshot.metadata.fromCache.should.equal(false); + await deleteDoc(ref); + }); + + it('gets data from cache', async function () { + const { getFirestore, doc, setDoc, getDocsFromCache, deleteDoc } = firestoreModular; + + const ref = doc(getFirestore(), `${COLLECTION}/get`); + const data = { foo: 'bar', bar: 123 }; + await setDoc(ref, data); + const snapshot = await getDocsFromCache(ref); + snapshot.data().should.eql(jet.contextify(data)); + snapshot.metadata.fromCache.should.equal(true); + await deleteDoc(ref); + }); }); }); diff --git a/packages/firestore/e2e/DocumentReference/isEqual.e2e.js b/packages/firestore/e2e/DocumentReference/isEqual.e2e.js index 06293b98bc..b7ce63bd46 100644 --- a/packages/firestore/e2e/DocumentReference/isEqual.e2e.js +++ b/packages/firestore/e2e/DocumentReference/isEqual.e2e.js @@ -17,35 +17,77 @@ const COLLECTION = 'firestore'; describe('firestore.doc().isEqual()', function () { - it('throws if other is not a DocumentReference', function () { - try { - firebase.firestore().doc('bar/baz').isEqual(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected a DocumentReference instance"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if other is not a DocumentReference', function () { + try { + firebase.firestore().doc('bar/baz').isEqual(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected a DocumentReference instance"); + return Promise.resolve(); + } + }); + + it('returns false when not equal', function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/baz`); + + const eql1 = docRef.isEqual(firebase.firestore().doc(`${COLLECTION}/foo`)); + const eql2 = docRef.isEqual( + firebase.firestore(firebase.app('secondaryFromNative')).doc(`${COLLECTION}/baz`), + ); - it('returns false when not equal', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/baz`); + eql1.should.be.False(); + eql2.should.be.False(); + }); - const eql1 = docRef.isEqual(firebase.firestore().doc(`${COLLECTION}/foo`)); - const eql2 = docRef.isEqual( - firebase.firestore(firebase.app('secondaryFromNative')).doc(`${COLLECTION}/baz`), - ); + it('returns true when equal', function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/baz`); - eql1.should.be.False(); - eql2.should.be.False(); + const eql1 = docRef.isEqual(docRef); + const eql2 = docRef.isEqual(firebase.firestore().doc(`${COLLECTION}/baz`)); + + eql1.should.be.True(); + eql2.should.be.True(); + }); }); - it('returns true when equal', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/baz`); + describe('modular', function () { + it('throws if other is not a DocumentReference', function () { + const { getFirestore, doc } = firestoreModular; + try { + doc(getFirestore(), 'bar/baz').isEqual(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected a DocumentReference instance"); + return Promise.resolve(); + } + }); + + it('returns false when not equal', function () { + const { getFirestore, doc } = firestoreModular; + const db = getFirestore(); + + const docRef = doc(db, `${COLLECTION}/baz`); + + const eql1 = docRef.isEqual(doc(db, `${COLLECTION}/foo`)); + const eql2 = docRef.isEqual( + doc(getFirestore(firebase.app('secondaryFromNative')), `${COLLECTION}/baz`), + ); + + eql1.should.be.False(); + eql2.should.be.False(); + }); + + it('returns true when equal', function () { + const { getFirestore, doc } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/baz`); - const eql1 = docRef.isEqual(docRef); - const eql2 = docRef.isEqual(firebase.firestore().doc(`${COLLECTION}/baz`)); + const eql1 = docRef.isEqual(docRef); + const eql2 = docRef.isEqual(doc(db, `${COLLECTION}/baz`)); - eql1.should.be.True(); - eql2.should.be.True(); + eql1.should.be.True(); + eql2.should.be.True(); + }); }); }); diff --git a/packages/firestore/e2e/DocumentReference/onSnapshot.e2e.js b/packages/firestore/e2e/DocumentReference/onSnapshot.e2e.js index 3d596aa404..238aca1acb 100644 --- a/packages/firestore/e2e/DocumentReference/onSnapshot.e2e.js +++ b/packages/firestore/e2e/DocumentReference/onSnapshot.e2e.js @@ -22,290 +22,583 @@ describe('firestore().doc().onSnapshot()', function () { before(function () { return wipe(); }); - it('throws if no arguments are provided', function () { - try { - firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('expected at least one argument'); - return Promise.resolve(); - } - }); - it('returns an unsubscribe function', function () { - const unsub = firebase - .firestore() - .doc(`${COLLECTION}/foo`) - .onSnapshot(() => {}); + describe('v8 compatibility', function () { + it('throws if no arguments are provided', function () { + try { + firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('expected at least one argument'); + return Promise.resolve(); + } + }); - unsub.should.be.a.Function(); - unsub(); - }); + it('returns an unsubscribe function', function () { + const unsub = firebase + .firestore() + .doc(`${COLLECTION}/foo`) + .onSnapshot(() => {}); - it('accepts a single callback function with snapshot', async function () { - const callback = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot(callback); + unsub.should.be.a.Function(); + unsub(); + }); - await Utils.spyToBeCalledOnceAsync(callback); + it('accepts a single callback function with snapshot', async function () { + const callback = sinon.spy(); + const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot(callback); - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); - should.equal(callback.args[0][1], null); - unsub(); - }); + await Utils.spyToBeCalledOnceAsync(callback); - it('accepts a single callback function with Error', async function () { - const callback = sinon.spy(); - const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot(callback); + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(callback.args[0][1], null); + unsub(); + }); - await Utils.spyToBeCalledOnceAsync(callback); + it('accepts a single callback function with Error', async function () { + const callback = sinon.spy(); + const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot(callback); - callback.should.be.calledOnce(); - callback.args[0][1].code.should.containEql('firestore/permission-denied'); - should.equal(callback.args[0][0], null); - unsub(); - }); + await Utils.spyToBeCalledOnceAsync(callback); - describe('multiple callbacks', function () { - it('calls onNext when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot(onNext, onError); + callback.should.be.calledOnce(); + callback.args[0][1].code.should.containEql('firestore/permission-denied'); + should.equal(callback.args[0][0], null); + unsub(); + }); - await Utils.spyToBeCalledOnceAsync(onNext); + describe('multiple callbacks', function () { + it('calls onNext when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot(onNext, onError); - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls onError with Error', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase + .firestore() + .doc(`${NO_RULE_COLLECTION}/nope`) + .onSnapshot(onNext, onError); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); }); - it('calls onError with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase - .firestore() - .doc(`${NO_RULE_COLLECTION}/nope`) - .onSnapshot(onNext, onError); + describe('objects of callbacks', function () { + it('calls next when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot({ + next: onNext, + error: onError, + }); - await Utils.spyToBeCalledOnceAsync(onError); + await Utils.spyToBeCalledOnceAsync(onNext); - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ + next: onNext, + error: onError, + }); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); }); - }); - describe('objects of callbacks', function () { - it('calls next when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot({ - next: onNext, - error: onError, + describe('SnapshotListenerOptions + callbacks', function () { + it('calls callback with snapshot when successful', async function () { + const callback = sinon.spy(); + const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot( + { + includeMetadataChanges: false, + }, + callback, + ); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(callback.args[0][1], null); + unsub(); }); - await Utils.spyToBeCalledOnceAsync(onNext); + it('calls callback with Error', async function () { + const callback = sinon.spy(); + const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot( + { + includeMetadataChanges: false, + }, + callback, + ); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][1].code.should.containEql('firestore/permission-denied'); + should.equal(callback.args[0][0], null); + unsub(); + }); - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); + it('calls next with snapshot when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot( + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot( + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); }); - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ - next: onNext, - error: onError, + describe('SnapshotListenerOptions + object of callbacks', function () { + it('calls next with snapshot when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot( + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); }); - await Utils.spyToBeCalledOnceAsync(onError); + it('calls error with Error', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot( + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); + it('throws if SnapshotListenerOptions is invalid', function () { + try { + firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ + includeMetadataChanges: 123, + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", + ); + return Promise.resolve(); + } + }); + + it('throws if next callback is invalid', function () { + try { + firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ + next: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.next' or 'onNext' expected a function"); + return Promise.resolve(); + } + }); + + it('throws if error callback is invalid', function () { + try { + firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ + error: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.error' or 'onError' expected a function"); + return Promise.resolve(); + } }); - }); - describe('SnapshotListenerOptions + callbacks', function () { - it('calls callback with snapshot when successful', async function () { + it('unsubscribes from further updates', async function () { const callback = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); + const doc = firebase.firestore().doc(`${COLLECTION}/unsub`); + const unsub = doc.onSnapshot(callback); await Utils.spyToBeCalledOnceAsync(callback); + await doc.set({ foo: 'bar' }); + unsub(); + await Utils.sleep(800); + await doc.set({ foo: 'bar2' }); + await Utils.spyToBeCalledTimesAsync(callback, 2); + callback.should.be.callCount(2); + }); + }); - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); - should.equal(callback.args[0][1], null); + describe('modular', function () { + it('throws if no arguments are provided', function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + try { + onSnapshot(doc(getFirestore(), `${COLLECTION}/foo`)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('expected at least one argument'); + return Promise.resolve(); + } + }); + + it('returns an unsubscribe function', function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + + const unsub = onSnapshot(doc(getFirestore(), `${COLLECTION}/foo`), () => {}); + + unsub.should.be.a.Function(); unsub(); }); - it('calls callback with Error', async function () { + it('accepts a single callback function with snapshot', async function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + const callback = sinon.spy(); - const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); + const unsub = onSnapshot(doc(getFirestore(), `${COLLECTION}/foo`), callback); await Utils.spyToBeCalledOnceAsync(callback); callback.should.be.calledOnce(); - callback.args[0][1].code.should.containEql('firestore/permission-denied'); - should.equal(callback.args[0][0], null); + callback.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(callback.args[0][1], null); unsub(); }); - it('calls next with snapshot when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot( - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); + describe('multiple callbacks', function () { + it('calls onNext when successful', async function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot( - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(doc(getFirestore(), `${COLLECTION}/foo`), onNext, onError); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls onError with Error', async function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + doc(getFirestore(), `${NO_RULE_COLLECTION}/nope`), + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); }); - }); - describe('SnapshotListenerOptions + object of callbacks', function () { - it('calls next with snapshot when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot( - { - includeMetadataChanges: false, - }, - { + describe('objects of callbacks', function () { + it('calls next when successful', async function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(doc(getFirestore(), `${COLLECTION}/foo`), { next: onNext, error: onError, - }, - ); + }); - await Utils.spyToBeCalledOnceAsync(onNext); + await Utils.spyToBeCalledOnceAsync(onNext); - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot( - { - includeMetadataChanges: false, - }, - { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(doc(getFirestore(), `${NO_RULE_COLLECTION}/nope`), { next: onNext, error: onError, - }, - ); + }); - await Utils.spyToBeCalledOnceAsync(onError); + await Utils.spyToBeCalledOnceAsync(onError); - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); }); - }); - it('throws if SnapshotListenerOptions is invalid', function () { - try { - firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ - includeMetadataChanges: 123, + describe('SnapshotListenerOptions + callbacks', function () { + it('calls callback with snapshot when successful', async function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + + const callback = sinon.spy(); + const unsub = onSnapshot( + doc(getFirestore(), `${COLLECTION}/foo`), + { + includeMetadataChanges: false, + }, + callback, + ); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(callback.args[0][1], null); + unsub(); }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", - ); - return Promise.resolve(); - } - }); - it('throws if next callback is invalid', function () { - try { - firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ - next: 'foo', + it('calls next with snapshot when successful', async function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + doc(getFirestore(), `${COLLECTION}/foo`), + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.next' or 'onNext' expected a function"); - return Promise.resolve(); - } - }); - it('throws if error callback is invalid', function () { - try { - firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ - error: 'foo', + it('calls error with Error', async function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + doc(getFirestore(), `${NO_RULE_COLLECTION}/nope`), + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.error' or 'onError' expected a function"); - return Promise.resolve(); - } - }); + }); + + describe('SnapshotListenerOptions + object of callbacks', function () { + it('calls next with snapshot when successful', async function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + doc(getFirestore(), `${COLLECTION}/foo`), + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + doc(getFirestore(), `${NO_RULE_COLLECTION}/nope`), + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); + + it('throws if SnapshotListenerOptions is invalid', function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + try { + onSnapshot(doc(getFirestore(), `${NO_RULE_COLLECTION}/nope`), { + includeMetadataChanges: 123, + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", + ); + return Promise.resolve(); + } + }); + + it('throws if next callback is invalid', function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + try { + onSnapshot(doc(getFirestore(), `${NO_RULE_COLLECTION}/nope`), { + next: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.next' or 'onNext' expected a function"); + return Promise.resolve(); + } + }); + + it('throws if error callback is invalid', function () { + const { getFirestore, doc, onSnapshot } = firestoreModular; + try { + onSnapshot(doc(getFirestore(), `${NO_RULE_COLLECTION}/nope`), { + error: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.error' or 'onError' expected a function"); + return Promise.resolve(); + } + }); - it('unsubscribes from further updates', async function () { - const callback = sinon.spy(); - const doc = firebase.firestore().doc(`${COLLECTION}/unsub`); - - const unsub = doc.onSnapshot(callback); - await Utils.spyToBeCalledOnceAsync(callback); - await doc.set({ foo: 'bar' }); - unsub(); - await Utils.sleep(800); - await doc.set({ foo: 'bar2' }); - await Utils.spyToBeCalledTimesAsync(callback, 2); - callback.should.be.callCount(2); + it('unsubscribes from further updates', async function () { + const { getFirestore, doc, onSnapshot, setDoc } = firestoreModular; + + const callback = sinon.spy(); + const docRef = doc(getFirestore(), `${COLLECTION}/unsub`); + + const unsub = onSnapshot(docRef, callback); + await Utils.spyToBeCalledOnceAsync(callback); + await setDoc(docRef, { foo: 'bar' }); + unsub(); + await Utils.sleep(800); + await setDoc(docRef, { foo: 'bar2' }); + await Utils.spyToBeCalledTimesAsync(callback, 2); + callback.should.be.callCount(2); + }); }); }); diff --git a/packages/firestore/e2e/DocumentReference/properties.e2e.js b/packages/firestore/e2e/DocumentReference/properties.e2e.js index 50432a40df..cdf035cdd0 100644 --- a/packages/firestore/e2e/DocumentReference/properties.e2e.js +++ b/packages/firestore/e2e/DocumentReference/properties.e2e.js @@ -18,25 +18,56 @@ const COLLECTION = 'firestore'; describe('firestore.doc()', function () { - it('returns a Firestore instance', function () { - const instance = firebase.firestore().doc(`${COLLECTION}/bar`); - should.equal(instance.firestore.constructor.name, 'FirebaseFirestoreModule'); - }); + describe('v8 compatibility', function () { + it('returns a Firestore instance', function () { + const instance = firebase.firestore().doc(`${COLLECTION}/bar`); + should.equal(instance.firestore.constructor.name, 'FirebaseFirestoreModule'); + }); - it('returns the document id', function () { - const instance = firebase.firestore().doc(`${COLLECTION}/bar`); - instance.id.should.equal('bar'); - }); + it('returns the document id', function () { + const instance = firebase.firestore().doc(`${COLLECTION}/bar`); + instance.id.should.equal('bar'); + }); + + it('returns the parent collection reference', function () { + const instance = firebase.firestore().doc(`${COLLECTION}/bar`); + instance.parent.id.should.equal(COLLECTION); + }); - it('returns the parent collection reference', function () { - const instance = firebase.firestore().doc(`${COLLECTION}/bar`); - instance.parent.id.should.equal(COLLECTION); + it('returns the path', function () { + const instance1 = firebase.firestore().doc(`${COLLECTION}/bar`); + const instance2 = firebase.firestore().collection(COLLECTION).doc('bar'); + instance1.path.should.equal(`${COLLECTION}/bar`); + instance2.path.should.equal(`${COLLECTION}/bar`); + }); }); - it('returns the path', function () { - const instance1 = firebase.firestore().doc(`${COLLECTION}/bar`); - const instance2 = firebase.firestore().collection(COLLECTION).doc('bar'); - instance1.path.should.equal(`${COLLECTION}/bar`); - instance2.path.should.equal(`${COLLECTION}/bar`); + describe('modular', function () { + it('returns a Firestore instance', function () { + const { getFirestore, doc } = firestoreModular; + const instance = doc(getFirestore(), `${COLLECTION}/bar`); + should.equal(instance.firestore.constructor.name, 'FirebaseFirestoreModule'); + }); + + it('returns the document id', function () { + const { getFirestore, doc } = firestoreModular; + const instance = doc(getFirestore(), `${COLLECTION}/bar`); + instance.id.should.equal('bar'); + }); + + it('returns the parent collection reference', function () { + const { getFirestore, doc } = firestoreModular; + const instance = doc(getFirestore(), `${COLLECTION}/bar`); + instance.parent.id.should.equal(COLLECTION); + }); + + it('returns the path', function () { + const { getFirestore, doc, collection } = firestoreModular; + const db = getFirestore(); + const instance1 = doc(db, `${COLLECTION}/bar`); + const instance2 = doc(collection(db, COLLECTION), 'bar'); + instance1.path.should.equal(`${COLLECTION}/bar`); + instance2.path.should.equal(`${COLLECTION}/bar`); + }); }); }); diff --git a/packages/firestore/e2e/DocumentReference/set.e2e.js b/packages/firestore/e2e/DocumentReference/set.e2e.js index c3b8a003af..d2098f5664 100644 --- a/packages/firestore/e2e/DocumentReference/set.e2e.js +++ b/packages/firestore/e2e/DocumentReference/set.e2e.js @@ -21,78 +21,307 @@ describe('firestore.doc().set()', function () { before(function () { return wipe(); }); - it('throws if data is not an object', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).set('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'data' must be an object"); - return Promise.resolve(); - } - }); - it('throws if options is not an object', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).set({}, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must be an object"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if data is not an object', function () { + try { + firebase.firestore().doc(`${COLLECTION}/baz`).set('foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'data' must be an object"); + return Promise.resolve(); + } + }); - it('throws if options contains both merge types', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).set( - {}, - { - merge: true, - mergeFields: [], - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must not contain both 'merge' & 'mergeFields'"); - return Promise.resolve(); - } - }); + it('throws if options is not an object', function () { + try { + firebase.firestore().doc(`${COLLECTION}/baz`).set({}, 'foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' must be an object"); + return Promise.resolve(); + } + }); + + it('throws if options contains both merge types', function () { + try { + firebase.firestore().doc(`${COLLECTION}/baz`).set( + {}, + { + merge: true, + mergeFields: [], + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' must not contain both 'merge' & 'mergeFields'"); + return Promise.resolve(); + } + }); + + it('throws if merge is not a boolean', function () { + try { + firebase.firestore().doc(`${COLLECTION}/baz`).set( + {}, + { + merge: 'foo', + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.merge' must be a boolean value"); + return Promise.resolve(); + } + }); + + it('throws if mergeFields is not an array', function () { + try { + firebase.firestore().doc(`${COLLECTION}/baz`).set( + {}, + { + mergeFields: 'foo', + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.mergeFields' must be an array"); + return Promise.resolve(); + } + }); + + it('throws if mergeFields contains invalid data', function () { + try { + firebase + .firestore() + .doc(`${COLLECTION}/baz`) + .set( + {}, + { + mergeFields: [ + 'foo', + 'foo.bar', + new firebase.firestore.FieldPath(COLLECTION, 'baz'), + 123, + ], + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options.mergeFields' all fields must be of type string or FieldPath, but the value at index 3 was number", + ); + return Promise.resolve(); + } + }); + + it('sets new data', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/set`); + const data1 = { foo: 'bar' }; + const data2 = { foo: 'baz', bar: 123 }; + await ref.set(data1); + const snapshot1 = await ref.get(); + snapshot1.data().should.eql(jet.contextify(data1)); + await ref.set(data2); + const snapshot2 = await ref.get(); + snapshot2.data().should.eql(jet.contextify(data2)); + await ref.delete(); + }); - it('throws if merge is not a boolean', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).set( - {}, - { - merge: 'foo', + it('merges all fields', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/merge`); + const data1 = { foo: 'bar' }; + const data2 = { bar: 'baz' }; + const merged = { ...data1, ...data2 }; + await ref.set(data1); + const snapshot1 = await ref.get(); + snapshot1.data().should.eql(jet.contextify(data1)); + await ref.set(data2, { + merge: true, + }); + const snapshot2 = await ref.get(); + snapshot2.data().should.eql(jet.contextify(merged)); + await ref.delete(); + }); + + it('merges specific fields', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/merge`); + const data1 = { foo: '123', bar: 123, baz: '456' }; + const data2 = { foo: '234', bar: 234, baz: '678' }; + const merged = { foo: data1.foo, bar: data2.bar, baz: data2.baz }; + await ref.set(data1); + const snapshot1 = await ref.get(); + snapshot1.data().should.eql(jet.contextify(data1)); + await ref.set(data2, { + mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], + }); + const snapshot2 = await ref.get(); + snapshot2.data().should.eql(jet.contextify(merged)); + await ref.delete(); + }); + + it('throws when nested undefined array value provided and ignored undefined is false', async function () { + await firebase.firestore().settings({ ignoreUndefinedProperties: false }); + const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); + try { + await docRef.set({ + myArray: [{ name: 'Tim', location: { state: undefined, country: 'United Kingdom' } }], + }); + return Promise.reject(new Error('Expected set() to throw')); + } catch (error) { + error.message.should.containEql('Unsupported field value: undefined'); + } + }); + + it('accepts undefined nested array values if ignoreUndefined is true', async function () { + await firebase.firestore().settings({ ignoreUndefinedProperties: true }); + const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); + await docRef.set({ + myArray: [{ name: 'Tim', location: { state: undefined, country: 'United Kingdom' } }], + }); + }); + + it('does not throw when nested undefined object value provided and ignore undefined is true', async function () { + await firebase.firestore().settings({ ignoreUndefinedProperties: true }); + const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); + await docRef.set({ + field1: 1, + field2: { + shouldNotWork: undefined, }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.merge' must be a boolean value"); - return Promise.resolve(); - } - }); + }); + }); + + it('filters out undefined properties when setting enabled', async function () { + await firebase.firestore().settings({ ignoreUndefinedProperties: true }); + + const docRef = firebase.firestore().doc(`${COLLECTION}/ignoreUndefinedTrueProp`); + await docRef.set({ + field1: 1, + field2: undefined, + }); - it('throws if mergeFields is not an array', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).set( - {}, - { - mergeFields: 'foo', + const snap = await docRef.get(); + const snapData = snap.data(); + if (!snapData) { + return Promise.reject(new Error('Snapshot not saved')); + } + + snapData.field1.should.eql(1); + snapData.hasOwnProperty('field2').should.eql(false); + }); + + it('filters out nested undefined properties when setting enabled', async function () { + await firebase.firestore().settings({ ignoreUndefinedProperties: true }); + + const docRef = firebase.firestore().doc(`${COLLECTION}/ignoreUndefinedTrueNestedProp`); + await docRef.set({ + field1: 1, + field2: { + shouldBeMissing: undefined, }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.mergeFields' must be an array"); - return Promise.resolve(); - } + field3: [ + { + shouldBeHere: 'Here', + shouldBeMissing: undefined, + }, + ], + }); + + const snap = await docRef.get(); + const snapData = snap.data(); + if (!snapData) { + return Promise.reject(new Error('Snapshot not saved')); + } + + snapData.field1.should.eql(1); + snapData.hasOwnProperty('field2').should.eql(true); + snapData.field2.hasOwnProperty('shouldBeMissing').should.eql(false); + snapData.hasOwnProperty('field3').should.eql(true); + snapData.field3[0].shouldBeHere.should.eql('Here'); + snapData.field3[0].hasOwnProperty('shouldBeMissing').should.eql(false); + }); }); - it('throws if mergeFields contains invalid data', function () { - try { - firebase - .firestore() - .doc(`${COLLECTION}/baz`) - .set( + describe('modular', function () { + it('throws if data is not an object', function () { + const { getFirestore, doc, setDoc } = firestoreModular; + try { + setDoc(doc(getFirestore(), `${COLLECTION}/baz`), 'foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'data' must be an object"); + return Promise.resolve(); + } + }); + + it('throws if options is not an object', function () { + const { getFirestore, doc, setDoc } = firestoreModular; + try { + setDoc(doc(getFirestore(), `${COLLECTION}/baz`), {}, 'foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' must be an object"); + return Promise.resolve(); + } + }); + + it('throws if options contains both merge types', function () { + const { getFirestore, doc, setDoc } = firestoreModular; + try { + setDoc( + doc(getFirestore(), `${COLLECTION}/baz`), + {}, + { + merge: true, + mergeFields: [], + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' must not contain both 'merge' & 'mergeFields'"); + return Promise.resolve(); + } + }); + + it('throws if merge is not a boolean', function () { + const { getFirestore, doc, setDoc } = firestoreModular; + try { + setDoc( + doc(getFirestore(), `${COLLECTION}/baz`), + {}, + { + merge: 'foo', + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.merge' must be a boolean value"); + return Promise.resolve(); + } + }); + + it('throws if mergeFields is not an array', function () { + const { getFirestore, doc, setDoc } = firestoreModular; + try { + setDoc( + doc(getFirestore(), `${COLLECTION}/baz`), + {}, + { + mergeFields: 'foo', + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.mergeFields' must be an array"); + return Promise.resolve(); + } + }); + + it('throws if mergeFields contains invalid data', function () { + const { getFirestore, doc, setDoc } = firestoreModular; + try { + setDoc( + doc(getFirestore(), `${COLLECTION}/baz`), {}, { mergeFields: [ @@ -103,139 +332,153 @@ describe('firestore.doc().set()', function () { ], }, ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options.mergeFields' all fields must be of type string or FieldPath, but the value at index 3 was number", - ); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options.mergeFields' all fields must be of type string or FieldPath, but the value at index 3 was number", + ); + return Promise.resolve(); + } + }); - it('sets new data', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/set`); - const data1 = { foo: 'bar' }; - const data2 = { foo: 'baz', bar: 123 }; - await ref.set(data1); - const snapshot1 = await ref.get(); - snapshot1.data().should.eql(jet.contextify(data1)); - await ref.set(data2); - const snapshot2 = await ref.get(); - snapshot2.data().should.eql(jet.contextify(data2)); - await ref.delete(); - }); + it('sets new data', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/set`); + const data1 = { foo: 'bar' }; + const data2 = { foo: 'baz', bar: 123 }; + await setDoc(ref, data1); + const snapshot1 = await getDocs(ref); + snapshot1.data().should.eql(jet.contextify(data1)); + await setDoc(ref, data2); + const snapshot2 = await getDocs(ref); + snapshot2.data().should.eql(jet.contextify(data2)); + await deleteDoc(ref); + }); - it('merges all fields', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/merge`); - const data1 = { foo: 'bar' }; - const data2 = { bar: 'baz' }; - const merged = { ...data1, ...data2 }; - await ref.set(data1); - const snapshot1 = await ref.get(); - snapshot1.data().should.eql(jet.contextify(data1)); - await ref.set(data2, { - merge: true, - }); - const snapshot2 = await ref.get(); - snapshot2.data().should.eql(jet.contextify(merged)); - await ref.delete(); - }); + it('merges all fields', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/merge`); + const data1 = { foo: 'bar' }; + const data2 = { bar: 'baz' }; + const merged = { ...data1, ...data2 }; + await setDoc(ref, data1); + const snapshot1 = await getDocs(ref); + snapshot1.data().should.eql(jet.contextify(data1)); + await setDoc(ref, data2, { + merge: true, + }); + const snapshot2 = await getDocs(ref); + snapshot2.data().should.eql(jet.contextify(merged)); + await deleteDoc(ref); + }); - it('merges specific fields', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/merge`); - const data1 = { foo: '123', bar: 123, baz: '456' }; - const data2 = { foo: '234', bar: 234, baz: '678' }; - const merged = { foo: data1.foo, bar: data2.bar, baz: data2.baz }; - await ref.set(data1); - const snapshot1 = await ref.get(); - snapshot1.data().should.eql(jet.contextify(data1)); - await ref.set(data2, { - mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], - }); - const snapshot2 = await ref.get(); - snapshot2.data().should.eql(jet.contextify(merged)); - await ref.delete(); - }); + it('merges specific fields', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/merge`); + const data1 = { foo: '123', bar: 123, baz: '456' }; + const data2 = { foo: '234', bar: 234, baz: '678' }; + const merged = { foo: data1.foo, bar: data2.bar, baz: data2.baz }; + await setDoc(ref, data1); + const snapshot1 = await getDocs(ref); + snapshot1.data().should.eql(jet.contextify(data1)); + await setDoc(ref, data2, { + mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], + }); + const snapshot2 = await getDocs(ref); + snapshot2.data().should.eql(jet.contextify(merged)); + await deleteDoc(ref); + }); - it('throws when nested undefined array value provided and ignored undefined is false', async function () { - await firebase.firestore().settings({ ignoreUndefinedProperties: false }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - try { - await docRef.set({ + it('throws when nested undefined array value provided and ignored undefined is false', async function () { + const { getFirestore, initializeFirestore, doc, setDoc } = firestoreModular; + const db = getFirestore(); + initializeFirestore(db.app, { ignoreUndefinedProperties: false }); + const docRef = doc(db, `${COLLECTION}/bar`); + try { + await setDoc(docRef, { + myArray: [{ name: 'Tim', location: { state: undefined, country: 'United Kingdom' } }], + }); + return Promise.reject(new Error('Expected set() to throw')); + } catch (error) { + error.message.should.containEql('Unsupported field value: undefined'); + } + }); + + it('accepts undefined nested array values if ignoreUndefined is true', async function () { + const { getFirestore, initializeFirestore, doc, setDoc } = firestoreModular; + const db = getFirestore(); + initializeFirestore(db.app, { ignoreUndefinedProperties: true }); + const docRef = doc(db, `${COLLECTION}/bar`); + await setDoc(docRef, { myArray: [{ name: 'Tim', location: { state: undefined, country: 'United Kingdom' } }], }); - return Promise.reject(new Error('Expected set() to throw')); - } catch (error) { - error.message.should.containEql('Unsupported field value: undefined'); - } - }); - - it('accepts undefined nested array values if ignoreUndefined is true', async function () { - await firebase.firestore().settings({ ignoreUndefinedProperties: true }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - await docRef.set({ - myArray: [{ name: 'Tim', location: { state: undefined, country: 'United Kingdom' } }], }); - }); - it('does not throw when nested undefined object value provided and ignore undefined is true', async function () { - await firebase.firestore().settings({ ignoreUndefinedProperties: true }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - await docRef.set({ - field1: 1, - field2: { - shouldNotWork: undefined, - }, + it('does not throw when nested undefined object value provided and ignore undefined is true', async function () { + const { getFirestore, initializeFirestore, doc, setDoc } = firestoreModular; + const db = getFirestore(); + initializeFirestore(db.app, { ignoreUndefinedProperties: true }); + const docRef = doc(db, `${COLLECTION}/bar`); + await setDoc(docRef, { + field1: 1, + field2: { + shouldNotWork: undefined, + }, + }); }); - }); - it('filters out undefined properties when setting enabled', async function () { - await firebase.firestore().settings({ ignoreUndefinedProperties: true }); + it('filters out undefined properties when setting enabled', async function () { + const { getFirestore, initializeFirestore, doc, setDoc, getDocs } = firestoreModular; + const db = getFirestore(); + initializeFirestore(db.app, { ignoreUndefinedProperties: true }); - const docRef = firebase.firestore().doc(`${COLLECTION}/ignoreUndefinedTrueProp`); - await docRef.set({ - field1: 1, - field2: undefined, - }); + const docRef = doc(db, `${COLLECTION}/ignoreUndefinedTrueProp`); + await setDoc(docRef, { + field1: 1, + field2: undefined, + }); - const snap = await docRef.get(); - const snapData = snap.data(); - if (!snapData) { - return Promise.reject(new Error('Snapshot not saved')); - } + const snap = await getDocs(docRef); + const snapData = snap.data(); + if (!snapData) { + return Promise.reject(new Error('Snapshot not saved')); + } - snapData.field1.should.eql(1); - snapData.hasOwnProperty('field2').should.eql(false); - }); + snapData.field1.should.eql(1); + snapData.hasOwnProperty('field2').should.eql(false); + }); + + it('filters out nested undefined properties when setting enabled', async function () { + const { getFirestore, initializeFirestore, doc, setDoc, getDocs } = firestoreModular; + const db = getFirestore(); + initializeFirestore(db.app, { ignoreUndefinedProperties: true }); - it('filters out nested undefined properties when setting enabled', async function () { - await firebase.firestore().settings({ ignoreUndefinedProperties: true }); - - const docRef = firebase.firestore().doc(`${COLLECTION}/ignoreUndefinedTrueNestedProp`); - await docRef.set({ - field1: 1, - field2: { - shouldBeMissing: undefined, - }, - field3: [ - { - shouldBeHere: 'Here', + const docRef = doc(db, `${COLLECTION}/ignoreUndefinedTrueNestedProp`); + await setDoc(docRef, { + field1: 1, + field2: { shouldBeMissing: undefined, }, - ], - }); + field3: [ + { + shouldBeHere: 'Here', + shouldBeMissing: undefined, + }, + ], + }); - const snap = await docRef.get(); - const snapData = snap.data(); - if (!snapData) { - return Promise.reject(new Error('Snapshot not saved')); - } + const snap = await getDocs(docRef); + const snapData = snap.data(); + if (!snapData) { + return Promise.reject(new Error('Snapshot not saved')); + } - snapData.field1.should.eql(1); - snapData.hasOwnProperty('field2').should.eql(true); - snapData.field2.hasOwnProperty('shouldBeMissing').should.eql(false); - snapData.hasOwnProperty('field3').should.eql(true); - snapData.field3[0].shouldBeHere.should.eql('Here'); - snapData.field3[0].hasOwnProperty('shouldBeMissing').should.eql(false); + snapData.field1.should.eql(1); + snapData.hasOwnProperty('field2').should.eql(true); + snapData.field2.hasOwnProperty('shouldBeMissing').should.eql(false); + snapData.hasOwnProperty('field3').should.eql(true); + snapData.field3[0].shouldBeHere.should.eql('Here'); + snapData.field3[0].hasOwnProperty('shouldBeMissing').should.eql(false); + }); }); }); diff --git a/packages/firestore/e2e/DocumentReference/update.e2e.js b/packages/firestore/e2e/DocumentReference/update.e2e.js index 2e517f7c59..6e59ad4260 100644 --- a/packages/firestore/e2e/DocumentReference/update.e2e.js +++ b/packages/firestore/e2e/DocumentReference/update.e2e.js @@ -21,68 +21,144 @@ describe('firestore.doc().update()', function () { before(function () { return wipe(); }); - it('throws if no arguments are provided', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).update(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'expected at least 1 argument but was called with 0 arguments', - ); - return Promise.resolve(); - } - }); - it('throws if document does not exist', async function () { - try { - await firebase.firestore().doc(`${COLLECTION}/idonotexistonthedatabase`).update({}); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.code.should.containEql('firestore/not-found'); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if no arguments are provided', function () { + try { + firebase.firestore().doc(`${COLLECTION}/baz`).update(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'expected at least 1 argument but was called with 0 arguments', + ); + return Promise.resolve(); + } + }); - it('throws if field/value sequence is invalid', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).update('foo', 'bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('or equal numbers of key/value pairs'); - return Promise.resolve(); - } - }); + it('throws if document does not exist', async function () { + try { + await firebase.firestore().doc(`${COLLECTION}/idonotexistonthedatabase`).update({}); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.code.should.containEql('firestore/not-found'); + return Promise.resolve(); + } + }); + + it('throws if field/value sequence is invalid', function () { + try { + firebase.firestore().doc(`${COLLECTION}/baz`).update('foo', 'bar', 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('or equal numbers of key/value pairs'); + return Promise.resolve(); + } + }); + + it('updates data with an object value', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/update-obj`); + const value = Date.now(); + const data1 = { foo: value }; + const data2 = { foo: 'bar' }; + await ref.set(data1); + const snapshot1 = await ref.get(); + snapshot1.data().should.eql(jet.contextify(data1)); + await ref.update(data2); + const snapshot2 = await ref.get(); + snapshot2.data().should.eql(jet.contextify(data2)); + await ref.delete(); + }); + + it('updates data with an key/value pairs', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/update-obj`); + const value = Date.now(); + const data1 = { foo: value, bar: value }; + await ref.set(data1); + const snapshot1 = await ref.get(); + snapshot1.data().should.eql(jet.contextify(data1)); - it('updates data with an object value', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/update-obj`); - const value = Date.now(); - const data1 = { foo: value }; - const data2 = { foo: 'bar' }; - await ref.set(data1); - const snapshot1 = await ref.get(); - snapshot1.data().should.eql(jet.contextify(data1)); - await ref.update(data2); - const snapshot2 = await ref.get(); - snapshot2.data().should.eql(jet.contextify(data2)); - await ref.delete(); + await ref.update('foo', 'bar', 'bar', 'baz', 'foo1', 'bar1'); + const expected = { + foo: 'bar', + bar: 'baz', + foo1: 'bar1', + }; + const snapshot2 = await ref.get(); + snapshot2.data().should.eql(jet.contextify(expected)); + await ref.delete(); + }); }); - it('updates data with an key/value pairs', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/update-obj`); - const value = Date.now(); - const data1 = { foo: value, bar: value }; - await ref.set(data1); - const snapshot1 = await ref.get(); - snapshot1.data().should.eql(jet.contextify(data1)); + describe('modular', function () { + it('throws if no arguments are provided', function () { + const { getFirestore, doc, updateDoc } = firestoreModular; + + try { + updateDoc(doc(getFirestore(), `${COLLECTION}/baz`)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'expected at least 1 argument but was called with 0 arguments', + ); + return Promise.resolve(); + } + }); + + it('throws if document does not exist', async function () { + const { getFirestore, doc, updateDoc } = firestoreModular; + try { + await updateDoc(doc(getFirestore(), `${COLLECTION}/idonotexistonthedatabase`), {}); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.code.should.containEql('firestore/not-found'); + return Promise.resolve(); + } + }); + + it('throws if field/value sequence is invalid', function () { + const { getFirestore, doc, updateDoc } = firestoreModular; + try { + updateDoc(doc(getFirestore(), `${COLLECTION}/baz`), 'foo', 'bar', 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('or equal numbers of key/value pairs'); + return Promise.resolve(); + } + }); + + it('updates data with an object value', async function () { + const { getFirestore, doc, setDoc, getDocs, updateDoc, deleteDoc } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/update-obj`); + const value = Date.now(); + const data1 = { foo: value }; + const data2 = { foo: 'bar' }; + await setDoc(ref, data1); + const snapshot1 = await getDocs(ref); + snapshot1.data().should.eql(jet.contextify(data1)); + await updateDoc(ref, data2); + const snapshot2 = await getDocs(ref); + snapshot2.data().should.eql(jet.contextify(data2)); + await deleteDoc(ref); + }); + + it('updates data with an key/value pairs', async function () { + const { getFirestore, doc, setDoc, getDocs, updateDoc, deleteDoc } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/update-obj`); + const value = Date.now(); + const data1 = { foo: value, bar: value }; + await setDoc(ref, data1); + const snapshot1 = await getDocs(ref); + snapshot1.data().should.eql(jet.contextify(data1)); - await ref.update('foo', 'bar', 'bar', 'baz', 'foo1', 'bar1'); - const expected = { - foo: 'bar', - bar: 'baz', - foo1: 'bar1', - }; - const snapshot2 = await ref.get(); - snapshot2.data().should.eql(jet.contextify(expected)); - await ref.delete(); + await updateDoc(ref, 'foo', 'bar', 'bar', 'baz', 'foo1', 'bar1'); + const expected = { + foo: 'bar', + bar: 'baz', + foo1: 'bar1', + }; + const snapshot2 = await getDocs(ref); + snapshot2.data().should.eql(jet.contextify(expected)); + await deleteDoc(ref); + }); }); }); diff --git a/packages/firestore/e2e/DocumentSnapshot/data.e2e.js b/packages/firestore/e2e/DocumentSnapshot/data.e2e.js index dbb79e4445..8edc2a6e2c 100644 --- a/packages/firestore/e2e/DocumentSnapshot/data.e2e.js +++ b/packages/firestore/e2e/DocumentSnapshot/data.e2e.js @@ -27,116 +27,238 @@ describe('firestore().doc() -> snapshot.data()', function () { before(function () { return wipe(); }); - it('returns undefined if documet does not exist', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - const snapshot = await ref.get(); - should.equal(snapshot.data(), undefined); - }); - it('returns an object if exists', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/getData`); - const data = { foo: 'bar' }; - await ref.set(data); - const snapshot = await ref.get(); - snapshot.data().should.eql(jet.contextify(data)); - await ref.delete(); - }); + describe('v8 compatibility', function () { + it('returns undefined if documet does not exist', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/idonotexist`); + const snapshot = await ref.get(); + should.equal(snapshot.data(), undefined); + }); + + it('returns an object if exists', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/getData`); + const data = { foo: 'bar' }; + await ref.set(data); + const snapshot = await ref.get(); + snapshot.data().should.eql(jet.contextify(data)); + await ref.delete(); + }); + + it('returns an object when document is empty', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/getData`); + const data = {}; + await ref.set(data); + const snapshot = await ref.get(); + snapshot.data().should.eql(jet.contextify(data)); + await ref.delete(); + }); + + // it('handles SnapshotOptions', function () { + // // TODO + // }); + + it('handles all data types', async function () { + const types = { + string: '123456', + stringEmpty: '', + number: 123456, + infinity: Infinity, + minusInfinity: -Infinity, + nan: 1 + undefined, + boolTrue: true, + boolFalse: false, + map: {}, // set after + array: [], // set after, + nullValue: null, + timestamp: new firebase.firestore.Timestamp(123, 123456), + date: new Date(), + geopoint: new firebase.firestore.GeoPoint(1, 2), + reference: firebase.firestore().doc(`${COLLECTION}/foobar`), + blob: firebase.firestore.Blob.fromBase64String(blobBase64), + }; + + const map = { foo: 'bar' }; + const array = [123, '456', null]; + types.map = map; + types.array = array; + + const ref = firebase.firestore().doc(`${COLLECTION}/types`); + await ref.set(types); + const snapshot = await ref.get(); + const data = snapshot.data(); + + // String + data.string.should.be.a.String(); + data.string.should.equal(types.string); + data.stringEmpty.should.be.a.String(); + data.stringEmpty.should.equal(types.stringEmpty); + + // Number + data.number.should.be.a.Number(); + data.number.should.equal(types.number); + data.infinity.should.be.Infinity(); + should.equal(data.infinity, Number.POSITIVE_INFINITY); + data.minusInfinity.should.be.Infinity(); + should.equal(data.minusInfinity, Number.NEGATIVE_INFINITY); + data.nan.should.be.eql(NaN); + + // Boolean + data.boolTrue.should.be.a.Boolean(); + data.boolTrue.should.be.true(); + data.boolFalse.should.be.a.Boolean(); + data.boolFalse.should.be.false(); + + // Map + data.map.should.be.an.Object(); + data.map.should.eql(jet.contextify(map)); + + // Array + data.array.should.be.an.Array(); + data.array.should.eql(jet.contextify(array)); + + // Null + should.equal(data.nullValue, null); + + // Timestamp + data.timestamp.should.be.an.instanceOf(firebase.firestore.Timestamp); + data.timestamp.seconds.should.be.a.Number(); + data.timestamp.nanoseconds.should.be.a.Number(); + data.date.should.be.an.instanceOf(firebase.firestore.Timestamp); + data.date.seconds.should.be.a.Number(); + data.date.nanoseconds.should.be.a.Number(); + + // GeoPoint + data.geopoint.should.be.an.instanceOf(firebase.firestore.GeoPoint); + data.geopoint.latitude.should.be.a.Number(); + data.geopoint.longitude.should.be.a.Number(); + + // Reference + // data.reference.should.be.an.instanceOf(); + data.reference.path.should.equal(`${COLLECTION}/foobar`); + + // Blob + data.blob.toBase64.should.be.a.Function(); - it('returns an object when document is empty', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/getData`); - const data = {}; - await ref.set(data); - const snapshot = await ref.get(); - snapshot.data().should.eql(jet.contextify(data)); - await ref.delete(); + await ref.delete(); + }); }); - // it('handles SnapshotOptions', function () { - // // TODO - // }); - - it('handles all data types', async function () { - const types = { - string: '123456', - stringEmpty: '', - number: 123456, - infinity: Infinity, - minusInfinity: -Infinity, - nan: 1 + undefined, - boolTrue: true, - boolFalse: false, - map: {}, // set after - array: [], // set after, - nullValue: null, - timestamp: new firebase.firestore.Timestamp(123, 123456), - date: new Date(), - geopoint: new firebase.firestore.GeoPoint(1, 2), - reference: firebase.firestore().doc(`${COLLECTION}/foobar`), - blob: firebase.firestore.Blob.fromBase64String(blobBase64), - }; - - const map = { foo: 'bar' }; - const array = [123, '456', null]; - types.map = map; - types.array = array; - - const ref = firebase.firestore().doc(`${COLLECTION}/types`); - await ref.set(types); - const snapshot = await ref.get(); - const data = snapshot.data(); - - // String - data.string.should.be.a.String(); - data.string.should.equal(types.string); - data.stringEmpty.should.be.a.String(); - data.stringEmpty.should.equal(types.stringEmpty); - - // Number - data.number.should.be.a.Number(); - data.number.should.equal(types.number); - data.infinity.should.be.Infinity(); - should.equal(data.infinity, Number.POSITIVE_INFINITY); - data.minusInfinity.should.be.Infinity(); - should.equal(data.minusInfinity, Number.NEGATIVE_INFINITY); - data.nan.should.be.eql(NaN); - - // Boolean - data.boolTrue.should.be.a.Boolean(); - data.boolTrue.should.be.true(); - data.boolFalse.should.be.a.Boolean(); - data.boolFalse.should.be.false(); - - // Map - data.map.should.be.an.Object(); - data.map.should.eql(jet.contextify(map)); - - // Array - data.array.should.be.an.Array(); - data.array.should.eql(jet.contextify(array)); - - // Null - should.equal(data.nullValue, null); - - // Timestamp - data.timestamp.should.be.an.instanceOf(firebase.firestore.Timestamp); - data.timestamp.seconds.should.be.a.Number(); - data.timestamp.nanoseconds.should.be.a.Number(); - data.date.should.be.an.instanceOf(firebase.firestore.Timestamp); - data.date.seconds.should.be.a.Number(); - data.date.nanoseconds.should.be.a.Number(); - - // GeoPoint - data.geopoint.should.be.an.instanceOf(firebase.firestore.GeoPoint); - data.geopoint.latitude.should.be.a.Number(); - data.geopoint.longitude.should.be.a.Number(); - - // Reference - // data.reference.should.be.an.instanceOf(); - data.reference.path.should.equal(`${COLLECTION}/foobar`); - - // Blob - data.blob.toBase64.should.be.a.Function(); - - await ref.delete(); + describe('modular', function () { + it('returns undefined if documet does not exist', async function () { + const { getFirestore, doc, getDocs } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/idonotexist`); + const snapshot = await getDocs(ref); + should.equal(snapshot.data(), undefined); + }); + + it('returns an object if exists', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/getData`); + const data = { foo: 'bar' }; + await setDoc(ref, data); + const snapshot = await getDocs(ref); + snapshot.data().should.eql(jet.contextify(data)); + await deleteDoc(ref); + }); + + it('returns an object when document is empty', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/getData`); + const data = {}; + await setDoc(ref, data); + const snapshot = await getDocs(ref); + snapshot.data().should.eql(jet.contextify(data)); + await deleteDoc(ref); + }); + + // it('handles SnapshotOptions', function () { + // // TODO + // }); + + it('handles all data types', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const types = { + string: '123456', + stringEmpty: '', + number: 123456, + infinity: Infinity, + minusInfinity: -Infinity, + nan: 1 + undefined, + boolTrue: true, + boolFalse: false, + map: {}, // set after + array: [], // set after, + nullValue: null, + timestamp: new firebase.firestore.Timestamp(123, 123456), + date: new Date(), + geopoint: new firebase.firestore.GeoPoint(1, 2), + reference: firebase.firestore().doc(`${COLLECTION}/foobar`), + blob: firebase.firestore.Blob.fromBase64String(blobBase64), + }; + + const map = { foo: 'bar' }; + const array = [123, '456', null]; + types.map = map; + types.array = array; + + const ref = doc(getFirestore(), `${COLLECTION}/types`); + await setDoc(ref, types); + const snapshot = await getDocs(ref); + const data = snapshot.data(); + + // String + data.string.should.be.a.String(); + data.string.should.equal(types.string); + data.stringEmpty.should.be.a.String(); + data.stringEmpty.should.equal(types.stringEmpty); + + // Number + data.number.should.be.a.Number(); + data.number.should.equal(types.number); + data.infinity.should.be.Infinity(); + should.equal(data.infinity, Number.POSITIVE_INFINITY); + data.minusInfinity.should.be.Infinity(); + should.equal(data.minusInfinity, Number.NEGATIVE_INFINITY); + data.nan.should.be.eql(NaN); + + // Boolean + data.boolTrue.should.be.a.Boolean(); + data.boolTrue.should.be.true(); + data.boolFalse.should.be.a.Boolean(); + data.boolFalse.should.be.false(); + + // Map + data.map.should.be.an.Object(); + data.map.should.eql(jet.contextify(map)); + + // Array + data.array.should.be.an.Array(); + data.array.should.eql(jet.contextify(array)); + + // Null + should.equal(data.nullValue, null); + + // Timestamp + data.timestamp.should.be.an.instanceOf(firebase.firestore.Timestamp); + data.timestamp.seconds.should.be.a.Number(); + data.timestamp.nanoseconds.should.be.a.Number(); + data.date.should.be.an.instanceOf(firebase.firestore.Timestamp); + data.date.seconds.should.be.a.Number(); + data.date.nanoseconds.should.be.a.Number(); + + // GeoPoint + data.geopoint.should.be.an.instanceOf(firebase.firestore.GeoPoint); + data.geopoint.latitude.should.be.a.Number(); + data.geopoint.longitude.should.be.a.Number(); + + // Reference + // data.reference.should.be.an.instanceOf(); + data.reference.path.should.equal(`${COLLECTION}/foobar`); + + // Blob + data.blob.toBase64.should.be.a.Function(); + + await deleteDoc(ref); + }); }); }); diff --git a/packages/firestore/e2e/DocumentSnapshot/get.e2e.js b/packages/firestore/e2e/DocumentSnapshot/get.e2e.js index c39ac60ecd..a4824c3d2d 100644 --- a/packages/firestore/e2e/DocumentSnapshot/get.e2e.js +++ b/packages/firestore/e2e/DocumentSnapshot/get.e2e.js @@ -21,145 +21,300 @@ describe('firestore().doc() -> snapshot.get()', function () { before(function () { return wipe(); }); - it('throws if invalid fieldPath argument', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - try { - snapshot.get(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' expected type string, array or FieldPath"); - return Promise.resolve(); - } - }); - it('throws if fieldPath is an empty string', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - try { - snapshot.get(''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if invalid fieldPath argument', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/foo`); + const snapshot = await ref.get(); - it('throws if fieldPath starts with a period (.)', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - try { - snapshot.get('.foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); + try { + snapshot.get(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' expected type string, array or FieldPath"); + return Promise.resolve(); + } + }); - it('throws if fieldPath ends with a period (.)', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - try { - snapshot.get('foo.'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); + it('throws if fieldPath is an empty string', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/foo`); + const snapshot = await ref.get(); - it('throws if fieldPath contains ..', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - try { - snapshot.get('foo..bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); + try { + snapshot.get(''); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); - it('returns undefined if the data does not exist', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - const val1 = snapshot.get('foo'); - const val2 = snapshot.get('foo.bar'); - const val3 = snapshot.get(new firebase.firestore.FieldPath('.')); - const val4 = snapshot.get(new firebase.firestore.FieldPath('foo')); - const val5 = snapshot.get(new firebase.firestore.FieldPath('foo.bar')); - - should.equal(val1, undefined); - should.equal(val2, undefined); - should.equal(val3, undefined); - should.equal(val4, undefined); - should.equal(val5, undefined); - }); + it('throws if fieldPath starts with a period (.)', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/foo`); + const snapshot = await ref.get(); + + try { + snapshot.get('.foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if fieldPath ends with a period (.)', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/foo`); + const snapshot = await ref.get(); + + try { + snapshot.get('foo.'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if fieldPath contains ..', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/foo`); + const snapshot = await ref.get(); + + try { + snapshot.get('foo..bar'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('returns undefined if the data does not exist', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/foo`); + const snapshot = await ref.get(); - it('returns the correct data with string fieldPath', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const types = { - string: '12345', - number: 1234, - map: { + const val1 = snapshot.get('foo'); + const val2 = snapshot.get('foo.bar'); + const val3 = snapshot.get(new firebase.firestore.FieldPath('.')); + const val4 = snapshot.get(new firebase.firestore.FieldPath('foo')); + const val5 = snapshot.get(new firebase.firestore.FieldPath('foo.bar')); + + should.equal(val1, undefined); + should.equal(val2, undefined); + should.equal(val3, undefined); + should.equal(val4, undefined); + should.equal(val5, undefined); + }); + + it('returns the correct data with string fieldPath', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/foo`); + const types = { string: '12345', number: 1234, - }, - array: [1, '2', null], - timestamp: new Date(), - }; - - await ref.set(types); - const snapshot = await ref.get(); - - const string1 = snapshot.get('string'); - const string2 = snapshot.get('map.string'); - const number1 = snapshot.get('number'); - const number2 = snapshot.get('map.number'); - const array = snapshot.get('array'); - - should.equal(string1, types.string); - should.equal(string2, types.string); - should.equal(number1, types.number); - should.equal(number2, types.number); - array.should.eql(jet.contextify(types.array)); - await ref.delete(); + map: { + string: '12345', + number: 1234, + }, + array: [1, '2', null], + timestamp: new Date(), + }; + + await ref.set(types); + const snapshot = await ref.get(); + + const string1 = snapshot.get('string'); + const string2 = snapshot.get('map.string'); + const number1 = snapshot.get('number'); + const number2 = snapshot.get('map.number'); + const array = snapshot.get('array'); + + should.equal(string1, types.string); + should.equal(string2, types.string); + should.equal(number1, types.number); + should.equal(number2, types.number); + array.should.eql(jet.contextify(types.array)); + await ref.delete(); + }); + + it('returns the correct data with FieldPath', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/foo`); + const types = { + string: '12345', + number: 1234, + map: { + string: '12345', + number: 1234, + }, + array: [1, '2', null], + timestamp: new Date(), + }; + + await ref.set(types); + const snapshot = await ref.get(); + + const string1 = snapshot.get(new firebase.firestore.FieldPath('string')); + const string2 = snapshot.get(new firebase.firestore.FieldPath('map', 'string')); + const number1 = snapshot.get(new firebase.firestore.FieldPath('number')); + const number2 = snapshot.get(new firebase.firestore.FieldPath('map', 'number')); + const array = snapshot.get(new firebase.firestore.FieldPath('array')); + + should.equal(string1, types.string); + should.equal(string2, types.string); + should.equal(number1, types.number); + should.equal(number2, types.number); + array.should.eql(jet.contextify(types.array)); + await ref.delete(); + }); }); - it('returns the correct data with FieldPath', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const types = { - string: '12345', - number: 1234, - map: { + describe('modular', function () { + it('throws if invalid fieldPath argument', async function () { + const { getFirestore, doc, getDocs } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/foo`); + const snapshot = await getDocs(ref); + + try { + snapshot.get(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' expected type string, array or FieldPath"); + return Promise.resolve(); + } + }); + + it('throws if fieldPath is an empty string', async function () { + const { getFirestore, doc, getDocs } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/foo`); + const snapshot = await getDocs(ref); + + try { + snapshot.get(''); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if fieldPath starts with a period (.)', async function () { + const { getFirestore, doc, getDocs } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/foo`); + const snapshot = await getDocs(ref); + + try { + snapshot.get('.foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if fieldPath ends with a period (.)', async function () { + const { getFirestore, doc, getDocs } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/foo`); + const snapshot = await getDocs(ref); + + try { + snapshot.get('foo.'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if fieldPath contains ..', async function () { + const { getFirestore, doc, getDocs } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/foo`); + const snapshot = await getDocs(ref); + + try { + snapshot.get('foo..bar'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('returns undefined if the data does not exist', async function () { + const { getFirestore, doc, getDocs, FieldPath } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/foo`); + const snapshot = await getDocs(ref); + + const val1 = snapshot.get('foo'); + const val2 = snapshot.get('foo.bar'); + const val3 = snapshot.get(new FieldPath('.')); + const val4 = snapshot.get(new FieldPath('foo')); + const val5 = snapshot.get(new FieldPath('foo.bar')); + + should.equal(val1, undefined); + should.equal(val2, undefined); + should.equal(val3, undefined); + should.equal(val4, undefined); + should.equal(val5, undefined); + }); + + it('returns the correct data with string fieldPath', async function () { + const { getFirestore, doc, getDocs, setDoc, deleteDoc } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/foo`); + const types = { string: '12345', number: 1234, - }, - array: [1, '2', null], - timestamp: new Date(), - }; - - await ref.set(types); - const snapshot = await ref.get(); - - const string1 = snapshot.get(new firebase.firestore.FieldPath('string')); - const string2 = snapshot.get(new firebase.firestore.FieldPath('map', 'string')); - const number1 = snapshot.get(new firebase.firestore.FieldPath('number')); - const number2 = snapshot.get(new firebase.firestore.FieldPath('map', 'number')); - const array = snapshot.get(new firebase.firestore.FieldPath('array')); - - should.equal(string1, types.string); - should.equal(string2, types.string); - should.equal(number1, types.number); - should.equal(number2, types.number); - array.should.eql(jet.contextify(types.array)); - await ref.delete(); + map: { + string: '12345', + number: 1234, + }, + array: [1, '2', null], + timestamp: new Date(), + }; + + await setDoc(ref, types); + const snapshot = await getDocs(ref); + + const string1 = snapshot.get('string'); + const string2 = snapshot.get('map.string'); + const number1 = snapshot.get('number'); + const number2 = snapshot.get('map.number'); + const array = snapshot.get('array'); + + should.equal(string1, types.string); + should.equal(string2, types.string); + should.equal(number1, types.number); + should.equal(number2, types.number); + array.should.eql(jet.contextify(types.array)); + await deleteDoc(ref); + }); + + it('returns the correct data with FieldPath', async function () { + const { getFirestore, doc, getDocs, setDoc, deleteDoc, FieldPath } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/foo`); + const types = { + string: '12345', + number: 1234, + map: { + string: '12345', + number: 1234, + }, + array: [1, '2', null], + timestamp: new Date(), + }; + + await setDoc(ref, types); + const snapshot = await getDocs(ref); + + const string1 = snapshot.get(new FieldPath('string')); + const string2 = snapshot.get(new FieldPath('map', 'string')); + const number1 = snapshot.get(new FieldPath('number')); + const number2 = snapshot.get(new FieldPath('map', 'number')); + const array = snapshot.get(new FieldPath('array')); + + should.equal(string1, types.string); + should.equal(string2, types.string); + should.equal(number1, types.number); + should.equal(number2, types.number); + array.should.eql(jet.contextify(types.array)); + await deleteDoc(ref); + }); }); }); diff --git a/packages/firestore/e2e/DocumentSnapshot/isEqual.e2e.js b/packages/firestore/e2e/DocumentSnapshot/isEqual.e2e.js index 2d9e86a4ce..1493eca7dd 100644 --- a/packages/firestore/e2e/DocumentSnapshot/isEqual.e2e.js +++ b/packages/firestore/e2e/DocumentSnapshot/isEqual.e2e.js @@ -17,43 +17,91 @@ const COLLECTION = 'firestore'; describe('firestore.doc() -> snapshot.isEqual()', function () { - it('throws if other is not a DocumentSnapshot', async function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/baz`); + describe('v8 compatibility', function () { + it('throws if other is not a DocumentSnapshot', async function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/baz`); + + const docSnapshot = await docRef.get(); + docSnapshot.isEqual(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected a DocumentSnapshot instance"); + return Promise.resolve(); + } + }); + + it('returns false when not equal', async function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/isEqual-false-exists`); + await docRef.set({ foo: 'bar' }); + + const docSnapshot1 = await docRef.get(); + const docSnapshot2 = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); + await docRef.set({ foo: 'baz' }); + const docSnapshot3 = await docRef.get(); + + const eql1 = docSnapshot1.isEqual(docSnapshot2); + const eql2 = docSnapshot1.isEqual(docSnapshot3); + + eql1.should.be.False(); + eql2.should.be.False(); + }); + + it('returns true when equal', async function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/isEqual-true-exists`); + await docRef.set({ foo: 'bar' }); const docSnapshot = await docRef.get(); - docSnapshot.isEqual(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected a DocumentSnapshot instance"); - return Promise.resolve(); - } + + const eql1 = docSnapshot.isEqual(docSnapshot); + + eql1.should.be.True(); + }); }); - it('returns false when not equal', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/isEqual-false-exists`); - await docRef.set({ foo: 'bar' }); + describe('modular', function () { + it('throws if other is not a DocumentSnapshot', async function () { + const { getFirestore, doc, getDocs } = firestoreModular; + try { + const docRef = doc(getFirestore(), `${COLLECTION}/baz`); - const docSnapshot1 = await docRef.get(); - const docSnapshot2 = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); - await docRef.set({ foo: 'baz' }); - const docSnapshot3 = await docRef.get(); + const docSnapshot = await getDocs(docRef); + docSnapshot.isEqual(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected a DocumentSnapshot instance"); + return Promise.resolve(); + } + }); - const eql1 = docSnapshot1.isEqual(docSnapshot2); - const eql2 = docSnapshot1.isEqual(docSnapshot3); + it('returns false when not equal', async function () { + const { getFirestore, doc, setDoc, getDocs } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/isEqual-false-exists`); + await setDoc(docRef, { foo: 'bar' }); - eql1.should.be.False(); - eql2.should.be.False(); - }); + const docSnapshot1 = await getDocs(docRef); + const docSnapshot2 = await doc(db, `${COLLECTION}/idonotexist`).get(); + await setDoc(docRef, { foo: 'baz' }); + const docSnapshot3 = await getDocs(docRef); + + const eql1 = docSnapshot1.isEqual(docSnapshot2); + const eql2 = docSnapshot1.isEqual(docSnapshot3); + + eql1.should.be.False(); + eql2.should.be.False(); + }); - it('returns true when equal', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/isEqual-true-exists`); - await docRef.set({ foo: 'bar' }); + it('returns true when equal', async function () { + const { getFirestore, doc, setDoc, getDocs } = firestoreModular; + const docRef = doc(getFirestore(), `${COLLECTION}/isEqual-true-exists`); + await setDoc(docRef, { foo: 'bar' }); - const docSnapshot = await docRef.get(); + const docSnapshot = await getDocs(docRef); - const eql1 = docSnapshot.isEqual(docSnapshot); + const eql1 = docSnapshot.isEqual(docSnapshot); - eql1.should.be.True(); + eql1.should.be.True(); + }); }); }); diff --git a/packages/firestore/e2e/DocumentSnapshot/properties.e2e.js b/packages/firestore/e2e/DocumentSnapshot/properties.e2e.js index 1ece46495c..5c8ba6a017 100644 --- a/packages/firestore/e2e/DocumentSnapshot/properties.e2e.js +++ b/packages/firestore/e2e/DocumentSnapshot/properties.e2e.js @@ -21,45 +21,101 @@ describe('firestore().doc() -> snapshot', function () { before(function () { return wipe(); }); - it('.exists -> returns a boolean for exists', async function () { - const ref1 = firebase.firestore().doc(`${COLLECTION}/exists`); - const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - await ref1.set({ foo: ' bar' }); - const snapshot1 = await ref1.get(); - const snapshot2 = await ref2.get(); - - snapshot1.exists.should.equal(true); - snapshot2.exists.should.equal(false); - await ref1.delete(); - }); - it('.id -> returns the correct id', async function () { - const ref1 = firebase.firestore().doc(`${COLLECTION}/exists`); - const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - await ref1.set({ foo: ' bar' }); - const snapshot1 = await ref1.get(); - const snapshot2 = await ref2.get(); + describe('v8 compatibility', function () { + it('.exists -> returns a boolean for exists', async function () { + const ref1 = firebase.firestore().doc(`${COLLECTION}/exists`); + const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); + await ref1.set({ foo: ' bar' }); + const snapshot1 = await ref1.get(); + const snapshot2 = await ref2.get(); - snapshot1.id.should.equal('exists'); - snapshot2.id.should.equal('idonotexist'); - await ref1.delete(); - }); + snapshot1.exists.should.equal(true); + snapshot2.exists.should.equal(false); + await ref1.delete(); + }); + + it('.id -> returns the correct id', async function () { + const ref1 = firebase.firestore().doc(`${COLLECTION}/exists`); + const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); + await ref1.set({ foo: ' bar' }); + const snapshot1 = await ref1.get(); + const snapshot2 = await ref2.get(); + + snapshot1.id.should.equal('exists'); + snapshot2.id.should.equal('idonotexist'); + await ref1.delete(); + }); - it('.metadata -> returns a SnapshotMetadata instance', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/exists`); - const snapshot = await ref.get(); - snapshot.metadata.constructor.name.should.eql('FirestoreSnapshotMetadata'); + it('.metadata -> returns a SnapshotMetadata instance', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/exists`); + const snapshot = await ref.get(); + snapshot.metadata.constructor.name.should.eql('FirestoreSnapshotMetadata'); + }); + + it('.ref -> returns the correct document ref', async function () { + const ref1 = firebase.firestore().doc(`${COLLECTION}/exists`); + const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); + await ref1.set({ foo: ' bar' }); + const snapshot1 = await ref1.get(); + const snapshot2 = await ref2.get(); + + snapshot1.ref.path.should.equal(`${COLLECTION}/exists`); + snapshot2.ref.path.should.equal(`${COLLECTION}/idonotexist`); + await ref1.delete(); + }); }); - it('.ref -> returns the correct document ref', async function () { - const ref1 = firebase.firestore().doc(`${COLLECTION}/exists`); - const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - await ref1.set({ foo: ' bar' }); - const snapshot1 = await ref1.get(); - const snapshot2 = await ref2.get(); + describe('modular', function () { + it('.exists -> returns a boolean for exists', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const db = getFirestore(firebase.app()); + + const ref1 = doc(db, `${COLLECTION}/exists`); + const ref2 = doc(db, `${COLLECTION}/idonotexist`); + await setDoc(ref1, { foo: ' bar' }); + const snapshot1 = await getDocs(ref1); + const snapshot2 = await getDocs(ref2); + + snapshot1.exists.should.equal(true); + snapshot2.exists.should.equal(false); + await deleteDoc(ref1); + }); + + it('.id -> returns the correct id', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const db = getFirestore(firebase.app()); + + const ref1 = doc(db, `${COLLECTION}/exists`); + const ref2 = doc(db, `${COLLECTION}/idonotexist`); + await setDoc(ref1, { foo: ' bar' }); + const snapshot1 = await getDocs(ref1); + const snapshot2 = await getDocs(ref2); + + snapshot1.id.should.equal('exists'); + snapshot2.id.should.equal('idonotexist'); + await deleteDoc(ref1); + }); + + it('.metadata -> returns a SnapshotMetadata instance', async function () { + const { getFirestore, doc, getDocs } = firestoreModular; + const ref = doc(getFirestore(), `${COLLECTION}/exists`); + const snapshot = await getDocs(ref); + snapshot.metadata.constructor.name.should.eql('FirestoreSnapshotMetadata'); + }); + + it('.ref -> returns the correct document ref', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const db = getFirestore(firebase.app()); + const ref1 = doc(db, `${COLLECTION}/exists`); + const ref2 = doc(db, `${COLLECTION}/idonotexist`); + await setDoc(ref1, { foo: ' bar' }); + const snapshot1 = await getDocs(ref1); + const snapshot2 = await getDocs(ref2); - snapshot1.ref.path.should.equal(`${COLLECTION}/exists`); - snapshot2.ref.path.should.equal(`${COLLECTION}/idonotexist`); - await ref1.delete(); + snapshot1.ref.path.should.equal(`${COLLECTION}/exists`); + snapshot2.ref.path.should.equal(`${COLLECTION}/idonotexist`); + await deleteDoc(ref1); + }); }); }); diff --git a/packages/firestore/e2e/FieldPath.e2e.js b/packages/firestore/e2e/FieldPath.e2e.js index d6d02fa73c..a4da44dfd9 100644 --- a/packages/firestore/e2e/FieldPath.e2e.js +++ b/packages/firestore/e2e/FieldPath.e2e.js @@ -14,102 +14,218 @@ * limitations under the License. * */ + const COLLECTION = 'firestore'; describe('firestore.FieldPath', function () { - it('should throw if no segments', function () { - try { - new firebase.firestore.FieldPath(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('cannot construct FieldPath with no segments'); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('should throw if no segments', function () { + try { + new firebase.firestore.FieldPath(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('cannot construct FieldPath with no segments'); + return Promise.resolve(); + } + }); - it('should throw if any segments are empty strings', function () { - try { - new firebase.firestore.FieldPath('foo', ''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('invalid segment at index'); - return Promise.resolve(); - } - }); + it('should throw if any segments are empty strings', function () { + try { + new firebase.firestore.FieldPath('foo', ''); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('invalid segment at index'); + return Promise.resolve(); + } + }); - it('should throw if any segments are not strings', function () { - try { - new firebase.firestore.FieldPath('foo', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('invalid segment at index'); - return Promise.resolve(); - } - }); + it('should throw if any segments are not strings', function () { + try { + new firebase.firestore.FieldPath('foo', 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('invalid segment at index'); + return Promise.resolve(); + } + }); - it('should throw if string fieldPath is invalid', function () { - try { - // Dummy create - firebase.firestore().collection(COLLECTION).where('.foo', '<', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Invalid field path'); - return Promise.resolve(); - } - }); + it('should throw if string fieldPath is invalid', function () { + try { + // Dummy create + firebase.firestore().collection(COLLECTION).where('.foo', '<', 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Invalid field path'); + return Promise.resolve(); + } + }); - it('should throw if string fieldPath contains invalid characters', function () { - try { - // Dummy create - firebase.firestore().collection(COLLECTION).where('foo/bar', '<', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Paths must not contain'); - return Promise.resolve(); - } - }); + it('should throw if string fieldPath contains invalid characters', function () { + try { + // Dummy create + firebase.firestore().collection(COLLECTION).where('foo/bar', '<', 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Paths must not contain'); + return Promise.resolve(); + } + }); - it('should provide access to segments as array', function () { - const expect = ['foo', 'bar', 'baz']; - const path = new firebase.firestore.FieldPath('foo', 'bar', 'baz'); - path._segments.should.eql(jet.contextify(expect)); - }); + it('should provide access to segments as array', function () { + const expect = ['foo', 'bar', 'baz']; + const path = new firebase.firestore.FieldPath('foo', 'bar', 'baz'); + path._segments.should.eql(jet.contextify(expect)); + }); - it('should provide access to string dot notated path', function () { - const expect = 'foo.bar.baz'; - const path = new firebase.firestore.FieldPath('foo', 'bar', 'baz'); - path._toPath().should.equal(expect); - }); + it('should provide access to string dot notated path', function () { + const expect = 'foo.bar.baz'; + const path = new firebase.firestore.FieldPath('foo', 'bar', 'baz'); + path._toPath().should.equal(expect); + }); + + it('should return document ID path', function () { + const expect = '__name__'; + const path = firebase.firestore.FieldPath.documentId(); + path._segments.length.should.equal(1); + path._toPath().should.equal(expect); + }); + + describe('isEqual()', function () { + it('throws if other isnt a FieldPath', function () { + try { + const path = new firebase.firestore.FieldPath('foo'); + path.isEqual({}); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected instance of FieldPath"); + return Promise.resolve(); + } + }); + + it('should return true if isEqual', function () { + const path1 = new firebase.firestore.FieldPath('foo', 'bar'); + const path2 = new firebase.firestore.FieldPath('foo', 'bar'); + path1.isEqual(path2).should.equal(true); + }); - it('should return document ID path', function () { - const expect = '__name__'; - const path = firebase.firestore.FieldPath.documentId(); - path._segments.length.should.equal(1); - path._toPath().should.equal(expect); + it('should return false if not isEqual', function () { + const path1 = new firebase.firestore.FieldPath('foo', 'bar'); + const path2 = new firebase.firestore.FieldPath('foo', 'baz'); + path1.isEqual(path2).should.equal(false); + }); + }); }); - describe('isEqual()', function () { - it('throws if other isnt a FieldPath', function () { + describe('modular', function () { + it('should throw if no segments', function () { + const { FieldPath } = firestoreModular; + try { - const path = new firebase.firestore.FieldPath('foo'); - path.isEqual({}); + new FieldPath(); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.containEql("'other' expected instance of FieldPath"); + error.message.should.containEql('cannot construct FieldPath with no segments'); return Promise.resolve(); } }); - it('should return true if isEqual', function () { - const path1 = new firebase.firestore.FieldPath('foo', 'bar'); - const path2 = new firebase.firestore.FieldPath('foo', 'bar'); - path1.isEqual(path2).should.equal(true); + it('should throw if any segments are empty strings', function () { + const { FieldPath } = firestoreModular; + + try { + new FieldPath('foo', ''); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('invalid segment at index'); + return Promise.resolve(); + } + }); + + it('should throw if any segments are not strings', function () { + const { FieldPath } = firestoreModular; + + try { + new FieldPath('foo', 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('invalid segment at index'); + return Promise.resolve(); + } }); - it('should return false if not isEqual', function () { - const path1 = new firebase.firestore.FieldPath('foo', 'bar'); - const path2 = new firebase.firestore.FieldPath('foo', 'baz'); - path1.isEqual(path2).should.equal(false); + it('should throw if string fieldPath is invalid', function () { + const { getFirestore, collection, query, where } = firestoreModular; + const db = getFirestore(); + + try { + // Dummy create + query(collection(db, COLLECTION), where('.foo', '<', 123)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Invalid field path'); + return Promise.resolve(); + } + }); + + it('should throw if string fieldPath contains invalid characters', function () { + const { getFirestore, collection, query, where } = firestoreModular; + const db = getFirestore(); + + try { + // Dummy create + query(collection(db, COLLECTION), where('foo/bar', '<', 123)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Paths must not contain'); + return Promise.resolve(); + } + }); + + it('should provide access to segments as array', function () { + const { FieldPath } = firestoreModular; + + const expect = ['foo', 'bar', 'baz']; + const path = new FieldPath('foo', 'bar', 'baz'); + path._segments.should.eql(jet.contextify(expect)); + }); + + it('should provide access to string dot notated path', function () { + const { FieldPath } = firestoreModular; + + const expect = 'foo.bar.baz'; + const path = new FieldPath('foo', 'bar', 'baz'); + path._toPath().should.equal(expect); + }); + + describe('isEqual()', function () { + it('throws if other isnt a FieldPath', function () { + const { FieldPath } = firestoreModular; + + try { + const path = new FieldPath('foo'); + path.isEqual({}); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected instance of FieldPath"); + return Promise.resolve(); + } + }); + + it('should return true if isEqual', function () { + const { FieldPath } = firestoreModular; + + const path1 = new FieldPath('foo', 'bar'); + const path2 = new FieldPath('foo', 'bar'); + path1.isEqual(path2).should.equal(true); + }); + + it('should return false if not isEqual', function () { + const { FieldPath } = firestoreModular; + + const path1 = new FieldPath('foo', 'bar'); + const path2 = new FieldPath('foo', 'baz'); + path1.isEqual(path2).should.equal(false); + }); }); }); }); diff --git a/packages/firestore/e2e/FieldValue.e2e.js b/packages/firestore/e2e/FieldValue.e2e.js index 768fd69f98..91074ba176 100644 --- a/packages/firestore/e2e/FieldValue.e2e.js +++ b/packages/firestore/e2e/FieldValue.e2e.js @@ -21,244 +21,550 @@ describe('firestore.FieldValue', function () { before(function () { return wipe(); }); - it('should throw if constructed manually', function () { - try { - new firebase.firestore.FieldValue(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('constructor is private'); - return Promise.resolve(); - } - }); - describe('isEqual()', function () { - it('throws if other is not a FieldValue', function () { + describe('v8 compatibility', function () { + it('should throw if constructed manually', function () { try { - firebase.firestore.FieldValue.increment(1).isEqual(1); + new firebase.firestore.FieldValue(); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.containEql("'other' expected a FieldValue instance"); + error.message.should.containEql('constructor is private'); return Promise.resolve(); } }); - it('returns false if not equal', function () { - const fv = firebase.firestore.FieldValue.increment(1); + describe('isEqual()', function () { + it('throws if other is not a FieldValue', function () { + try { + firebase.firestore.FieldValue.increment(1).isEqual(1); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected a FieldValue instance"); + return Promise.resolve(); + } + }); - const fieldValue1 = firebase.firestore.FieldValue.increment(2); - const fieldValue2 = firebase.firestore.FieldValue.arrayRemove('123'); + it('returns false if not equal', function () { + const fv = firebase.firestore.FieldValue.increment(1); - const eql1 = fv.isEqual(fieldValue1); - const eql2 = fv.isEqual(fieldValue2); + const fieldValue1 = firebase.firestore.FieldValue.increment(2); + const fieldValue2 = firebase.firestore.FieldValue.arrayRemove('123'); - eql1.should.be.False(); - eql2.should.be.False(); - }); + const eql1 = fv.isEqual(fieldValue1); + const eql2 = fv.isEqual(fieldValue2); - it('returns true if equal', function () { - const fv = firebase.firestore.FieldValue.arrayUnion(1, '123', 3); + eql1.should.be.False(); + eql2.should.be.False(); + }); - const fieldValue1 = firebase.firestore.FieldValue.arrayUnion(1, '123', 3); + it('returns true if equal', function () { + const fv = firebase.firestore.FieldValue.arrayUnion(1, '123', 3); - const eql1 = fv.isEqual(fieldValue1); + const fieldValue1 = firebase.firestore.FieldValue.arrayUnion(1, '123', 3); - eql1.should.be.True(); - }); - }); + const eql1 = fv.isEqual(fieldValue1); - describe('increment()', function () { - it('throws if value is not a number', function () { - try { - firebase.firestore.FieldValue.increment('1'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'n' expected a number value"); - return Promise.resolve(); - } + eql1.should.be.True(); + }); }); - it('increments a number if it exists', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/increment`); - await ref.set({ foo: 2 }); - await ref.update({ foo: firebase.firestore.FieldValue.increment(1) }); - const snapshot = await ref.get(); - snapshot.data().foo.should.equal(3); - await ref.delete(); + describe('increment()', function () { + it('throws if value is not a number', function () { + try { + firebase.firestore.FieldValue.increment('1'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'n' expected a number value"); + return Promise.resolve(); + } + }); + + it('increments a number if it exists', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/increment`); + await ref.set({ foo: 2 }); + await ref.update({ foo: firebase.firestore.FieldValue.increment(1) }); + const snapshot = await ref.get(); + snapshot.data().foo.should.equal(3); + await ref.delete(); + }); + + it('sets the value if it doesnt exist or being set', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/increment`); + await ref.set({ foo: firebase.firestore.FieldValue.increment(1) }); + const snapshot = await ref.get(); + snapshot.data().foo.should.equal(1); + await ref.delete(); + }); }); - it('sets the value if it doesnt exist or being set', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/increment`); - await ref.set({ foo: firebase.firestore.FieldValue.increment(1) }); - const snapshot = await ref.get(); - snapshot.data().foo.should.equal(1); - await ref.delete(); + describe('serverTime()', function () { + it('sets a new server time value', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/servertime`); + await ref.set({ foo: firebase.firestore.FieldValue.serverTimestamp() }); + const snapshot = await ref.get(); + snapshot.data().foo.seconds.should.be.a.Number(); + await ref.delete(); + }); + + it('updates a server time value', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/servertime`); + await ref.set({ foo: firebase.firestore.FieldValue.serverTimestamp() }); + const snapshot1 = await ref.get(); + snapshot1.data().foo.nanoseconds.should.be.a.Number(); + const current = snapshot1.data().foo.nanoseconds; + await Utils.sleep(100); + await ref.update({ foo: firebase.firestore.FieldValue.serverTimestamp() }); + const snapshot2 = await ref.get(); + snapshot2.data().foo.nanoseconds.should.be.a.Number(); + should.equal(current === snapshot2.data().foo.nanoseconds, false); + await ref.delete(); + }); }); - }); - describe('serverTime()', function () { - it('sets a new server time value', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/servertime`); - await ref.set({ foo: firebase.firestore.FieldValue.serverTimestamp() }); - const snapshot = await ref.get(); - snapshot.data().foo.seconds.should.be.a.Number(); - await ref.delete(); + describe('delete()', function () { + it('removes a value', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/valuedelete`); + await ref.set({ foo: 'bar', bar: 'baz' }); + await ref.update({ bar: firebase.firestore.FieldValue.delete() }); + const snapshot = await ref.get(); + snapshot.data().should.eql(jet.contextify({ foo: 'bar' })); + await ref.delete(); + }); }); - it('updates a server time value', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/servertime`); - await ref.set({ foo: firebase.firestore.FieldValue.serverTimestamp() }); - const snapshot1 = await ref.get(); - snapshot1.data().foo.nanoseconds.should.be.a.Number(); - const current = snapshot1.data().foo.nanoseconds; - await Utils.sleep(100); - await ref.update({ foo: firebase.firestore.FieldValue.serverTimestamp() }); - const snapshot2 = await ref.get(); - snapshot2.data().foo.nanoseconds.should.be.a.Number(); - should.equal(current === snapshot2.data().foo.nanoseconds, false); - await ref.delete(); + describe('arrayUnion()', function () { + it('throws if attempting to use own class', function () { + try { + firebase.firestore.FieldValue.arrayUnion(firebase.firestore.FieldValue.serverTimestamp()); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'FieldValue instance cannot be used with other FieldValue methods', + ); + return Promise.resolve(); + } + }); + + it('throws if using nested arrays', function () { + try { + firebase.firestore.FieldValue.arrayUnion([1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Nested arrays are not supported'); + return Promise.resolve(); + } + }); + + it('updates an existing array', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); + await ref.set({ + foo: [1, 2], + }); + await ref.update({ + foo: firebase.firestore.FieldValue.arrayUnion(3, 4), + }); + const snapshot = await ref.get(); + const expected = [1, 2, 3, 4]; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await ref.delete(); + }); + + it('sets an array if existing value isnt an array with update', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); + await ref.set({ + foo: 123, + }); + await ref.update({ + foo: firebase.firestore.FieldValue.arrayUnion(3, 4), + }); + const snapshot = await ref.get(); + const expected = [3, 4]; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await ref.delete(); + }); + + it('sets an existing array to the new array with set', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); + await ref.set({ + foo: [1, 2], + }); + await ref.set({ + foo: firebase.firestore.FieldValue.arrayUnion(3, 4), + }); + const snapshot = await ref.get(); + const expected = [3, 4]; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await ref.delete(); + }); }); - }); - describe('delete()', function () { - it('removes a value', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/valuedelete`); - await ref.set({ foo: 'bar', bar: 'baz' }); - await ref.update({ bar: firebase.firestore.FieldValue.delete() }); - const snapshot = await ref.get(); - snapshot.data().should.eql(jet.contextify({ foo: 'bar' })); - await ref.delete(); + describe('arrayRemove()', function () { + it('throws if attempting to use own class', function () { + try { + firebase.firestore.FieldValue.arrayRemove( + firebase.firestore.FieldValue.serverTimestamp(), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'FieldValue instance cannot be used with other FieldValue methods', + ); + return Promise.resolve(); + } + }); + + it('throws if using nested arrays', function () { + try { + firebase.firestore.FieldValue.arrayRemove([1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Nested arrays are not supported'); + return Promise.resolve(); + } + }); + + it('removes items in an array', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/arrayremove`); + await ref.set({ + foo: [1, 2, 3, 4], + }); + await ref.update({ + foo: firebase.firestore.FieldValue.arrayRemove(3, 4), + }); + const snapshot = await ref.get(); + const expected = [1, 2]; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await ref.delete(); + }); + + it('removes all items in the array if existing value isnt array with update', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); + await ref.set({ + foo: 123, + }); + await ref.update({ + foo: firebase.firestore.FieldValue.arrayRemove(3, 4), + }); + const snapshot = await ref.get(); + const expected = []; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await ref.delete(); + }); + + it('removes all items in the array if existing value isnt array with set', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); + await ref.set({ + foo: 123, + }); + await ref.set({ + foo: firebase.firestore.FieldValue.arrayRemove(3, 4), + }); + const snapshot = await ref.get(); + const expected = []; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await ref.delete(); + }); }); }); - describe('arrayUnion()', function () { - it('throws if attempting to use own class', function () { - try { - firebase.firestore.FieldValue.arrayUnion(firebase.firestore.FieldValue.serverTimestamp()); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'FieldValue instance cannot be used with other FieldValue methods', - ); - return Promise.resolve(); - } - }); + describe('modular', function () { + it('should throw if constructed manually', function () { + const { FieldValue } = firestoreModular; - it('throws if using nested arrays', function () { try { - firebase.firestore.FieldValue.arrayUnion([1]); + new FieldValue(); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.containEql('Nested arrays are not supported'); + error.message.should.containEql('constructor is private'); return Promise.resolve(); } }); - it('updates an existing array', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); - await ref.set({ - foo: [1, 2], + describe('isEqual()', function () { + it('throws if other is not a FieldValue', function () { + const { increment } = firestoreModular; + + try { + increment(1).isEqual(1); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected a FieldValue instance"); + return Promise.resolve(); + } }); - await ref.update({ - foo: firebase.firestore.FieldValue.arrayUnion(3, 4), + + it('returns false if not equal', function () { + const { increment, arrayRemove } = firestoreModular; + + const fv = increment(1); + + const fieldValue1 = increment(2); + const fieldValue2 = arrayRemove('123'); + + const eql1 = fv.isEqual(fieldValue1); + const eql2 = fv.isEqual(fieldValue2); + + eql1.should.be.False(); + eql2.should.be.False(); + }); + + it('returns true if equal', function () { + const { arrayUnion } = firestoreModular; + + const fv = arrayUnion(1, '123', 3); + + const fieldValue1 = arrayUnion(1, '123', 3); + + const eql1 = fv.isEqual(fieldValue1); + + eql1.should.be.True(); }); - const snapshot = await ref.get(); - const expected = [1, 2, 3, 4]; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); }); - it('sets an array if existing value isnt an array with update', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); - await ref.set({ - foo: 123, + describe('increment()', function () { + it('throws if value is not a number', function () { + const { increment } = firestoreModular; + + try { + increment('1'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'n' expected a number value"); + return Promise.resolve(); + } + }); + + it('increments a number if it exists', async function () { + const { getFirestore, doc, setDoc, updateDoc, getDocs, increment, deleteDoc } = + firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/increment`); + await setDoc(ref, { foo: 2 }); + await updateDoc(ref, { foo: increment(1) }); + const snapshot = await getDocs(ref); + snapshot.data().foo.should.equal(3); + await deleteDoc(ref); }); - await ref.update({ - foo: firebase.firestore.FieldValue.arrayUnion(3, 4), + + it('sets the value if it doesnt exist or being set', async function () { + const { getFirestore, doc, setDoc, getDocs, increment, deleteDoc } = firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/increment`); + await setDoc(ref, { foo: increment(1) }); + const snapshot = await getDocs(ref); + snapshot.data().foo.should.equal(1); + await deleteDoc(ref); }); - const snapshot = await ref.get(); - const expected = [3, 4]; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); }); - it('sets an existing array to the new array with set', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); - await ref.set({ - foo: [1, 2], + describe('serverTime()', function () { + it('sets a new server time value', async function () { + const { getFirestore, doc, setDoc, getDocs, serverTimestamp, deleteDoc } = firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/servertime`); + await setDoc(ref, { foo: serverTimestamp() }); + const snapshot = await getDocs(ref); + snapshot.data().foo.seconds.should.be.a.Number(); + await deleteDoc(ref); }); - await ref.set({ - foo: firebase.firestore.FieldValue.arrayUnion(3, 4), + + it('updates a server time value', async function () { + const { getFirestore, doc, setDoc, updateDoc, getDocs, serverTimestamp, deleteDoc } = + firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/servertime`); + await setDoc(ref, { foo: serverTimestamp() }); + const snapshot1 = await getDocs(ref); + snapshot1.data().foo.nanoseconds.should.be.a.Number(); + const current = snapshot1.data().foo.nanoseconds; + await Utils.sleep(100); + await updateDoc(ref, { foo: firebase.firestore.FieldValue.serverTimestamp() }); + const snapshot2 = await ref.get(); + snapshot2.data().foo.nanoseconds.should.be.a.Number(); + should.equal(current === snapshot2.data().foo.nanoseconds, false); + await deleteDoc(ref); }); - const snapshot = await ref.get(); - const expected = [3, 4]; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); }); - }); - describe('arrayRemove()', function () { - it('throws if attempting to use own class', function () { - try { - firebase.firestore.FieldValue.arrayRemove(firebase.firestore.FieldValue.serverTimestamp()); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'FieldValue instance cannot be used with other FieldValue methods', - ); - return Promise.resolve(); - } - }); + describe('delete()', function () { + it('removes a value', async function () { + const { getFirestore, doc, setDoc, updateDoc, getDocs, deleteDoc, deleteField } = + firestoreModular; + const db = getFirestore(); - it('throws if using nested arrays', function () { - try { - firebase.firestore.FieldValue.arrayRemove([1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Nested arrays are not supported'); - return Promise.resolve(); - } + const ref = doc(db, `${COLLECTION}/valuedelete`); + await setDoc(ref, { foo: 'bar', bar: 'baz' }); + await updateDoc(ref, { bar: deleteField() }); + const snapshot = await getDocs(ref); + snapshot.data().should.eql(jet.contextify({ foo: 'bar' })); + await deleteDoc(ref); + }); }); - it('removes items in an array', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayremove`); - await ref.set({ - foo: [1, 2, 3, 4], + describe('arrayUnion()', function () { + it('throws if attempting to use own class', function () { + const { arrayUnion, serverTimestamp } = firestoreModular; + + try { + arrayUnion(serverTimestamp()); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'FieldValue instance cannot be used with other FieldValue methods', + ); + return Promise.resolve(); + } }); - await ref.update({ - foo: firebase.firestore.FieldValue.arrayRemove(3, 4), + + it('throws if using nested arrays', function () { + const { arrayUnion } = firestoreModular; + + try { + arrayUnion([1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Nested arrays are not supported'); + return Promise.resolve(); + } }); - const snapshot = await ref.get(); - const expected = [1, 2]; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); - }); - it('removes all items in the array if existing value isnt array with update', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); - await ref.set({ - foo: 123, + it('updates an existing array', async function () { + const { getFirestore, doc, setDoc, updateDoc, getDocs, arrayUnion, deleteDoc } = + firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/arrayunion`); + await setDoc(ref, { + foo: [1, 2], + }); + await updateDoc(ref, { + foo: arrayUnion(3, 4), + }); + const snapshot = await getDocs(ref); + const expected = [1, 2, 3, 4]; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await deleteDoc(ref); + }); + + it('sets an array if existing value isnt an array with update', async function () { + const { getFirestore, doc, setDoc, updateDoc, getDocs, arrayUnion, deleteDoc } = + firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/arrayunion`); + await setDoc(ref, { + foo: 123, + }); + await updateDoc(ref, { + foo: arrayUnion(3, 4), + }); + const snapshot = await getDocs(ref); + const expected = [3, 4]; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await deleteDoc(ref); }); - await ref.update({ - foo: firebase.firestore.FieldValue.arrayRemove(3, 4), + + it('sets an existing array to the new array with set', async function () { + const { getFirestore, doc, setDoc, getDocs, arrayUnion, deleteDoc } = firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/arrayunion`); + await setDoc(ref, { + foo: [1, 2], + }); + await setDoc(ref, { + foo: arrayUnion(3, 4), + }); + const snapshot = await getDocs(ref); + const expected = [3, 4]; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await deleteDoc(ref); }); - const snapshot = await ref.get(); - const expected = []; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); }); - it('removes all items in the array if existing value isnt array with set', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); - await ref.set({ - foo: 123, + describe('arrayRemove()', function () { + it('throws if attempting to use own class', function () { + const { arrayRemove, serverTimestamp } = firestoreModular; + + try { + arrayRemove(serverTimestamp()); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'FieldValue instance cannot be used with other FieldValue methods', + ); + return Promise.resolve(); + } + }); + + it('throws if using nested arrays', function () { + const { arrayRemove } = firestoreModular; + + try { + arrayRemove([1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Nested arrays are not supported'); + return Promise.resolve(); + } + }); + + it('removes items in an array', async function () { + const { getFirestore, doc, setDoc, updateDoc, getDocs, arrayRemove, deleteDoc } = + firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/arrayremove`); + await setDoc(ref, { + foo: [1, 2, 3, 4], + }); + await updateDoc(ref, { + foo: arrayRemove(3, 4), + }); + const snapshot = await getDocs(ref); + const expected = [1, 2]; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await deleteDoc(ref); + }); + + it('removes all items in the array if existing value isnt array with update', async function () { + const { getFirestore, doc, setDoc, updateDoc, getDocs, arrayRemove, deleteDoc } = + firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/arrayunion`); + await setDoc(ref, { + foo: 123, + }); + await updateDoc(ref, { + foo: arrayRemove(3, 4), + }); + const snapshot = await getDocs(ref); + const expected = []; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await deleteDoc(ref); }); - await ref.set({ - foo: firebase.firestore.FieldValue.arrayRemove(3, 4), + + it('removes all items in the array if existing value isnt array with set', async function () { + const { getFirestore, doc, setDoc, getDocs, arrayRemove, deleteDoc } = firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/arrayunion`); + await setDoc(ref, { + foo: 123, + }); + await setDoc(ref, { + foo: arrayRemove(3, 4), + }); + const snapshot = await getDocs(ref); + const expected = []; + snapshot.data().foo.should.eql(jet.contextify(expected)); + await deleteDoc(ref); }); - const snapshot = await ref.get(); - const expected = []; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); }); }); }); diff --git a/packages/firestore/e2e/FirestoreStatics.e2e.js b/packages/firestore/e2e/FirestoreStatics.e2e.js index b422578aec..a42686cdc6 100644 --- a/packages/firestore/e2e/FirestoreStatics.e2e.js +++ b/packages/firestore/e2e/FirestoreStatics.e2e.js @@ -16,20 +16,49 @@ */ describe('firestore.X', function () { - describe('setLogLevel', function () { - it('throws if invalid level', function () { - try { - firebase.firestore.setLogLevel('verbose'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'logLevel' expected one of 'debug', 'error' or 'silent'"); - return Promise.resolve(); - } + describe('v8 compatibility', function () { + describe('setLogLevel', function () { + it('throws if invalid level', function () { + try { + firebase.firestore.setLogLevel('verbose'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'logLevel' expected one of 'debug', 'error' or 'silent'", + ); + return Promise.resolve(); + } + }); + + it('enabled and disables logging', function () { + firebase.firestore.setLogLevel('silent'); + firebase.firestore.setLogLevel('debug'); + }); }); + }); + + describe('modular', function () { + describe('setLogLevel', function () { + it('throws if invalid level', function () { + const { setLogLevel } = firestoreModular; + + try { + setLogLevel('verbose'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'logLevel' expected one of 'debug', 'error' or 'silent'", + ); + return Promise.resolve(); + } + }); + + it('enabled and disables logging', function () { + const { setLogLevel } = firestoreModular; - it('enabled and disables logging', function () { - firebase.firestore.setLogLevel('silent'); - firebase.firestore.setLogLevel('debug'); + setLogLevel('silent'); + setLogLevel('debug'); + }); }); }); }); diff --git a/packages/firestore/e2e/GeoPoint.e2e.js b/packages/firestore/e2e/GeoPoint.e2e.js index a250d5778b..22de41a0c0 100644 --- a/packages/firestore/e2e/GeoPoint.e2e.js +++ b/packages/firestore/e2e/GeoPoint.e2e.js @@ -20,112 +20,251 @@ describe('firestore.GeoPoint', function () { before(function () { return wipe(); }); - it('throws if invalid number of arguments', function () { - try { - new firebase.firestore.GeoPoint(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('constructor expected latitude and longitude values'); - return Promise.resolve(); - } - }); - it('throws if latitude is not a number', function () { - try { - new firebase.firestore.GeoPoint('123', 0); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'latitude' must be a number value"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if invalid number of arguments', function () { + try { + new firebase.firestore.GeoPoint(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('constructor expected latitude and longitude values'); + return Promise.resolve(); + } + }); - it('throws if longitude is not a number', function () { - try { - new firebase.firestore.GeoPoint(0, '123'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'longitude' must be a number value"); - return Promise.resolve(); - } - }); + it('throws if latitude is not a number', function () { + try { + new firebase.firestore.GeoPoint('123', 0); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'latitude' must be a number value"); + return Promise.resolve(); + } + }); - it('throws if latitude is not valid', function () { - try { - new firebase.firestore.GeoPoint(-100, 0); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'latitude' must be a number between -90 and 90"); - return Promise.resolve(); - } - }); + it('throws if longitude is not a number', function () { + try { + new firebase.firestore.GeoPoint(0, '123'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'longitude' must be a number value"); + return Promise.resolve(); + } + }); - it('throws if longitude is not valid', function () { - try { - new firebase.firestore.GeoPoint(0, 200); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'longitude' must be a number between -180 and 180"); - return Promise.resolve(); - } - }); + it('throws if latitude is not valid', function () { + try { + new firebase.firestore.GeoPoint(-100, 0); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'latitude' must be a number between -90 and 90"); + return Promise.resolve(); + } + }); - it('gets the latitude value', function () { - const geo = new firebase.firestore.GeoPoint(20, 0); - geo.latitude.should.equal(20); - }); + it('throws if longitude is not valid', function () { + try { + new firebase.firestore.GeoPoint(0, 200); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'longitude' must be a number between -180 and 180"); + return Promise.resolve(); + } + }); - it('gets the longitude value', function () { - const geo = new firebase.firestore.GeoPoint(20, 15); - geo.longitude.should.equal(15); + it('gets the latitude value', function () { + const geo = new firebase.firestore.GeoPoint(20, 0); + geo.latitude.should.equal(20); + }); + + it('gets the longitude value', function () { + const geo = new firebase.firestore.GeoPoint(20, 15); + geo.longitude.should.equal(15); + }); + + describe('isEqual()', function () { + it('throws if other is a GeoPoint instance', function () { + try { + const geo = new firebase.firestore.GeoPoint(0, 0); + geo.isEqual(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected an instance of GeoPoint"); + return Promise.resolve(); + } + }); + + it('returns false if not the same', function () { + const geo1 = new firebase.firestore.GeoPoint(0, 0); + const geo2 = new firebase.firestore.GeoPoint(0, 1); + const equal = geo1.isEqual(geo2); + equal.should.equal(false); + }); + + it('returns true if the same', function () { + const geo1 = new firebase.firestore.GeoPoint(40, 40); + const geo2 = new firebase.firestore.GeoPoint(40, 40); + const equal = geo1.isEqual(geo2); + equal.should.equal(true); + }); + }); + + describe('toJSON()', function () { + it('returns a json representation of the GeoPoint', function () { + const geo = new firebase.firestore.GeoPoint(30, 35); + const json = geo.toJSON(); + json.latitude.should.eql(30); + json.longitude.should.eql(35); + }); + }); + + it('sets & returns correctly', async function () { + const ref = firebase.firestore().doc(`${COLLECTION}/geopoint`); + await ref.set({ + geopoint: new firebase.firestore.GeoPoint(20, 30), + }); + const snapshot = await ref.get(); + const geo = snapshot.data().geopoint; + should.equal(geo.constructor.name, 'FirestoreGeoPoint'); + geo.latitude.should.equal(20); + geo.longitude.should.equal(30); + await ref.delete(); + }); }); - describe('isEqual()', function () { - it('throws if other is a GeoPoint instance', function () { + describe('modular', function () { + it('throws if invalid number of arguments', function () { + const { GeoPoint } = firestoreModular; + try { - const geo = new firebase.firestore.GeoPoint(0, 0); - geo.isEqual(); + new GeoPoint(123); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.containEql("'other' expected an instance of GeoPoint"); + error.message.should.containEql('constructor expected latitude and longitude values'); return Promise.resolve(); } }); - it('returns false if not the same', function () { - const geo1 = new firebase.firestore.GeoPoint(0, 0); - const geo2 = new firebase.firestore.GeoPoint(0, 1); - const equal = geo1.isEqual(geo2); - equal.should.equal(false); + it('throws if latitude is not a number', function () { + const { GeoPoint } = firestoreModular; + + try { + new GeoPoint('123', 0); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'latitude' must be a number value"); + return Promise.resolve(); + } }); - it('returns true if the same', function () { - const geo1 = new firebase.firestore.GeoPoint(40, 40); - const geo2 = new firebase.firestore.GeoPoint(40, 40); - const equal = geo1.isEqual(geo2); - equal.should.equal(true); + it('throws if longitude is not a number', function () { + const { GeoPoint } = firestoreModular; + + try { + new GeoPoint(0, '123'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'longitude' must be a number value"); + return Promise.resolve(); + } }); - }); - describe('toJSON()', function () { - it('returns a json representation of the GeoPoint', function () { - const geo = new firebase.firestore.GeoPoint(30, 35); - const json = geo.toJSON(); - json.latitude.should.eql(30); - json.longitude.should.eql(35); + it('throws if latitude is not valid', function () { + const { GeoPoint } = firestoreModular; + + try { + new GeoPoint(-100, 0); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'latitude' must be a number between -90 and 90"); + return Promise.resolve(); + } }); - }); - it('sets & returns correctly', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/geopoint`); - await ref.set({ - geopoint: new firebase.firestore.GeoPoint(20, 30), - }); - const snapshot = await ref.get(); - const geo = snapshot.data().geopoint; - should.equal(geo.constructor.name, 'FirestoreGeoPoint'); - geo.latitude.should.equal(20); - geo.longitude.should.equal(30); - await ref.delete(); + it('throws if longitude is not valid', function () { + const { GeoPoint } = firestoreModular; + + try { + new GeoPoint(0, 200); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'longitude' must be a number between -180 and 180"); + return Promise.resolve(); + } + }); + + it('gets the latitude value', function () { + const { GeoPoint } = firestoreModular; + + const geo = new GeoPoint(20, 0); + geo.latitude.should.equal(20); + }); + + it('gets the longitude value', function () { + const { GeoPoint } = firestoreModular; + + const geo = new GeoPoint(20, 15); + geo.longitude.should.equal(15); + }); + + describe('isEqual()', function () { + it('throws if other is a GeoPoint instance', function () { + const { GeoPoint } = firestoreModular; + + try { + const geo = new GeoPoint(0, 0); + geo.isEqual(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected an instance of GeoPoint"); + return Promise.resolve(); + } + }); + + it('returns false if not the same', function () { + const { GeoPoint } = firestoreModular; + + const geo1 = new GeoPoint(0, 0); + const geo2 = new GeoPoint(0, 1); + const equal = geo1.isEqual(geo2); + equal.should.equal(false); + }); + + it('returns true if the same', function () { + const { GeoPoint } = firestoreModular; + + const geo1 = new GeoPoint(40, 40); + const geo2 = new GeoPoint(40, 40); + const equal = geo1.isEqual(geo2); + equal.should.equal(true); + }); + }); + + describe('toJSON()', function () { + it('returns a json representation of the GeoPoint', function () { + const { GeoPoint } = firestoreModular; + + const geo = new GeoPoint(30, 35); + const json = geo.toJSON(); + json.latitude.should.eql(30); + json.longitude.should.eql(35); + }); + }); + + it('sets & returns correctly', async function () { + const { getFirestore, doc, setDoc, getDocs, deleteDoc, GeoPoint } = firestoreModular; + const db = getFirestore(); + + const ref = doc(db, `${COLLECTION}/geopoint`); + await setDoc(ref, { + geopoint: new GeoPoint(20, 30), + }); + const snapshot = await getDocs(ref); + const geo = snapshot.data().geopoint; + should.equal(geo.constructor.name, 'FirestoreGeoPoint'); + geo.latitude.should.equal(20); + geo.longitude.should.equal(30); + await deleteDoc(ref); + }); }); }); diff --git a/packages/firestore/e2e/Query/endAt.e2e.js b/packages/firestore/e2e/Query/endAt.e2e.js index 9b8c909968..e203f0dbf8 100644 --- a/packages/firestore/e2e/Query/endAt.e2e.js +++ b/packages/firestore/e2e/Query/endAt.e2e.js @@ -21,121 +21,265 @@ describe('firestore().collection().endAt()', function () { before(function () { return wipe(); }); - it('throws if no argument provided', function () { - try { - firebase.firestore().collection(COLLECTION).endAt(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Expected a DocumentSnapshot or list of field values but got undefined', - ); - return Promise.resolve(); - } - }); - it('throws if a inconsistent order number', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo').endAt('bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('The number of arguments must be less than or equal'); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if no argument provided', function () { + try { + firebase.firestore().collection(COLLECTION).endAt(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'Expected a DocumentSnapshot or list of field values but got undefined', + ); + return Promise.resolve(); + } + }); - it('throws if providing snapshot and field values', async function () { - try { - const doc = await firebase.firestore().collection(COLLECTION).doc('foo').get(); - firebase.firestore().collection(COLLECTION).endAt(doc, 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Expected DocumentSnapshot or list of field values'); - return Promise.resolve(); - } - }); + it('throws if a inconsistent order number', function () { + try { + firebase.firestore().collection(COLLECTION).orderBy('foo').endAt('bar', 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('The number of arguments must be less than or equal'); + return Promise.resolve(); + } + }); - it('throws if provided snapshot does not exist', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); - firebase.firestore().collection(COLLECTION).endAt(doc); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); - return Promise.resolve(); - } - }); + it('throws if providing snapshot and field values', async function () { + try { + const doc = await firebase.firestore().collection(COLLECTION).doc('foo').get(); + firebase.firestore().collection(COLLECTION).endAt(doc, 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Expected DocumentSnapshot or list of field values'); + return Promise.resolve(); + } + }); - it('throws if order used with snapshot but fields do not exist', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); - await doc.set({ foo: { bar: 'baz' } }); - const snap = await doc.get(); - - firebase.firestore().collection(COLLECTION).orderBy('foo.baz').endAt(snap); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'You are trying to start or end a query using a document for which the field', - ); - return Promise.resolve(); - } - }); + it('throws if provided snapshot does not exist', async function () { + try { + const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); + firebase.firestore().collection(COLLECTION).endAt(doc); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); + return Promise.resolve(); + } + }); - it('ends at field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/collection`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + it('throws if order used with snapshot but fields do not exist', async function () { + try { + const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); + await doc.set({ foo: { bar: 'baz' } }); + const snap = await doc.get(); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); + firebase.firestore().collection(COLLECTION).orderBy('foo.baz').endAt(snap); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'You are trying to start or end a query using a document for which the field', + ); + return Promise.resolve(); + } + }); - const qs = await colRef.orderBy('bar.value', 'desc').endAt(2).get(); + it('ends at field values', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/collection`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc3'); - qs.docs[1].id.should.eql('doc2'); - }); + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 1 } }), + doc2.set({ foo: 2, bar: { value: 2 } }), + doc3.set({ foo: 3, bar: { value: 3 } }), + ]); + + const qs = await colRef.orderBy('bar.value', 'desc').endAt(2).get(); + + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc3'); + qs.docs[1].id.should.eql('doc2'); + }); - it('ends at snapshot field values', async function () { - // await Utils.sleep(3000); - const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshotFields`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + it('ends at snapshot field values', async function () { + // await Utils.sleep(3000); + const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshotFields`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 3 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 1 } }), - ]); + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 3 } }), + doc2.set({ foo: 2, bar: { value: 2 } }), + doc3.set({ foo: 3, bar: { value: 1 } }), + ]); - const endAt = await doc2.get(); + const endAt = await doc2.get(); - const qs = await colRef.orderBy('bar.value').endAt(endAt).get(); + const qs = await colRef.orderBy('bar.value').endAt(endAt).get(); - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc3'); - qs.docs[1].id.should.eql('doc2'); + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc3'); + qs.docs[1].id.should.eql('doc2'); + }); + + it('ends at snapshot', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshot`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); + + await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); + + const endAt = await doc2.get(); + + const qs = await colRef.endAt(endAt).get(); + + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc1'); + qs.docs[1].id.should.eql('doc2'); + }); }); - it('ends at snapshot', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshot`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + describe('modular', function () { + it('throws if no argument provided', function () { + const { getFirestore, collection, endAt, query } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), endAt()); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'Expected a DocumentSnapshot or list of field values but got undefined', + ); + return Promise.resolve(); + } + }); + + it('throws if a inconsistent order number', function () { + const { getFirestore, collection, orderBy, endAt, query } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), orderBy('foo'), endAt('bar', 'baz')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('The number of arguments must be less than or equal'); + return Promise.resolve(); + } + }); + + it('throws if providing snapshot and field values', async function () { + const { getFirestore, collection, doc, endAt, query, getDocs } = firestoreModular; + const db = getFirestore(); + try { + const docRef = await getDocs(doc(collection(db, COLLECTION), 'foo')); + query(collection(db, COLLECTION), endAt(docRef, 'baz')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Expected DocumentSnapshot or list of field values'); + return Promise.resolve(); + } + }); + + it('throws if provided snapshot does not exist', async function () { + const { getFirestore, collection, doc, endAt, query, getDocs } = firestoreModular; + const db = getFirestore(); + try { + const docSnapshot = await getDocs(doc(db, `${COLLECTION}/idonotexist`)); + query(collection(db, COLLECTION), endAt(docSnapshot)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); + return Promise.resolve(); + } + }); + + it('throws if order used with snapshot but fields do not exist', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, endAt, orderBy, query } = + firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/iexist`); + await setDoc(docRef, { foo: { bar: 'baz' } }); + const snap = await getDocs(docRef); + + query(collection(db, COLLECTION), orderBy('foo.baz'), endAt(snap)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'You are trying to start or end a query using a document for which the field', + ); + return Promise.resolve(); + } + }); + + it('ends at field values', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, endAt, orderBy, query } = + firestoreModular; + const db = getFirestore(); + + const colRef = collection(db, `${COLLECTION}/endsAt/collection`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + + await Promise.all([ + setDoc(doc1, { foo: 1, bar: { value: 1 } }), + setDoc(doc2, { foo: 2, bar: { value: 2 } }), + setDoc(doc3, { foo: 3, bar: { value: 3 } }), + ]); + + const qs = await getDocs(query(colRef, orderBy('bar.value', 'desc'), endAt(2))); + + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc3'); + qs.docs[1].id.should.eql('doc2'); + }); + + it('ends at snapshot field values', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, endAt, orderBy, query } = + firestoreModular; + // await Utils.sleep(3000); + const colRef = collection(getFirestore(), `${COLLECTION}/endsAt/snapshotFields`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + + await Promise.all([ + setDoc(doc1, { foo: 1, bar: { value: 3 } }), + setDoc(doc2, { foo: 2, bar: { value: 2 } }), + setDoc(doc3, { foo: 3, bar: { value: 1 } }), + ]); + + const endAtSnapshot = await getDocs(doc2); + + const qs = await getDocs(query(colRef, orderBy('bar.value'), endAt(endAtSnapshot))); + + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc3'); + qs.docs[1].id.should.eql('doc2'); + }); + + it('ends at snapshot', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, endAt, query } = firestoreModular; + + const colRef = collection(getFirestore(), `${COLLECTION}/endsAt/snapshot`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); - await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); + await Promise.all([ + setDoc(doc1, { foo: 1 }), + setDoc(doc2, { foo: 1 }), + setDoc(doc3, { foo: 1 }), + ]); - const endAt = await doc2.get(); + const endAtSnapshot = await getDocs(doc2); - const qs = await colRef.endAt(endAt).get(); + const qs = await getDocs(query(colRef, endAt(endAtSnapshot))); - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc1'); - qs.docs[1].id.should.eql('doc2'); + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc1'); + qs.docs[1].id.should.eql('doc2'); + }); }); }); diff --git a/packages/firestore/e2e/Query/endBefore.e2e.js b/packages/firestore/e2e/Query/endBefore.e2e.js index 0b18cf583c..554b244df2 100644 --- a/packages/firestore/e2e/Query/endBefore.e2e.js +++ b/packages/firestore/e2e/Query/endBefore.e2e.js @@ -22,118 +22,256 @@ describe('firestore().collection().endBefore()', function () { return wipe(); }); - it('throws if no argument provided', function () { - try { - firebase.firestore().collection(COLLECTION).endBefore(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Expected a DocumentSnapshot or list of field values but got undefined', - ); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if no argument provided', function () { + try { + firebase.firestore().collection(COLLECTION).endBefore(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'Expected a DocumentSnapshot or list of field values but got undefined', + ); + return Promise.resolve(); + } + }); - it('throws if a inconsistent order number', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo').endBefore('bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('The number of arguments must be less than or equal'); - return Promise.resolve(); - } - }); + it('throws if a inconsistent order number', function () { + try { + firebase.firestore().collection(COLLECTION).orderBy('foo').endBefore('bar', 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('The number of arguments must be less than or equal'); + return Promise.resolve(); + } + }); - it('throws if providing snapshot and field values', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/stuff`).get(); + it('throws if providing snapshot and field values', async function () { + try { + const doc = await firebase.firestore().doc(`${COLLECTION}/stuff`).get(); - firebase.firestore().collection(COLLECTION).endBefore(doc, 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Expected DocumentSnapshot or list of field values'); - return Promise.resolve(); - } - }); + firebase.firestore().collection(COLLECTION).endBefore(doc, 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Expected DocumentSnapshot or list of field values'); + return Promise.resolve(); + } + }); - it('throws if provided snapshot does not exist', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); - firebase.firestore().collection(COLLECTION).endBefore(doc); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); - return Promise.resolve(); - } - }); + it('throws if provided snapshot does not exist', async function () { + try { + const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); + firebase.firestore().collection(COLLECTION).endBefore(doc); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); + return Promise.resolve(); + } + }); - it('throws if order used with snapshot but fields do not exist', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); - await doc.set({ foo: { bar: 'baz' } }); - const snap = await doc.get(); - - firebase.firestore().collection(COLLECTION).orderBy('foo.baz').endBefore(snap); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'You are trying to start or end a query using a document for which the field', - ); - return Promise.resolve(); - } - }); + it('throws if order used with snapshot but fields do not exist', async function () { + try { + const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); + await doc.set({ foo: { bar: 'baz' } }); + const snap = await doc.get(); - it('ends before field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endBefore/collection`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + firebase.firestore().collection(COLLECTION).orderBy('foo.baz').endBefore(snap); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'You are trying to start or end a query using a document for which the field', + ); + return Promise.resolve(); + } + }); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); + it('ends before field values', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/endBefore/collection`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); - const qs = await colRef.orderBy('bar.value', 'desc').endBefore(2).get(); + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 1 } }), + doc2.set({ foo: 2, bar: { value: 2 } }), + doc3.set({ foo: 3, bar: { value: 3 } }), + ]); - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc3'); - }); + const qs = await colRef.orderBy('bar.value', 'desc').endBefore(2).get(); + + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc3'); + }); + + it('ends before snapshot field values', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/endBefore/snapshotFields`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); - it('ends before snapshot field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endBefore/snapshotFields`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 3 } }), + doc2.set({ foo: 2, bar: { value: 2 } }), + doc3.set({ foo: 3, bar: { value: 1 } }), + ]); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 3 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 1 } }), - ]); + const endBefore = await doc2.get(); - const endBefore = await doc2.get(); + const qs = await colRef.orderBy('bar.value').endBefore(endBefore).get(); - const qs = await colRef.orderBy('bar.value').endBefore(endBefore).get(); + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc3'); + }); - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc3'); + it('ends before snapshot', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/endBefore/snapshot`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); + + await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); + + const endBefore = await doc2.get(); + + const qs = await colRef.endBefore(endBefore).get(); + + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc1'); + }); }); - it('ends before snapshot', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endBefore/snapshot`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + describe('modular', function () { + it('throws if no argument provided', function () { + const { getFirestore, collection, endBefore, query } = firestoreModular; + + try { + query(collection(getFirestore(), COLLECTION), endBefore()); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'Expected a DocumentSnapshot or list of field values but got undefined', + ); + return Promise.resolve(); + } + }); + + it('throws if a inconsistent order number', function () { + const { getFirestore, collection, orderBy, endBefore, query } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), orderBy('foo'), endBefore('bar', 'baz')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('The number of arguments must be less than or equal'); + return Promise.resolve(); + } + }); + + it('throws if providing snapshot and field values', async function () { + const { getFirestore, collection, doc, getDocs, query, endBefore } = firestoreModular; + const db = getFirestore(); + try { + const docRef = await getDocs(doc(db, `${COLLECTION}/stuff`)); + + query(collection(db, COLLECTION), endBefore(docRef, 'baz')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Expected DocumentSnapshot or list of field values'); + return Promise.resolve(); + } + }); + + it('throws if provided snapshot does not exist', async function () { + const { getFirestore, collection, doc, getDocs, query, endBefore } = firestoreModular; + const db = getFirestore(); + try { + const docRef = await getDocs(doc(db, `${COLLECTION}/idonotexist`)); + query(collection(db, COLLECTION), endBefore(docRef)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); + return Promise.resolve(); + } + }); + + it('throws if order used with snapshot but fields do not exist', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, query, orderBy, endBefore } = + firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/iexist`); + await setDoc(docRef, { foo: { bar: 'baz' } }); + const snap = await getDocs(docRef); + + query(collection(db, COLLECTION), orderBy('foo.baz'), endBefore(snap)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'You are trying to start or end a query using a document for which the field', + ); + return Promise.resolve(); + } + }); + + it('ends before field values', async function () { + const { getFirestore, collection, doc, setDoc, query, orderBy, endBefore, getDocs } = + firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/endBefore/collection`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + + await Promise.all([ + setDoc(doc1, { foo: 1, bar: { value: 1 } }), + setDoc(doc2, { foo: 2, bar: { value: 2 } }), + setDoc(doc3, { foo: 3, bar: { value: 3 } }), + ]); + + const qs = await getDocs(query(colRef, orderBy('bar.value', 'desc'), endBefore(2))); + + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc3'); + }); + + it('ends before snapshot field values', async function () { + const { getFirestore, collection, doc, setDoc, query, orderBy, endBefore, getDocs } = + firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/endBefore/snapshotFields`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + + await Promise.all([ + setDoc(doc1, { foo: 1, bar: { value: 3 } }), + setDoc(doc2, { foo: 2, bar: { value: 2 } }), + setDoc(doc3, { foo: 3, bar: { value: 1 } }), + ]); + + const endBeforeSnapshot = await getDocs(doc2); + + const qs = await getDocs(query(colRef, orderBy('bar.value'), endBefore(endBeforeSnapshot))); + + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc3'); + }); + + it('ends before snapshot', async function () { + const { getFirestore, collection, doc, setDoc, query, endBefore, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/endBefore/snapshot`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); - await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); + await Promise.all([ + setDoc(doc1, { foo: 1 }), + setDoc(doc2, { foo: 1 }), + setDoc(doc3, { foo: 1 }), + ]); - const endBefore = await doc2.get(); + const endBeforeSnapshot = await getDocs(doc2); - const qs = await colRef.endBefore(endBefore).get(); + const qs = await getDocs(query(colRef, endBefore(endBeforeSnapshot))); - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc1'); + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc1'); + }); }); }); diff --git a/packages/firestore/e2e/Query/get.e2e.js b/packages/firestore/e2e/Query/get.e2e.js index 037708c50e..ba762e39b9 100644 --- a/packages/firestore/e2e/Query/get.e2e.js +++ b/packages/firestore/e2e/Query/get.e2e.js @@ -21,58 +21,94 @@ describe('firestore().collection().get()', function () { before(function () { return wipe(); }); - it('throws if get options is not an object', function () { - try { - firebase.firestore().collection(COLLECTION).get(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must be an object is provided"); - return Promise.resolve(); - } - }); - it('throws if get options.source is not valid', function () { - try { - firebase.firestore().collection(COLLECTION).get({ - source: 'foo', + describe('v8 compatibility', function () { + it('throws if get options is not an object', function () { + try { + firebase.firestore().collection(COLLECTION).get(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' must be an object is provided"); + return Promise.resolve(); + } + }); + + it('throws if get options.source is not valid', function () { + try { + firebase.firestore().collection(COLLECTION).get({ + source: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' GetOptions.source must be one of 'default', 'server' or 'cache'", + ); + return Promise.resolve(); + } + }); + + it('returns a QuerySnapshot', async function () { + const docRef = firebase.firestore().collection(COLLECTION).doc('nestedcollection'); + const colRef = docRef.collection('get'); + const snapshot = await colRef.get(); + + snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); + }); + + it('returns a correct cache setting (true)', async function () { + const docRef = firebase.firestore().collection(COLLECTION).doc('nestedcollection'); + const colRef = docRef.collection('get'); + const snapshot = await colRef.get({ + source: 'cache', }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' GetOptions.source must be one of 'default', 'server' or 'cache'", - ); - return Promise.resolve(); - } - }); - it('returns a QuerySnapshot', async function () { - const docRef = firebase.firestore().collection(COLLECTION).doc('nestedcollection'); - const colRef = docRef.collection('get'); - const snapshot = await colRef.get(); + snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); + snapshot.metadata.fromCache.should.be.True(); + }); + + it('returns a correct cache setting (false)', async function () { + const docRef = firebase.firestore().collection(COLLECTION).doc('nestedcollection'); + const colRef = docRef.collection('get'); + await colRef.get(); // Puts it in cache + const snapshot = await colRef.get({ + source: 'server', + }); - snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); + snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); + snapshot.metadata.fromCache.should.be.False(); + }); }); - it('returns a correct cache setting (true)', async function () { - const docRef = firebase.firestore().collection(COLLECTION).doc('nestedcollection'); - const colRef = docRef.collection('get'); - const snapshot = await colRef.get({ - source: 'cache', + describe('modular', function () { + it('returns a QuerySnapshot', async function () { + const { getFirestore, collection, doc, getDocs } = firestoreModular; + + const docRef = doc(collection(getFirestore(), COLLECTION), 'nestedcollection'); + const colRef = collection(docRef, 'get'); + const snapshot = await getDocs(colRef); + + snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); }); - snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); - snapshot.metadata.fromCache.should.be.True(); - }); + it('returns a correct cache setting (true)', async function () { + const { getFirestore, collection, doc, getDocsFromCache } = firestoreModular; + const docRef = doc(collection(getFirestore(), COLLECTION), 'nestedcollection'); + const colRef = collection(docRef, 'get'); + const snapshot = await getDocsFromCache(colRef); - it('returns a correct cache setting (false)', async function () { - const docRef = firebase.firestore().collection(COLLECTION).doc('nestedcollection'); - const colRef = docRef.collection('get'); - await colRef.get(); // Puts it in cache - const snapshot = await colRef.get({ - source: 'server', + snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); + snapshot.metadata.fromCache.should.be.True(); }); - snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); - snapshot.metadata.fromCache.should.be.False(); + it('returns a correct cache setting (false)', async function () { + const { getFirestore, collection, doc, getDocs, getDocsFromServer } = firestoreModular; + const docRef = doc(collection(getFirestore(), COLLECTION), 'nestedcollection'); + const colRef = collection(docRef, 'get'); + await getDocs(colRef); // Puts it in cache + const snapshot = await getDocsFromServer(colRef); + + snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); + snapshot.metadata.fromCache.should.be.False(); + }); }); }); diff --git a/packages/firestore/e2e/Query/isEqual.e2e.js b/packages/firestore/e2e/Query/isEqual.e2e.js index 9b69a6ae37..b05d5e2f86 100644 --- a/packages/firestore/e2e/Query/isEqual.e2e.js +++ b/packages/firestore/e2e/Query/isEqual.e2e.js @@ -17,112 +17,236 @@ const COLLECTION = 'firestore'; describe('firestore().collection().isEqual()', function () { - it('throws if other is not a Query', function () { - try { - firebase.firestore().collection(COLLECTION).isEqual(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected a Query instance"); - return Promise.resolve(); - } + describe('v8 compatibility', function () { + it('throws if other is not a Query', function () { + try { + firebase.firestore().collection(COLLECTION).isEqual(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected a Query instance"); + return Promise.resolve(); + } + }); + + it('returns false when not equal (simple checks)', function () { + const subCol = `${COLLECTION}/isequal/simplechecks`; + const query = firebase.firestore().collection(subCol); + + const q1 = firebase.firestore(firebase.app('secondaryFromNative')).collection(subCol); + const q2 = firebase.firestore().collection(subCol).where('foo', '==', 'bar'); + const q3 = firebase.firestore().collection(subCol).orderBy('foo'); + const q4 = firebase.firestore().collection(subCol).limit(3); + + const ref1 = firebase.firestore().collection(subCol).where('bar', '==', true); + const ref2 = firebase.firestore().collection(subCol).where('bar', '==', true); + + const eql1 = query.isEqual(q1); + const eql2 = query.isEqual(q2); + const eql3 = query.isEqual(q3); + const eql4 = query.isEqual(q4); + const eql5 = ref1.isEqual(ref2); + + eql1.should.be.False(); + eql2.should.be.False(); + eql3.should.be.False(); + eql4.should.be.False(); + eql5.should.be.True(); + }); + + it('returns false when not equal (expensive checks)', function () { + const query = firebase + .firestore() + .collection(COLLECTION) + .where('foo', '==', 'bar') + .orderBy('bam') + .limit(1) + .endAt(2); + + const q1 = firebase + .firestore() + .collection(COLLECTION) + .where('foo', '<', 'bar') + .orderBy('foo') + .limit(1) + .endAt(2); + + const q2 = firebase + .firestore() + .collection(COLLECTION) + .where('foo', '==', 'bar') + .orderBy('foob') + .limit(1) + .endAt(2); + + const q3 = firebase + .firestore() + .collection(COLLECTION) + .where('foo', '==', 'bar') + .orderBy('baz') + .limit(2) + .endAt(2); + + const q4 = firebase + .firestore() + .collection(COLLECTION) + .where('foo', '==', 'bar') + .orderBy('baz') + .limit(1) + .endAt(1); + + const eql1 = query.isEqual(q1); + const eql2 = query.isEqual(q2); + const eql3 = query.isEqual(q3); + const eql4 = query.isEqual(q4); + + eql1.should.be.False(); + eql2.should.be.False(); + eql3.should.be.False(); + eql4.should.be.False(); + }); + + it('returns true when equal', function () { + const query = firebase + .firestore() + .collection(COLLECTION) + .where('foo', '==', 'bar') + .orderBy('baz') + .limit(1) + .endAt(2); + + const query2 = firebase + .firestore() + .collection(COLLECTION) + .where('foo', '==', 'bar') + .orderBy('baz') + .limit(1) + .endAt(2); + + const eql1 = query.isEqual(query2); + + eql1.should.be.True(); + }); }); - it('returns false when not equal (simple checks)', function () { - const subCol = `${COLLECTION}/isequal/simplechecks`; - const query = firebase.firestore().collection(subCol); - - const q1 = firebase.firestore(firebase.app('secondaryFromNative')).collection(subCol); - const q2 = firebase.firestore().collection(subCol).where('foo', '==', 'bar'); - const q3 = firebase.firestore().collection(subCol).orderBy('foo'); - const q4 = firebase.firestore().collection(subCol).limit(3); - - const ref1 = firebase.firestore().collection(subCol).where('bar', '==', true); - const ref2 = firebase.firestore().collection(subCol).where('bar', '==', true); - - const eql1 = query.isEqual(q1); - const eql2 = query.isEqual(q2); - const eql3 = query.isEqual(q3); - const eql4 = query.isEqual(q4); - const eql5 = ref1.isEqual(ref2); - - eql1.should.be.False(); - eql2.should.be.False(); - eql3.should.be.False(); - eql4.should.be.False(); - eql5.should.be.True(); - }); - - it('returns false when not equal (expensive checks)', function () { - const query = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('bam') - .limit(1) - .endAt(2); - - const q1 = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '<', 'bar') - .orderBy('foo') - .limit(1) - .endAt(2); - - const q2 = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('foob') - .limit(1) - .endAt(2); - - const q3 = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('baz') - .limit(2) - .endAt(2); - - const q4 = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('baz') - .limit(1) - .endAt(1); - - const eql1 = query.isEqual(q1); - const eql2 = query.isEqual(q2); - const eql3 = query.isEqual(q3); - const eql4 = query.isEqual(q4); - - eql1.should.be.False(); - eql2.should.be.False(); - eql3.should.be.False(); - eql4.should.be.False(); - }); - - it('returns true when equal', function () { - const query = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('baz') - .limit(1) - .endAt(2); - - const query2 = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('baz') - .limit(1) - .endAt(2); - - const eql1 = query.isEqual(query2); - - eql1.should.be.True(); + describe('modular', function () { + it('throws if other is not a Query', function () { + const { getFirestore, collection } = firestoreModular; + try { + collection(getFirestore(), COLLECTION).isEqual(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected a Query instance"); + return Promise.resolve(); + } + }); + + it('returns false when not equal (simple checks)', function () { + const { getFirestore, collection, query, where, orderBy, limit } = firestoreModular; + const db = getFirestore(); + const secondaryDb = getFirestore(firebase.app('secondaryFromNative')); + + const subCol = `${COLLECTION}/isequal/simplechecks`; + const queryRef = collection(db, subCol); + + const q1 = collection(secondaryDb, subCol); + const q2 = query(collection(db, subCol), where('foo', '==', 'bar')); + const q3 = query(collection(db, subCol), orderBy('foo')); + const q4 = query(collection(db, subCol), limit(3)); + + const ref1 = query(collection(db, subCol), where('bar', '==', true)); + const ref2 = query(collection(db, subCol), where('bar', '==', true)); + + const eql1 = queryRef.isEqual(q1); + const eql2 = queryRef.isEqual(q2); + const eql3 = queryRef.isEqual(q3); + const eql4 = queryRef.isEqual(q4); + const eql5 = ref1.isEqual(ref2); + + eql1.should.be.False(); + eql2.should.be.False(); + eql3.should.be.False(); + eql4.should.be.False(); + eql5.should.be.True(); + }); + + it('returns false when not equal (expensive checks)', function () { + const { getFirestore, collection, query, where, orderBy, limit, endAt } = firestoreModular; + const db = getFirestore(); + + const queryRef = query( + collection(db, COLLECTION), + where('foo', '==', 'bar'), + orderBy('bam'), + limit(1), + endAt(2), + ); + + const q1 = query( + collection(db, COLLECTION), + where('foo', '<', 'bar'), + orderBy('foo'), + limit(1), + endAt(2), + ); + + const q2 = query( + collection(db, COLLECTION), + where('foo', '==', 'bar'), + orderBy('foob'), + limit(1), + endAt(2), + ); + + const q3 = query( + collection(db, COLLECTION), + where('foo', '==', 'bar'), + orderBy('baz'), + limit(2), + endAt(2), + ); + + const q4 = query( + collection(db, COLLECTION), + where('foo', '==', 'bar'), + orderBy('baz'), + limit(1), + endAt(1), + ); + + const eql1 = queryRef.isEqual(q1); + const eql2 = queryRef.isEqual(q2); + const eql3 = queryRef.isEqual(q3); + const eql4 = queryRef.isEqual(q4); + + eql1.should.be.False(); + eql2.should.be.False(); + eql3.should.be.False(); + eql4.should.be.False(); + }); + + it('returns true when equal', function () { + const { getFirestore, collection, query, where, orderBy, limit, endAt } = firestoreModular; + const db = getFirestore(); + + const queryRef = query( + collection(db, COLLECTION), + where('foo', '==', 'bar'), + orderBy('baz'), + limit(1), + endAt(2), + ); + + const query2 = query( + collection(db, COLLECTION), + where('foo', '==', 'bar'), + orderBy('baz'), + limit(1), + endAt(2), + ); + + const eql1 = queryRef.isEqual(query2); + + eql1.should.be.True(); + }); }); }); diff --git a/packages/firestore/e2e/Query/limit.e2e.js b/packages/firestore/e2e/Query/limit.e2e.js index da42ff1796..142edeafa7 100644 --- a/packages/firestore/e2e/Query/limit.e2e.js +++ b/packages/firestore/e2e/Query/limit.e2e.js @@ -21,31 +21,67 @@ describe('firestore().collection().limit()', function () { before(function () { return wipe(); }); - it('throws if limit is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).limit(-1); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'limit' must be a positive integer value"); - return Promise.resolve(); - } - }); - it('sets limit on internals', async function () { - const colRef = firebase.firestore().collection(COLLECTION).limit(123); + describe('v8 compatibility', function () { + it('throws if limit is invalid', function () { + try { + firebase.firestore().collection(COLLECTION).limit(-1); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'limit' must be a positive integer value"); + return Promise.resolve(); + } + }); + + it('sets limit on internals', async function () { + const colRef = firebase.firestore().collection(COLLECTION).limit(123); + + colRef._modifiers.options.limit.should.eql(123); + }); + + it('limits the number of documents', async function () { + const colRef = firebase.firestore().collection(COLLECTION); - colRef._modifiers.options.limit.should.eql(123); + // Add 3 + await colRef.add({}); + await colRef.add({}); + await colRef.add({}); + + const snapshot = await colRef.limit(2).get(); + snapshot.size.should.eql(2); + }); }); - it('limits the number of documents', async function () { - const colRef = firebase.firestore().collection(COLLECTION); + describe('modular', function () { + it('throws if limit is invalid', function () { + const { getFirestore, collection, limit, query } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), limit(-1)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'limit' must be a positive integer value"); + return Promise.resolve(); + } + }); + + it('sets limit on internals', async function () { + const { getFirestore, collection, limit, query } = firestoreModular; + const colRef = query(collection(getFirestore(), COLLECTION), limit(123)); + + colRef._modifiers.options.limit.should.eql(123); + }); + + it('limits the number of documents', async function () { + const { getFirestore, collection, addDoc, getDocs, limit, query } = firestoreModular; + const colRef = collection(getFirestore(), COLLECTION); - // Add 3 - await colRef.add({}); - await colRef.add({}); - await colRef.add({}); + // Add 3 + await addDoc(colRef, {}); + await addDoc(colRef, {}); + await addDoc(colRef, {}); - const snapshot = await colRef.limit(2).get(); - snapshot.size.should.eql(2); + const snapshot = await getDocs(query(colRef, limit(2))); + snapshot.size.should.eql(2); + }); }); }); diff --git a/packages/firestore/e2e/Query/limitToLast.e2e.js b/packages/firestore/e2e/Query/limitToLast.e2e.js index 6d6546da9a..23be1bf9b1 100644 --- a/packages/firestore/e2e/Query/limitToLast.e2e.js +++ b/packages/firestore/e2e/Query/limitToLast.e2e.js @@ -21,77 +21,162 @@ describe('firestore().collection().limitToLast()', function () { before(function () { return wipe(); }); - it('throws if limitToLast is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).limitToLast(-1); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'limitToLast' must be a positive integer value"); - return Promise.resolve(); - } - }); - it('sets limitToLast on internals', async function () { - const colRef = firebase.firestore().collection(COLLECTION).limitToLast(123); + describe('v8 compatibility', function () { + it('throws if limitToLast is invalid', function () { + try { + firebase.firestore().collection(COLLECTION).limitToLast(-1); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'limitToLast' must be a positive integer value"); + return Promise.resolve(); + } + }); - should(colRef._modifiers.options.limitToLast).equal(123); - }); + it('sets limitToLast on internals', async function () { + const colRef = firebase.firestore().collection(COLLECTION).limitToLast(123); - it('removes limit query if limitToLast is set afterwards', function () { - const colRef = firebase.firestore().collection(COLLECTION).limit(2).limitToLast(123); + should(colRef._modifiers.options.limitToLast).equal(123); + }); - should(colRef._modifiers.options.limit).equal(undefined); - }); + it('removes limit query if limitToLast is set afterwards', function () { + const colRef = firebase.firestore().collection(COLLECTION).limit(2).limitToLast(123); + + should(colRef._modifiers.options.limit).equal(undefined); + }); + + it('removes limitToLast query if limit is set afterwards', function () { + const colRef = firebase + .firestore() + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/limitToLast-limit-after`); + const colRef2 = colRef.limitToLast(123).limit(2); + + should(colRef2._modifiers.options.limitToLast).equal(undefined); + }); - it('removes limitToLast query if limit is set afterwards', function () { - const colRef = firebase - .firestore() + it('limitToLast the number of documents', async function () { // Firestore caches aggressively, even if you wipe the emulator, local documents are cached // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/limitToLast-limit-after`); - const colRef2 = colRef.limitToLast(123).limit(2); + const subCol = `${COLLECTION}/${Utils.randString(12, '#aA')}/limitToLast-count`; + const colRef = firebase.firestore().collection(subCol); + + // Add 3 + await colRef.add({ count: 1 }); + await colRef.add({ count: 2 }); + await colRef.add({ count: 3 }); + + const docs = await firebase + .firestore() + .collection(subCol) + .limitToLast(2) + .orderBy('count', 'desc') + .get(); + + const results = []; + docs.forEach(doc => { + results.push(doc.data()); + }); + + should(results.length).equal(2); - should(colRef2._modifiers.options.limitToLast).equal(undefined); + should(results[0].count).equal(2); + should(results[1].count).equal(1); + }); + + it("throws error if no 'orderBy' is set on the query", function () { + try { + firebase.firestore().collection(COLLECTION).limitToLast(3).get(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'limitToLast() queries require specifying at least one firebase.firestore().collection().orderBy() clause', + ); + return Promise.resolve(); + } + }); }); - it('limitToLast the number of documents', async function () { - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - const subCol = `${COLLECTION}/${Utils.randString(12, '#aA')}/limitToLast-count`; - const colRef = firebase.firestore().collection(subCol); - - // Add 3 - await colRef.add({ count: 1 }); - await colRef.add({ count: 2 }); - await colRef.add({ count: 3 }); - - const docs = await firebase - .firestore() - .collection(subCol) - .limitToLast(2) - .orderBy('count', 'desc') - .get(); - - const results = []; - docs.forEach(doc => { - results.push(doc.data()); + describe('modular', function () { + it('throws if limitToLast is invalid', function () { + const { getFirestore, collection, limitToLast, query } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), limitToLast(-1)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'limitToLast' must be a positive integer value"); + return Promise.resolve(); + } }); - should(results.length).equal(2); + it('sets limitToLast on internals', async function () { + const { getFirestore, collection, query, limitToLast } = firestoreModular; + const colRef = query(collection(getFirestore(), COLLECTION), limitToLast(123)); - should(results[0].count).equal(2); - should(results[1].count).equal(1); - }); + should(colRef._modifiers.options.limitToLast).equal(123); + }); + + it('removes limit query if limitToLast is set afterwards', function () { + const { getFirestore, collection, limit, limitToLast, query } = firestoreModular; + const colRef = query(collection(getFirestore(), COLLECTION), limit(2), limitToLast(123)); + + should(colRef._modifiers.options.limit).equal(undefined); + }); + + it('removes limitToLast query if limit is set afterwards', function () { + const { getFirestore, collection, limit, limitToLast, query } = firestoreModular; + const colRef = collection( + getFirestore(), + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/limitToLast-limit-after`, + ); + const colRef2 = query(colRef, limitToLast(123), limit(2)); + + should(colRef2._modifiers.options.limitToLast).equal(undefined); + }); + + it('limitToLast the number of documents', async function () { + const { getFirestore, collection, addDoc, getDocs, query, limitToLast, orderBy } = + firestoreModular; + const db = getFirestore(); + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + const subCol = `${COLLECTION}/${Utils.randString(12, '#aA')}/limitToLast-count`; + const colRef = collection(db, subCol); + + // Add 3 + await addDoc(colRef, { count: 1 }); + await addDoc(colRef, { count: 2 }); + await addDoc(colRef, { count: 3 }); - it("throws error if no 'orderBy' is set on the query", function () { - try { - firebase.firestore().collection(COLLECTION).limitToLast(3).get(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'limitToLast() queries require specifying at least one firebase.firestore().collection().orderBy() clause', + const docs = await getDocs( + query(collection(db, subCol), limitToLast(2), orderBy('count', 'desc')), ); - return Promise.resolve(); - } + + const results = []; + docs.forEach(doc => { + results.push(doc.data()); + }); + + should(results.length).equal(2); + + should(results[0].count).equal(2); + should(results[1].count).equal(1); + }); + + it("throws error if no 'orderBy' is set on the query", function () { + const { getFirestore, collection, getDocs, limitToLast, query } = firestoreModular; + try { + getDocs(query(collection(getFirestore(), COLLECTION), limitToLast(3))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'limitToLast() queries require specifying at least one firebase.firestore().collection().orderBy() clause', + ); + return Promise.resolve(); + } + }); }); }); diff --git a/packages/firestore/e2e/Query/onSnapshot.e2e.js b/packages/firestore/e2e/Query/onSnapshot.e2e.js index 6bef3cbec3..f6dc4f1d53 100644 --- a/packages/firestore/e2e/Query/onSnapshot.e2e.js +++ b/packages/firestore/e2e/Query/onSnapshot.e2e.js @@ -22,303 +22,603 @@ describe('firestore().collection().onSnapshot()', function () { before(function () { return wipe(); }); - it('throws if no arguments are provided', function () { - try { - firebase.firestore().collection(COLLECTION).onSnapshot(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('expected at least one argument'); - return Promise.resolve(); - } - }); - it('returns an unsubscribe function', function () { - const unsub = firebase - .firestore() - .collection(`${COLLECTION}/foo/bar1`) - .onSnapshot(() => {}); + describe('v8 compatibility', function () { + it('throws if no arguments are provided', function () { + try { + firebase.firestore().collection(COLLECTION).onSnapshot(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('expected at least one argument'); + return Promise.resolve(); + } + }); - unsub.should.be.a.Function(); - unsub(); - }); + it('returns an unsubscribe function', function () { + const unsub = firebase + .firestore() + .collection(`${COLLECTION}/foo/bar1`) + .onSnapshot(() => {}); - it('accepts a single callback function with snapshot', async function () { - const callback = sinon.spy(); - const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar2`).onSnapshot(callback); + unsub.should.be.a.Function(); + unsub(); + }); - await Utils.spyToBeCalledOnceAsync(callback); + it('accepts a single callback function with snapshot', async function () { + const callback = sinon.spy(); + const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar2`).onSnapshot(callback); - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); - should.equal(callback.args[0][1], null); - unsub(); - }); + await Utils.spyToBeCalledOnceAsync(callback); - it('accepts a single callback function with Error', async function () { - const callback = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot(callback); + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(callback.args[0][1], null); + unsub(); + }); - await Utils.spyToBeCalledOnceAsync(callback); + it('accepts a single callback function with Error', async function () { + const callback = sinon.spy(); + const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot(callback); - callback.should.be.calledOnce(); - callback.args[0][1].code.should.containEql('firestore/permission-denied'); - should.equal(callback.args[0][0], null); - unsub(); - }); + await Utils.spyToBeCalledOnceAsync(callback); - describe('multiple callbacks', function () { - it('calls onNext when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase - .firestore() - .collection(`${COLLECTION}/foo/bar3`) - .onSnapshot(onNext, onError); + callback.should.be.calledOnce(); + callback.args[0][1].code.should.containEql('firestore/permission-denied'); + should.equal(callback.args[0][0], null); + unsub(); + }); - await Utils.spyToBeCalledOnceAsync(onNext); + describe('multiple callbacks', function () { + it('calls onNext when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase + .firestore() + .collection(`${COLLECTION}/foo/bar3`) + .onSnapshot(onNext, onError); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); + it('calls onError with Error', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase + .firestore() + .collection(NO_RULE_COLLECTION) + .onSnapshot(onNext, onError); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); }); - it('calls onError with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot(onNext, onError); + describe('objects of callbacks', function () { + it('calls next when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar4`).onSnapshot({ + next: onNext, + error: onError, + }); + + await Utils.spyToBeCalledOnceAsync(onNext); - await Utils.spyToBeCalledOnceAsync(onError); + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); + it('calls error with Error', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ + next: onNext, + error: onError, + }); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); }); - }); - describe('objects of callbacks', function () { - it('calls next when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar4`).onSnapshot({ - next: onNext, - error: onError, + describe('SnapshotListenerOptions + callbacks', function () { + it('calls callback with snapshot when successful', async function () { + const callback = sinon.spy(); + const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar5`).onSnapshot( + { + includeMetadataChanges: false, + }, + callback, + ); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(callback.args[0][1], null); + unsub(); }); - await Utils.spyToBeCalledOnceAsync(onNext); + it('calls callback with Error', async function () { + const callback = sinon.spy(); + const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot( + { + includeMetadataChanges: false, + }, + callback, + ); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][1].code.should.containEql('firestore/permission-denied'); + should.equal(callback.args[0][0], null); + unsub(); + }); - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); + it('calls next with snapshot when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const colRef = firebase + .firestore() + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/next-with-snapshot`); + const unsub = colRef.onSnapshot( + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot( + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); }); - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ - next: onNext, - error: onError, + describe('SnapshotListenerOptions + object of callbacks', function () { + it('calls next with snapshot when successful', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar7`).onSnapshot( + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); }); - await Utils.spyToBeCalledOnceAsync(onError); + it('calls error with Error', async function () { + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot( + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); + it('throws if SnapshotListenerOptions is invalid', function () { + try { + firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ + includeMetadataChanges: 123, + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", + ); + return Promise.resolve(); + } + }); + + it('throws if next callback is invalid', function () { + try { + firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ + next: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.next' or 'onNext' expected a function"); + return Promise.resolve(); + } + }); + + it('throws if error callback is invalid', function () { + try { + firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ + error: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.error' or 'onError' expected a function"); + return Promise.resolve(); + } }); - }); - describe('SnapshotListenerOptions + callbacks', function () { - it('calls callback with snapshot when successful', async function () { + // FIXME test disabled due to flakiness in CI E2E tests. + // Registered 4 of 3 expected calls once (!?), 3 of 2 expected calls once. + it('unsubscribes from further updates', async function () { const callback = sinon.spy(); - const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar5`).onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); - await Utils.spyToBeCalledOnceAsync(callback); + const collection = firebase + .firestore() + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/unsubscribe-updates`); - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); - should.equal(callback.args[0][1], null); + const unsub = collection.onSnapshot(callback); + await Utils.sleep(2000); + await collection.add({}); + await collection.add({}); unsub(); + await Utils.sleep(2000); + await collection.add({}); + await Utils.sleep(2000); + callback.should.be.callCount(3); }); + }); - it('calls callback with Error', async function () { - const callback = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); + describe('modular', function () { + it('throws if no arguments are provided', function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(getFirestore(), COLLECTION)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('expected at least one argument'); + return Promise.resolve(); + } + }); - await Utils.spyToBeCalledOnceAsync(callback); + it('returns an unsubscribe function', function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const unsub = onSnapshot(collection(getFirestore(), `${COLLECTION}/foo/bar1`), () => {}); - callback.should.be.calledOnce(); - callback.args[0][1].code.should.containEql('firestore/permission-denied'); - should.equal(callback.args[0][0], null); + unsub.should.be.a.Function(); unsub(); }); - it('calls next with snapshot when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/next-with-snapshot`); - const unsub = colRef.onSnapshot( - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); + it('accepts a single callback function with snapshot', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const callback = sinon.spy(); + const unsub = onSnapshot(collection(getFirestore(), `${COLLECTION}/foo/bar2`), callback); - await Utils.spyToBeCalledOnceAsync(onNext); + await Utils.spyToBeCalledOnceAsync(callback); - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); - should.equal(onNext.args[0][1], undefined); + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(callback.args[0][1], null); unsub(); }); - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot( - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); + describe('multiple callbacks', function () { + it('calls onNext when successful', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(getFirestore(), `${COLLECTION}/foo/bar3`), + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); - await Utils.spyToBeCalledOnceAsync(onError); + it('calls onError with Error', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(collection(getFirestore(), NO_RULE_COLLECTION), onNext, onError); - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); }); - }); - describe('SnapshotListenerOptions + object of callbacks', function () { - it('calls next with snapshot when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar7`).onSnapshot( - { - includeMetadataChanges: false, - }, - { + describe('objects of callbacks', function () { + it('calls next when successful', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(collection(getFirestore(), `${COLLECTION}/foo/bar4`), { next: onNext, error: onError, - }, - ); + }); - await Utils.spyToBeCalledOnceAsync(onNext); + await Utils.spyToBeCalledOnceAsync(onNext); - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot( - { - includeMetadataChanges: false, - }, - { + it('calls error with Error', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(collection(getFirestore(), NO_RULE_COLLECTION), { next: onNext, error: onError, - }, - ); + }); - await Utils.spyToBeCalledOnceAsync(onError); + await Utils.spyToBeCalledOnceAsync(onError); - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); }); - }); - it('throws if SnapshotListenerOptions is invalid', function () { - try { - firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ - includeMetadataChanges: 123, + describe('SnapshotListenerOptions + callbacks', function () { + it('calls callback with snapshot when successful', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const callback = sinon.spy(); + const unsub = onSnapshot( + collection(getFirestore(), `${COLLECTION}/foo/bar5`), + { + includeMetadataChanges: false, + }, + callback, + ); + + await Utils.spyToBeCalledOnceAsync(callback); + + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(callback.args[0][1], null); + unsub(); }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", - ); - return Promise.resolve(); - } - }); - it('throws if next callback is invalid', function () { - try { - firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ - next: 'foo', + it('calls next with snapshot when successful', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const colRef = collection( + getFirestore(), + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/next-with-snapshot`, + ); + const unsub = onSnapshot( + colRef, + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.next' or 'onNext' expected a function"); - return Promise.resolve(); - } - }); - it('throws if error callback is invalid', function () { - try { - firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ - error: 'foo', + it('calls error with Error', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(getFirestore(), NO_RULE_COLLECTION), + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.error' or 'onError' expected a function"); - return Promise.resolve(); - } - }); + }); + + describe('SnapshotListenerOptions + object of callbacks', function () { + it('calls next with snapshot when successful', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(getFirestore(), `${COLLECTION}/foo/bar7`), + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onNext); + + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); + }); + + it('calls error with Error', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(getFirestore(), NO_RULE_COLLECTION), + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); + + await Utils.spyToBeCalledOnceAsync(onError); + + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); - // FIXME test disabled due to flakiness in CI E2E tests. - // Registered 4 of 3 expected calls once (!?), 3 of 2 expected calls once. - it('unsubscribes from further updates', async function () { - const callback = sinon.spy(); - - const collection = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/unsubscribe-updates`); - - const unsub = collection.onSnapshot(callback); - await Utils.sleep(2000); - await collection.add({}); - await collection.add({}); - unsub(); - await Utils.sleep(2000); - await collection.add({}); - await Utils.sleep(2000); - callback.should.be.callCount(3); + it('throws if SnapshotListenerOptions is invalid', function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(getFirestore(), NO_RULE_COLLECTION), { + includeMetadataChanges: 123, + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", + ); + return Promise.resolve(); + } + }); + + it('throws if next callback is invalid', function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(getFirestore(), NO_RULE_COLLECTION), { + next: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.next' or 'onNext' expected a function"); + return Promise.resolve(); + } + }); + + it('throws if error callback is invalid', function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(getFirestore(), NO_RULE_COLLECTION), { + error: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.error' or 'onError' expected a function"); + return Promise.resolve(); + } + }); + + // FIXME test disabled due to flakiness in CI E2E tests. + // Registered 4 of 3 expected calls once (!?), 3 of 2 expected calls once. + it('unsubscribes from further updates', async function () { + const { getFirestore, collection, onSnapshot, addDoc } = firestoreModular; + const callback = sinon.spy(); + + const collectionRef = collection( + getFirestore(), + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/unsubscribe-updates`, + ); + + const unsub = onSnapshot(collectionRef, callback); + await Utils.sleep(2000); + await addDoc(collectionRef, {}); + await addDoc(collectionRef, {}); + unsub(); + await Utils.sleep(2000); + await addDoc(collectionRef, {}); + await Utils.sleep(2000); + callback.should.be.callCount(3); + }); }); }); diff --git a/packages/firestore/e2e/Query/orderBy.e2e.js b/packages/firestore/e2e/Query/orderBy.e2e.js index cce5ec6430..f2b3e580bc 100644 --- a/packages/firestore/e2e/Query/orderBy.e2e.js +++ b/packages/firestore/e2e/Query/orderBy.e2e.js @@ -20,109 +20,237 @@ describe('firestore().collection().orderBy()', function () { before(function () { return wipe(); }); - it('throws if fieldPath is not valid', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' must be a string or instance of FieldPath"); - return Promise.resolve(); - } - }); - it('throws if fieldPath string is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('.foo.bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if fieldPath is not valid', function () { + try { + firebase.firestore().collection(COLLECTION).orderBy(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' must be a string or instance of FieldPath"); + return Promise.resolve(); + } + }); - it('throws if direction string is not valid', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo', 'up'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'directionStr' must be one of 'asc' or 'desc'"); - return Promise.resolve(); - } - }); + it('throws if fieldPath string is invalid', function () { + try { + firebase.firestore().collection(COLLECTION).orderBy('.foo.bar'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); - it('throws if a startAt()/startAfter() has already been set', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/startATstartAfter`); - await doc.set({ foo: 'bar' }); - const snapshot = await doc.get(); - - firebase.firestore().collection(COLLECTION).startAt(snapshot).orderBy('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You must not call startAt() or startAfter()'); - return Promise.resolve(); - } - }); + it('throws if direction string is not valid', function () { + try { + firebase.firestore().collection(COLLECTION).orderBy('foo', 'up'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'directionStr' must be one of 'asc' or 'desc'"); + return Promise.resolve(); + } + }); - it('throws if a endAt()/endBefore() has already been set', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/endAtendBefore`); - await doc.set({ foo: 'bar' }); - const snapshot = await doc.get(); - - firebase.firestore().collection(COLLECTION).endAt(snapshot).orderBy('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You must not call endAt() or endBefore()'); - return Promise.resolve(); - } - }); + it('throws if a startAt()/startAfter() has already been set', async function () { + try { + const doc = firebase.firestore().doc(`${COLLECTION}/startATstartAfter`); + await doc.set({ foo: 'bar' }); + const snapshot = await doc.get(); - it('throws if duplicating the order field path', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo.bar').orderBy('foo.bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Order by clause cannot contain duplicate fields'); - return Promise.resolve(); - } - }); + firebase.firestore().collection(COLLECTION).startAt(snapshot).orderBy('foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You must not call startAt() or startAfter()'); + return Promise.resolve(); + } + }); + + it('throws if a endAt()/endBefore() has already been set', async function () { + try { + const doc = firebase.firestore().doc(`${COLLECTION}/endAtendBefore`); + await doc.set({ foo: 'bar' }); + const snapshot = await doc.get(); + + firebase.firestore().collection(COLLECTION).endAt(snapshot).orderBy('foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You must not call endAt() or endBefore()'); + return Promise.resolve(); + } + }); + + it('throws if duplicating the order field path', function () { + try { + firebase.firestore().collection(COLLECTION).orderBy('foo.bar').orderBy('foo.bar'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Order by clause cannot contain duplicate fields'); + return Promise.resolve(); + } + }); + + it('orders by a value ASC', async function () { + const colRef = firebase + .firestore() + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/order-asc`); + + await colRef.add({ value: 1 }); + await colRef.add({ value: 3 }); + await colRef.add({ value: 2 }); + + const snapshot = await colRef.orderBy('value', 'asc').get(); + const expected = [1, 2, 3]; + + snapshot.forEach((docSnap, i) => { + docSnap.data().value.should.eql(expected[i]); + }); + }); - it('orders by a value ASC', async function () { - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/order-asc`); + it('orders by a value DESC', async function () { + const colRef = firebase + .firestore() + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/order-desc`); - await colRef.add({ value: 1 }); - await colRef.add({ value: 3 }); - await colRef.add({ value: 2 }); + await colRef.add({ value: 1 }); + await colRef.add({ value: 3 }); + await colRef.add({ value: 2 }); - const snapshot = await colRef.orderBy('value', 'asc').get(); - const expected = [1, 2, 3]; + const snapshot = await colRef + .orderBy(new firebase.firestore.FieldPath('value'), 'desc') + .get(); + const expected = [3, 2, 1]; - snapshot.forEach((docSnap, i) => { - docSnap.data().value.should.eql(expected[i]); + snapshot.forEach((docSnap, i) => { + docSnap.data().value.should.eql(expected[i]); + }); }); }); - it('orders by a value DESC', async function () { - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/order-desc`); + describe('modular', function () { + it('throws if fieldPath is not valid', function () { + const { getFirestore, collection, query, orderBy } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), orderBy(123)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' must be a string or instance of FieldPath"); + return Promise.resolve(); + } + }); + + it('throws if fieldPath string is invalid', function () { + const { getFirestore, collection, query, orderBy } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), orderBy('.foo.bar')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if direction string is not valid', function () { + const { getFirestore, collection, query, orderBy } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), orderBy('foo', 'up')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'directionStr' must be one of 'asc' or 'desc'"); + return Promise.resolve(); + } + }); + + it('throws if a startAt()/startAfter() has already been set', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, query, startAt, orderBy } = + firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/startATstartAfter`); + await setDoc(docRef, { foo: 'bar' }); + const snapshot = await getDocs(docRef); + + query(collection(db, COLLECTION), startAt(snapshot), orderBy('foo')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You must not call startAt() or startAfter()'); + return Promise.resolve(); + } + }); + + it('throws if a endAt()/endBefore() has already been set', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, query, endAt, orderBy } = + firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/endAtendBefore`); + await setDoc(docRef, { foo: 'bar' }); + const snapshot = await getDocs(docRef); + + query(collection(db, COLLECTION), endAt(snapshot), orderBy('foo')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You must not call endAt() or endBefore()'); + return Promise.resolve(); + } + }); + + it('throws if duplicating the order field path', function () { + const { getFirestore, collection, query, orderBy } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), orderBy('foo.bar'), orderBy('foo.bar')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Order by clause cannot contain duplicate fields'); + return Promise.resolve(); + } + }); + + it('orders by a value ASC', async function () { + const { getFirestore, collection, addDoc, getDocs, query, orderBy } = firestoreModular; + const colRef = collection( + getFirestore(), + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/order-asc`, + ); + + await addDoc(colRef, { value: 1 }); + await addDoc(colRef, { value: 3 }); + await addDoc(colRef, { value: 2 }); + + const snapshot = await getDocs(query(colRef, orderBy('value', 'asc'))); + const expected = [1, 2, 3]; + + snapshot.forEach((docSnap, i) => { + docSnap.data().value.should.eql(expected[i]); + }); + }); + + it('orders by a value DESC', async function () { + const { getFirestore, collection, addDoc, getDocs, query, orderBy, FieldPath } = + firestoreModular; + const colRef = collection( + getFirestore(), + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/order-desc`, + ); - await colRef.add({ value: 1 }); - await colRef.add({ value: 3 }); - await colRef.add({ value: 2 }); + await addDoc(colRef, { value: 1 }); + await addDoc(colRef, { value: 3 }); + await addDoc(colRef, { value: 2 }); - const snapshot = await colRef.orderBy(new firebase.firestore.FieldPath('value'), 'desc').get(); - const expected = [3, 2, 1]; + const snapshot = await getDocs(query(colRef, orderBy(new FieldPath('value'), 'desc'))); + const expected = [3, 2, 1]; - snapshot.forEach((docSnap, i) => { - docSnap.data().value.should.eql(expected[i]); + snapshot.forEach((docSnap, i) => { + docSnap.data().value.should.eql(expected[i]); + }); }); }); }); diff --git a/packages/firestore/e2e/Query/query.e2e.js b/packages/firestore/e2e/Query/query.e2e.js index d8da393392..4b462f4911 100644 --- a/packages/firestore/e2e/Query/query.e2e.js +++ b/packages/firestore/e2e/Query/query.e2e.js @@ -16,58 +16,121 @@ const COLLECTION = 'firestore'; describe('FirestoreQuery/FirestoreQueryModifiers', function () { - it('should not mutate previous queries (#2691)', async function () { - const queryBefore = firebase.firestore().collection(COLLECTION).where('age', '>', 30); - const queryAfter = queryBefore.orderBy('age'); - queryBefore._modifiers._orders.length.should.equal(0); - queryBefore._modifiers._filters.length.should.equal(1); + describe('v8 compatibility', function () { + it('should not mutate previous queries (#2691)', async function () { + const queryBefore = firebase.firestore().collection(COLLECTION).where('age', '>', 30); + const queryAfter = queryBefore.orderBy('age'); + queryBefore._modifiers._orders.length.should.equal(0); + queryBefore._modifiers._filters.length.should.equal(1); - queryAfter._modifiers._orders.length.should.equal(1); - queryAfter._modifiers._filters.length.should.equal(1); - }); + queryAfter._modifiers._orders.length.should.equal(1); + queryAfter._modifiers._filters.length.should.equal(1); + }); + + it('throws if where equality operator is invoked, and the where fieldPath parameter matches any orderBy parameter', async function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where('foo', '==', 'bar') + .orderBy('foo') + .limit(1) + .endAt(2); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Invalid query'); + } - it('throws if where equality operator is invoked, and the where fieldPath parameter matches any orderBy parameter', async function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('foo') - .limit(1) - .endAt(2); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Invalid query'); - } + try { + firebase + .firestore() + .collection(COLLECTION) + .where('foo', '==', 'bar') + .orderBy('bar') + .orderBy('foo') + .limit(1) + .endAt(2); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Invalid query'); + } + }); - try { - firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('bar') - .orderBy('foo') - .limit(1) - .endAt(2); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Invalid query'); - } + it('throws if where inequality operator is invoked, and the where fieldPath does not match initial orderBy parameter', async function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where('foo', '>', 'bar') + .orderBy('bar') + .orderBy('foo') + .limit(1) + .endAt(2); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Invalid query'); + } + }); }); - it('throws if where inequality operator is invoked, and the where fieldPath does not match initial orderBy parameter', async function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where('foo', '>', 'bar') - .orderBy('bar') - .orderBy('foo') - .limit(1) - .endAt(2); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Invalid query'); - } + describe('modular', function () { + it('should not mutate previous queries (#2691)', async function () { + const { getFirestore, collection, query, where, orderBy } = firestoreModular; + const queryBefore = query(collection(getFirestore(), COLLECTION), where('age', '>', 30)); + const queryAfter = query(queryBefore, orderBy('age')); + queryBefore._modifiers._orders.length.should.equal(0); + queryBefore._modifiers._filters.length.should.equal(1); + + queryAfter._modifiers._orders.length.should.equal(1); + queryAfter._modifiers._filters.length.should.equal(1); + }); + + it('throws if where equality operator is invoked, and the where fieldPath parameter matches any orderBy parameter', async function () { + const { getFirestore, collection, query, where, orderBy, limit, endAt } = firestoreModular; + const db = getFirestore(); + try { + query( + collection(db, COLLECTION), + where('foo', '==', 'bar'), + orderBy('foo'), + limit(1), + endAt(2), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Invalid query'); + } + + try { + query( + collection(db, COLLECTION) + .where('foo', '==', 'bar') + .orderBy('bar') + .orderBy('foo') + .limit(1) + .endAt(2), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Invalid query'); + } + }); + + it('throws if where inequality operator is invoked, and the where fieldPath does not match initial orderBy parameter', async function () { + const { getFirestore, collection, query, where, orderBy, limit, endAt } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where('foo', '>', 'bar'), + orderBy('bar'), + orderBy('foo'), + limit(1), + endAt(2), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Invalid query'); + } + }); }); }); diff --git a/packages/firestore/e2e/Query/startAfter.e2e.js b/packages/firestore/e2e/Query/startAfter.e2e.js index d4171add0b..0df620c1f0 100644 --- a/packages/firestore/e2e/Query/startAfter.e2e.js +++ b/packages/firestore/e2e/Query/startAfter.e2e.js @@ -20,134 +20,307 @@ describe('firestore().collection().startAfter()', function () { before(function () { return wipe(); }); - it('throws if no argument provided', function () { - try { - firebase.firestore().collection(COLLECTION).startAfter(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Expected a DocumentSnapshot or list of field values but got undefined', - ); - return Promise.resolve(); - } - }); - it('throws if a inconsistent order number', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo').startAfter('bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('The number of arguments must be less than or equal'); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if no argument provided', function () { + try { + firebase.firestore().collection(COLLECTION).startAfter(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'Expected a DocumentSnapshot or list of field values but got undefined', + ); + return Promise.resolve(); + } + }); - it('throws if providing snapshot and field values', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/foo`).get(); - firebase.firestore().collection(COLLECTION).startAfter(doc, 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Expected DocumentSnapshot or list of field values'); - return Promise.resolve(); - } - }); + it('throws if a inconsistent order number', function () { + try { + firebase.firestore().collection(COLLECTION).orderBy('foo').startAfter('bar', 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('The number of arguments must be less than or equal'); + return Promise.resolve(); + } + }); - it('throws if provided snapshot does not exist', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); - firebase.firestore().collection(COLLECTION).startAfter(doc); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); - return Promise.resolve(); - } - }); + it('throws if providing snapshot and field values', async function () { + try { + const doc = await firebase.firestore().doc(`${COLLECTION}/foo`).get(); + firebase.firestore().collection(COLLECTION).startAfter(doc, 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Expected DocumentSnapshot or list of field values'); + return Promise.resolve(); + } + }); - it('throws if order used with snapshot but fields do not exist', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); - await doc.set({ foo: { bar: 'baz' } }); - const snap = await doc.get(); - - firebase.firestore().collection(COLLECTION).orderBy('foo.baz').startAfter(snap); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'You are trying to start or end a query using a document for which the field', - ); - return Promise.resolve(); - } - }); + it('throws if provided snapshot does not exist', async function () { + try { + const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); + firebase.firestore().collection(COLLECTION).startAfter(doc); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); + return Promise.resolve(); + } + }); - it('starts after field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/startAfter/collection`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + it('throws if order used with snapshot but fields do not exist', async function () { + try { + const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); + await doc.set({ foo: { bar: 'baz' } }); + const snap = await doc.get(); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); + firebase.firestore().collection(COLLECTION).orderBy('foo.baz').startAfter(snap); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'You are trying to start or end a query using a document for which the field', + ); + return Promise.resolve(); + } + }); - const qs = await colRef.orderBy('bar.value', 'desc').startAfter(2).get(); + it('starts after field values', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/startAfter/collection`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc1'); - }); + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 1 } }), + doc2.set({ foo: 2, bar: { value: 2 } }), + doc3.set({ foo: 3, bar: { value: 3 } }), + ]); - it('starts after snapshot field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/startAfter/snapshotFields`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + const qs = await colRef.orderBy('bar.value', 'desc').startAfter(2).get(); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 'a' } }), - doc2.set({ foo: 2, bar: { value: 'b' } }), - doc3.set({ foo: 3, bar: { value: 'c' } }), - ]); + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc1'); + }); - const startAfter = await doc2.get(); + it('starts after snapshot field values', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/startAfter/snapshotFields`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); - const qs = await colRef.orderBy('bar.value').startAfter(startAfter).get(); + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 'a' } }), + doc2.set({ foo: 2, bar: { value: 'b' } }), + doc3.set({ foo: 3, bar: { value: 'c' } }), + ]); - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc3'); - }); + const startAfter = await doc2.get(); + + const qs = await colRef.orderBy('bar.value').startAfter(startAfter).get(); - it('startAfter snapshot', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshot`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc3'); + }); - await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); + it('startAfter snapshot', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshot`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); - const startAfter = await doc2.get(); + await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); - const qs = await colRef.startAfter(startAfter).get(); + const startAfter = await doc2.get(); - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc3'); + const qs = await colRef.startAfter(startAfter).get(); + + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc3'); + }); + + it('runs startAfter & endBefore in the same query', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/startAfter/snapshot`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); + + await Promise.all([doc1.set({ age: 1 }), doc2.set({ age: 2 }), doc3.set({ age: 3 })]); + + const first = await doc1.get(); + const last = await doc3.get(); + + const inBetween = await colRef.orderBy('age', 'asc').startAfter(first).endBefore(last).get(); + + inBetween.docs.length.should.eql(1); + inBetween.docs[0].id.should.eql('doc2'); + }); }); - it('runs startAfter & endBefore in the same query', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/startAfter/snapshot`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + describe('modular', function () { + it('throws if no argument provided', function () { + const { getFirestore, collection, query, startAfter } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), startAfter()); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'Expected a DocumentSnapshot or list of field values but got undefined', + ); + return Promise.resolve(); + } + }); + + it('throws if a inconsistent order number', function () { + const { getFirestore, collection, query, orderBy, startAfter } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), orderBy('foo'), startAfter('bar', 'baz')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('The number of arguments must be less than or equal'); + return Promise.resolve(); + } + }); + + it('throws if providing snapshot and field values', async function () { + const { getFirestore, collection, doc, getDocs, query, startAfter } = firestoreModular; + const db = getFirestore(); + try { + const docRef = await getDocs(doc(db, `${COLLECTION}/foo`)); + query(collection(db, COLLECTION), startAfter(docRef, 'baz')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Expected DocumentSnapshot or list of field values'); + return Promise.resolve(); + } + }); + + it('throws if provided snapshot does not exist', async function () { + const { getFirestore, collection, doc, getDocs, query, startAfter } = firestoreModular; + const db = getFirestore(); + try { + const docRef = await getDocs(doc(db, `${COLLECTION}/idonotexist`)); + query(collection(db, COLLECTION), startAfter(docRef)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); + return Promise.resolve(); + } + }); - await Promise.all([doc1.set({ age: 1 }), doc2.set({ age: 2 }), doc3.set({ age: 3 })]); + it('throws if order used with snapshot but fields do not exist', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, query, orderBy, startAfter } = + firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/iexist`); + await setDoc(docRef, { foo: { bar: 'baz' } }); + const snap = await getDocs(docRef); - const first = await doc1.get(); - const last = await doc3.get(); + query(collection(db, COLLECTION), orderBy('foo.baz'), startAfter(snap)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'You are trying to start or end a query using a document for which the field', + ); + return Promise.resolve(); + } + }); - const inBetween = await colRef.orderBy('age', 'asc').startAfter(first).endBefore(last).get(); + it('starts after field values', async function () { + const { getFirestore, collection, doc, setDoc, query, orderBy, startAfter, getDocs } = + firestoreModular; + const db = getFirestore(); + const colRef = collection(db, `${COLLECTION}/startAfter/collection`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + + await Promise.all([ + setDoc(doc1, { foo: 1, bar: { value: 1 } }), + setDoc(doc2, { foo: 2, bar: { value: 2 } }), + setDoc(doc3, { foo: 3, bar: { value: 3 } }), + ]); + + const qs = await getDocs(query(colRef, orderBy('bar.value', 'desc'), startAfter(2))); + + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc1'); + }); + + it('starts after snapshot field values', async function () { + const { getFirestore, collection, doc, setDoc, query, startAfter, getDocs } = + firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/startAfter/snapshotFields`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + + await Promise.all([ + setDoc(doc1, { foo: 1, bar: { value: 'a' } }), + setDoc(doc2, { foo: 2, bar: { value: 'b' } }), + setDoc(doc3, { foo: 3, bar: { value: 'c' } }), + ]); + + const startAfterSnapshot = await getDocs(doc2); + + const qs = await getDocs(query(colRef.orderBy('bar.value'), startAfter(startAfterSnapshot))); + + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc3'); + }); + + it('startAfter snapshot', async function () { + const { getFirestore, collection, doc, setDoc, query, startAfter, getDocs } = + firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/endsAt/snapshot`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + + await Promise.all([ + setDoc(doc1, { foo: 1 }), + setDoc(doc2, { foo: 1 }), + setDoc(doc3, { foo: 1 }), + ]); + + const startAfterSnapshot = await getDocs(doc2); + + const qs = await getDocs(query(colRef, startAfter(startAfterSnapshot))); + + qs.docs.length.should.eql(1); + qs.docs[0].id.should.eql('doc3'); + }); + + it('runs startAfter & endBefore in the same query', async function () { + const { + getFirestore, + collection, + doc, + setDoc, + getDocs, + query, + orderBy, + startAfter, + endBefore, + } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/startAfter/snapshot`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + + await Promise.all([ + setDoc(doc1, { age: 1 }), + setDoc(doc2, { age: 2 }), + setDoc(doc3, { age: 3 }), + ]); + + const first = await getDocs(doc1); + const last = await getDocs(doc3); + + const inBetween = await getDocs( + query(colRef, orderBy('age', 'asc'), startAfter(first), endBefore(last)), + ); - inBetween.docs.length.should.eql(1); - inBetween.docs[0].id.should.eql('doc2'); + inBetween.docs.length.should.eql(1); + inBetween.docs[0].id.should.eql('doc2'); + }); }); }); diff --git a/packages/firestore/e2e/Query/startAt.e2e.js b/packages/firestore/e2e/Query/startAt.e2e.js index b9e4a76980..7bdaa2e048 100644 --- a/packages/firestore/e2e/Query/startAt.e2e.js +++ b/packages/firestore/e2e/Query/startAt.e2e.js @@ -20,121 +20,262 @@ describe('firestore().collection().startAt()', function () { before(function () { return wipe(); }); - it('throws if no argument provided', function () { - try { - firebase.firestore().collection(COLLECTION).startAt(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Expected a DocumentSnapshot or list of field values but got undefined', - ); - return Promise.resolve(); - } - }); - it('throws if a inconsistent order number', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo').startAt('bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('The number of arguments must be less than or equal'); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if no argument provided', function () { + try { + firebase.firestore().collection(COLLECTION).startAt(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'Expected a DocumentSnapshot or list of field values but got undefined', + ); + return Promise.resolve(); + } + }); - it('throws if providing snapshot and field values', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/foo`).get(); - firebase.firestore().collection(COLLECTION).startAt(doc, 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Expected DocumentSnapshot or list of field values'); - return Promise.resolve(); - } - }); + it('throws if a inconsistent order number', function () { + try { + firebase.firestore().collection(COLLECTION).orderBy('foo').startAt('bar', 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('The number of arguments must be less than or equal'); + return Promise.resolve(); + } + }); - it('throws if provided snapshot does not exist', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); - firebase.firestore().collection(COLLECTION).startAt(doc); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); - return Promise.resolve(); - } - }); + it('throws if providing snapshot and field values', async function () { + try { + const doc = await firebase.firestore().doc(`${COLLECTION}/foo`).get(); + firebase.firestore().collection(COLLECTION).startAt(doc, 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Expected DocumentSnapshot or list of field values'); + return Promise.resolve(); + } + }); - it('throws if order used with snapshot but fields do not exist', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); - - await doc.set({ foo: { bar: 'baz' } }); - const snap = await doc.get(); - - firebase.firestore().collection(COLLECTION).orderBy('foo.baz').startAt(snap); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'You are trying to start or end a query using a document for which the field', - ); - return Promise.resolve(); - } - }); + it('throws if provided snapshot does not exist', async function () { + try { + const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); + firebase.firestore().collection(COLLECTION).startAt(doc); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); + return Promise.resolve(); + } + }); - it('starts at field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/startAt/collection`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + it('throws if order used with snapshot but fields do not exist', async function () { + try { + const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); + await doc.set({ foo: { bar: 'baz' } }); + const snap = await doc.get(); - const qs = await colRef.orderBy('bar.value', 'desc').startAt(2).get(); + firebase.firestore().collection(COLLECTION).orderBy('foo.baz').startAt(snap); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'You are trying to start or end a query using a document for which the field', + ); + return Promise.resolve(); + } + }); - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc2'); - qs.docs[1].id.should.eql('doc1'); - }); + it('starts at field values', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/startAt/collection`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); + + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 1 } }), + doc2.set({ foo: 2, bar: { value: 2 } }), + doc3.set({ foo: 3, bar: { value: 3 } }), + ]); + + const qs = await colRef.orderBy('bar.value', 'desc').startAt(2).get(); - it('starts at snapshot field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/startAt/snapshotFields`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc2'); + qs.docs[1].id.should.eql('doc1'); + }); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 'a' } }), - doc2.set({ foo: 2, bar: { value: 'b' } }), - doc3.set({ foo: 3, bar: { value: 'c' } }), - ]); + it('starts at snapshot field values', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/startAt/snapshotFields`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); - const startAt = await doc2.get(); + await Promise.all([ + doc1.set({ foo: 1, bar: { value: 'a' } }), + doc2.set({ foo: 2, bar: { value: 'b' } }), + doc3.set({ foo: 3, bar: { value: 'c' } }), + ]); - const qs = await colRef.orderBy('bar.value').startAt(startAt).get(); + const startAt = await doc2.get(); - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc2'); - qs.docs[1].id.should.eql('doc3'); + const qs = await colRef.orderBy('bar.value').startAt(startAt).get(); + + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc2'); + qs.docs[1].id.should.eql('doc3'); + }); + + it('startAt at snapshot', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshot`); + const doc1 = colRef.doc('doc1'); + const doc2 = colRef.doc('doc2'); + const doc3 = colRef.doc('doc3'); + + await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); + + const startAt = await doc2.get(); + + const qs = await colRef.startAt(startAt).get(); + + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc2'); + qs.docs[1].id.should.eql('doc3'); + }); }); - it('startAt at snapshot', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshot`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); + describe('modular', function () { + it('throws if no argument provided', function () { + const { getFirestore, collection, query, startAt } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), startAt()); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'Expected a DocumentSnapshot or list of field values but got undefined', + ); + return Promise.resolve(); + } + }); + + it('throws if a inconsistent order number', function () { + const { getFirestore, collection, query, orderBy, startAt } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), orderBy('foo'), startAt('bar', 'baz')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('The number of arguments must be less than or equal'); + return Promise.resolve(); + } + }); + + it('throws if providing snapshot and field values', async function () { + const { getFirestore, collection, doc, getDocs, query, startAt } = firestoreModular; + const db = getFirestore(); + try { + const docSnapshot = await getDocs(doc(db, `${COLLECTION}/foo`)); + query(collection(db, COLLECTION), startAt(docSnapshot, 'baz')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Expected DocumentSnapshot or list of field values'); + return Promise.resolve(); + } + }); + + it('throws if provided snapshot does not exist', async function () { + const { getFirestore, collection, doc, getDocs, query, startAt } = firestoreModular; + const db = getFirestore(); + try { + const docSnapshot = await getDocs(doc(db, `${COLLECTION}/idonotexist`)); + query(collection(db, COLLECTION), startAt(docSnapshot)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); + return Promise.resolve(); + } + }); + + it('throws if order used with snapshot but fields do not exist', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, query, orderBy, startAt } = + firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/iexist`); + + await setDoc(docRef, { foo: { bar: 'baz' } }); + const snap = await getDocs(docRef); + + query(collection(db, COLLECTION), orderBy('foo.baz'), startAt(snap)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'You are trying to start or end a query using a document for which the field', + ); + return Promise.resolve(); + } + }); + + it('starts at field values', async function () { + const { getFirestore, collection, doc, setDoc, query, orderBy, startAt, getDocs } = + firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/startAt/collection`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + + await Promise.all([ + setDoc(doc1, { foo: 1, bar: { value: 1 } }), + setDoc(doc2, { foo: 2, bar: { value: 2 } }), + setDoc(doc3, { foo: 3, bar: { value: 3 } }), + ]); + + const qs = await getDocs(query(colRef, orderBy('bar.value', 'desc'), startAt(2))); + + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc2'); + qs.docs[1].id.should.eql('doc1'); + }); + + it('starts at snapshot field values', async function () { + const { getFirestore, collection, doc, setDoc, query, orderBy, startAt, getDocs } = + firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/startAt/snapshotFields`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); + + await Promise.all([ + setDoc(doc1, { foo: 1, bar: { value: 'a' } }), + setDoc(doc2, { foo: 2, bar: { value: 'b' } }), + setDoc(doc3, { foo: 3, bar: { value: 'c' } }), + ]); + + const startAtSnapshot = await getDocs(doc2); + + const qs = await getDocs(query(colRef, orderBy('bar.value'), startAt(startAtSnapshot))); + + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc2'); + qs.docs[1].id.should.eql('doc3'); + }); + + it('startAt at snapshot', async function () { + const { getFirestore, collection, doc, setDoc, query, startAt, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/endsAt/snapshot`); + const doc1 = doc(colRef, 'doc1'); + const doc2 = doc(colRef, 'doc2'); + const doc3 = doc(colRef, 'doc3'); - await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); + await Promise.all([ + setDoc(doc1, { foo: 1 }), + setDoc(doc2, { foo: 1 }), + setDoc(doc3, { foo: 1 }), + ]); - const startAt = await doc2.get(); + const startAtSnapshot = await getDocs(doc2); - const qs = await colRef.startAt(startAt).get(); + const qs = await getDocs(query(colRef, startAt(startAtSnapshot))); - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc2'); - qs.docs[1].id.should.eql('doc3'); + qs.docs.length.should.eql(2); + qs.docs[0].id.should.eql('doc2'); + qs.docs[1].id.should.eql('doc3'); + }); }); }); diff --git a/packages/firestore/e2e/Query/where.and.filter.e2e.js b/packages/firestore/e2e/Query/where.and.filter.e2e.js index c7369d8ff5..1b1ebc7e12 100644 --- a/packages/firestore/e2e/Query/where.and.filter.e2e.js +++ b/packages/firestore/e2e/Query/where.and.filter.e2e.js @@ -24,664 +24,1389 @@ describe(' firestore().collection().where(AND Filters)', function () { return await wipe(); }); - it('throws if fieldPath string is invalid', function () { - try { + describe('v8 compatibility', function () { + it('throws if fieldPath string is invalid', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where(Filter.and(Filter('.foo.bar', '==', 1), Filter('.foo.bar', '==', 1))); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if operator string is invalid', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where(Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1))); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'opStr' is invalid"); + return Promise.resolve(); + } + }); + + it('throws if query contains multiple array-contains', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.and( + Filter('foo.bar', 'array-contains', 1), + Filter('foo.bar', 'array-contains', 1), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Queries only support a single array-contains filter'); + return Promise.resolve(); + } + }); + + it('throws if value is not defined', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.and(Filter('foo.bar', 'array-contains'), Filter('foo.bar', 'array-contains')), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'value' argument expected"); + return Promise.resolve(); + } + }); + + it('throws if null value and no equal operator', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', 'array-contains', null)), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You can only perform equals comparisons on null'); + return Promise.resolve(); + } + }); + + it('allows null to be used with equal operator', function () { firebase .firestore() .collection(COLLECTION) - .where(Filter.and(Filter('.foo.bar', '==', 1), Filter('.foo.bar', '==', 1))); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); + .where(Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', null))); + }); - it('throws if operator string is invalid', function () { - try { + it('allows null to be used with not equal operator', function () { firebase .firestore() .collection(COLLECTION) - .where(Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1))); + .where(Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '!=', null))); + }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'opStr' is invalid"); - return Promise.resolve(); - } - }); + it('throws if multiple inequalities on different paths is provided', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where(Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '>', 123))); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('All where filters with an inequality'); + return Promise.resolve(); + } + }); - it('throws if query contains multiple array-contains', function () { - try { + it('allows inequality on the same path', function () { firebase .firestore() .collection(COLLECTION) .where( Filter.and( - Filter('foo.bar', 'array-contains', 1), - Filter('foo.bar', 'array-contains', 1), + Filter('foo.bar', '>', 123), + Filter(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234), ), ); + }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Queries only support a single array-contains filter'); - return Promise.resolve(); - } - }); + it('throws if in query with no array value', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where(Filter.and(Filter('foo.bar', 'in', '123'), Filter('foo.bar', 'in', '123'))); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - it('throws if value is not defined', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and(Filter('foo.bar', 'array-contains'), Filter('foo.bar', 'array-contains')), - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' argument expected"); - return Promise.resolve(); - } - }); + it('throws if array-contains-any query with no array value', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.and( + Filter('foo.bar', 'array-contains-any', '123'), + Filter('foo.bar', 'array-contains-any', '123'), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - it('throws if null value and no equal operator', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', 'array-contains', null)), - ); + it('throws if in query array length is greater than 10', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.and( + Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('maximum of 10 elements in the value'); + return Promise.resolve(); + } + }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You can only perform equals comparisons on null'); - return Promise.resolve(); - } - }); + it('throws if query has multiple array-contains-any filter', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.and( + Filter('foo.bar', 'array-contains-any', [1]), + Filter('foo.bar', 'array-contains-any', [1]), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); + return Promise.resolve(); + } + }); - it('allows null to be used with equal operator', function () { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', null))); - }); + it("should throw error when using 'not-in' operator twice", async function () { + const ref = firebase.firestore().collection(COLLECTION); - it('allows null to be used with not equal operator', function () { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '!=', null))); - }); + try { + ref.where(Filter.and(Filter('foo.bar', 'not-in', [1]), Filter('foo.bar', 'not-in', [2]))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } + }); - it('throws if multiple inequalities on different paths is provided', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '>', 123))); + it("should throw error when combining 'not-in' operator with '!=' operator", async function () { + const ref = firebase.firestore().collection(COLLECTION); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('All where filters with an inequality'); - return Promise.resolve(); - } - }); + try { + ref.where(Filter.and(Filter('foo.bar', '!=', [1]), Filter('foo.bar', 'not-in', [2]))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", + ); + return Promise.resolve(); + } + }); - it('allows inequality on the same path', function () { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and( - Filter('foo.bar', '>', 123), - Filter(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234), - ), - ); - }); + it("should throw error when combining 'not-in' operator with 'in' operator", async function () { + const ref = firebase.firestore().collection(COLLECTION); - it('throws if in query with no array value', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter.and(Filter('foo.bar', 'in', '123'), Filter('foo.bar', 'in', '123'))); + try { + ref.where(Filter.and(Filter('foo.bar', 'in', [1]), Filter('foo.bar', 'not-in', [2]))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { + const ref = firebase.firestore().collection(COLLECTION); - it('throws if array-contains-any query with no array value', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( + try { + ref.where( Filter.and( - Filter('foo.bar', 'array-contains-any', '123'), - Filter('foo.bar', 'array-contains-any', '123'), + Filter('foo.bar', 'array-contains-any', [1]), + Filter('foo.bar', 'not-in', [2]), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if in query array length is greater than 10', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and( - Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), - Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), - ), + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", ); + return Promise.resolve(); + } + }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('maximum of 10 elements in the value'); - return Promise.resolve(); - } - }); + it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { + const ref = firebase.firestore().collection(COLLECTION); - it('throws if query has multiple array-contains-any filter', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( + try { + ref.where( Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'array-contains-any', [1]), + Filter('foo.bar', '==', 1), + Filter('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 10 elements in the value array.', + ); + return Promise.resolve(); + } + }); - it("should throw error when using 'not-in' operator twice", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.and( + Filter(firebase.firestore.FieldPath.documentId(), '==', ['document-id']), + Filter('foo.bar', 'not-in', [1, 2, 3, 4]), + ), + ) + .orderBy('differentOrderBy', 'desc'); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); + return Promise.resolve(); + } + }); - try { - ref.where(Filter.and(Filter('foo.bar', 'not-in', [1]), Filter('foo.bar', 'not-in', [2]))); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'not-in' filter."); - return Promise.resolve(); - } - }); + it("should throw error when using '!=' operator twice ", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('foo.baz', '!=', 2))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); - it("should throw error when combining 'not-in' operator with '!=' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it("should throw error when combining '!=' operator with any other inequality operator on a different field", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>', 2))); + return Promise.reject(new Error('Did not throw an Error on >.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<', 2))); + return Promise.reject(new Error('Did not throw an Error on <.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<=', 2))); + return Promise.reject(new Error('Did not throw an Error <=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>=', 2))); + return Promise.reject(new Error('Did not throw an Error >=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } - try { - ref.where(Filter.and(Filter('foo.bar', '!=', [1]), Filter('foo.bar', 'not-in', [2]))); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with '!=' inequality filters", - ); return Promise.resolve(); - } - }); + }); - it("should throw error when combining 'not-in' operator with 'in' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + /* Queries */ + // Equals and another filter: '==', '>', '>=', '<', '<=', '!=' + it('returns with where "==" & "==" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', bar: 'baz' }; + await Promise.all([ + colRef.add({ foo: [1, '1', 'something'] }), + colRef.add(expected), + colRef.add(expected), + ]); + + const snapshot = await colRef + .where(Filter.and(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz'))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); - try { - ref.where(Filter.and(Filter('foo.bar', 'in', [1]), Filter('foo.bar', 'not-in', [2]))); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); - return Promise.resolve(); - } - }); + it('returns with where "==" & "!=" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + const expected = { foo: 'bar', baz: 'baz' }; + const notExpected = { foo: 'bar', baz: 'something' }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - try { - ref.where( - Filter.and(Filter('foo.bar', 'array-contains-any', [1]), Filter('foo.bar', 'not-in', [2])), - ); + const snapshot = await colRef + .where(Filter.and(Filter('foo', '==', 'bar'), Filter('baz', '!=', 'something'))) + .get(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with 'array-contains-any' filters.", - ); - return Promise.resolve(); - } - }); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); - it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it('returns with where "==" & ">" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - try { - ref.where( - Filter.and( - Filter('foo.bar', '==', 1), - Filter('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), - ), - ); + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1 }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'filters support a maximum of 10 elements in the value array.', - ); - return Promise.resolve(); - } - }); + const snapshot = await colRef + .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>', 2))) + .get(); - it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { - try { - firebase - .firestore() - .collection(COLLECTION) + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & "<" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1000 }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<', 201))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & "<=" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1000 }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<=', 200))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & ">=" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 100 }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>=', 200))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + // Filters using single "array-contains", "array-contains-any", "not-in" and "in" filters + it('returns with where "array-contains" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains`); + + const expected = [101, 102]; + const data = { foo: expected }; + + await Promise.all([colRef.add({ foo: [1, 2, 3] }), colRef.add(data), colRef.add(data)]); + + const snapshot = await colRef.where(Filter('foo', 'array-contains', 101)).get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "array-contains-any" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains-any`); + const expected = [101, 102, 103, 104]; + const data = { foo: expected }; + + await Promise.all([colRef.add({ foo: [1, 2, 3] }), colRef.add(data), colRef.add(data)]); + + const snapshot = await colRef.where(Filter('foo', 'array-contains-any', [120, 101])).get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "not-in" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); + const expected = 'bar'; + const data = { foo: expected }; + + await Promise.all([ + colRef.add({ foo: 'not' }), + colRef.add({ foo: 'this' }), + colRef.add(data), + colRef.add(data), + ]); + + const snapshot = await colRef.where(Filter('foo', 'not-in', ['not', 'this'])).get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "in" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); + const expected1 = 'bar'; + const expected2 = 'baz'; + const data1 = { foo: expected1 }; + const data2 = { foo: expected2 }; + + await Promise.all([ + colRef.add({ foo: 'not' }), + colRef.add({ foo: 'this' }), + colRef.add(data1), + colRef.add(data2), + ]); + + const snapshot = await colRef + .where(Filter('foo', 'in', [expected1, expected2])) + .orderBy('foo') + .get(); + + snapshot.size.should.eql(2); + snapshot.docs[0].data().foo.should.eql(expected1); + snapshot.docs[1].data().foo.should.eql(expected2); + }); + + // Using AND query combinations with Equals && "array-contains", "array-contains-any", "not-in" and "in" filters + it('returns with where "==" & "array-contains" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const match = Date.now(); + await Promise.all([ + colRef.add({ foo: [1, '1', match] }), + colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), + colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), + ]); + + const snapshot = await colRef + .where( + Filter.and(Filter('foo', 'array-contains', match.toString()), Filter('bar', '==', 'baz')), + ) + .get(); + const expected = [1, '2', match.toString()]; + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & "array-contains-any" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const match = Date.now(); + await Promise.all([ + colRef.add({ foo: [1, '1', match] }), + colRef.add({ foo: [1, '2'], bar: 'baz' }), + colRef.add({ foo: ['2', match.toString()], bar: 'baz' }), + ]); + + const snapshot = await colRef .where( Filter.and( - Filter(firebase.firestore.FieldPath.documentId(), '==', ['document-id']), - Filter('foo.bar', 'not-in', [1, 2, 3, 4]), + Filter('foo', 'array-contains-any', [match.toString(), 1]), + Filter('bar', '==', 'baz'), ), ) - .orderBy('differentOrderBy', 'desc'); + .get(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); - return Promise.resolve(); - } - }); + snapshot.size.should.eql(2); + snapshot.docs[0].data().bar.should.equal('baz'); + snapshot.docs[1].data().bar.should.equal('baz'); + }); - it("should throw error when using '!=' operator twice ", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it('returns with where "==" & "not-in" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); - try { - ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('foo.baz', '!=', 2))); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one '!=' inequality filter."); - return Promise.resolve(); - } - }); + await Promise.all([ + colRef.add({ foo: 'bar', bar: 'baz' }), + colRef.add({ foo: 'thing', bar: 'baz' }), + colRef.add({ foo: 'bar', bar: 'baz' }), + colRef.add({ foo: 'yolo', bar: 'baz' }), + ]); - it("should throw error when combining '!=' operator with any other inequality operator on a different field", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>', 2))); - return Promise.reject(new Error('Did not throw an Error on >.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - try { - ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<', 2))); - return Promise.reject(new Error('Did not throw an Error on <.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - try { - ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<=', 2))); - return Promise.reject(new Error('Did not throw an Error <=.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - try { - ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>=', 2))); - return Promise.reject(new Error('Did not throw an Error >=.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - return Promise.resolve(); - }); + const snapshot = await colRef + .where(Filter.and(Filter('foo', 'not-in', ['yolo', 'thing']), Filter('bar', '==', 'baz'))) + .get(); + + snapshot.size.should.eql(2); + snapshot.docs[0].data().foo.should.equal('bar'); + snapshot.docs[1].data().foo.should.equal('bar'); + }); + + it('returns with where "==" & "in" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); - /* Queries */ + await Promise.all([ + colRef.add({ foo: 'bar', bar: 'baz' }), + colRef.add({ foo: 'thing', bar: 'baz' }), + colRef.add({ foo: 'yolo', bar: 'baz' }), + ]); - // Equals and another filter: '==', '>', '>=', '<', '<=', '!=' + const snapshot = await colRef + .where(Filter.and(Filter('foo', 'in', ['bar', 'yolo']), Filter('bar', '==', 'baz'))) + .orderBy('foo') + .get(); - it('returns with where "==" & "==" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + snapshot.size.should.eql(2); + snapshot.docs[0].data().foo.should.equal('bar'); + snapshot.docs[1].data().foo.should.equal('yolo'); + }); - const expected = { foo: 'bar', bar: 'baz' }; - await Promise.all([ - colRef.add({ foo: [1, '1', 'something'] }), - colRef.add(expected), - colRef.add(expected), - ]); + // Special Filter queries + it('returns when combining greater than and lesser than on the same nested field', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz'))) - .get(); + await Promise.all([ + colRef.doc('doc1').set({ foo: { bar: 1 } }), + colRef.doc('doc2').set({ foo: { bar: 2 } }), + colRef.doc('doc3').set({ foo: { bar: 3 } }), + ]); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + const snapshot = await colRef + .where(Filter.and(Filter('foo.bar', '>', 1), Filter('foo.bar', '<', 3))) + .get(); + + snapshot.size.should.eql(1); }); - }); - it('returns with where "==" & "!=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); - const expected = { foo: 'bar', baz: 'baz' }; - const notExpected = { foo: 'bar', baz: 'something' }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + await Promise.all([ + colRef.doc('doc1').set({ foo: { bar: 1 } }), + colRef.doc('doc2').set({ foo: { bar: 2 } }), + colRef.doc('doc3').set({ foo: { bar: 3 } }), + ]); - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('baz', '!=', 'something'))) - .get(); + const snapshot = await colRef + .where(Filter.and(Filter('foo.bar', '>', 1), Filter('foo.bar', '<', 3))) + .orderBy(new firebase.firestore.FieldPath('foo', 'bar')) + .get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(1); + }); + + it('returns with a FieldPath', async function () { + const colRef = firebase + .firestore() + .collection(`${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`); + const fieldPath = new firebase.firestore.FieldPath('map', 'foo.bar@gmail.com'); + + await colRef.add({ + map: { + 'foo.bar@gmail.com': true, + }, + }); + await colRef.add({ + map: { + 'bar.foo@gmail.com': true, + }, + }); + + const snapshot = await colRef.where(Filter(fieldPath, '==', true)).get(); + snapshot.size.should.eql(1); // 2nd record should only be returned once + const data = snapshot.docs[0].data(); + should.equal(data.map['foo.bar@gmail.com'], true); }); }); - it('returns with where "==" & ">" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + describe('modular', function () { + it('throws if fieldPath string is invalid', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and(where('.foo.bar', '==', 1), where('.foo.bar', '==', 1)), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if operator string is invalid', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and(where('foo.bar', '!', 1), where('foo.bar', '!', 1)), + ); - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'opStr' is invalid"); + return Promise.resolve(); + } + }); - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>', 2))) - .get(); + it('throws if query contains multiple array-contains', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and(where('foo.bar', 'array-contains', 1), where('foo.bar', 'array-contains', 1)), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Queries only support a single array-contains filter'); + return Promise.resolve(); + } }); - }); - it('returns with where "==" & "<" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + it('throws if value is not defined', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and(where('foo.bar', 'array-contains'), where('foo.bar', 'array-contains')), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'value' argument expected"); + return Promise.resolve(); + } + }); - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1000 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + it('throws if null value and no equal operator', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and(where('foo.bar', '==', null), where('foo.bar', 'array-contains', null)), + ); - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<', 201))) - .get(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You can only perform equals comparisons on null'); + return Promise.resolve(); + } + }); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + it('allows null to be used with equal operator', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + query( + collection(getFirestore(), COLLECTION), + and(where('foo.bar', '==', null), where('foo.bar', '==', null)), + ); }); - }); - it('returns with where "==" & "<=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + it('allows null to be used with not equal operator', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + query( + collection(getFirestore(), COLLECTION), + and(where('foo.bar', '==', null), where('foo.bar', '!=', null)), + ); + }); - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1000 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + it('throws if multiple inequalities on different paths is provided', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and(where('foo.bar', '>', 123), where('bar', '>', 123)), + ); - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<=', 200))) - .get(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('All where filters with an inequality'); + return Promise.resolve(); + } + }); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + it('allows inequality on the same path', function () { + const { getFirestore, collection, query, and, where, FieldPath } = firestoreModular; + query( + collection(getFirestore(), COLLECTION), + and(where('foo.bar', '>', 123), where(new FieldPath('foo', 'bar'), '>', 1234)), + ); }); - }); - it('returns with where "==" & ">=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + it('throws if in query with no array value', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and(where('foo.bar', 'in', '123'), where('foo.bar', 'in', '123')), + ); - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 100 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>=', 200))) - .get(); + it('throws if array-contains-any query with no array value', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and( + where('foo.bar', 'array-contains-any', '123'), + where('foo.bar', 'array-contains-any', '123'), + ), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } }); - }); - // Filters using single "array-contains", "array-contains-any", "not-in" and "in" filters + it('throws if in query array length is greater than 10', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and( + where('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + where('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + ), + ); - it('returns with where "array-contains" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains`); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('maximum of 10 elements in the value'); + return Promise.resolve(); + } + }); - const expected = [101, 102]; - const data = { foo: expected }; + it('throws if query has multiple array-contains-any filter', function () { + const { getFirestore, collection, query, and, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and( + where('foo.bar', 'array-contains-any', [1]), + where('foo.bar', 'array-contains-any', [1]), + ), + ); - await Promise.all([colRef.add({ foo: [1, 2, 3] }), colRef.add(data), colRef.add(data)]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); + return Promise.resolve(); + } + }); - const snapshot = await colRef.where(Filter('foo', 'array-contains', 101)).get(); + it("should throw error when using 'not-in' operator twice", async function () { + const { getFirestore, collection, query, where, and } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, and(where('foo.bar', 'not-in', [1]), where('foo.bar', 'not-in', [2]))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } + }); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); + it("should throw error when combining 'not-in' operator with '!=' operator", async function () { + const { getFirestore, collection, query, where, and } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, and(where('foo.bar', '!=', [1]), where('foo.bar', 'not-in', [2]))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", + ); + return Promise.resolve(); + } }); - }); - it('returns with where "array-contains-any" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains-any`); - const expected = [101, 102, 103, 104]; - const data = { foo: expected }; + it("should throw error when combining 'not-in' operator with 'in' operator", async function () { + const { getFirestore, collection, query, where, and } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, and(where('foo.bar', 'in', [1]), where('foo.bar', 'not-in', [2]))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); - await Promise.all([colRef.add({ foo: [1, 2, 3] }), colRef.add(data), colRef.add(data)]); + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { + const { getFirestore, collection, query, where, and } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); - const snapshot = await colRef.where(Filter('foo', 'array-contains-any', [120, 101])).get(); + try { + query( + ref, + and(where('foo.bar', 'array-contains-any', [1]), where('foo.bar', 'not-in', [2])), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", + ); + return Promise.resolve(); + } + }); + + it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { + const { getFirestore, collection, query, where, and } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query( + ref, + and( + where('foo.bar', '==', 1), + where('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 10 elements in the value array.', + ); + return Promise.resolve(); + } }); - }); - it('returns with where "not-in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); - const expected = 'bar'; - const data = { foo: expected }; + it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { + const { getFirestore, collection, query, where, and, orderBy, FieldPath } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + and( + where(FieldPath.documentId(), '==', ['document-id']), + where('foo.bar', 'not-in', [1, 2, 3, 4]), + ), + orderBy('differentOrderBy', 'desc'), + ); - await Promise.all([ - colRef.add({ foo: 'not' }), - colRef.add({ foo: 'this' }), - colRef.add(data), - colRef.add(data), - ]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); + return Promise.resolve(); + } + }); + + it("should throw error when using '!=' operator twice ", async function () { + const { getFirestore, collection, query, where, and } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, and(where('foo.bar', '!=', 1), where('foo.baz', '!=', 2))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); - const snapshot = await colRef.where(Filter('foo', 'not-in', ['not', 'this'])).get(); + it("should throw error when combining '!=' operator with any other inequality operator on a different field", async function () { + const { getFirestore, collection, query, where, and } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, and(where('foo.bar', '!=', 1), where('differentField', '>', 2))); + return Promise.reject(new Error('Did not throw an Error on >.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + query(ref, and(where('foo.bar', '!=', 1), where('differentField', '<', 2))); + return Promise.reject(new Error('Did not throw an Error on <.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + query(ref, and(where('foo.bar', '!=', 1), where('differentField', '<=', 2))); + return Promise.reject(new Error('Did not throw an Error <=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + query(ref, and(where('foo.bar', '!=', 1), where('differentField', '>=', 2))); + return Promise.reject(new Error('Did not throw an Error >=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); + return Promise.resolve(); }); - }); - it('returns with where "in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); - const expected1 = 'bar'; - const expected2 = 'baz'; - const data1 = { foo: expected1 }; - const data2 = { foo: expected2 }; - - await Promise.all([ - colRef.add({ foo: 'not' }), - colRef.add({ foo: 'this' }), - colRef.add(data1), - colRef.add(data2), - ]); - - const snapshot = await colRef - .where(Filter('foo', 'in', [expected1, expected2])) - .orderBy('foo') - .get(); - - snapshot.size.should.eql(2); - snapshot.docs[0].data().foo.should.eql(expected1); - snapshot.docs[1].data().foo.should.eql(expected2); - }); + /* Queries */ + // Equals and another filter: '==', '>', '>=', '<', '<=', '!=' + it('returns with where "==" & "==" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, and } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', bar: 'baz' }; + await Promise.all([ + addDoc(colRef, { foo: [1, '1', 'something'] }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); + + const snapshot = await getDocs( + query(colRef, and(where('foo', '==', 'bar'), where('bar', '==', 'baz'))), + ); - // Using AND query combinations with Equals && "array-contains", "array-contains-any", "not-in" and "in" filters + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); - it('returns with where "==" & "array-contains" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + it('returns with where "==" & "!=" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, and } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals`); - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), - ]); + const expected = { foo: 'bar', baz: 'baz' }; + const notExpected = { foo: 'bar', baz: 'something' }; + await Promise.all([ + addDoc(colRef, notExpected), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef - .where( - Filter.and(Filter('foo', 'array-contains', match.toString()), Filter('bar', '==', 'baz')), - ) - .get(); - const expected = [1, '2', match.toString()]; + const snapshot = await getDocs( + query(colRef, and(where('foo', '==', 'bar'), where('baz', '!=', 'something'))), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where "==" & "array-contains-any" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ foo: [1, '2'], bar: 'baz' }), - colRef.add({ foo: ['2', match.toString()], bar: 'baz' }), - ]); - - const snapshot = await colRef - .where( - Filter.and( - Filter('foo', 'array-contains-any', [match.toString(), 1]), - Filter('bar', '==', 'baz'), - ), - ) - .get(); + it('returns with where "==" & ">" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, and } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals`); - snapshot.size.should.eql(2); - snapshot.docs[0].data().bar.should.equal('baz'); - snapshot.docs[1].data().bar.should.equal('baz'); - }); + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1 }; + await Promise.all([ + addDoc(colRef, notExpected), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - it('returns with where "==" & "not-in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); + const snapshot = await getDocs( + query(colRef, and(where('foo', '==', 'bar'), where('population', '>', 2))), + ); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); - await Promise.all([ - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'thing', bar: 'baz' }), - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'yolo', bar: 'baz' }), - ]); + it('returns with where "==" & "<" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, and } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals`); - const snapshot = await colRef - .where(Filter.and(Filter('foo', 'not-in', ['yolo', 'thing']), Filter('bar', '==', 'baz'))) - .get(); + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1000 }; + await Promise.all([ + addDoc(colRef, notExpected), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - snapshot.size.should.eql(2); - snapshot.docs[0].data().foo.should.equal('bar'); - snapshot.docs[1].data().foo.should.equal('bar'); - }); + const snapshot = await getDocs( + query(colRef, and(where('foo', '==', 'bar'), where('population', '<', 201))), + ); - it('returns with where "==" & "in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); - await Promise.all([ - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'thing', bar: 'baz' }), - colRef.add({ foo: 'yolo', bar: 'baz' }), - ]); + it('returns with where "==" & "<=" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, and } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals`); - const snapshot = await colRef - .where(Filter.and(Filter('foo', 'in', ['bar', 'yolo']), Filter('bar', '==', 'baz'))) - .orderBy('foo') - .get(); + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1000 }; + await Promise.all([ + addDoc(colRef, notExpected), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - snapshot.size.should.eql(2); - snapshot.docs[0].data().foo.should.equal('bar'); - snapshot.docs[1].data().foo.should.equal('yolo'); - }); + const snapshot = await getDocs( + query(colRef, and(where('foo', '==', 'bar'), where('population', '<=', 200))), + ); - // Special Filter queries + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); - it('returns when combining greater than and lesser than on the same nested field', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); + it('returns with where "==" & ">=" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, and } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals`); - await Promise.all([ - colRef.doc('doc1').set({ foo: { bar: 1 } }), - colRef.doc('doc2').set({ foo: { bar: 2 } }), - colRef.doc('doc3').set({ foo: { bar: 3 } }), - ]); + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 100 }; + await Promise.all([ + addDoc(colRef, notExpected), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef - .where(Filter.and(Filter('foo.bar', '>', 1), Filter('foo.bar', '<', 3))) - .get(); + const snapshot = await getDocs( + query(colRef, and(where('foo', '==', 'bar'), where('population', '>=', 200))), + ); - snapshot.size.should.eql(1); - }); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); - it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); + // Filters using single "array-contains", "array-contains-any", "not-in" and "in" filters + it('returns with where "array-contains" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/array-contains-modular`); - await Promise.all([ - colRef.doc('doc1').set({ foo: { bar: 1 } }), - colRef.doc('doc2').set({ foo: { bar: 2 } }), - colRef.doc('doc3').set({ foo: { bar: 3 } }), - ]); + const expected = [101, 102]; + const data = { foo: expected }; - const snapshot = await colRef - .where(Filter.and(Filter('foo.bar', '>', 1), Filter('foo.bar', '<', 3))) - .orderBy(new firebase.firestore.FieldPath('foo', 'bar')) - .get(); + await Promise.all([ + addDoc(colRef, { foo: [1, 2, 3] }), + addDoc(colRef, data), + addDoc(colRef, data), + ]); - snapshot.size.should.eql(1); - }); + const snapshot = await getDocs(query(colRef, where('foo', 'array-contains', 101))); - it('returns with a FieldPath', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`); - const fieldPath = new firebase.firestore.FieldPath('map', 'foo.bar@gmail.com'); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "array-contains-any" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/array-contains-any-modular`); + const expected = [101, 102, 103, 104]; + const data = { foo: expected }; + + await Promise.all([ + addDoc(colRef, { foo: [1, 2, 3] }), + addDoc(colRef, data), + addDoc(colRef, data), + ]); - await colRef.add({ - map: { - 'foo.bar@gmail.com': true, - }, + const snapshot = await getDocs(query(colRef, where('foo', 'array-contains-any', [120, 101]))); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); }); - await colRef.add({ - map: { - 'bar.foo@gmail.com': true, - }, + + it('returns with where "not-in" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/not-in-modular`); + const expected = 'bar'; + const data = { foo: expected }; + + await Promise.all([ + addDoc(colRef, { foo: 'not' }), + addDoc(colRef, { foo: 'this' }), + addDoc(colRef, data), + addDoc(colRef, data), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', 'not-in', ['not', 'this']))); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "in" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, orderBy } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/where-in-modular`); + const expected1 = 'bar'; + const expected2 = 'baz'; + const data1 = { foo: expected1 }; + const data2 = { foo: expected2 }; + + await Promise.all([ + addDoc(colRef, { foo: 'not' }), + addDoc(colRef, { foo: 'this' }), + addDoc(colRef, data1), + addDoc(colRef, data2), + ]); + + const snapshot = await getDocs( + query(colRef, where('foo', 'in', [expected1, expected2]), orderBy('foo')), + ); + + snapshot.size.should.eql(2); + snapshot.docs[0].data().foo.should.eql(expected1); + snapshot.docs[1].data().foo.should.eql(expected2); }); - const snapshot = await colRef.where(Filter(fieldPath, '==', true)).get(); - snapshot.size.should.eql(1); // 2nd record should only be returned once - const data = snapshot.docs[0].data(); - should.equal(data.map['foo.bar@gmail.com'], true); + // Using AND query combinations with Equals && "array-contains", "array-contains-any", "not-in" and "in" filters + it('returns with where "==" & "array-contains" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, and } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const match = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: [1, '1', match] }), + addDoc(colRef, { foo: [1, '2', match.toString()], bar: 'baz' }), + addDoc(colRef, { foo: [1, '2', match.toString()], bar: 'baz' }), + ]); + + const snapshot = await getDocs( + query( + colRef, + and(where('foo', 'array-contains', match.toString()), where('bar', '==', 'baz')), + ), + ); + const expected = [1, '2', match.toString()]; + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & "array-contains-any" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, and } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const match = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: [1, '1', match] }), + addDoc(colRef, { foo: [1, '2'], bar: 'baz' }), + addDoc(colRef, { foo: ['2', match.toString()], bar: 'baz' }), + ]); + + const snapshot = await getDocs( + query( + colRef, + and(where('foo', 'array-contains-any', [match.toString(), 1]), where('bar', '==', 'baz')), + ), + ); + + snapshot.size.should.eql(2); + snapshot.docs[0].data().bar.should.equal('baz'); + snapshot.docs[1].data().bar.should.equal('baz'); + }); + + it('returns with where "==" & "not-in" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, and } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/not-in`); + + await Promise.all([ + addDoc(colRef, { foo: 'bar', bar: 'baz' }), + addDoc(colRef, { foo: 'thing', bar: 'baz' }), + addDoc(colRef, { foo: 'bar', bar: 'baz' }), + addDoc(colRef, { foo: 'yolo', bar: 'baz' }), + ]); + + const snapshot = await getDocs( + query(colRef, and(where('foo', 'not-in', ['yolo', 'thing']), where('bar', '==', 'baz'))), + ); + + snapshot.size.should.eql(2); + snapshot.docs[0].data().foo.should.equal('bar'); + snapshot.docs[1].data().foo.should.equal('bar'); + }); + + it('returns with where "==" & "in" filter', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, and, orderBy } = + firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/in`); + + await Promise.all([ + addDoc(colRef, { foo: 'bar', bar: 'baz' }), + addDoc(colRef, { foo: 'thing', bar: 'baz' }), + addDoc(colRef, { foo: 'yolo', bar: 'baz' }), + ]); + + const snapshot = await getDocs( + query( + colRef, + and(where('foo', 'in', ['bar', 'yolo']), where('bar', '==', 'baz')), + orderBy('foo'), + ), + ); + + snapshot.size.should.eql(2); + snapshot.docs[0].data().foo.should.equal('bar'); + snapshot.docs[1].data().foo.should.equal('yolo'); + }); + + // Special Filter queries + it('returns when combining greater than and lesser than on the same nested field', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, query, where, and } = + firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/greaterandless`); + + await Promise.all([ + setDoc(doc(colRef, 'doc1'), { foo: { bar: 1 } }), + setDoc(doc(colRef, 'doc2'), { foo: { bar: 2 } }), + setDoc(doc(colRef, 'doc3'), { foo: { bar: 3 } }), + ]); + + const snapshot = await getDocs( + query(colRef, and(where('foo.bar', '>', 1), where('foo.bar', '<', 3))), + ); + + snapshot.size.should.eql(1); + }); + + it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { + const { + getFirestore, + collection, + doc, + setDoc, + getDocs, + query, + where, + and, + orderBy, + FieldPath, + } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/greaterandless`); + + await Promise.all([ + setDoc(doc(colRef, 'doc1'), { foo: { bar: 1 } }), + setDoc(doc(colRef, 'doc2'), { foo: { bar: 2 } }), + setDoc(doc(colRef, 'doc3'), { foo: { bar: 3 } }), + ]); + + const snapshot = await getDocs( + query( + colRef, + and( + where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1), + where(new firebase.firestore.FieldPath('foo', 'bar'), '<', 3), + ), + orderBy(new FieldPath('foo', 'bar')), + ), + ); + + snapshot.size.should.eql(1); + }); + + it('returns with a FieldPath', async function () { + const { getFirestore, collection, addDoc, getDocs, query, where, FieldPath } = + firestoreModular; + const colRef = collection( + getFirestore(), + `${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`, + ); + const fieldPath = new FieldPath('map', 'foo.bar@gmail.com'); + + await addDoc(colRef, { + map: { + 'foo.bar@gmail.com': true, + }, + }); + await addDoc(colRef, { + map: { + 'bar.foo@gmail.com': true, + }, + }); + + const snapshot = await getDocs(query(colRef, where(fieldPath, '==', true))); + snapshot.size.should.eql(1); // 2nd record should only be returned once + const data = snapshot.docs[0].data(); + should.equal(data.map['foo.bar@gmail.com'], true); + }); }); }); diff --git a/packages/firestore/e2e/Query/where.e2e.js b/packages/firestore/e2e/Query/where.e2e.js index f92526cc08..49a8ea7f67 100644 --- a/packages/firestore/e2e/Query/where.e2e.js +++ b/packages/firestore/e2e/Query/where.e2e.js @@ -20,552 +20,1163 @@ describe('firestore().collection().where()', function () { beforeEach(async function () { return await wipe(); }); - it('throws if fieldPath is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).where(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'must be a string, instance of FieldPath or instance of Filter', - ); - return Promise.resolve(); - } - }); - it('throws if fieldPath string is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).where('.foo.bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if fieldPath is invalid', function () { + try { + firebase.firestore().collection(COLLECTION).where(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'must be a string, instance of FieldPath or instance of Filter', + ); + return Promise.resolve(); + } + }); - it('throws if operator string is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', '!'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'opStr' is invalid"); - return Promise.resolve(); - } - }); + it('throws if fieldPath string is invalid', function () { + try { + firebase.firestore().collection(COLLECTION).where('.foo.bar'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if operator string is invalid', function () { + try { + firebase.firestore().collection(COLLECTION).where('foo.bar', '!'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'opStr' is invalid"); + return Promise.resolve(); + } + }); + + it('throws if query contains multiple array-contains', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where('foo.bar', 'array-contains', 123) + .where('foo.bar', 'array-contains', 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Queries only support a single array-contains filter'); + return Promise.resolve(); + } + }); + + it('throws if value is not defined', function () { + try { + firebase.firestore().collection(COLLECTION).where('foo.bar', 'array-contains'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'value' argument expected"); + return Promise.resolve(); + } + }); + + it('throws if null value and no equal operator', function () { + try { + firebase.firestore().collection(COLLECTION).where('foo.bar', 'array-contains', null); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You can only perform equals comparisons on null'); + return Promise.resolve(); + } + }); + + it('allows null to be used with equal operator', function () { + firebase.firestore().collection(COLLECTION).where('foo.bar', '==', null); + }); + + it('allows null to be used with not equal operator', function () { + firebase.firestore().collection(COLLECTION).where('foo.bar', '!=', null); + }); - it('throws if query contains multiple array-contains', function () { - try { + it('throws if multiple inequalities on different paths is provided', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where('foo.bar', '>', 123) + .where('bar', '>', 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('All where filters with an inequality'); + return Promise.resolve(); + } + }); + + it('allows inequality on the same path', function () { firebase .firestore() .collection(COLLECTION) - .where('foo.bar', 'array-contains', 123) - .where('foo.bar', 'array-contains', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Queries only support a single array-contains filter'); - return Promise.resolve(); - } - }); + .where('foo.bar', '>', 123) + .where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234); + }); - it('throws if value is not defined', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', 'array-contains'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' argument expected"); - return Promise.resolve(); - } - }); + it('throws if in query with no array value', function () { + try { + firebase.firestore().collection(COLLECTION).where('foo.bar', 'in', '123'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - it('throws if null value and no equal operator', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', 'array-contains', null); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You can only perform equals comparisons on null'); - return Promise.resolve(); - } - }); + it('throws if array-contains-any query with no array value', function () { + try { + firebase.firestore().collection(COLLECTION).where('foo.bar', 'array-contains-any', '123'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - it('allows null to be used with equal operator', function () { - firebase.firestore().collection(COLLECTION).where('foo.bar', '==', null); - }); + it('throws if in query array length is greater than 10', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('maximum of 10 elements in the value'); + return Promise.resolve(); + } + }); - it('allows null to be used with not equal operator', function () { - firebase.firestore().collection(COLLECTION).where('foo.bar', '!=', null); - }); + it('throws if query has multiple array-contains-any filter', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where('foo.bar', 'array-contains-any', [1]) + .where('foo.bar', 'array-contains-any', [2]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); + return Promise.resolve(); + } + }); - it('throws if multiple inequalities on different paths is provided', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', '>', 123).where('bar', '>', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('All where filters with an inequality'); - return Promise.resolve(); - } - }); + /* Queries */ - it('allows inequality on the same path', function () { - firebase - .firestore() - .collection(COLLECTION) - .where('foo.bar', '>', 123) - .where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234); - }); + it('returns with where equal filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equal`); - it('throws if in query with no array value', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', 'in', '123'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); + const search = Date.now(); + await Promise.all([ + colRef.add({ foo: search }), + colRef.add({ foo: search }), + colRef.add({ foo: search + 1234 }), + ]); - it('throws if array-contains-any query with no array value', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', 'array-contains-any', '123'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); + const snapshot = await colRef.where('foo', '==', search).get(); - it('throws if in query array length is greater than 10', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('maximum of 10 elements in the value'); - return Promise.resolve(); - } - }); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(search); + }); + }); - it('throws if query has multiple array-contains-any filter', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where('foo.bar', 'array-contains-any', [1]) - .where('foo.bar', 'array-contains-any', [2]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); - return Promise.resolve(); - } - }); + it('returns with where greater than filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater`); + + const search = Date.now(); + await Promise.all([ + colRef.add({ foo: search - 1234 }), + colRef.add({ foo: search }), + colRef.add({ foo: search + 1234 }), + colRef.add({ foo: search + 1234 }), + ]); - /* Queries */ + const snapshot = await colRef.where('foo', '>', search).get(); - it('returns with where equal filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equal`); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(search + 1234); + }); + }); + + it('returns with where greater than or equal filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterequal`); - const search = Date.now(); - await Promise.all([ - colRef.add({ foo: search }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - ]); + const search = Date.now(); + await Promise.all([ + colRef.add({ foo: search - 1234 }), + colRef.add({ foo: search }), + colRef.add({ foo: search + 1234 }), + colRef.add({ foo: search + 1234 }), + ]); - const snapshot = await colRef.where('foo', '==', search).get(); + const snapshot = await colRef.where('foo', '>=', search).get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(search); + snapshot.size.should.eql(3); + snapshot.forEach(s => { + s.data().foo.should.be.aboveOrEqual(search); + }); }); - }); - it('returns with where greater than filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater`); + it('returns with where less than filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less`); - const search = Date.now(); - await Promise.all([ - colRef.add({ foo: search - 1234 }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - colRef.add({ foo: search + 1234 }), - ]); + const search = -Date.now(); + await Promise.all([ + colRef.add({ foo: search + -1234 }), + colRef.add({ foo: search + -1234 }), + colRef.add({ foo: search }), + ]); - const snapshot = await colRef.where('foo', '>', search).get(); + const snapshot = await colRef.where('foo', '<', search).get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(search + 1234); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.be.below(search); + }); }); - }); - it('returns with where greater than or equal filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterequal`); + it('returns with where less than or equal filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/lessequal`); - const search = Date.now(); - await Promise.all([ - colRef.add({ foo: search - 1234 }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - colRef.add({ foo: search + 1234 }), - ]); + const search = -Date.now(); + await Promise.all([ + colRef.add({ foo: search + -1234 }), + colRef.add({ foo: search + -1234 }), + colRef.add({ foo: search }), + colRef.add({ foo: search + 1234 }), + ]); - const snapshot = await colRef.where('foo', '>=', search).get(); + const snapshot = await colRef.where('foo', '<=', search).get(); - snapshot.size.should.eql(3); - snapshot.forEach(s => { - s.data().foo.should.be.aboveOrEqual(search); + snapshot.size.should.eql(3); + snapshot.forEach(s => { + s.data().foo.should.be.belowOrEqual(search); + }); }); - }); - it('returns with where less than filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less`); + it('returns when combining greater than and lesser than on the same nested field', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); - const search = -Date.now(); - await Promise.all([ - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search }), - ]); + await Promise.all([ + colRef.doc('doc1').set({ foo: { bar: 1 } }), + colRef.doc('doc2').set({ foo: { bar: 2 } }), + colRef.doc('doc3').set({ foo: { bar: 3 } }), + ]); - const snapshot = await colRef.where('foo', '<', search).get(); + const snapshot = await colRef + .where('foo.bar', '>', 1) + .where('foo.bar', '<', 3) + .orderBy('foo.bar') + .get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.be.below(search); + snapshot.size.should.eql(1); }); - }); - it('returns with where less than or equal filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/lessequal`); + it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); - const search = -Date.now(); - await Promise.all([ - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - ]); + await Promise.all([ + colRef.doc('doc1').set({ foo: { bar: 1 } }), + colRef.doc('doc2').set({ foo: { bar: 2 } }), + colRef.doc('doc3').set({ foo: { bar: 3 } }), + ]); - const snapshot = await colRef.where('foo', '<=', search).get(); + const snapshot = await colRef + .where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1) + .where(new firebase.firestore.FieldPath('foo', 'bar'), '<', 3) + .orderBy(new firebase.firestore.FieldPath('foo', 'bar')) + .get(); - snapshot.size.should.eql(3); - snapshot.forEach(s => { - s.data().foo.should.be.belowOrEqual(search); + snapshot.size.should.eql(1); }); - }); - it('returns when combining greater than and lesser than on the same nested field', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); + it('returns with where array-contains filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains`); - await Promise.all([ - colRef.doc('doc1').set({ foo: { bar: 1 } }), - colRef.doc('doc2').set({ foo: { bar: 2 } }), - colRef.doc('doc3').set({ foo: { bar: 3 } }), - ]); + const match = Date.now(); + await Promise.all([ + colRef.add({ foo: [1, '2', match] }), + colRef.add({ foo: [1, '2', match.toString()] }), + colRef.add({ foo: [1, '2', match.toString()] }), + ]); - const snapshot = await colRef - .where('foo.bar', '>', 1) - .where('foo.bar', '<', 3) - .orderBy('foo.bar') - .get(); + const snapshot = await colRef.where('foo', 'array-contains', match.toString()).get(); + const expected = [1, '2', match.toString()]; - snapshot.size.should.eql(1); - }); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); - it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); + it('returns with in filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in${Date.now() + ''}`); - await Promise.all([ - colRef.doc('doc1').set({ foo: { bar: 1 } }), - colRef.doc('doc2').set({ foo: { bar: 2 } }), - colRef.doc('doc3').set({ foo: { bar: 3 } }), - ]); + await Promise.all([ + colRef.add({ status: 'Ordered' }), + colRef.add({ status: 'Ready to Ship' }), + colRef.add({ status: 'Ready to Ship' }), + colRef.add({ status: 'Incomplete' }), + ]); - const snapshot = await colRef - .where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1) - .where(new firebase.firestore.FieldPath('foo', 'bar'), '<', 3) - .orderBy(new firebase.firestore.FieldPath('foo', 'bar')) - .get(); + const expect = ['Ready to Ship', 'Ordered']; + const snapshot = await colRef.where('status', 'in', expect).get(); + snapshot.size.should.eql(3); - snapshot.size.should.eql(1); - }); + snapshot.forEach(s => { + s.data().status.should.equalOneOf(...expect); + }); + }); + + it('returns with array-contains-any filter', async function () { + const colRef = firebase + .firestore() + .collection(`${COLLECTION}/filter/array-contains-any${Date.now() + ''}`); + + await Promise.all([ + colRef.add({ category: ['Appliances', 'Housewares', 'Cooking'] }), + colRef.add({ category: ['Appliances', 'Electronics', 'Nursery'] }), + colRef.add({ category: ['Audio/Video', 'Electronics'] }), + colRef.add({ category: ['Beauty'] }), + ]); + + const expect = ['Appliances', 'Electronics']; + const snapshot = await colRef.where('category', 'array-contains-any', expect).get(); + snapshot.size.should.eql(3); // 2nd record should only be returned once + }); - it('returns with where array-contains filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains`); + it('returns with a FieldPath', async function () { + const colRef = firebase + .firestore() + .collection(`${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`); + const fieldPath = new firebase.firestore.FieldPath('map', 'foo.bar@gmail.com'); + + await colRef.add({ + map: { + 'foo.bar@gmail.com': true, + }, + }); + await colRef.add({ + map: { + 'bar.foo@gmail.com': true, + }, + }); + + const snapshot = await colRef.where(fieldPath, '==', true).get(); + snapshot.size.should.eql(1); // 2nd record should only be returned once + const data = snapshot.docs[0].data(); + should.equal(data.map['foo.bar@gmail.com'], true); + }); + + it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where(firebase.firestore.FieldPath.documentId(), 'in', ['document-id']) + .orderBy('differentOrderBy', 'desc'); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); + return Promise.resolve(); + } + }); - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '2', match] }), - colRef.add({ foo: [1, '2', match.toString()] }), - colRef.add({ foo: [1, '2', match.toString()] }), - ]); + it('should correctly query integer values with in operator', async function () { + const ref = firebase.firestore().collection(`${COLLECTION}/filter/int-in${Date.now() + ''}`); - const snapshot = await colRef.where('foo', 'array-contains', match.toString()).get(); - const expected = [1, '2', match.toString()]; + await ref.add({ status: 1 }); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); + const items = []; + await ref + .where('status', 'in', [1, 2]) + .get() + .then($ => $.forEach(doc => items.push(doc.data()))); + + items.length.should.equal(1); }); - }); - it('returns with in filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in${Date.now() + ''}`); + it('should correctly query integer values with array-contains operator', async function () { + const ref = firebase + .firestore() + .collection(`${COLLECTION}/filter/int-array-contains${Date.now() + ''}`); - await Promise.all([ - colRef.add({ status: 'Ordered' }), - colRef.add({ status: 'Ready to Ship' }), - colRef.add({ status: 'Ready to Ship' }), - colRef.add({ status: 'Incomplete' }), - ]); + await ref.add({ status: [1, 2, 3] }); - const expect = ['Ready to Ship', 'Ordered']; - const snapshot = await colRef.where('status', 'in', expect).get(); - snapshot.size.should.eql(3); + const items = []; + await ref + .where('status', 'array-contains', 2) + .get() + .then($ => $.forEach(doc => items.push(doc.data()))); - snapshot.forEach(s => { - s.data().status.should.equalOneOf(...expect); + items.length.should.equal(1); }); - }); - it('returns with array-contains-any filter', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/array-contains-any${Date.now() + ''}`); - - await Promise.all([ - colRef.add({ category: ['Appliances', 'Housewares', 'Cooking'] }), - colRef.add({ category: ['Appliances', 'Electronics', 'Nursery'] }), - colRef.add({ category: ['Audio/Video', 'Electronics'] }), - colRef.add({ category: ['Beauty'] }), - ]); - - const expect = ['Appliances', 'Electronics']; - const snapshot = await colRef.where('category', 'array-contains-any', expect).get(); - snapshot.size.should.eql(3); // 2nd record should only be returned once - }); + it("should correctly retrieve data when using 'not-in' operator", async function () { + const ref = firebase.firestore().collection(`${COLLECTION}/filter/not-in${Date.now() + ''}`); - it('returns with a FieldPath', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`); - const fieldPath = new firebase.firestore.FieldPath('map', 'foo.bar@gmail.com'); + await Promise.all([ref.add({ notIn: 'here' }), ref.add({ notIn: 'now' })]); - await colRef.add({ - map: { - 'foo.bar@gmail.com': true, - }, + const result = await ref.where('notIn', 'not-in', ['here', 'there', 'everywhere']).get(); + should(result.docs.length).equal(1); + should(result.docs[0].data().notIn).equal('now'); }); - await colRef.add({ - map: { - 'bar.foo@gmail.com': true, - }, + + it("should throw error when using 'not-in' operator twice", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', 'not-in', [1]).where('test', 'not-in', [2]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } }); - const snapshot = await colRef.where(fieldPath, '==', true).get(); - snapshot.size.should.eql(1); // 2nd record should only be returned once - const data = snapshot.docs[0].data(); - should.equal(data.map['foo.bar@gmail.com'], true); - }); + it("should throw error when combining 'not-in' operator with '!=' operator", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', '!=', 1).where('test', 'not-in', [1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", + ); + return Promise.resolve(); + } + }); - it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { - try { - firebase + it("should throw error when combining 'not-in' operator with 'in' operator", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', 'in', [2]).where('test', 'not-in', [1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', 'array-contains-any', [2]).where('test', 'not-in', [1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", + ); + return Promise.resolve(); + } + }); + + it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 10 elements in the value array.', + ); + return Promise.resolve(); + } + }); + + it("should correctly retrieve data when using '!=' operator", async function () { + const ref = firebase .firestore() - .collection(COLLECTION) - .where(firebase.firestore.FieldPath.documentId(), 'in', ['document-id']) - .orderBy('differentOrderBy', 'desc'); + .collection(`${COLLECTION}/filter/bang-equals${Date.now() + ''}`); + + await Promise.all([ref.add({ notEqual: 'here' }), ref.add({ notEqual: 'now' })]); + + const result = await ref.where('notEqual', '!=', 'here').get(); + + should(result.docs.length).equal(1); + should(result.docs[0].data().notEqual).equal('now'); + }); + + it("should throw error when using '!=' operator twice ", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', '!=', 1).where('test', '!=', 2); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); + + it("should throw error when combining '!=' operator with any other inequality operator on a different field", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', '!=', 1).where('differentField', '>', 1); + return Promise.reject(new Error('Did not throw an Error on >.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where('test', '!=', 1).where('differentField', '<', 1); + return Promise.reject(new Error('Did not throw an Error on <.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where('test', '!=', 1).where('differentField', '<=', 1); + return Promise.reject(new Error('Did not throw an Error <=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where('test', '!=', 1).where('differentField', '>=', 1); + return Promise.reject(new Error('Did not throw an Error >=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); return Promise.resolve(); - } + }); + + it('should handle where clause after sort by', async function () { + const ref = firebase + .firestore() + .collection(`${COLLECTION}/filter/sort-by-where${Date.now() + ''}`); + + await ref.add({ status: 1 }); + await ref.add({ status: 2 }); + await ref.add({ status: 3 }); + + const items = []; + await ref + .orderBy('status', 'desc') + .where('status', '<=', 2) + .get() + .then($ => $.forEach(doc => items.push(doc.data()))); + + items.length.should.equal(2); + items[0].status.should.equal(2); + items[1].status.should.equal(1); + }); }); - it('should correctly query integer values with in operator', async function () { - const ref = firebase.firestore().collection(`${COLLECTION}/filter/int-in${Date.now() + ''}`); + describe('modular', function () { + it('throws if fieldPath is invalid', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), where(123)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'must be a string, instance of FieldPath or instance of Filter', + ); + return Promise.resolve(); + } + }); - await ref.add({ status: 1 }); + it('throws if fieldPath string is invalid', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), where('.foo.bar')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); - const items = []; - await ref - .where('status', 'in', [1, 2]) - .get() - .then($ => $.forEach(doc => items.push(doc.data()))); + it('throws if operator string is invalid', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), where('foo.bar', '!')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'opStr' is invalid"); + return Promise.resolve(); + } + }); - items.length.should.equal(1); - }); + it('throws if query contains multiple array-contains', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where('foo.bar', 'array-contains', 123), + where('foo.bar', 'array-contains', 123), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Queries only support a single array-contains filter'); + return Promise.resolve(); + } + }); - it('should correctly query integer values with array-contains operator', async function () { - const ref = firebase - .firestore() - .collection(`${COLLECTION}/filter/int-array-contains${Date.now() + ''}`); + it('throws if value is not defined', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), where('foo.bar', 'array-contains')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'value' argument expected"); + return Promise.resolve(); + } + }); - await ref.add({ status: [1, 2, 3] }); + it('throws if null value and no equal operator', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), where('foo.bar', 'array-contains', null)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You can only perform equals comparisons on null'); + return Promise.resolve(); + } + }); - const items = []; - await ref - .where('status', 'array-contains', 2) - .get() - .then($ => $.forEach(doc => items.push(doc.data()))); + it('allows null to be used with equal operator', function () { + const { getFirestore, collection, query, where } = firestoreModular; + query(collection(getFirestore(), COLLECTION), where('foo.bar', '==', null)); + }); - items.length.should.equal(1); - }); + it('allows null to be used with not equal operator', function () { + const { getFirestore, collection, query, where } = firestoreModular; + query(collection(getFirestore(), COLLECTION), where('foo.bar', '!=', null)); + }); - it("should correctly retrieve data when using 'not-in' operator", async function () { - const ref = firebase.firestore().collection(`${COLLECTION}/filter/not-in${Date.now() + ''}`); + it('throws if multiple inequalities on different paths is provided', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where('foo.bar', '>', 123), + where('bar', '>', 123), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('All where filters with an inequality'); + return Promise.resolve(); + } + }); - await Promise.all([ref.add({ notIn: 'here' }), ref.add({ notIn: 'now' })]); + it('allows inequality on the same path', function () { + const { getFirestore, collection, query, where, FieldPath } = firestoreModular; + query( + collection(getFirestore(), COLLECTION), + where('foo.bar', '>', 123), + where(new FieldPath('foo', 'bar'), '>', 1234), + ); + }); - const result = await ref.where('notIn', 'not-in', ['here', 'there', 'everywhere']).get(); - should(result.docs.length).equal(1); - should(result.docs[0].data().notIn).equal('now'); - }); + it('throws if in query with no array value', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), where('foo.bar', 'in', '123')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - it("should throw error when using 'not-in' operator twice", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it('throws if array-contains-any query with no array value', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where('foo.bar', 'array-contains-any', '123'), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - try { - ref.where('test', 'not-in', [1]).where('test', 'not-in', [2]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'not-in' filter."); - return Promise.resolve(); - } - }); + it('throws if in query array length is greater than 10', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('maximum of 10 elements in the value'); + return Promise.resolve(); + } + }); + + it('throws if query has multiple array-contains-any filter', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where('foo.bar', 'array-contains-any', [1]), + where('foo.bar', 'array-contains-any', [2]), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); + return Promise.resolve(); + } + }); - it("should throw error when combining 'not-in' operator with '!=' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + /* Queries */ - try { - ref.where('test', '!=', 1).where('test', 'not-in', [1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with '!=' inequality filters", + it('returns with where equal filter', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equal`); + + const search = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', '==', search))); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(search); + }); + }); + + it('returns with where greater than filter', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/greater`); + + const search = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search - 1234 }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + addDoc(colRef, { foo: search + 1234 }), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', '>', search))); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(search + 1234); + }); + }); + + it('returns with where greater than or equal filter', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/greaterequal`); + + const search = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search - 1234 }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + addDoc(colRef, { foo: search + 1234 }), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', '>=', search))); + + snapshot.size.should.eql(3); + snapshot.forEach(s => { + s.data().foo.should.be.aboveOrEqual(search); + }); + }); + + it('returns with where less than filter', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/less`); + + const search = -Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search }), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', '<', search))); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.be.below(search); + }); + }); + + it('returns with where less than or equal filter', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/lessequal`); + + const search = -Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + ]); + + const snapshot = await getDocs(query(colRef, where('foo', '<=', search))); + + snapshot.size.should.eql(3); + snapshot.forEach(s => { + s.data().foo.should.be.belowOrEqual(search); + }); + }); + + it('returns when combining greater than and lesser than on the same nested field', async function () { + const { getFirestore, collection, doc, setDoc, query, where, orderBy, getDocs } = + firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/greaterandless`); + + await Promise.all([ + setDoc(doc(colRef, 'doc1'), { foo: { bar: 1 } }), + setDoc(doc(colRef, 'doc2'), { foo: { bar: 2 } }), + setDoc(doc(colRef, 'doc3'), { foo: { bar: 3 } }), + ]); + + const snapshot = await getDocs( + query(colRef, where('foo.bar', '>', 1), where('foo.bar', '<', 3), orderBy('foo.bar')), ); - return Promise.resolve(); - } - }); - it("should throw error when combining 'not-in' operator with 'in' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + snapshot.size.should.eql(1); + }); - try { - ref.where('test', 'in', [2]).where('test', 'not-in', [1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); - return Promise.resolve(); - } - }); + it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { + const { getFirestore, collection, doc, setDoc, query, where, getDocs, orderBy, FieldPath } = + firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/greaterandless`); + + await Promise.all([ + setDoc(doc(colRef, 'doc1'), { foo: { bar: 1 } }), + setDoc(doc(colRef, 'doc2'), { foo: { bar: 2 } }), + setDoc(doc(colRef, 'doc3'), { foo: { bar: 3 } }), + ]); + + const snapshot = await getDocs( + query( + colRef, + where(new FieldPath('foo', 'bar'), '>', 1), + where(new FieldPath('foo', 'bar'), '<', 3), + orderBy(new FieldPath('foo', 'bar')), + ), + ); + + snapshot.size.should.eql(1); + }); - it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it('returns with where array-contains filter', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/array-contains`); - try { - ref.where('test', 'array-contains-any', [2]).where('test', 'not-in', [1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with 'array-contains-any' filters.", + const match = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: [1, '2', match] }), + addDoc(colRef, { foo: [1, '2', match.toString()] }), + addDoc(colRef, { foo: [1, '2', match.toString()] }), + ]); + + const snapshot = await getDocs( + query(colRef, where('foo', 'array-contains', match.toString())), ); - return Promise.resolve(); - } - }); + const expected = [1, '2', match.toString()]; + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with in filter', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/in${Date.now() + ''}`); - it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { - const ref = firebase.firestore().collection(COLLECTION); + await Promise.all([ + addDoc(colRef, { status: 'Ordered' }), + addDoc(colRef, { status: 'Ready to Ship' }), + addDoc(colRef, { status: 'Ready to Ship' }), + addDoc(colRef, { status: 'Incomplete' }), + ]); - try { - ref.where('test', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'filters support a maximum of 10 elements in the value array.', + const expect = ['Ready to Ship', 'Ordered']; + const snapshot = await getDocs(query(colRef, where('status', 'in', expect))); + snapshot.size.should.eql(3); + + snapshot.forEach(s => { + s.data().status.should.equalOneOf(...expect); + }); + }); + + it('returns with array-contains-any filter', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection( + getFirestore(), + `${COLLECTION}/filter/array-contains-any${Date.now() + ''}`, ); - return Promise.resolve(); - } - }); - it("should correctly retrieve data when using '!=' operator", async function () { - const ref = firebase - .firestore() - .collection(`${COLLECTION}/filter/bang-equals${Date.now() + ''}`); + await Promise.all([ + addDoc(colRef, { category: ['Appliances', 'Housewares', 'Cooking'] }), + addDoc(colRef, { category: ['Appliances', 'Electronics', 'Nursery'] }), + addDoc(colRef, { category: ['Audio/Video', 'Electronics'] }), + addDoc(colRef, { category: ['Beauty'] }), + ]); + + const expect = ['Appliances', 'Electronics']; + const snapshot = await getDocs( + query(colRef, where('category', 'array-contains-any', expect)), + ); + snapshot.size.should.eql(3); // 2nd record should only be returned once + }); + + it('returns with a FieldPath', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs, FieldPath } = + firestoreModular; + const colRef = collection( + getFirestore(), + `${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`, + ); + const fieldPath = new FieldPath('map', 'foo.bar@gmail.com'); + + await addDoc(colRef, { + map: { + 'foo.bar@gmail.com': true, + }, + }); + await addDoc(colRef, { + map: { + 'bar.foo@gmail.com': true, + }, + }); + + const snapshot = await getDocs(query(colRef, where(fieldPath, '==', true))); + snapshot.size.should.eql(1); // 2nd record should only be returned once + const data = snapshot.docs[0].data(); + should.equal(data.map['foo.bar@gmail.com'], true); + }); - await Promise.all([ref.add({ notEqual: 'here' }), ref.add({ notEqual: 'now' })]); + it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { + const { getFirestore, collection, query, where, orderBy, FieldPath } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where(FieldPath.documentId(), 'in', ['document-id']), + orderBy('differentOrderBy', 'desc'), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); + return Promise.resolve(); + } + }); - const result = await ref.where('notEqual', '!=', 'here').get(); + it('should correctly query integer values with in operator', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection(getFirestore(), `${COLLECTION}/filter/int-in${Date.now() + ''}`); - should(result.docs.length).equal(1); - should(result.docs[0].data().notEqual).equal('now'); - }); + await addDoc(ref, { status: 1 }); + + const items = []; + await getDocs(query(ref, where('status', 'in', [1, 2]))).then($ => + $.forEach(doc => items.push(doc.data())), + ); + + items.length.should.equal(1); + }); + + it('should correctly query integer values with array-contains operator', async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection( + getFirestore(), + `${COLLECTION}/filter/int-array-contains${Date.now() + ''}`, + ); + + await addDoc(ref, { status: [1, 2, 3] }); + + const items = []; + await getDocs(query(ref, where('status', 'array-contains', 2))).then($ => + $.forEach(doc => items.push(doc.data())), + ); + + items.length.should.equal(1); + }); + + it("should correctly retrieve data when using 'not-in' operator", async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection(getFirestore(), `${COLLECTION}/filter/not-in${Date.now() + ''}`); + + await Promise.all([addDoc(ref, { notIn: 'here' }), addDoc(ref, { notIn: 'now' })]); + + const result = await getDocs( + query(ref, where('notIn', 'not-in', ['here', 'there', 'everywhere'])), + ); + should(result.docs.length).equal(1); + should(result.docs[0].data().notIn).equal('now'); + }); + + it("should throw error when using 'not-in' operator twice", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, where('test', 'not-in', [1]), where('test', 'not-in', [2])); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with '!=' operator", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, where('test', 'not-in', [1]), where('test', '!=', 1)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", + ); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'in' operator", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, where('test', 'in', [2]), where('test', 'not-in', [1])); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, where('test', 'array-contains-any', [2]), where('test', 'not-in', [1])); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", + ); + return Promise.resolve(); + } + }); + + it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, where('test', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 10 elements in the value array.', + ); + return Promise.resolve(); + } + }); + + it("should correctly retrieve data when using '!=' operator", async function () { + const { getFirestore, collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection(getFirestore(), `${COLLECTION}/filter/bang-equals${Date.now() + ''}`); + + await Promise.all([addDoc(ref, { notEqual: 'here' }), addDoc(ref, { notEqual: 'now' })]); - it("should throw error when using '!=' operator twice ", async function () { - const ref = firebase.firestore().collection(COLLECTION); + const result = await getDocs(query(ref, where('notEqual', '!=', 'here'))); + + should(result.docs.length).equal(1); + should(result.docs[0].data().notEqual).equal('now'); + }); + + it("should throw error when using '!=' operator twice ", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, where('test', '!=', 1), where('test', '!=', 2)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); + + it("should throw error when combining '!=' operator with any other inequality operator on a different field", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, where('test', '!=', 1), where('differentField', '>', 1)); + return Promise.reject(new Error('Did not throw an Error on >.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + query(ref, where('test', '!=', 1), where('differentField', '<', 1)); + return Promise.reject(new Error('Did not throw an Error on <.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + query(ref, where('test', '!=', 1), where('differentField', '<=', 1)); + return Promise.reject(new Error('Did not throw an Error <=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + query(ref, where('test', '!=', 1), where('differentField', '>=', 1)); + return Promise.reject(new Error('Did not throw an Error >=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } - try { - ref.where('test', '!=', 1).where('test', '!=', 2); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one '!=' inequality filter."); return Promise.resolve(); - } - }); + }); - it("should throw error when combining '!=' operator with any other inequality operator on a different field", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where('test', '!=', 1).where('differentField', '>', 1); - return Promise.reject(new Error('Did not throw an Error on >.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - try { - ref.where('test', '!=', 1).where('differentField', '<', 1); - return Promise.reject(new Error('Did not throw an Error on <.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - try { - ref.where('test', '!=', 1).where('differentField', '<=', 1); - return Promise.reject(new Error('Did not throw an Error <=.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - try { - ref.where('test', '!=', 1).where('differentField', '>=', 1); - return Promise.reject(new Error('Did not throw an Error >=.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - return Promise.resolve(); - }); + it('should handle where clause after sort by', async function () { + const { getFirestore, collection, addDoc, query, where, orderBy, getDocs } = firestoreModular; + const ref = collection( + getFirestore(), + `${COLLECTION}/filter/sort-by-where${Date.now() + ''}`, + ); + + await addDoc(ref, { status: 1 }); + await addDoc(ref, { status: 2 }); + await addDoc(ref, { status: 3 }); - it('should handle where clause after sort by', async function () { - const ref = firebase - .firestore() - .collection(`${COLLECTION}/filter/sort-by-where${Date.now() + ''}`); - - await ref.add({ status: 1 }); - await ref.add({ status: 2 }); - await ref.add({ status: 3 }); - - const items = []; - await ref - .orderBy('status', 'desc') - .where('status', '<=', 2) - .get() - .then($ => $.forEach(doc => items.push(doc.data()))); - - items.length.should.equal(2); - items[0].status.should.equal(2); - items[1].status.should.equal(1); + const items = []; + await getDocs(query(ref, orderBy('status', 'desc'), where('status', '<=', 2))).then($ => + $.forEach(doc => items.push(doc.data())), + ); + + items.length.should.equal(2); + items[0].status.should.equal(2); + items[1].status.should.equal(1); + }); }); }); diff --git a/packages/firestore/e2e/Query/where.or.filter.e2e.js b/packages/firestore/e2e/Query/where.or.filter.e2e.js index ba74f89be4..2d94303331 100644 --- a/packages/firestore/e2e/Query/where.or.filter.e2e.js +++ b/packages/firestore/e2e/Query/where.or.filter.e2e.js @@ -24,1071 +24,2217 @@ describe('firestore().collection().where(OR Filters)', function () { return await wipe(); }); - it('throws if using nested Filter.or() queries', async function () { - try { + describe('v8 compatibility', function () { + it('throws if using nested Filter.or() queries', async function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.or(Filter('foo', '==', 'bar'), Filter('bar', '==', 'foo')), + Filter.or(Filter('foo', '==', 'baz'), Filter('bar', '==', 'baz')), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('OR Filters with nested OR Filters are not supported'); + } + + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and( + Filter.or(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), + Filter('more', '==', 'stuff'), + ), + Filter.and( + Filter.or(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), + Filter('baz', '==', 'foo'), + ), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('OR Filters with nested OR Filters are not supported'); + } + return Promise.resolve(); + }); + + it('throws if fieldPath string is invalid', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and(Filter('.foo.bar', '!=', 1), Filter('.foo.bar', '==', 1)), + Filter.and(Filter('.foo.bar', '!=', 1), Filter('foo.bar', '==', 1)), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if operator string is invalid', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), + Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'opStr' is invalid"); + return Promise.resolve(); + } + }); + + it('throws if query contains multiple array-contains', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and( + Filter('foo.bar', 'array-contains', 1), + Filter('foo.bar', 'array-contains', 1), + ), + Filter.and(Filter('foo.bar', '==', 1), Filter('foo.bar', '==', 2)), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Queries only support a single array-contains filter'); + return Promise.resolve(); + } + }); + + it('throws if value is not defined', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and(Filter('foo.bar', 'array-contains'), Filter('foo.bar', 'array-contains')), + Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'value' argument expected"); + return Promise.resolve(); + } + }); + + it('throws if null value and no equal operator', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', 'array-contains', null)), + Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You can only perform equals comparisons on null'); + return Promise.resolve(); + } + }); + + it('allows null to be used with equal operator', function () { firebase .firestore() .collection(COLLECTION) .where( Filter.or( - Filter.or(Filter('foo', '==', 'bar'), Filter('bar', '==', 'foo')), - Filter.or(Filter('foo', '==', 'baz'), Filter('bar', '==', 'baz')), + Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', null)), + Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', null)), ), ); + }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('OR Filters with nested OR Filters are not supported'); - } + it('allows null to be used with not equal operator', function () { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '!=', null)), + Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', 'something')), + ), + ); + }); + + it('throws if multiple inequalities on different paths is provided', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '>', 123)), + Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '>', 123)), + ), + ); - try { + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('All where filters with an inequality'); + return Promise.resolve(); + } + }); + + it('allows inequality on the same path', function () { firebase .firestore() .collection(COLLECTION) .where( Filter.or( Filter.and( - Filter.or(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), - Filter('more', '==', 'stuff'), + Filter('foo.bar', '>', 123), + Filter(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234), ), Filter.and( - Filter.or(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), - Filter('baz', '==', 'foo'), + Filter('foo.bar', '>', 123), + Filter(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234), ), ), ); + }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('OR Filters with nested OR Filters are not supported'); - } - return Promise.resolve(); - }); + it('throws if in query with no array value', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and(Filter('foo.bar', 'in', '123'), Filter('foo.bar', 'in', '123')), + Filter.and(Filter('foo.bar', 'in', '123'), Filter('foo.bar', 'in', '123')), + ), + ); - it('throws if fieldPath string is invalid', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); + + it('throws if array-contains-any query with no array value', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and( + Filter('foo.bar', 'array-contains-any', '123'), + Filter('foo.bar', 'array-contains-any', '123'), + ), + Filter.and( + Filter('foo.bar', 'array-contains-any', '123'), + Filter('foo.bar', 'array-contains-any', '123'), + ), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); + + it('throws if in query array length is greater than 10', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and( + Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + ), + Filter.and( + Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + ), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('maximum of 10 elements in the value'); + return Promise.resolve(); + } + }); + + it('throws if query has multiple array-contains-any filter', function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and( + Filter('foo.bar', 'array-contains-any', [1]), + Filter('foo.bar', 'array-contains-any', [1]), + ), + Filter.and( + Filter('foo.bar', 'array-contains-any', [1]), + Filter('foo.bar', 'array-contains-any', [1]), + ), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); + return Promise.resolve(); + } + }); + + it("should throw error when using 'not-in' operator twice", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where( + Filter.or( + Filter.and(Filter('foo.bar', 'not-in', [1]), Filter('foo.bar', 'not-in', [2])), + Filter.and(Filter('foo.bar', 'not-in', [1]), Filter('foo.bar', 'not-in', [2])), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with '!=' operator", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where( + Filter.or( + Filter.and(Filter('foo.bar', '!=', [1]), Filter('foo.bar', 'not-in', [2])), + Filter.and(Filter('foo.bar', '!=', [1]), Filter('foo.bar', 'not-in', [2])), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", + ); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'in' operator", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where( + Filter.or( + Filter.and(Filter('foo.bar', 'in', [1]), Filter('foo.bar', 'not-in', [2])), + Filter.and(Filter('foo.bar', 'in', [1]), Filter('foo.bar', 'not-in', [2])), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where( Filter.or( - Filter.and(Filter('.foo.bar', '!=', 1), Filter('.foo.bar', '==', 1)), - Filter.and(Filter('.foo.bar', '!=', 1), Filter('foo.bar', '==', 1)), + Filter.and( + Filter('foo.bar', 'array-contains-any', [1]), + Filter('foo.bar', 'not-in', [2]), + ), + Filter.and( + Filter('foo.bar', 'array-contains-any', [1]), + Filter('foo.bar', 'not-in', [2]), + ), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", + ); + return Promise.resolve(); + } + }); + + it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where( + Filter.or( + Filter.and( + Filter('foo.bar', '==', 1), + Filter('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + ), + Filter.and( + Filter('foo.bar', '==', 1), + Filter('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + ), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 10 elements in the value array.', + ); + return Promise.resolve(); + } + }); + + it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { + try { + firebase + .firestore() + .collection(COLLECTION) + .where( + Filter.or( + Filter.and( + Filter(firebase.firestore.FieldPath.documentId(), '==', ['document-id']), + Filter('foo.bar', 'not-in', [1, 2, 3, 4]), + ), + Filter.and( + Filter(firebase.firestore.FieldPath.documentId(), '==', ['document-id']), + Filter('foo.bar', '==', 'something'), + ), + ), + ) + .orderBy('differentOrderBy', 'desc'); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); + return Promise.resolve(); + } + }); + + it("should throw error when using '!=' operator twice ", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where( + Filter.or( + Filter.and(Filter('foo.bar', '!=', 1), Filter('foo.baz', '!=', 2)), + Filter.and(Filter('foo.bar', '!=', 1), Filter('foo.baz', '!=', 2)), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); + + it("should throw error when combining '!=' operator with any other inequality operator on a different field", async function () { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where( + Filter.or( + Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>', 2)), + Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>', 2)), + ), + ); + return Promise.reject(new Error('Did not throw an Error on >.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where( + Filter.or( + Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<', 2)), + Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<', 2)), + ), + ); + return Promise.reject(new Error('Did not throw an Error on <.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where( + Filter.or( + Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<=', 2)), + Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<=', 2)), + ), + ); + return Promise.reject(new Error('Did not throw an Error <=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where( + Filter.or( + Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>=', 2)), + Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>=', 2)), + ), + ); + return Promise.reject(new Error('Did not throw an Error >=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + return Promise.resolve(); - } + }); + + /* Queries */ + + // OR queries without ANDs + + // Equals OR another filter that works: '==', '>', '>=', '<', '<=', '!=' + + it('returns with where "==" Filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar' }; + const expected2 = { foo: 'farm' }; + + await Promise.all([ + colRef.add({ foo: 'something' }), + colRef.add(expected), + colRef.add(expected2), + ]); + + const snapshot = await colRef + .where(Filter.or(Filter('foo', '==', 'bar'), Filter('foo', '==', 'farm'))) + .get(); + + snapshot.size.should.eql(2); + const results = snapshot.docs.map(doc => doc.data().foo); + results.should.containEql('bar'); + results.should.containEql('farm'); + }); + + it('returns with where ">" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater-than`); + + const expected = { foo: 100 }; + + await Promise.all([colRef.add({ foo: 2 }), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where(Filter.or(Filter('foo', '>', 2), Filter('foo', '==', 30))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "<" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less-than`); + + const expected = { foo: 2 }; + + await Promise.all([colRef.add({ foo: 100 }), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where(Filter.or(Filter('foo', '<', 3), Filter('foo', '==', 22))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where ">=" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater-than-or-equal`); + + const expected = { foo: 100 }; + + await Promise.all([colRef.add({ foo: 2 }), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where(Filter.or(Filter('foo', '>=', 100), Filter('foo', '==', 45))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "<=" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less-than-or-equal`); + + const expected = { foo: 100 }; + + await Promise.all([colRef.add({ foo: 101 }), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where(Filter.or(Filter('foo', '<=', 100), Filter('foo', '==', 90))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + // // Equals OR another filter that works: "array-contains", "in", "array-contains-any", "not-in" + + it('returns "array-contains" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains`); + + const expected = { foo: 'bar', something: [1, 2, 3] }; + + await Promise.all([ + colRef.add({ foo: 'something' }), + colRef.add(expected), + colRef.add(expected), + ]); + + const snapshot = await colRef + .where(Filter.or(Filter('foo', '==', 'not-this'), Filter('something', 'array-contains', 2))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + const data = s.data(); + data.foo.should.eql('bar'); + data.something[0].should.eql(1); + data.something[1].should.eql(2); + data.something[2].should.eql(3); + }); + }); + + it('returns "array-contains-any" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains-any`); + + const expected = { foo: 'bar', something: [1, 2, 3] }; + + await Promise.all([ + colRef.add({ foo: 'something' }), + colRef.add(expected), + colRef.add(expected), + ]); + + const snapshot = await colRef + .where( + Filter.or( + Filter('foo', '==', 'not-this'), + Filter('something', 'array-contains-any', [2, 45]), + ), + ) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + const data = s.data(); + data.foo.should.eql('bar'); + data.something[0].should.eql(1); + data.something[1].should.eql(2); + data.something[2].should.eql(3); + }); + }); + + it('returns with where "not-in" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); + const expected = 'bar'; + const data = { foo: expected }; + + await Promise.all([ + colRef.add({ foo: 'not' }), + colRef.add({ foo: 'this' }), + colRef.add(data), + colRef.add(data), + ]); + + const snapshot = await colRef + .where(Filter.or(Filter('foo', 'not-in', ['not', 'this']), Filter('foo', '==', 'not-this'))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "in" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); + const expected1 = 'bar'; + const expected2 = 'baz'; + const data1 = { foo: expected1 }; + const data2 = { foo: expected2 }; + + await Promise.all([ + colRef.add({ foo: 'not' }), + colRef.add({ foo: 'this' }), + colRef.add(data1), + colRef.add(data2), + ]); + + const snapshot = await colRef + .where( + Filter.or(Filter('foo', 'in', [expected1, expected2]), Filter('foo', '==', 'not-this')), + ) + .get(); + + snapshot.size.should.eql(2); + const results = snapshot.docs.map(d => d.data().foo); + results.should.containEql(expected1); + results.should.containEql(expected2); + }); + + // OR queries with ANDs. Equals and: '==', '>', '>=', '<', '<=', '!=' + it('returns with where "==" && "==" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', bar: 'baz' }; + await Promise.all([ + colRef.add({ foo: [1, '1', 'something'] }), + colRef.add(expected), + colRef.add(expected), + ]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), + Filter.and(Filter('blah', '==', 'blah'), Filter('not', '==', 'this')), + ), + ) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & "!=" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', baz: 'baz' }; + const notExpected = { foo: 'bar', baz: 'something' }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where(Filter.and(Filter('foo', '==', 'bar'), Filter('baz', '!=', 'something'))) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & ">" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals-not-equals`); + + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1 }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>', 2)), + Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '>', 199)), + ), + ) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & "<" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1000 }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<', 201)), + Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '<', 201)), + ), + ) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & "<=" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1000 }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<=', 200)), + Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '<=', 200)), + ), + ) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & ">=" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 100 }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>=', 200)), + Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '>=', 200)), + ), + ) + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + // Using OR and AND query combinations with Equals && "array-contains", "array-contains-any", "not-in" and "in" filters + + it('returns with where "==" & "array-contains" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const match = Date.now(); + await Promise.all([ + colRef.add({ foo: [1, '1', match] }), + colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), + colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), + ]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and( + Filter('foo', 'array-contains', match.toString()), + Filter('bar', '==', 'baz'), + ), + Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + ), + ) + .get(); + const expected = [1, '2', match.toString()]; + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('returns with where "==" & "array-contains-any" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const match = Date.now(); + await Promise.all([ + colRef.add({ foo: [1, '1', match] }), + colRef.add({ foo: [1, '2'], bar: 'baz' }), + colRef.add({ foo: ['2', match.toString()], bar: 'baz' }), + ]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and( + Filter('foo', 'array-contains-any', [match.toString(), 1]), + Filter('bar', '==', 'baz'), + ), + Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + ), + ) + .get(); + + snapshot.size.should.eql(2); + snapshot.docs[0].data().bar.should.equal('baz'); + snapshot.docs[1].data().bar.should.equal('baz'); + }); + + it('returns with where "==" & "not-in" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); + + await Promise.all([ + colRef.add({ foo: 'bar', bar: 'baz' }), + colRef.add({ foo: 'thing', bar: 'baz' }), + colRef.add({ foo: 'bar', bar: 'baz' }), + colRef.add({ foo: 'yolo', bar: 'baz' }), + ]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and(Filter('foo', 'not-in', ['yolo', 'thing']), Filter('bar', '==', 'baz')), + Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + ), + ) + .get(); + + snapshot.size.should.eql(2); + snapshot.docs[0].data().foo.should.equal('bar'); + snapshot.docs[1].data().foo.should.equal('bar'); + }); + + it('returns with where "==" & "in" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); + + await Promise.all([ + colRef.add({ foo: 'bar', bar: 'baz' }), + colRef.add({ foo: 'thing', bar: 'baz' }), + colRef.add({ foo: 'yolo', bar: 'baz' }), + ]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and(Filter('foo', 'in', ['bar', 'yolo']), Filter('bar', '==', 'baz')), + Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + ), + ) + .get(); + + snapshot.size.should.eql(2); + const result = snapshot.docs.map(d => d.data().foo); + result.should.containEql('bar'); + result.should.containEql('yolo'); + }); + + // Backwards compatibility Filter queries. Add where() queries and also use multiple where() queries with Filters to check it works + + it('backwards compatible with existing where() "==" && "==" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const expected = { foo: 'bar', bar: 'baz', existing: 'where' }; + await Promise.all([ + colRef.add({ foo: [1, '1', 'something'] }), + colRef.add(expected), + colRef.add(expected), + ]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), + Filter.and(Filter('blah', '==', 'blah'), Filter('not', '==', 'this')), + ), + ) + .where('existing', '==', 'where') + .get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); + + it('backwards compatible with existing where() query, returns with where "==" & "array-contains" filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const match = Date.now(); + await Promise.all([ + colRef.add({ foo: [1, '1', match] }), + colRef.add({ foo: [1, '2', match.toString()], bar: 'baz', existing: 'where' }), + colRef.add({ foo: [1, '2', match.toString()], bar: 'baz', existing: 'where' }), + ]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and( + Filter('foo', 'array-contains', match.toString()), + Filter('bar', '==', 'baz'), + ), + Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + ), + ) + .where('existing', '==', 'where') + .get(); + const expected = [1, '2', match.toString()]; + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('backwards compatible whilst chaining Filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const match = Date.now(); + await Promise.all([ + colRef.add({ foo: [1, '1', match] }), + colRef.add({ + foo: [1, '2', match.toString()], + bar: 'baz', + existing: 'where', + another: 'filter', + }), + colRef.add({ + foo: [1, '2', match.toString()], + bar: 'baz', + existing: 'where', + another: 'filter', + }), + ]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and( + Filter('foo', 'array-contains', match.toString()), + Filter('bar', '==', 'baz'), + ), + Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + ), + ) + .where('existing', '==', 'where') + .where(Filter('another', '==', 'filter')) + .get(); + const expected = [1, '2', match.toString()]; + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); + + it('backwards compatible whilst chaining AND Filter', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + + const match = Date.now(); + await Promise.all([ + colRef.add({ foo: [1, '1', match] }), + colRef.add({ + foo: [1, '2', match.toString()], + bar: 'baz', + existing: 'where', + another: 'filter', + chain: 'and', + }), + colRef.add({ + foo: [1, '2', match.toString()], + bar: 'baz', + existing: 'where', + another: 'filter', + chain: 'and', + }), + ]); + + const snapshot = await colRef + .where( + Filter.or( + Filter.and( + Filter('foo', 'array-contains', match.toString()), + Filter('bar', '==', 'baz'), + ), + Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + ), + ) + .where('existing', '==', 'where') + .where(Filter.and(Filter('another', '==', 'filter'), Filter('chain', '==', 'and'))) + .get(); + const expected = [1, '2', match.toString()]; + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); }); - it('throws if operator string is invalid', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), - Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), + describe('modular', function () { + it('throws if using nested or() queries', async function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + const firestore = getFirestore(); + try { + query( + collection(firestore, COLLECTION), + where( + or( + or(where('foo', '==', 'bar'), where('bar', '==', 'foo')), + or(where('foo', '==', 'baz'), where('bar', '==', 'baz')), + ), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('OR Filters with nested OR Filters are not supported'); + } + + try { + query( + collection(firestore, COLLECTION), + where( + or( + and( + or(where('foo', '==', 'bar'), where('bar', '==', 'baz')), + where('more', '==', 'stuff'), + ), + and( + or(where('foo', '==', 'bar'), where('bar', '==', 'baz')), + where('baz', '==', 'foo'), + ), + ), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('OR Filters with nested OR Filters are not supported'); + } + return Promise.resolve(); + }); + + it('throws if fieldPath string is invalid', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and(where('.foo.bar', '!=', 1), where('.foo.bar', '==', 1)), + and(where('.foo.bar', '!=', 1), where('foo.bar', '==', 1)), + ), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); + + it('throws if operator string is invalid', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and(where('foo.bar', '!', 1), where('foo.bar', '!', 1)), + and(where('foo.bar', '!', 1), where('foo.bar', '!', 1)), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'opStr' is invalid"); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'opStr' is invalid"); + return Promise.resolve(); + } + }); - it('throws if query contains multiple array-contains', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter('foo.bar', 'array-contains', 1), - Filter('foo.bar', 'array-contains', 1), - ), - Filter.and(Filter('foo.bar', '==', 1), Filter('foo.bar', '==', 2)), + it('throws if query contains multiple array-contains', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and(where('foo.bar', 'array-contains', 1), where('foo.bar', 'array-contains', 1)), + and(where('foo.bar', '==', 1), where('foo.bar', '==', 2)), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Queries only support a single array-contains filter'); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Queries only support a single array-contains filter'); + return Promise.resolve(); + } + }); - it('throws if value is not defined', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', 'array-contains'), Filter('foo.bar', 'array-contains')), - Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), + it('throws if value is not defined', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and(where('foo.bar', 'array-contains'), where('foo.bar', 'array-contains')), + and(where('foo.bar', '!', 1), where('foo.bar', '!', 1)), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' argument expected"); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'value' argument expected"); + return Promise.resolve(); + } + }); - it('throws if null value and no equal operator', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', 'array-contains', null)), - Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), + it('throws if null value and no equal operator', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and(where('foo.bar', '==', null), where('foo.bar', 'array-contains', null)), + and(where('foo.bar', '!', 1), where('foo.bar', '!', 1)), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You can only perform equals comparisons on null'); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You can only perform equals comparisons on null'); + return Promise.resolve(); + } + }); - it('allows null to be used with equal operator', function () { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', null)), - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', null)), + it('allows null to be used with equal operator', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + query( + collection(getFirestore(), COLLECTION), + or( + and(where('foo.bar', '==', null), where('foo.bar', '==', null)), + and(where('foo.bar', '==', null), where('foo.bar', '==', null)), ), ); - }); + }); - it('allows null to be used with not equal operator', function () { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '!=', null)), - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', 'something')), + it('allows null to be used with not equal operator', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + query( + collection(getFirestore(), COLLECTION), + or( + and(where('foo.bar', '==', null), where('foo.bar', '!=', null)), + and(where('foo.bar', '==', null), where('foo.bar', '==', 'something')), ), ); - }); + }); - it('throws if multiple inequalities on different paths is provided', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '>', 123)), - Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '>', 123)), + it('throws if multiple inequalities on different paths is provided', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and(where('foo.bar', '>', 123), where('bar', '>', 123)), + and(where('foo.bar', '>', 123), where('bar', '>', 123)), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('All where filters with an inequality'); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('All where filters with an inequality'); + return Promise.resolve(); + } + }); - it('allows inequality on the same path', function () { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter('foo.bar', '>', 123), - Filter(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234), - ), - Filter.and( - Filter('foo.bar', '>', 123), - Filter(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234), - ), + it('allows inequality on the same path', function () { + const { getFirestore, collection, where, or, and, query, FieldPath } = firestoreModular; + query( + collection(getFirestore(), COLLECTION), + or( + and(where('foo.bar', '>', 123), where(new FieldPath('foo', 'bar'), '>', 1234)), + and(where('foo.bar', '>', 123), where(new FieldPath('foo', 'bar'), '>', 1234)), ), ); - }); + }); - it('throws if in query with no array value', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', 'in', '123'), Filter('foo.bar', 'in', '123')), - Filter.and(Filter('foo.bar', 'in', '123'), Filter('foo.bar', 'in', '123')), + it('throws if in query with no array value', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and(where('foo.bar', 'in', '123'), where('foo.bar', 'in', '123')), + and(where('foo.bar', 'in', '123'), where('foo.bar', 'in', '123')), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - it('throws if array-contains-any query with no array value', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter('foo.bar', 'array-contains-any', '123'), - Filter('foo.bar', 'array-contains-any', '123'), + it('throws if array-contains-any query with no array value', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and( + where('foo.bar', 'array-contains-any', '123'), + where('foo.bar', 'array-contains-any', '123'), ), - Filter.and( - Filter('foo.bar', 'array-contains-any', '123'), - Filter('foo.bar', 'array-contains-any', '123'), + and( + where('foo.bar', 'array-contains-any', '123'), + where('foo.bar', 'array-contains-any', '123'), ), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - it('throws if in query array length is greater than 10', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), - Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + it('throws if in query array length is greater than 10', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and( + where('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + where('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), ), - Filter.and( - Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), - Filter('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + and( + where('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + where('foo.bar', 'in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), ), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('maximum of 10 elements in the value'); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('maximum of 10 elements in the value'); + return Promise.resolve(); + } + }); - it('throws if query has multiple array-contains-any filter', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'array-contains-any', [1]), + it('throws if query has multiple array-contains-any filter', function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and( + where('foo.bar', 'array-contains-any', [1]), + where('foo.bar', 'array-contains-any', [1]), ), - Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'array-contains-any', [1]), + and( + where('foo.bar', 'array-contains-any', [1]), + where('foo.bar', 'array-contains-any', [1]), ), ), ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); + return Promise.resolve(); + } + }); - it("should throw error when using 'not-in' operator twice", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it("should throw error when using 'not-in' operator twice", async function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', 'not-in', [1]), Filter('foo.bar', 'not-in', [2])), - Filter.and(Filter('foo.bar', 'not-in', [1]), Filter('foo.bar', 'not-in', [2])), - ), - ); + try { + query( + ref, + or( + and(where('foo.bar', 'not-in', [1]), where('foo.bar', 'not-in', [2])), + and(where('foo.bar', 'not-in', [1]), where('foo.bar', 'not-in', [2])), + ), + ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'not-in' filter."); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } + }); - it("should throw error when combining 'not-in' operator with '!=' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it("should throw error when combining 'not-in' operator with '!=' operator", async function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', '!=', [1]), Filter('foo.bar', 'not-in', [2])), - Filter.and(Filter('foo.bar', '!=', [1]), Filter('foo.bar', 'not-in', [2])), - ), - ); + try { + query( + ref, + or( + and(where('foo.bar', '!=', [1]), where('foo.bar', 'not-in', [2])), + and(where('foo.bar', '!=', [1]), where('foo.bar', 'not-in', [2])), + ), + ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with '!=' inequality filters", - ); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", + ); + return Promise.resolve(); + } + }); - it("should throw error when combining 'not-in' operator with 'in' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it("should throw error when combining 'not-in' operator with 'in' operator", async function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', 'in', [1]), Filter('foo.bar', 'not-in', [2])), - Filter.and(Filter('foo.bar', 'in', [1]), Filter('foo.bar', 'not-in', [2])), - ), - ); + try { + query( + ref, + or( + and(where('foo.bar', 'in', [1]), where('foo.bar', 'not-in', [2])), + and(where('foo.bar', 'in', [1]), where('foo.bar', 'not-in', [2])), + ), + ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); - it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); - try { - ref.where( - Filter.or( - Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'not-in', [2]), - ), - Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'not-in', [2]), + try { + query( + ref, + or( + and(where('foo.bar', 'array-contains-any', [1]), where('foo.bar', 'not-in', [2])), + and(where('foo.bar', 'array-contains-any', [1]), where('foo.bar', 'not-in', [2])), ), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with 'array-contains-any' filters.", - ); - return Promise.resolve(); - } - }); + ); - it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { - const ref = firebase.firestore().collection(COLLECTION); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", + ); + return Promise.resolve(); + } + }); - try { - ref.where( - Filter.or( - Filter.and( - Filter('foo.bar', '==', 1), - Filter('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), - ), - Filter.and( - Filter('foo.bar', '==', 1), - Filter('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query( + ref, + or( + and( + where('foo.bar', '==', 1), + where('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + ), + and( + where('foo.bar', '==', 1), + where('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + ), ), - ), - ); + ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'filters support a maximum of 10 elements in the value array.', - ); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 10 elements in the value array.', + ); + return Promise.resolve(); + } + }); - it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter(firebase.firestore.FieldPath.documentId(), '==', ['document-id']), - Filter('foo.bar', 'not-in', [1, 2, 3, 4]), + it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { + const { getFirestore, collection, where, orderBy, or, and, query, FieldPath } = + firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + or( + and( + where(FieldPath.documentId(), '==', ['document-id']), + where('foo.bar', 'not-in', [1, 2, 3, 4]), ), - Filter.and( - Filter(firebase.firestore.FieldPath.documentId(), '==', ['document-id']), - Filter('foo.bar', '==', 'something'), + and( + where(FieldPath.documentId(), '==', ['document-id']), + where('foo.bar', '==', 'something'), ), ), - ) - .orderBy('differentOrderBy', 'desc'); + orderBy('differentOrderBy', 'desc'), + ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); + return Promise.resolve(); + } + }); - it("should throw error when using '!=' operator twice ", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it("should throw error when using '!=' operator twice ", async function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', '!=', 1), Filter('foo.baz', '!=', 2)), - Filter.and(Filter('foo.bar', '!=', 1), Filter('foo.baz', '!=', 2)), - ), - ); + try { + query( + ref, + or( + and(where('foo.bar', '!=', 1), where('foo.baz', '!=', 2)), + and(where('foo.bar', '!=', 1), where('foo.baz', '!=', 2)), + ), + ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one '!=' inequality filter."); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); - it("should throw error when combining '!=' operator with any other inequality operator on a different field", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it("should throw error when combining '!=' operator with any other inequality operator on a different field", async function () { + const { getFirestore, collection, where, or, and, query } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>', 2)), - Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>', 2)), - ), - ); - return Promise.reject(new Error('Did not throw an Error on >.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<', 2)), - Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<', 2)), - ), - ); - return Promise.reject(new Error('Did not throw an Error on <.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<=', 2)), - Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '<=', 2)), - ), - ); - return Promise.reject(new Error('Did not throw an Error <=.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } - - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>=', 2)), - Filter.and(Filter('foo.bar', '!=', 1), Filter('differentField', '>=', 2)), - ), - ); - return Promise.reject(new Error('Did not throw an Error >=.')); - } catch (error) { - error.message.should.containEql('must be on the same field.'); - } + try { + query( + ref, + or( + and(where('foo.bar', '!=', 1), where('differentField', '>', 2)), + and(where('foo.bar', '!=', 1), where('differentField', '>', 2)), + ), + ); + return Promise.reject(new Error('Did not throw an Error on >.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + query( + ref, + or( + and(where('foo.bar', '!=', 1), where('differentField', '<', 2)), + and(where('foo.bar', '!=', 1), where('differentField', '<', 2)), + ), + ); + return Promise.reject(new Error('Did not throw an Error on <.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + query( + ref, + or( + and(where('foo.bar', '!=', 1), where('differentField', '<=', 2)), + and(where('foo.bar', '!=', 1), where('differentField', '<=', 2)), + ), + ); + return Promise.reject(new Error('Did not throw an Error <=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + query( + ref, + or( + and(where('foo.bar', '!=', 1), where('differentField', '>=', 2)), + and(where('foo.bar', '!=', 1), where('differentField', '>=', 2)), + ), + ); + return Promise.reject(new Error('Did not throw an Error >=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } - return Promise.resolve(); - }); + return Promise.resolve(); + }); - /* Queries */ + /* Queries */ - // OR queries without ANDs + // OR queries without ANDs - // Equals OR another filter that works: '==', '>', '>=', '<', '<=', '!=' + // Equals OR another filter that works: '==', '>', '>=', '<', '<=', '!=' - it('returns with where "==" Filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + it('returns with where "==" Filter', async function () { + const { getFirestore, collection, where, or, query, addDoc, getDocs } = firestoreModular; - const expected = { foo: 'bar' }; - const expected2 = { foo: 'farm' }; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); - await Promise.all([ - colRef.add({ foo: 'something' }), - colRef.add(expected), - colRef.add(expected2), - ]); + const expected = { foo: 'bar' }; + const expected2 = { foo: 'farm' }; - const snapshot = await colRef - .where(Filter.or(Filter('foo', '==', 'bar'), Filter('foo', '==', 'farm'))) - .get(); + await Promise.all([ + addDoc(colRef, { foo: 'something' }), + addDoc(colRef, expected), + addDoc(colRef, expected2), + ]); - snapshot.size.should.eql(2); - const results = snapshot.docs.map(doc => doc.data().foo); - results.should.containEql('bar'); - results.should.containEql('farm'); - }); + const snapshot = await getDocs( + query(colRef, or(where('foo', '==', 'bar'), where('foo', '==', 'farm'))), + ); - it('returns with where ">" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater-than`); + snapshot.size.should.eql(2); + const results = snapshot.docs.map(doc => doc.data().foo); + results.should.containEql('bar'); + results.should.containEql('farm'); + }); + + it('returns with where ">" filter', async function () { + const { getFirestore, collection, where, or, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/greater-than-modular`); - const expected = { foo: 100 }; + const expected = { foo: 100 }; - await Promise.all([colRef.add({ foo: 2 }), colRef.add(expected), colRef.add(expected)]); + await Promise.all([ + addDoc(colRef, { foo: 2 }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef - .where(Filter.or(Filter('foo', '>', 2), Filter('foo', '==', 30))) - .get(); + const snapshot = await getDocs( + query(colRef, or(where('foo', '>', 2), where('foo', '==', 30))), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where "<" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less-than`); + it('returns with where "<" filter', async function () { + const { getFirestore, collection, where, or, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/less-than-modular`); - const expected = { foo: 2 }; + const expected = { foo: 2 }; - await Promise.all([colRef.add({ foo: 100 }), colRef.add(expected), colRef.add(expected)]); + await Promise.all([ + addDoc(colRef, { foo: 100 }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef - .where(Filter.or(Filter('foo', '<', 3), Filter('foo', '==', 22))) - .get(); + const snapshot = await getDocs( + query(colRef, or(where('foo', '<', 3), where('foo', '==', 22))), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where ">=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater-than-or-equal`); + it('returns with where ">=" filter', async function () { + const { getFirestore, collection, where, or, query, addDoc, getDocs } = firestoreModular; + const colRef = collection( + getFirestore(), + `${COLLECTION}/filter/greater-than-or-equal-modular`, + ); - const expected = { foo: 100 }; + const expected = { foo: 100 }; - await Promise.all([colRef.add({ foo: 2 }), colRef.add(expected), colRef.add(expected)]); + await Promise.all([ + addDoc(colRef, { foo: 2 }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef - .where(Filter.or(Filter('foo', '>=', 100), Filter('foo', '==', 45))) - .get(); + const snapshot = await getDocs( + query(colRef, or(where('foo', '>=', 100), where('foo', '==', 45))), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where "<=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less-than-or-equal`); + it('returns with where "<=" filter', async function () { + const { getFirestore, collection, where, or, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/less-than-or-equal-modular`); - const expected = { foo: 100 }; + const expected = { foo: 100 }; - await Promise.all([colRef.add({ foo: 101 }), colRef.add(expected), colRef.add(expected)]); + await Promise.all([ + addDoc(colRef, { foo: 101 }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef - .where(Filter.or(Filter('foo', '<=', 100), Filter('foo', '==', 90))) - .get(); + const snapshot = await getDocs( + query(colRef, or(where('foo', '<=', 100), where('foo', '==', 90))), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - // // Equals OR another filter that works: "array-contains", "in", "array-contains-any", "not-in" + // // Equals OR another filter that works: "array-contains", "in", "array-contains-any", "not-in" - it('returns "array-contains" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains`); + it('returns "array-contains" filter', async function () { + const { getFirestore, collection, where, or, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/array-contains-modular`); - const expected = { foo: 'bar', something: [1, 2, 3] }; + const expected = { foo: 'bar', something: [1, 2, 3] }; - await Promise.all([ - colRef.add({ foo: 'something' }), - colRef.add(expected), - colRef.add(expected), - ]); + await Promise.all([ + addDoc(colRef, { foo: 'something' }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef - .where(Filter.or(Filter('foo', '==', 'not-this'), Filter('something', 'array-contains', 2))) - .get(); + const snapshot = await getDocs( + query(colRef, or(where('foo', '==', 'not-this'), where('something', 'array-contains', 2))), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - const data = s.data(); - data.foo.should.eql('bar'); - data.something[0].should.eql(1); - data.something[1].should.eql(2); - data.something[2].should.eql(3); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + const data = s.data(); + data.foo.should.eql('bar'); + data.something[0].should.eql(1); + data.something[1].should.eql(2); + data.something[2].should.eql(3); + }); }); - }); - it('returns "array-contains-any" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains-any`); + it('returns "array-contains-any" filter', async function () { + const { getFirestore, collection, where, or, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/array-contains-any-modular`); - const expected = { foo: 'bar', something: [1, 2, 3] }; + const expected = { foo: 'bar', something: [1, 2, 3] }; - await Promise.all([ - colRef.add({ foo: 'something' }), - colRef.add(expected), - colRef.add(expected), - ]); + await Promise.all([ + addDoc(colRef, { foo: 'something' }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef - .where( - Filter.or( - Filter('foo', '==', 'not-this'), - Filter('something', 'array-contains-any', [2, 45]), + const snapshot = await getDocs( + query( + colRef, + or(where('foo', '==', 'not-this'), where('something', 'array-contains-any', [2, 45])), ), - ) - .get(); + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - const data = s.data(); - data.foo.should.eql('bar'); - data.something[0].should.eql(1); - data.something[1].should.eql(2); - data.something[2].should.eql(3); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + const data = s.data(); + data.foo.should.eql('bar'); + data.something[0].should.eql(1); + data.something[1].should.eql(2); + data.something[2].should.eql(3); + }); }); - }); - it('returns with where "not-in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); - const expected = 'bar'; - const data = { foo: expected }; + it('returns with where "not-in" filter', async function () { + const { getFirestore, collection, where, or, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/not-in-modular`); + const expected = 'bar'; + const data = { foo: expected }; + + await Promise.all([ + addDoc(colRef, { foo: 'not' }), + addDoc(colRef, { foo: 'this' }), + addDoc(colRef, data), + addDoc(colRef, data), + ]); + + const snapshot = await getDocs( + query(colRef, or(where('foo', 'not-in', ['not', 'this']), where('foo', '==', 'not-this'))), + ); - await Promise.all([ - colRef.add({ foo: 'not' }), - colRef.add({ foo: 'this' }), - colRef.add(data), - colRef.add(data), - ]); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); + }); - const snapshot = await colRef - .where(Filter.or(Filter('foo', 'not-in', ['not', 'this']), Filter('foo', '==', 'not-this'))) - .get(); + it('returns with where "in" filter', async function () { + const { getFirestore, collection, where, or, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/in-modular`); + const expected1 = 'bar'; + const expected2 = 'baz'; + const data1 = { foo: expected1 }; + const data2 = { foo: expected2 }; + + await Promise.all([ + addDoc(colRef, { foo: 'not' }), + addDoc(colRef, { foo: 'this' }), + addDoc(colRef, data1), + addDoc(colRef, data2), + ]); + + const snapshot = await getDocs( + query( + colRef, + or(where('foo', 'in', [expected1, expected2]), where('foo', '==', 'not-this')), + ), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + const results = snapshot.docs.map(d => d.data().foo); + results.should.containEql(expected1); + results.should.containEql(expected2); }); - }); - it('returns with where "in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); - const expected1 = 'bar'; - const expected2 = 'baz'; - const data1 = { foo: expected1 }; - const data2 = { foo: expected2 }; - - await Promise.all([ - colRef.add({ foo: 'not' }), - colRef.add({ foo: 'this' }), - colRef.add(data1), - colRef.add(data2), - ]); - - const snapshot = await colRef - .where( - Filter.or(Filter('foo', 'in', [expected1, expected2]), Filter('foo', '==', 'not-this')), - ) - .get(); - - snapshot.size.should.eql(2); - const results = snapshot.docs.map(d => d.data().foo); - results.should.containEql(expected1); - results.should.containEql(expected2); - }); - - // OR queries with ANDs. Equals and: '==', '>', '>=', '<', '<=', '!=' - it('returns with where "==" && "==" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', bar: 'baz' }; - await Promise.all([ - colRef.add({ foo: [1, '1', 'something'] }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), - Filter.and(Filter('blah', '==', 'blah'), Filter('not', '==', 'this')), + // OR queries with ANDs. Equals and: '==', '>', '>=', '<', '<=', '!=' + it('returns with where "==" && "==" filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const expected = { foo: 'bar', bar: 'baz' }; + await Promise.all([ + addDoc(colRef, { foo: [1, '1', 'something'] }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', '==', 'bar'), where('bar', '==', 'baz')), + and(where('blah', '==', 'blah'), where('not', '==', 'this')), + ), ), - ) - .get(); + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where "==" & "!=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + it('returns with where "==" & "!=" filter', async function () { + const { getFirestore, collection, where, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); - const expected = { foo: 'bar', baz: 'baz' }; - const notExpected = { foo: 'bar', baz: 'something' }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + const expected = { foo: 'bar', baz: 'baz' }; + const notExpected = { foo: 'bar', baz: 'something' }; + await Promise.all([ + addDoc(colRef, notExpected), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('baz', '!=', 'something'))) - .get(); + const snapshot = await getDocs( + query(colRef, and(where('foo', '==', 'bar'), where('baz', '!=', 'something'))), + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - - it('returns with where "==" & ">" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals-not-equals`); - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>', 2)), - Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '>', 199)), + it('returns with where "==" & ">" filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-not-equals-modular`); + + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1 }; + await Promise.all([ + addDoc(colRef, notExpected), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', '==', 'bar'), where('population', '>', 2)), + and(where('foo', '==', 'not-this'), where('population', '>', 199)), + ), ), - ) - .get(); + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where "==" & "<" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + it('returns with where "==" & "<" filter', async function () { + const { getFirestore, collection, where, or, and, query, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1000 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1000 }; + await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<', 201)), - Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '<', 201)), + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', '==', 'bar'), where('population', '<', 201)), + and(where('foo', '==', 'not-this'), where('population', '<', 201)), + ), ), - ) - .get(); + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - - it('returns with where "==" & "<=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1000 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<=', 200)), - Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '<=', 200)), + it('returns with where "==" & "<=" filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 1000 }; + await Promise.all([ + addDoc(colRef, notExpected), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', '==', 'bar'), where('population', '<=', 200)), + and(where('foo', '==', 'not-this'), where('population', '<=', 200)), + ), ), - ) - .get(); + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where "==" & ">=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 100 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>=', 200)), - Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '>=', 200)), + it('returns with where "==" & ">=" filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const expected = { foo: 'bar', population: 200 }; + const notExpected = { foo: 'bar', population: 100 }; + await Promise.all([ + addDoc(colRef, notExpected), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', '==', 'bar'), where('population', '>=', 200)), + and(where('foo', '==', 'not-this'), where('population', '>=', 200)), + ), ), - ) - .get(); + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - - // Using OR and AND query combinations with Equals && "array-contains", "array-contains-any", "not-in" and "in" filters - - it('returns with where "==" & "array-contains" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), - ]); - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', 'array-contains', match.toString()), Filter('bar', '==', 'baz')), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + // Using OR and AND query combinations with Equals && "array-contains", "array-contains-any", "not-in" and "in" filters + + it('returns with where "==" & "array-contains" filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const match = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: [1, '1', match] }), + addDoc(colRef, { foo: [1, '2', match.toString()], bar: 'baz' }), + addDoc(colRef, { foo: [1, '2', match.toString()], bar: 'baz' }), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', 'array-contains', match.toString()), where('bar', '==', 'baz')), + and(where('foo', '==', 'not-this'), where('bar', '==', 'baz')), + ), ), - ) - .get(); - const expected = [1, '2', match.toString()]; + ); + const expected = [1, '2', match.toString()]; - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where "==" & "array-contains-any" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ foo: [1, '2'], bar: 'baz' }), - colRef.add({ foo: ['2', match.toString()], bar: 'baz' }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and( - Filter('foo', 'array-contains-any', [match.toString(), 1]), - Filter('bar', '==', 'baz'), + it('returns with where "==" & "array-contains-any" filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const match = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: [1, '1', match] }), + addDoc(colRef, { foo: [1, '2'], bar: 'baz' }), + addDoc(colRef, { foo: ['2', match.toString()], bar: 'baz' }), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and( + where('foo', 'array-contains-any', [match.toString(), 1]), + where('bar', '==', 'baz'), + ), + and(where('foo', '==', 'not-this'), where('bar', '==', 'baz')), ), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), ), - ) - .get(); + ); - snapshot.size.should.eql(2); - snapshot.docs[0].data().bar.should.equal('baz'); - snapshot.docs[1].data().bar.should.equal('baz'); - }); + snapshot.size.should.eql(2); + snapshot.docs[0].data().bar.should.equal('baz'); + snapshot.docs[1].data().bar.should.equal('baz'); + }); - it('returns with where "==" & "not-in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); - - await Promise.all([ - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'thing', bar: 'baz' }), - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'yolo', bar: 'baz' }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', 'not-in', ['yolo', 'thing']), Filter('bar', '==', 'baz')), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + it('returns with where "==" & "not-in" filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/not-in-modular`); + + await Promise.all([ + addDoc(colRef, { foo: 'bar', bar: 'baz' }), + addDoc(colRef, { foo: 'thing', bar: 'baz' }), + addDoc(colRef, { foo: 'bar', bar: 'baz' }), + addDoc(colRef, { foo: 'yolo', bar: 'baz' }), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', 'not-in', ['yolo', 'thing']), where('bar', '==', 'baz')), + and(where('foo', '==', 'not-this'), where('bar', '==', 'baz')), + ), ), - ) - .get(); - - snapshot.size.should.eql(2); - snapshot.docs[0].data().foo.should.equal('bar'); - snapshot.docs[1].data().foo.should.equal('bar'); - }); - - it('returns with where "==" & "in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); + ); - await Promise.all([ - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'thing', bar: 'baz' }), - colRef.add({ foo: 'yolo', bar: 'baz' }), - ]); + snapshot.size.should.eql(2); + snapshot.docs[0].data().foo.should.equal('bar'); + snapshot.docs[1].data().foo.should.equal('bar'); + }); - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', 'in', ['bar', 'yolo']), Filter('bar', '==', 'baz')), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + it('returns with where "==" & "in" filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/in-modular`); + + await Promise.all([ + addDoc(colRef, { foo: 'bar', bar: 'baz' }), + addDoc(colRef, { foo: 'thing', bar: 'baz' }), + addDoc(colRef, { foo: 'yolo', bar: 'baz' }), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', 'in', ['bar', 'yolo']), where('bar', '==', 'baz')), + and(where('foo', '==', 'not-this'), where('bar', '==', 'baz')), + ), ), - ) - .get(); - - snapshot.size.should.eql(2); - const result = snapshot.docs.map(d => d.data().foo); - result.should.containEql('bar'); - result.should.containEql('yolo'); - }); - - // Backwards compatibility Filter queries. Add where() queries and also use multiple where() queries with Filters to check it works - - it('backwards compatible with existing where() "==" && "==" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + ); - const expected = { foo: 'bar', bar: 'baz', existing: 'where' }; - await Promise.all([ - colRef.add({ foo: [1, '1', 'something'] }), - colRef.add(expected), - colRef.add(expected), - ]); + snapshot.size.should.eql(2); + const result = snapshot.docs.map(d => d.data().foo); + result.should.containEql('bar'); + result.should.containEql('yolo'); + }); - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), - Filter.and(Filter('blah', '==', 'blah'), Filter('not', '==', 'this')), + // Backwards compatibility Filter queries. Add where() queries and also use multiple where() queries with Filters to check it works + + it('backwards compatible with existing where() "==" && "==" filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const expected = { foo: 'bar', bar: 'baz', existing: 'where' }; + await Promise.all([ + addDoc(colRef, { foo: [1, '1', 'something'] }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', '==', 'bar'), where('bar', '==', 'baz')), + and(where('blah', '==', 'blah'), where('not', '==', 'this')), + ), + where('existing', '==', 'where'), ), - ) - .where('existing', '==', 'where') - .get(); + ); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('backwards compatible with existing where() query, returns with where "==" & "array-contains" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz', existing: 'where' }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz', existing: 'where' }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', 'array-contains', match.toString()), Filter('bar', '==', 'baz')), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + it('backwards compatible with existing where() query, returns with where "==" & "array-contains" filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const match = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: [1, '1', match] }), + addDoc(colRef, { foo: [1, '2', match.toString()], bar: 'baz', existing: 'where' }), + addDoc(colRef, { foo: [1, '2', match.toString()], bar: 'baz', existing: 'where' }), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', 'array-contains', match.toString()), where('bar', '==', 'baz')), + and(where('foo', '==', 'not-this'), where('bar', '==', 'baz')), + ), + where('existing', '==', 'where'), ), - ) - .where('existing', '==', 'where') - .get(); - const expected = [1, '2', match.toString()]; + ); + const expected = [1, '2', match.toString()]; - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); }); - }); - it('backwards compatible whilst chaining Filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ - foo: [1, '2', match.toString()], - bar: 'baz', - existing: 'where', - another: 'filter', - }), - colRef.add({ - foo: [1, '2', match.toString()], - bar: 'baz', - existing: 'where', - another: 'filter', - }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', 'array-contains', match.toString()), Filter('bar', '==', 'baz')), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + it('backwards compatible whilst chaining Filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const match = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: [1, '1', match] }), + addDoc(colRef, { + foo: [1, '2', match.toString()], + bar: 'baz', + existing: 'where', + another: 'filter', + }), + addDoc(colRef, { + foo: [1, '2', match.toString()], + bar: 'baz', + existing: 'where', + another: 'filter', + }), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', 'array-contains', match.toString()), where('bar', '==', 'baz')), + and(where('foo', '==', 'not-this'), where('bar', '==', 'baz')), + ), + where('existing', '==', 'where'), + where('another', '==', 'filter'), ), - ) - .where('existing', '==', 'where') - .where(Filter('another', '==', 'filter')) - .get(); - const expected = [1, '2', match.toString()]; + ); + const expected = [1, '2', match.toString()]; - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); }); - }); - it('backwards compatible whilst chaining AND Filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ - foo: [1, '2', match.toString()], - bar: 'baz', - existing: 'where', - another: 'filter', - chain: 'and', - }), - colRef.add({ - foo: [1, '2', match.toString()], - bar: 'baz', - existing: 'where', - another: 'filter', - chain: 'and', - }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', 'array-contains', match.toString()), Filter('bar', '==', 'baz')), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), + it('backwards compatible whilst chaining AND Filter', async function () { + const { getFirestore, collection, where, or, and, query, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals-modular`); + + const match = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: [1, '1', match] }), + addDoc(colRef, { + foo: [1, '2', match.toString()], + bar: 'baz', + existing: 'where', + another: 'filter', + chain: 'and', + }), + addDoc(colRef, { + foo: [1, '2', match.toString()], + bar: 'baz', + existing: 'where', + another: 'filter', + chain: 'and', + }), + ]); + + const snapshot = await getDocs( + query( + colRef, + or( + and(where('foo', 'array-contains', match.toString()), where('bar', '==', 'baz')), + and(where('foo', '==', 'not-this'), where('bar', '==', 'baz')), + ), + where('existing', '==', 'where'), + and(where('another', '==', 'filter'), where('chain', '==', 'and')), ), - ) - .where('existing', '==', 'where') - .where(Filter.and(Filter('another', '==', 'filter'), Filter('chain', '==', 'and'))) - .get(); - const expected = [1, '2', match.toString()]; - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); + ); + const expected = [1, '2', match.toString()]; + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); + }); }); }); }); diff --git a/packages/firestore/e2e/QuerySnapshot.e2e.js b/packages/firestore/e2e/QuerySnapshot.e2e.js index 3c02051abf..ce6b5f5dcb 100644 --- a/packages/firestore/e2e/QuerySnapshot.e2e.js +++ b/packages/firestore/e2e/QuerySnapshot.e2e.js @@ -21,288 +21,618 @@ describe('firestore.QuerySnapshot', function () { before(function () { return wipe(); }); - it('is returned from a collection get()', async function () { - const snapshot = await firebase.firestore().collection(COLLECTION).get(); - snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); - }); + describe('v8 compatibility', function () { + it('is returned from a collection get()', async function () { + const snapshot = await firebase.firestore().collection(COLLECTION).get(); - it('is returned from a collection onSnapshot()', async function () { - const callback = sinon.spy(); - firebase.firestore().collection(COLLECTION).onSnapshot(callback); - await Utils.spyToBeCalledOnceAsync(callback); - callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); - }); + snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); + }); - it('returns an array of DocumentSnapshots', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - await colRef.add({}); - const snapshot = await colRef.get(); - snapshot.docs.should.be.Array(); - snapshot.docs.length.should.be.aboveOrEqual(1); - snapshot.docs[0].constructor.name.should.eql('FirestoreDocumentSnapshot'); - }); + it('is returned from a collection onSnapshot()', async function () { + const callback = sinon.spy(); + firebase.firestore().collection(COLLECTION).onSnapshot(callback); + await Utils.spyToBeCalledOnceAsync(callback); + callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); + }); - it('returns false if not empty', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - await colRef.add({}); - const snapshot = await colRef.get(); - snapshot.empty.should.be.Boolean(); - snapshot.empty.should.be.False(); - }); + it('returns an array of DocumentSnapshots', async function () { + const colRef = firebase.firestore().collection(COLLECTION); + await colRef.add({}); + const snapshot = await colRef.get(); + snapshot.docs.should.be.Array(); + snapshot.docs.length.should.be.aboveOrEqual(1); + snapshot.docs[0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + }); - it('returns true if empty', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/foo/emptycollection`); - const snapshot = await colRef.get(); - snapshot.empty.should.be.Boolean(); - snapshot.empty.should.be.True(); - }); + it('returns false if not empty', async function () { + const colRef = firebase.firestore().collection(COLLECTION); + await colRef.add({}); + const snapshot = await colRef.get(); + snapshot.empty.should.be.Boolean(); + snapshot.empty.should.be.False(); + }); - it('returns a SnapshotMetadata instance', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - const snapshot = await colRef.get(); - snapshot.metadata.constructor.name.should.eql('FirestoreSnapshotMetadata'); - }); + it('returns true if empty', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/foo/emptycollection`); + const snapshot = await colRef.get(); + snapshot.empty.should.be.Boolean(); + snapshot.empty.should.be.True(); + }); - it('returns a Query instance', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - const snapshot = await colRef.get(); - snapshot.query.constructor.name.should.eql('FirestoreCollectionReference'); - }); + it('returns a SnapshotMetadata instance', async function () { + const colRef = firebase.firestore().collection(COLLECTION); + const snapshot = await colRef.get(); + snapshot.metadata.constructor.name.should.eql('FirestoreSnapshotMetadata'); + }); - it('returns size as a number', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - const snapshot = await colRef.get(); - snapshot.size.should.be.Number(); - }); + it('returns a Query instance', async function () { + const colRef = firebase.firestore().collection(COLLECTION); + const snapshot = await colRef.get(); + snapshot.query.constructor.name.should.eql('FirestoreCollectionReference'); + }); + + it('returns size as a number', async function () { + const colRef = firebase.firestore().collection(COLLECTION); + const snapshot = await colRef.get(); + snapshot.size.should.be.Number(); + }); + + describe('docChanges()', function () { + it('throws if options is not an object', async function () { + try { + const colRef = firebase.firestore().collection(COLLECTION); + const snapshot = await colRef.limit(1).get(); + snapshot.docChanges(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' expected an object"); + return Promise.resolve(); + } + }); + + it('throws if options.includeMetadataChanges is not a boolean', async function () { + try { + const colRef = firebase.firestore().collection(COLLECTION); + const snapshot = await colRef.limit(1).get(); + snapshot.docChanges({ includeMetadataChanges: 123 }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.includeMetadataChanges' expected a boolean"); + return Promise.resolve(); + } + }); - describe('docChanges()', function () { - it('throws if options is not an object', async function () { - try { + it('throws if options.includeMetadataChanges is true, but snapshot does not include those changes', async function () { + const callback = sinon.spy(); const colRef = firebase.firestore().collection(COLLECTION); + const unsub = colRef.onSnapshot( + { + includeMetadataChanges: false, + }, + callback, + ); + await Utils.spyToBeCalledOnceAsync(callback); + unsub(); + const snapshot = callback.args[0][0]; + + try { + snapshot.docChanges({ includeMetadataChanges: true }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('To include metadata changes with your document changes'); + return Promise.resolve(); + } + }); + + it('returns an array of DocumentChange instances', async function () { + const colRef = firebase.firestore().collection(COLLECTION); + await colRef.add({}); const snapshot = await colRef.limit(1).get(); - snapshot.docChanges(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' expected an object"); - return Promise.resolve(); - } + const changes = snapshot.docChanges(); + changes.should.be.Array(); + changes.length.should.be.eql(1); + changes[0].constructor.name.should.eql('FirestoreDocumentChange'); + }); + + // FIXME flakey in CI - the changes length comes back unstable + xit('returns the correct number of document changes if listening to metadata changes', async function () { + const callback = sinon.spy(); + const colRef = firebase + .firestore() + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/metadatachanges-true-true`); + const unsub = colRef.onSnapshot({ includeMetadataChanges: true }, callback); + await colRef.add({ foo: 'bar' }); + await Utils.spyToBeCalledTimesAsync(callback, 3); + unsub(); + + const snap1 = callback.args[0][0]; + const snap2 = callback.args[1][0]; + const snap3 = callback.args[2][0]; + + snap1.docChanges({ includeMetadataChanges: true }).length.should.be.eql(1); + snap2.docChanges({ includeMetadataChanges: true }).length.should.be.eql(0); + snap3.docChanges({ includeMetadataChanges: true }).length.should.be.eql(1); + }); + + // FIXME this flakes on CI, disabling for now + xit('returns the correct number of document changes if listening to metadata changes, but not including them in docChanges', async function () { + const callback = sinon.spy(); + const colRef = firebase + .firestore() + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/metadatachanges-true-false`); + const unsub = colRef.onSnapshot({ includeMetadataChanges: true }, callback); + await Utils.sleep(1000); + await colRef.add({ foo: 'bar' }); + await Utils.spyToBeCalledTimesAsync(callback, 3, 15000); + unsub(); + + const snap1 = callback.args[0][0]; + const snap2 = callback.args[1][0]; + const snap3 = callback.args[2][0]; + + snap1.docChanges({ includeMetadataChanges: false }).length.should.be.eql(1); // FIXME when it flakes, this comes back as 0 + snap2.docChanges({ includeMetadataChanges: false }).length.should.be.eql(0); + snap3.docChanges({ includeMetadataChanges: false }).length.should.be.eql(0); + }); }); - it('throws if options.includeMetadataChanges is not a boolean', async function () { - try { - const colRef = firebase.firestore().collection(COLLECTION); + describe('forEach()', function () { + it('throws if callback is not a function', async function () { + try { + const colRef = firebase.firestore().collection(`${COLLECTION}/callbacks/nonfunction`); + await colRef.add({}); + const snapshot = await colRef.limit(1).get(); + snapshot.forEach(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'callback' expected a function"); + return Promise.resolve(); + } + }); + + it('calls back a function', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/callbacks/function`); + await colRef.add({}); + await colRef.add({}); + const snapshot = await colRef.limit(2).get(); + const callback = sinon.spy(); + snapshot.forEach.should.be.Function(); + snapshot.forEach(callback); + await Utils.spyToBeCalledTimesAsync(callback, 2, 20000); + callback.should.be.calledTwice(); + callback.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + callback.args[0][1].should.be.Number(); + callback.args[1][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + callback.args[1][1].should.be.Number(); + }); + + it('provides context to the callback', async function () { + const colRef = firebase.firestore().collection(`${COLLECTION}/callbacks/function-context`); + await colRef.add({}); const snapshot = await colRef.limit(1).get(); - snapshot.docChanges({ includeMetadataChanges: 123 }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.includeMetadataChanges' expected a boolean"); - return Promise.resolve(); - } + const callback = sinon.spy(); + snapshot.forEach.should.be.Function(); + + class Foo {} + + snapshot.forEach(callback, Foo); + await Utils.spyToBeCalledOnceAsync(callback, 20000); + callback.should.be.calledOnce(); + callback.firstCall.thisValue.should.eql(Foo); + }); }); - it('throws if options.includeMetadataChanges is true, but snapshot does not include those changes', async function () { - const callback = sinon.spy(); - const colRef = firebase.firestore().collection(COLLECTION); - const unsub = colRef.onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); - await Utils.spyToBeCalledOnceAsync(callback); - unsub(); - const snapshot = callback.args[0][0]; - - try { - snapshot.docChanges({ includeMetadataChanges: true }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('To include metadata changes with your document changes'); - return Promise.resolve(); - } + describe('isEqual()', function () { + it('throws if other is not a QuerySnapshot', async function () { + try { + const qs = await firebase.firestore().collection(COLLECTION).get(); + qs.isEqual(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected a QuerySnapshot instance"); + return Promise.resolve(); + } + }); + + it('returns false if not equal (simple checks)', async function () { + const colRef = firebase.firestore().collection(COLLECTION); + // Ensure a doc exists + await colRef.add({}); + + const qs = await colRef.get(); + + const querySnap1 = await firebase + .firestore() + .collection(`${COLLECTION}/querysnapshot/querySnapshotIsEqual`) + .get(); + + const eq1 = qs.isEqual(querySnap1); + + eq1.should.be.False(); + }); + + it('returns false if not equal (expensive checks)', async function () { + const colRef = firebase + .firestore() + .collection(`${COLLECTION}/querysnapshot/querySnapshotIsEqual-False`); + // Ensure a doc exists + const docRef = colRef.doc('firstdoc'); + await docRef.set({ + foo: 'bar', + bar: { + foo: 1, + }, + }); + + // Grab snapshot + const qs1 = await colRef.get(); + + // Update same collection + await docRef.update({ + bar: { + foo: 2, + }, + }); + + const qs2 = await colRef.get(); + + const eq1 = qs1.isEqual(qs2); + + eq1.should.be.False(); + }); + + it('returns true when equal', async function () { + const colRef = firebase + .firestore() + .collection(`${COLLECTION}/querysnapshot/querySnapshotIsEqual-True`); + + await Promise.all([ + colRef.add({ foo: 'bar' }), + colRef.add({ foo: 1 }), + colRef.add({ + foo: { + foo: 'bar', + }, + }), + ]); + + const qs1 = await colRef.get(); + const qs2 = await colRef.get(); + + const eq = qs1.isEqual(qs2); + + eq.should.be.True(); + }); }); + }); - it('returns an array of DocumentChange instances', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - await colRef.add({}); - const snapshot = await colRef.limit(1).get(); - const changes = snapshot.docChanges(); - changes.should.be.Array(); - changes.length.should.be.eql(1); - changes[0].constructor.name.should.eql('FirestoreDocumentChange'); + describe('modular', function () { + it('is returned from a collection get()', async function () { + const { getFirestore, getDocs, collection } = firestoreModular; + + const snapshot = await getDocs(collection(getFirestore(), COLLECTION)); + snapshot.constructor.name.should.eql('FirestoreQuerySnapshot'); }); - // FIXME flakey in CI - the changes length comes back unstable - xit('returns the correct number of document changes if listening to metadata changes', async function () { + it('is returned from a collection onSnapshot()', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; const callback = sinon.spy(); - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/metadatachanges-true-true`); - const unsub = colRef.onSnapshot({ includeMetadataChanges: true }, callback); - await colRef.add({ foo: 'bar' }); - await Utils.spyToBeCalledTimesAsync(callback, 3); - unsub(); - - const snap1 = callback.args[0][0]; - const snap2 = callback.args[1][0]; - const snap3 = callback.args[2][0]; - - snap1.docChanges({ includeMetadataChanges: true }).length.should.be.eql(1); - snap2.docChanges({ includeMetadataChanges: true }).length.should.be.eql(0); - snap3.docChanges({ includeMetadataChanges: true }).length.should.be.eql(1); + onSnapshot(collection(getFirestore(), COLLECTION), callback); + await Utils.spyToBeCalledOnceAsync(callback); + callback.args[0][0].constructor.name.should.eql('FirestoreQuerySnapshot'); }); - // FIXME this flakes on CI, disabling for now - xit('returns the correct number of document changes if listening to metadata changes, but not including them in docChanges', async function () { - const callback = sinon.spy(); - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/metadatachanges-true-false`); - const unsub = colRef.onSnapshot({ includeMetadataChanges: true }, callback); - await Utils.sleep(1000); - await colRef.add({ foo: 'bar' }); - await Utils.spyToBeCalledTimesAsync(callback, 3, 15000); - unsub(); - - const snap1 = callback.args[0][0]; - const snap2 = callback.args[1][0]; - const snap3 = callback.args[2][0]; - - snap1.docChanges({ includeMetadataChanges: false }).length.should.be.eql(1); // FIXME when it flakes, this comes back as 0 - snap2.docChanges({ includeMetadataChanges: false }).length.should.be.eql(0); - snap3.docChanges({ includeMetadataChanges: false }).length.should.be.eql(0); + it('returns an array of DocumentSnapshots', async function () { + const { getFirestore, collection, addDoc, getDocs } = firestoreModular; + + const colRef = collection(getFirestore(), COLLECTION); + await addDoc(colRef, {}); + const snapshot = await getDocs(colRef); + snapshot.docs.should.be.Array(); + snapshot.docs.length.should.be.aboveOrEqual(1); + snapshot.docs[0].constructor.name.should.eql('FirestoreDocumentSnapshot'); }); - }); - describe('forEach()', function () { - it('throws if callback is not a function', async function () { - try { - const colRef = firebase.firestore().collection(`${COLLECTION}/callbacks/nonfunction`); - await colRef.add({}); - const snapshot = await colRef.limit(1).get(); - snapshot.forEach(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'callback' expected a function"); - return Promise.resolve(); - } + it('returns false if not empty', async function () { + const { getFirestore, collection, addDoc, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), COLLECTION); + await addDoc(colRef, {}); + const snapshot = await getDocs(colRef); + snapshot.empty.should.be.Boolean(); + snapshot.empty.should.be.False(); }); - it('calls back a function', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/callbacks/function`); - await colRef.add({}); - await colRef.add({}); - const snapshot = await colRef.limit(2).get(); - const callback = sinon.spy(); - snapshot.forEach.should.be.Function(); - snapshot.forEach(callback); - await Utils.spyToBeCalledTimesAsync(callback, 2, 20000); - callback.should.be.calledTwice(); - callback.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); - callback.args[0][1].should.be.Number(); - callback.args[1][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); - callback.args[1][1].should.be.Number(); + it('returns true if empty', async function () { + const { getFirestore, collection, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/foo/emptycollection`); + const snapshot = await getDocs(colRef); + snapshot.empty.should.be.Boolean(); + snapshot.empty.should.be.True(); }); - it('provides context to the callback', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/callbacks/function-context`); - await colRef.add({}); - const snapshot = await colRef.limit(1).get(); - const callback = sinon.spy(); - snapshot.forEach.should.be.Function(); - class Foo {} - snapshot.forEach(callback, Foo); - await Utils.spyToBeCalledOnceAsync(callback, 20000); - callback.should.be.calledOnce(); - callback.firstCall.thisValue.should.eql(Foo); + it('returns a SnapshotMetadata instance', async function () { + const { getFirestore, collection, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), COLLECTION); + const snapshot = await getDocs(colRef); + snapshot.metadata.constructor.name.should.eql('FirestoreSnapshotMetadata'); }); - }); - describe('isEqual()', function () { - it('throws if other is not a QuerySnapshot', async function () { - try { - const qs = await firebase.firestore().collection(COLLECTION).get(); - qs.isEqual(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected a QuerySnapshot instance"); - return Promise.resolve(); - } + it('returns a Query instance', async function () { + const { getFirestore, collection, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), COLLECTION); + const snapshot = await getDocs(colRef); + snapshot.query.constructor.name.should.eql('FirestoreCollectionReference'); }); - it('returns false if not equal (simple checks)', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - // Ensure a doc exists - await colRef.add({}); + it('returns size as a number', async function () { + const { getFirestore, collection, getDocs } = firestoreModular; + const colRef = collection(getFirestore(), COLLECTION); + const snapshot = await getDocs(colRef); + snapshot.size.should.be.Number(); + }); - const qs = await colRef.get(); + describe('docChanges()', function () { + it('throws if options is not an object', async function () { + const { getFirestore, collection, getDocs, query, limit } = firestoreModular; + try { + const colRef = collection(getFirestore(), COLLECTION); + const snapshot = await getDocs(query(colRef, limit(1))); + snapshot.docChanges(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' expected an object"); + return Promise.resolve(); + } + }); - const querySnap1 = await firebase - .firestore() - .collection(`${COLLECTION}/querysnapshot/querySnapshotIsEqual`) - .get(); + it('throws if options.includeMetadataChanges is not a boolean', async function () { + const { getFirestore, collection, getDocs, query, limit } = firestoreModular; + try { + const colRef = collection(getFirestore(), COLLECTION); + const snapshot = await getDocs(query(colRef, limit(1))); + snapshot.docChanges({ includeMetadataChanges: 123 }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.includeMetadataChanges' expected a boolean"); + return Promise.resolve(); + } + }); - const eq1 = qs.isEqual(querySnap1); + it('throws if options.includeMetadataChanges is true, but snapshot does not include those changes', async function () { + const { getFirestore, collection, onSnapshot } = firestoreModular; - eq1.should.be.False(); - }); + const callback = sinon.spy(); + const colRef = collection(getFirestore(), COLLECTION); + const unsub = onSnapshot( + colRef, + { + includeMetadataChanges: false, + }, + callback, + ); + await Utils.spyToBeCalledOnceAsync(callback); + unsub(); + const snapshot = callback.args[0][0]; + + try { + snapshot.docChanges({ includeMetadataChanges: true }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('To include metadata changes with your document changes'); + return Promise.resolve(); + } + }); + + it('returns an array of DocumentChange instances', async function () { + const { getFirestore, collection, addDoc, getDocs, query, limit } = firestoreModular; + const colRef = collection(getFirestore(), COLLECTION); + await addDoc(colRef, {}); + const snapshot = await getDocs(query(colRef, limit(1))); + const changes = snapshot.docChanges(); + changes.should.be.Array(); + changes.length.should.be.eql(1); + changes[0].constructor.name.should.eql('FirestoreDocumentChange'); + }); + + // FIXME flakey in CI - the changes length comes back unstable + xit('returns the correct number of document changes if listening to metadata changes', async function () { + const { getFirestore, collection, onSnapshot, addDoc } = firestoreModular; + const callback = sinon.spy(); + const colRef = collection( + getFirestore(), + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/metadatachanges-true-true`, + ); + const unsub = onSnapshot(colRef, { includeMetadataChanges: true }, callback); + await addDoc(colRef, { foo: 'bar' }); + await Utils.spyToBeCalledTimesAsync(callback, 3); + unsub(); + + const snap1 = callback.args[0][0]; + const snap2 = callback.args[1][0]; + const snap3 = callback.args[2][0]; + + snap1.docChanges({ includeMetadataChanges: true }).length.should.be.eql(1); + snap2.docChanges({ includeMetadataChanges: true }).length.should.be.eql(0); + snap3.docChanges({ includeMetadataChanges: true }).length.should.be.eql(1); + }); - it('returns false if not equal (expensive checks)', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/querysnapshot/querySnapshotIsEqual-False`); - // Ensure a doc exists - const docRef = colRef.doc('firstdoc'); - await docRef.set({ - foo: 'bar', - bar: { - foo: 1, - }, + // FIXME this flakes on CI, disabling for now + xit('returns the correct number of document changes if listening to metadata changes, but not including them in docChanges', async function () { + const { getFirestore, collection, onSnapshot, addDoc } = firestoreModular; + + const callback = sinon.spy(); + const colRef = collection( + getFirestore(), + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/metadatachanges-true-false`, + ); + const unsub = onSnapshot(colRef, { includeMetadataChanges: true }, callback); + await Utils.sleep(1000); + await addDoc(colRef, { foo: 'bar' }); + await Utils.spyToBeCalledTimesAsync(callback, 3, 15000); + unsub(); + + const snap1 = callback.args[0][0]; + const snap2 = callback.args[1][0]; + const snap3 = callback.args[2][0]; + + snap1.docChanges({ includeMetadataChanges: false }).length.should.be.eql(1); // FIXME when it flakes, this comes back as 0 + snap2.docChanges({ includeMetadataChanges: false }).length.should.be.eql(0); + snap3.docChanges({ includeMetadataChanges: false }).length.should.be.eql(0); }); + }); - // Grab snapshot - const qs1 = await colRef.get(); + describe('forEach()', function () { + it('throws if callback is not a function', async function () { + const { getFirestore, collection, getDocs, query, limit, addDoc } = firestoreModular; + + try { + const colRef = collection(getFirestore(), `${COLLECTION}/callbacks/nonfunction`); + await addDoc(colRef, {}); + const snapshot = await getDocs(query(colRef, limit(1))); + snapshot.forEach(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'callback' expected a function"); + return Promise.resolve(); + } + }); - // Update same collection - await docRef.update({ - bar: { - foo: 2, - }, + it('calls back a function', async function () { + const { getFirestore, collection, addDoc, getDocs, query, limit } = firestoreModular; + + const colRef = collection(getFirestore(), `${COLLECTION}/callbacks/function`); + await addDoc(colRef, {}); + await addDoc(colRef, {}); + const snapshot = await getDocs(query(colRef, limit(2))); + const callback = sinon.spy(); + snapshot.forEach.should.be.Function(); + snapshot.forEach(callback); + await Utils.spyToBeCalledTimesAsync(callback, 2, 20000); + callback.should.be.calledTwice(); + callback.args[0][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + callback.args[0][1].should.be.Number(); + callback.args[1][0].constructor.name.should.eql('FirestoreDocumentSnapshot'); + callback.args[1][1].should.be.Number(); }); - const qs2 = await colRef.get(); + it('provides context to the callback', async function () { + const { getFirestore, collection, addDoc, getDocs, query, limit } = firestoreModular; - const eq1 = qs1.isEqual(qs2); + const colRef = collection(getFirestore(), `${COLLECTION}/callbacks/function-context`); + await addDoc(colRef, {}); + const snapshot = await getDocs(query(colRef, limit(1))); + const callback = sinon.spy(); + snapshot.forEach.should.be.Function(); - eq1.should.be.False(); + class Foo {} + + snapshot.forEach(callback, Foo); + await Utils.spyToBeCalledOnceAsync(callback, 20000); + callback.should.be.calledOnce(); + callback.firstCall.thisValue.should.eql(Foo); + }); }); - it('returns true when equal', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/querysnapshot/querySnapshotIsEqual-True`); - - await Promise.all([ - colRef.add({ foo: 'bar' }), - colRef.add({ foo: 1 }), - colRef.add({ - foo: { - foo: 'bar', + describe('isEqual()', function () { + it('throws if other is not a QuerySnapshot', async function () { + const { getFirestore, collection, getDocs } = firestoreModular; + + try { + const qs = await getDocs(collection(getFirestore(), COLLECTION)); + qs.isEqual(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected a QuerySnapshot instance"); + return Promise.resolve(); + } + }); + + it('returns false if not equal (simple checks)', async function () { + const { getFirestore, collection, addDoc, getDocs } = firestoreModular; + const db = getFirestore(); + + const colRef = collection(db, COLLECTION); + // Ensure a doc exists + await addDoc(colRef, {}); + + const qs = await getDocs(colRef); + + const querySnap1 = await getDocs( + collection(db, `${COLLECTION}/querysnapshot/querySnapshotIsEqual`), + ); + + const eq1 = qs.isEqual(querySnap1); + + eq1.should.be.False(); + }); + + it('returns false if not equal (expensive checks)', async function () { + const { getFirestore, collection, doc, setDoc, getDocs, updateDoc } = firestoreModular; + + const colRef = collection( + getFirestore(), + `${COLLECTION}/querysnapshot/querySnapshotIsEqual-False`, + ); + // Ensure a doc exists + const docRef = doc(colRef, 'firstdoc'); + await setDoc(docRef, { + foo: 'bar', + bar: { + foo: 1, + }, + }); + + // Grab snapshot + const qs1 = await getDocs(colRef); + + // Update same collection + await updateDoc(docRef, { + bar: { + foo: 2, }, - }), - ]); + }); - const qs1 = await colRef.get(); - const qs2 = await colRef.get(); + const qs2 = await colRef.get(); - const eq = qs1.isEqual(qs2); + const eq1 = qs1.isEqual(qs2); - eq.should.be.True(); + eq1.should.be.False(); + }); + + it('returns true when equal', async function () { + const { getFirestore, collection, addDoc, getDocs } = firestoreModular; + + const colRef = collection( + getFirestore(), + `${COLLECTION}/querysnapshot/querySnapshotIsEqual-True`, + ); + + await Promise.all([ + addDoc(colRef, { foo: 'bar' }), + addDoc(colRef, { foo: 1 }), + addDoc(colRef, { + foo: { + foo: 'bar', + }, + }), + ]); + + const qs1 = await getDocs(colRef); + const qs2 = await getDocs(colRef); + + const eq = qs1.isEqual(qs2); + + eq.should.be.True(); + }); }); }); }); diff --git a/packages/firestore/e2e/SnapshotMetadata.e2e.js b/packages/firestore/e2e/SnapshotMetadata.e2e.js index a4276618c7..ade28485be 100644 --- a/packages/firestore/e2e/SnapshotMetadata.e2e.js +++ b/packages/firestore/e2e/SnapshotMetadata.e2e.js @@ -21,46 +21,120 @@ describe('firestore.SnapshotMetadata', function () { before(function () { return wipe(); }); - it('.fromCache -> returns a boolean', async function () { - const ref1 = firebase.firestore().collection(COLLECTION); - const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - const colRef = await ref1.get(); - const docRef = await ref2.get(); - colRef.metadata.fromCache.should.be.Boolean(); - docRef.metadata.fromCache.should.be.Boolean(); - }); - it('.hasPendingWrites -> returns a boolean', async function () { - const ref1 = firebase.firestore().collection(COLLECTION); - const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - const colRef = await ref1.get(); - const docRef = await ref2.get(); - colRef.metadata.hasPendingWrites.should.be.Boolean(); - docRef.metadata.hasPendingWrites.should.be.Boolean(); + describe('v8 compatibility', function () { + it('.fromCache -> returns a boolean', async function () { + const ref1 = firebase.firestore().collection(COLLECTION); + const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); + const colRef = await ref1.get(); + const docRef = await ref2.get(); + colRef.metadata.fromCache.should.be.Boolean(); + docRef.metadata.fromCache.should.be.Boolean(); + }); + + it('.hasPendingWrites -> returns a boolean', async function () { + const ref1 = firebase.firestore().collection(COLLECTION); + const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); + const colRef = await ref1.get(); + const docRef = await ref2.get(); + colRef.metadata.hasPendingWrites.should.be.Boolean(); + docRef.metadata.hasPendingWrites.should.be.Boolean(); + }); + + describe('isEqual()', function () { + it('throws if other is not a valid type', async function () { + try { + const snapshot = await firebase.firestore().collection(COLLECTION).get(); + snapshot.metadata.isEqual(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected instance of SnapshotMetadata"); + return Promise.resolve(); + } + }); + + it('returns true if is equal', async function () { + const snapshot1 = await firebase + .firestore() + .collection(COLLECTION) + .get({ source: 'cache' }); + const snapshot2 = await firebase + .firestore() + .collection(COLLECTION) + .get({ source: 'cache' }); + snapshot1.metadata.isEqual(snapshot2.metadata).should.eql(true); + }); + + it('returns false if not equal', async function () { + const snapshot1 = await firebase + .firestore() + .collection(COLLECTION) + .get({ source: 'cache' }); + const snapshot2 = await firebase + .firestore() + .collection(COLLECTION) + .get({ source: 'server' }); + snapshot1.metadata.isEqual(snapshot2.metadata).should.eql(false); + }); + }); }); - describe('isEqual()', function () { - it('throws if other is not a valid type', async function () { - try { - const snapshot = await firebase.firestore().collection(COLLECTION).get(); - snapshot.metadata.isEqual(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected instance of SnapshotMetadata"); - return Promise.resolve(); - } + describe('modular', function () { + it('.fromCache -> returns a boolean', async function () { + const { getFirestore, collection, doc, getDocs } = firestoreModular; + const db = getFirestore(); + + const ref1 = collection(db, COLLECTION); + const ref2 = doc(db, `${COLLECTION}/idonotexist`); + const colRef = await getDocs(ref1); + const docRef = await getDocs(ref2); + colRef.metadata.fromCache.should.be.Boolean(); + docRef.metadata.fromCache.should.be.Boolean(); }); - it('returns true if is equal', async function () { - const snapshot1 = await firebase.firestore().collection(COLLECTION).get({ source: 'cache' }); - const snapshot2 = await firebase.firestore().collection(COLLECTION).get({ source: 'cache' }); - snapshot1.metadata.isEqual(snapshot2.metadata).should.eql(true); + it('.hasPendingWrites -> returns a boolean', async function () { + const { getFirestore, collection, doc, getDocs } = firestoreModular; + const db = getFirestore(); + + const ref1 = collection(db, COLLECTION); + const ref2 = doc(db, `${COLLECTION}/idonotexist`); + const colRef = await getDocs(ref1); + const docRef = await getDocs(ref2); + colRef.metadata.hasPendingWrites.should.be.Boolean(); + docRef.metadata.hasPendingWrites.should.be.Boolean(); }); - it('returns false if not equal', async function () { - const snapshot1 = await firebase.firestore().collection(COLLECTION).get({ source: 'cache' }); - const snapshot2 = await firebase.firestore().collection(COLLECTION).get({ source: 'server' }); - snapshot1.metadata.isEqual(snapshot2.metadata).should.eql(false); + describe('isEqual()', function () { + it('throws if other is not a valid type', async function () { + const { getFirestore, collection, getDocs } = firestoreModular; + + try { + const snapshot = await getDocs(collection(getFirestore(), COLLECTION)); + snapshot.metadata.isEqual(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected instance of SnapshotMetadata"); + return Promise.resolve(); + } + }); + + it('returns true if is equal', async function () { + const { getFirestore, collection, getDocsFromCache } = firestoreModular; + const db = getFirestore(); + + const snapshot1 = await getDocsFromCache(collection(db, COLLECTION)); + const snapshot2 = await getDocsFromCache(collection(db, COLLECTION)); + snapshot1.metadata.isEqual(snapshot2.metadata).should.eql(true); + }); + + it('returns false if not equal', async function () { + const { getFirestore, collection, getDocsFromCache, getDocsFromServer } = firestoreModular; + const db = getFirestore(); + + const snapshot1 = await getDocsFromCache(collection(db, COLLECTION)); + const snapshot2 = await getDocsFromServer(collection(db, COLLECTION)); + snapshot1.metadata.isEqual(snapshot2.metadata).should.eql(false); + }); }); }); }); diff --git a/packages/firestore/e2e/Timestamp.e2e.js b/packages/firestore/e2e/Timestamp.e2e.js index 25353cc1fa..7990d5dd6b 100644 --- a/packages/firestore/e2e/Timestamp.e2e.js +++ b/packages/firestore/e2e/Timestamp.e2e.js @@ -16,153 +16,343 @@ */ describe('firestore.Timestamp', function () { - it('throws if seconds is not a number', function () { - try { - new firebase.firestore.Timestamp('1234'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'seconds' expected a number value"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if seconds is not a number', function () { + try { + new firebase.firestore.Timestamp('1234'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'seconds' expected a number value"); + return Promise.resolve(); + } + }); - it('throws if nanoseconds is not a number', function () { - try { - new firebase.firestore.Timestamp(123, '456'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'nanoseconds' expected a number value"); - return Promise.resolve(); - } - }); + it('throws if nanoseconds is not a number', function () { + try { + new firebase.firestore.Timestamp(123, '456'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'nanoseconds' expected a number value"); + return Promise.resolve(); + } + }); - it('throws if nanoseconds less than 0', function () { - try { - new firebase.firestore.Timestamp(123, -1); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'nanoseconds' out of range"); - return Promise.resolve(); - } - }); + it('throws if nanoseconds less than 0', function () { + try { + new firebase.firestore.Timestamp(123, -1); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'nanoseconds' out of range"); + return Promise.resolve(); + } + }); - it('throws if nanoseconds greater than 1e9', function () { - try { - new firebase.firestore.Timestamp(123, 10000000000); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'nanoseconds' out of range"); - return Promise.resolve(); - } - }); + it('throws if nanoseconds greater than 1e9', function () { + try { + new firebase.firestore.Timestamp(123, 10000000000); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'nanoseconds' out of range"); + return Promise.resolve(); + } + }); - it('throws if seconds less than -62135596800', function () { - try { - new firebase.firestore.Timestamp(-63135596800, 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'seconds' out of range"); - return Promise.resolve(); - } - }); + it('throws if seconds less than -62135596800', function () { + try { + new firebase.firestore.Timestamp(-63135596800, 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'seconds' out of range"); + return Promise.resolve(); + } + }); - it('throws if seconds greater-equal than 253402300800', function () { - try { - new firebase.firestore.Timestamp(253402300800, 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'seconds' out of range"); - return Promise.resolve(); - } - }); + it('throws if seconds greater-equal than 253402300800', function () { + try { + new firebase.firestore.Timestamp(253402300800, 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'seconds' out of range"); + return Promise.resolve(); + } + }); - it('returns number of seconds', function () { - const ts = new firebase.firestore.Timestamp(123, 123); - ts.seconds.should.equal(123); - }); + it('returns number of seconds', function () { + const ts = new firebase.firestore.Timestamp(123, 123); + ts.seconds.should.equal(123); + }); + + it('returns number of nanoseconds', function () { + const ts = new firebase.firestore.Timestamp(123, 123456); + ts.nanoseconds.should.equal(123456); + }); + + describe('isEqual()', function () { + it('throws if invalid other is procided', function () { + try { + const ts = new firebase.firestore.Timestamp(123, 1234); + ts.isEqual(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected an instance of Timestamp"); + return Promise.resolve(); + } + }); - it('returns number of nanoseconds', function () { - const ts = new firebase.firestore.Timestamp(123, 123456); - ts.nanoseconds.should.equal(123456); + it('returns false if not equal', function () { + const ts1 = new firebase.firestore.Timestamp(123, 123456); + const ts2 = new firebase.firestore.Timestamp(1234, 123456); + ts1.isEqual(ts2).should.equal(false); + }); + + it('returns true if equal', function () { + const ts1 = new firebase.firestore.Timestamp(123, 123456); + const ts2 = new firebase.firestore.Timestamp(123, 123456); + ts1.isEqual(ts2).should.equal(true); + }); + }); + + describe('toDate()', function () { + it('returns a valid Date object', function () { + const ts = new firebase.firestore.Timestamp(123, 123456); + const date = ts.toDate(); + should.equal(date.constructor.name, 'Date'); + }); + }); + + describe('toMillis()', function () { + it('returns the number of milliseconds', function () { + const ts = new firebase.firestore.Timestamp(123, 123456); + const ms = ts.toMillis(); + should.equal(typeof ms, 'number'); + }); + }); + + describe('toString()', function () { + it('returns a string representation of the class', function () { + const ts = new firebase.firestore.Timestamp(123, 123456); + const str = ts.toString(); + str.should.equal(`FirestoreTimestamp(seconds=${123}, nanoseconds=${123456})`); + }); + }); + + describe('Timestamp.now()', function () { + it('returns a new instance', function () { + const ts = firebase.firestore.Timestamp.now(); + should.equal(ts.constructor.name, 'FirestoreTimestamp'); + }); + }); + + describe('Timestamp.fromDate()', function () { + it('throws if date is not a valid Date', function () { + try { + firebase.firestore.Timestamp.fromDate(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'date' expected a valid Date object"); + return Promise.resolve(); + } + }); + + it('returns a new instance', function () { + const ts = firebase.firestore.Timestamp.fromDate(new Date()); + should.equal(ts.constructor.name, 'FirestoreTimestamp'); + }); + }); + + describe('Timestamp.fromMillis()', function () { + it('returns a new instance', function () { + const ts = firebase.firestore.Timestamp.fromMillis(123); + should.equal(ts.constructor.name, 'FirestoreTimestamp'); + }); + }); }); - describe('isEqual()', function () { - it('throws if invalid other is procided', function () { + describe('modular', function () { + it('throws if seconds is not a number', function () { + const { Timestamp } = firestoreModular; + try { - const ts = new firebase.firestore.Timestamp(123, 1234); - ts.isEqual(123); + new Timestamp('1234'); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.containEql("'other' expected an instance of Timestamp"); + error.message.should.containEql("'seconds' expected a number value"); return Promise.resolve(); } }); - it('returns false if not equal', function () { - const ts1 = new firebase.firestore.Timestamp(123, 123456); - const ts2 = new firebase.firestore.Timestamp(1234, 123456); - ts1.isEqual(ts2).should.equal(false); - }); + it('throws if nanoseconds is not a number', function () { + const { Timestamp } = firestoreModular; - it('returns true if equal', function () { - const ts1 = new firebase.firestore.Timestamp(123, 123456); - const ts2 = new firebase.firestore.Timestamp(123, 123456); - ts1.isEqual(ts2).should.equal(true); + try { + new Timestamp(123, '456'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'nanoseconds' expected a number value"); + return Promise.resolve(); + } }); - }); - describe('toDate()', function () { - it('returns a valid Date object', function () { - const ts = new firebase.firestore.Timestamp(123, 123456); - const date = ts.toDate(); - should.equal(date.constructor.name, 'Date'); - }); - }); + it('throws if nanoseconds less than 0', function () { + const { Timestamp } = firestoreModular; - describe('toMillis()', function () { - it('returns the number of milliseconds', function () { - const ts = new firebase.firestore.Timestamp(123, 123456); - const ms = ts.toMillis(); - should.equal(typeof ms, 'number'); + try { + new Timestamp(123, -1); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'nanoseconds' out of range"); + return Promise.resolve(); + } }); - }); - describe('toString()', function () { - it('returns a string representation of the class', function () { - const ts = new firebase.firestore.Timestamp(123, 123456); - const str = ts.toString(); - str.should.equal(`FirestoreTimestamp(seconds=${123}, nanoseconds=${123456})`); + it('throws if nanoseconds greater than 1e9', function () { + const { Timestamp } = firestoreModular; + + try { + new Timestamp(123, 10000000000); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'nanoseconds' out of range"); + return Promise.resolve(); + } }); - }); - describe('Timestamp.now()', function () { - it('returns a new instance', function () { - const ts = firebase.firestore.Timestamp.now(); - should.equal(ts.constructor.name, 'FirestoreTimestamp'); + it('throws if seconds less than -62135596800', function () { + const { Timestamp } = firestoreModular; + + try { + new Timestamp(-63135596800, 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'seconds' out of range"); + return Promise.resolve(); + } }); - }); - describe('Timestamp.fromDate()', function () { - it('throws if date is not a valid Date', function () { + it('throws if seconds greater-equal than 253402300800', function () { + const { Timestamp } = firestoreModular; + try { - firebase.firestore.Timestamp.fromDate(123); + new Timestamp(253402300800, 123); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.containEql("'date' expected a valid Date object"); + error.message.should.containEql("'seconds' out of range"); return Promise.resolve(); } }); - it('returns a new instance', function () { - const ts = firebase.firestore.Timestamp.fromDate(new Date()); - should.equal(ts.constructor.name, 'FirestoreTimestamp'); + it('returns number of seconds', function () { + const { Timestamp } = firestoreModular; + + const ts = new Timestamp(123, 123); + ts.seconds.should.equal(123); }); - }); - describe('Timestamp.fromMillis()', function () { - it('returns a new instance', function () { - const ts = firebase.firestore.Timestamp.fromMillis(123); - should.equal(ts.constructor.name, 'FirestoreTimestamp'); + it('returns number of nanoseconds', function () { + const { Timestamp } = firestoreModular; + + const ts = new Timestamp(123, 123456); + ts.nanoseconds.should.equal(123456); + }); + + describe('isEqual()', function () { + it('throws if invalid other is procided', function () { + const { Timestamp } = firestoreModular; + + try { + const ts = new Timestamp(123, 1234); + ts.isEqual(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' expected an instance of Timestamp"); + return Promise.resolve(); + } + }); + + it('returns false if not equal', function () { + const { Timestamp } = firestoreModular; + + const ts1 = new Timestamp(123, 123456); + const ts2 = new Timestamp(1234, 123456); + ts1.isEqual(ts2).should.equal(false); + }); + + it('returns true if equal', function () { + const { Timestamp } = firestoreModular; + + const ts1 = new Timestamp(123, 123456); + const ts2 = new Timestamp(123, 123456); + ts1.isEqual(ts2).should.equal(true); + }); + }); + + describe('toDate()', function () { + it('returns a valid Date object', function () { + const { Timestamp } = firestoreModular; + + const ts = new Timestamp(123, 123456); + const date = ts.toDate(); + should.equal(date.constructor.name, 'Date'); + }); + }); + + describe('toMillis()', function () { + it('returns the number of milliseconds', function () { + const { Timestamp } = firestoreModular; + + const ts = new Timestamp(123, 123456); + const ms = ts.toMillis(); + should.equal(typeof ms, 'number'); + }); + }); + + describe('toString()', function () { + it('returns a string representation of the class', function () { + const { Timestamp } = firestoreModular; + + const ts = new Timestamp(123, 123456); + const str = ts.toString(); + str.should.equal(`FirestoreTimestamp(seconds=${123}, nanoseconds=${123456})`); + }); + }); + + describe('Timestamp.now()', function () { + it('returns a new instance', function () { + const { Timestamp } = firestoreModular; + + const ts = Timestamp.now(); + should.equal(ts.constructor.name, 'FirestoreTimestamp'); + }); + }); + + describe('Timestamp.fromDate()', function () { + it('throws if date is not a valid Date', function () { + const { Timestamp } = firestoreModular; + + try { + Timestamp.fromDate(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'date' expected a valid Date object"); + return Promise.resolve(); + } + }); + + it('returns a new instance', function () { + const { Timestamp } = firestoreModular; + + const ts = Timestamp.fromDate(new Date()); + should.equal(ts.constructor.name, 'FirestoreTimestamp'); + }); + }); + + describe('Timestamp.fromMillis()', function () { + it('returns a new instance', function () { + const { Timestamp } = firestoreModular; + + const ts = Timestamp.fromMillis(123); + should.equal(ts.constructor.name, 'FirestoreTimestamp'); + }); }); }); }); diff --git a/packages/firestore/e2e/Transaction.e2e.js b/packages/firestore/e2e/Transaction.e2e.js index 3f54ae26e9..18f06cffb3 100644 --- a/packages/firestore/e2e/Transaction.e2e.js +++ b/packages/firestore/e2e/Transaction.e2e.js @@ -18,360 +18,750 @@ const COLLECTION = 'firestore'; const NO_RULE_COLLECTION = 'no_rules'; describe('firestore.Transaction', function () { - it('should throw if updateFunction is not a Promise', async function () { - try { - await firebase.firestore().runTransaction(() => 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'updateFunction' must return a Promise"); - return Promise.resolve(); - } - }); - - it('should return an instance of FirestoreTransaction', async function () { - await firebase.firestore().runTransaction(async transaction => { - transaction.constructor.name.should.eql('FirestoreTransaction'); - return null; + describe('v8 compatibility', function () { + it('should throw if updateFunction is not a Promise', async function () { + try { + await firebase.firestore().runTransaction(() => 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'updateFunction' must return a Promise"); + return Promise.resolve(); + } }); - }); - - it('should resolve with user value', async function () { - const expected = Date.now(); - const value = await firebase.firestore().runTransaction(async () => { - return expected; + it('should return an instance of FirestoreTransaction', async function () { + await firebase.firestore().runTransaction(async transaction => { + transaction.constructor.name.should.eql('FirestoreTransaction'); + return null; + }); }); - value.should.eql(expected); - }); - - it('should reject with user Error', async function () { - const message = `Error: ${Date.now()}`; + it('should resolve with user value', async function () { + const expected = Date.now(); - try { - await firebase.firestore().runTransaction(async () => { - throw new Error(message); + const value = await firebase.firestore().runTransaction(async () => { + return expected; }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (error) { - error.message.should.eql(message); - return Promise.resolve(); - } - }); - it('should reject a native error', async function () { - const docRef = firebase.firestore().doc(`${NO_RULE_COLLECTION}/foo`); + value.should.eql(expected); + }); - try { - await firebase.firestore().runTransaction(async t => { - t.set(docRef, { - foo: 'bar', - }); - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (error) { - error.code.should.eql('firestore/permission-denied'); - return Promise.resolve(); - } - }); + it('should reject with user Error', async function () { + const message = `Error: ${Date.now()}`; - describe('transaction.get()', function () { - it('should throw if not providing a document reference', async function () { try { - await firebase.firestore().runTransaction(t => { - return t.get(123); + await firebase.firestore().runTransaction(async () => { + throw new Error(message); }); - return Promise.reject(new Error('Did not throw an Error.')); + return Promise.reject(new Error('Did not throw Error.')); } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); + error.message.should.eql(message); return Promise.resolve(); } }); - it('should get a document and return a DocumentSnapshot', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/transactions/transaction/get-delete`); - await docRef.set({}); - - await firebase.firestore().runTransaction(async t => { - const docSnapshot = await t.get(docRef); - docSnapshot.constructor.name.should.eql('FirestoreDocumentSnapshot'); - docSnapshot.exists.should.eql(true); - docSnapshot.id.should.eql('get-delete'); + it('should reject a native error', async function () { + const docRef = firebase.firestore().doc(`${NO_RULE_COLLECTION}/foo`); - t.delete(docRef); - }); - }); - }); - - describe('transaction.delete()', function () { - it('should throw if not providing a document reference', async function () { try { await firebase.firestore().runTransaction(async t => { - t.delete(123); + t.set(docRef, { + foo: 'bar', + }); }); - return Promise.reject(new Error('Did not throw an Error.')); + return Promise.reject(new Error('Did not throw Error.')); } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); + error.code.should.eql('firestore/permission-denied'); return Promise.resolve(); } }); - it('should delete documents', async function () { - const docRef1 = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/delete-delete1`); - await docRef1.set({}); + describe('transaction.get()', function () { + it('should throw if not providing a document reference', async function () { + try { + await firebase.firestore().runTransaction(t => { + return t.get(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); - const docRef2 = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/delete-delete2`); - await docRef2.set({}); + it('should get a document and return a DocumentSnapshot', async function () { + const docRef = firebase + .firestore() + .doc(`${COLLECTION}/transactions/transaction/get-delete`); + await docRef.set({}); - await firebase.firestore().runTransaction(async t => { - t.delete(docRef1); - t.delete(docRef2); + await firebase.firestore().runTransaction(async t => { + const docSnapshot = await t.get(docRef); + docSnapshot.constructor.name.should.eql('FirestoreDocumentSnapshot'); + docSnapshot.exists.should.eql(true); + docSnapshot.id.should.eql('get-delete'); + + t.delete(docRef); + }); }); + }); - const snapshot1 = await docRef1.get(); - snapshot1.exists.should.eql(false); + describe('transaction.delete()', function () { + it('should throw if not providing a document reference', async function () { + try { + await firebase.firestore().runTransaction(async t => { + t.delete(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); - const snapshot2 = await docRef2.get(); - snapshot2.exists.should.eql(false); - }); - }); + it('should delete documents', async function () { + const docRef1 = firebase + .firestore() + .doc(`${COLLECTION}/transactions/transaction/delete-delete1`); + await docRef1.set({}); + + const docRef2 = firebase + .firestore() + .doc(`${COLLECTION}/transactions/transaction/delete-delete2`); + await docRef2.set({}); - describe('transaction.update()', function () { - it('should throw if not providing a document reference', async function () { - try { await firebase.firestore().runTransaction(async t => { - t.update(123); + t.delete(docRef1); + t.delete(docRef2); }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } + + const snapshot1 = await docRef1.get(); + snapshot1.exists.should.eql(false); + + const snapshot2 = await docRef2.get(); + snapshot2.exists.should.eql(false); + }); }); - it('should throw if update args are invalid', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + describe('transaction.update()', function () { + it('should throw if not providing a document reference', async function () { + try { + await firebase.firestore().runTransaction(async t => { + t.update(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should throw if update args are invalid', async function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + + try { + await firebase.firestore().runTransaction(async t => { + t.update(docRef, 123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('it must be an object'); + return Promise.resolve(); + } + }); + + it('should update documents', async function () { + const value = Date.now(); + + const docRef1 = firebase + .firestore() + .doc(`${COLLECTION}/transactions/transaction/delete-delete1`); + await docRef1.set({ + foo: 'bar', + bar: 'baz', + }); + + const docRef2 = firebase + .firestore() + .doc(`${COLLECTION}/transactions/transaction/delete-delete2`); + await docRef2.set({ + foo: 'bar', + bar: 'baz', + }); - try { await firebase.firestore().runTransaction(async t => { - t.update(docRef, 123); + t.update(docRef1, { + bar: value, + }); + t.update(docRef2, 'bar', value); }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('it must be an object'); - return Promise.resolve(); - } + + const expected = { + foo: 'bar', + bar: value, + }; + + const snapshot1 = await docRef1.get(); + snapshot1.exists.should.eql(true); + snapshot1.data().should.eql(jet.contextify(expected)); + + const snapshot2 = await docRef2.get(); + snapshot2.exists.should.eql(true); + snapshot2.data().should.eql(jet.contextify(expected)); + }); }); - it('should update documents', async function () { - const value = Date.now(); + describe('transaction.set()', function () { + it('should throw if not providing a document reference', async function () { + try { + await firebase.firestore().runTransaction(async t => { + t.set(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); - const docRef1 = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/delete-delete1`); - await docRef1.set({ - foo: 'bar', - bar: 'baz', + it('should throw if set data is invalid', async function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + + try { + await firebase.firestore().runTransaction(async t => { + t.set(docRef, 123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'data' must be an object."); + return Promise.resolve(); + } }); - const docRef2 = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/delete-delete2`); - await docRef2.set({ - foo: 'bar', - bar: 'baz', + it('should throw if set options are invalid', async function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + + try { + await firebase.firestore().runTransaction(async t => { + t.set( + docRef, + {}, + { + merge: true, + mergeFields: [], + }, + ); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' must not contain both 'merge' & 'mergeFields'", + ); + return Promise.resolve(); + } }); - await firebase.firestore().runTransaction(async t => { - t.update(docRef1, { - bar: value, + it('should set data', async function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/transactions/transaction/set`); + await docRef.set({ + foo: 'bar', + }); + const expected = { + foo: 'baz', + }; + + await firebase.firestore().runTransaction(async t => { + t.set(docRef, expected); + }); + + const snapshot = await docRef.get(); + snapshot.data().should.eql(jet.contextify(expected)); + }); + + it('should set data with merge', async function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/transactions/transaction/set-merge`); + await docRef.set({ + foo: 'bar', + bar: 'baz', + }); + const expected = { + foo: 'bar', + bar: 'foo', + }; + + await firebase.firestore().runTransaction(async t => { + t.set( + docRef, + { + bar: 'foo', + }, + { + merge: true, + }, + ); + }); + + const snapshot = await docRef.get(); + snapshot.data().should.eql(jet.contextify(expected)); + }); + + it('should set data with merge fields', async function () { + const docRef = firebase + .firestore() + .doc(`${COLLECTION}/transactions/transaction/set-mergefields`); + await docRef.set({ + foo: 'bar', + bar: 'baz', + baz: 'ben', + }); + const expected = { + foo: 'bar', + bar: 'foo', + baz: 'foo', + }; + + await firebase.firestore().runTransaction(async t => { + t.set( + docRef, + { + bar: 'foo', + baz: 'foo', + }, + { + mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], + }, + ); }); - t.update(docRef2, 'bar', value); + + const snapshot = await docRef.get(); + snapshot.data().should.eql(jet.contextify(expected)); }); - const expected = { - foo: 'bar', - bar: value, - }; + it('should roll back any updates that failed', async function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/transactions/transaction/rollback`); - const snapshot1 = await docRef1.get(); - snapshot1.exists.should.eql(true); - snapshot1.data().should.eql(jet.contextify(expected)); + await docRef.set({ + turn: 0, + }); - const snapshot2 = await docRef2.get(); - snapshot2.exists.should.eql(true); - snapshot2.data().should.eql(jet.contextify(expected)); + const prop1 = 'prop1'; + const prop2 = 'prop2'; + const turn = 0; + const errorMessage = 'turn cannot exceed 1'; + + const createTransaction = prop => { + return firebase.firestore().runTransaction(async transaction => { + const doc = await transaction.get(docRef); + const data = doc.data(); + + if (data.turn !== turn) { + throw new Error(errorMessage); + } + + const update = { + turn: turn + 1, + [prop]: 1, + }; + + transaction.update(docRef, update); + }); + }; + + const promises = [createTransaction(prop1), createTransaction(prop2)]; + + try { + await Promise.all(promises); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql(errorMessage); + } + const result = await docRef.get(); + should(result.data()).not.have.properties([prop1, prop2]); + }); }); }); - describe('transaction.set()', function () { - it('should throw if not providing a document reference', async function () { + describe('modular', function () { + it('should throw if updateFunction is not a Promise', async function () { + const { getFirestore, runTransaction } = firestoreModular; + try { - await firebase.firestore().runTransaction(async t => { - t.set(123); - }); + await runTransaction(getFirestore(), () => 123); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); + error.message.should.containEql("'updateFunction' must return a Promise"); return Promise.resolve(); } }); - it('should throw if set data is invalid', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + it('should return an instance of FirestoreTransaction', async function () { + const { getFirestore, runTransaction } = firestoreModular; + await runTransaction(getFirestore(), async transaction => { + transaction.constructor.name.should.eql('FirestoreTransaction'); + return null; + }); + }); + + it('should resolve with user value', async function () { + const { getFirestore, runTransaction } = firestoreModular; + const expected = Date.now(); + + const value = await runTransaction(getFirestore(), async () => { + return expected; + }); + + value.should.eql(expected); + }); + + it('should reject with user Error', async function () { + const { getFirestore, runTransaction } = firestoreModular; + const message = `Error: ${Date.now()}`; try { - await firebase.firestore().runTransaction(async t => { - t.set(docRef, 123); + await runTransaction(getFirestore(), async () => { + throw new Error(message); }); - return Promise.reject(new Error('Did not throw an Error.')); + return Promise.reject(new Error('Did not throw Error.')); } catch (error) { - error.message.should.containEql("'data' must be an object."); + error.message.should.eql(message); return Promise.resolve(); } }); - it('should throw if set options are invalid', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + it('should reject a native error', async function () { + const { getFirestore, runTransaction, doc } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${NO_RULE_COLLECTION}/foo`); try { - await firebase.firestore().runTransaction(async t => { - t.set( - docRef, - {}, - { - merge: true, - mergeFields: [], - }, - ); + await runTransaction(db, async t => { + t.set(docRef, { + foo: 'bar', + }); }); - return Promise.reject(new Error('Did not throw an Error.')); + return Promise.reject(new Error('Did not throw Error.')); } catch (error) { - error.message.should.containEql("'options' must not contain both 'merge' & 'mergeFields'"); + error.code.should.eql('firestore/permission-denied'); return Promise.resolve(); } }); - it('should set data', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/transactions/transaction/set`); - await docRef.set({ - foo: 'bar', + describe('transaction.get()', function () { + it('should throw if not providing a document reference', async function () { + const { getFirestore, runTransaction } = firestoreModular; + try { + await runTransaction(getFirestore(), t => { + return t.get(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } }); - const expected = { - foo: 'baz', - }; - await firebase.firestore().runTransaction(async t => { - t.set(docRef, expected); - }); + it('should get a document and return a DocumentSnapshot', async function () { + const { getFirestore, runTransaction, doc, setDoc } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/transactions/transaction/get-delete`); + await setDoc(docRef, {}); + + await runTransaction(db, async t => { + const docSnapshot = await t.get(docRef); + docSnapshot.constructor.name.should.eql('FirestoreDocumentSnapshot'); + docSnapshot.exists.should.eql(true); + docSnapshot.id.should.eql('get-delete'); - const snapshot = await docRef.get(); - snapshot.data().should.eql(jet.contextify(expected)); + t.delete(docRef); + }); + }); }); - it('should set data with merge', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/transactions/transaction/set-merge`); - await docRef.set({ - foo: 'bar', - bar: 'baz', - }); - const expected = { - foo: 'bar', - bar: 'foo', - }; - - await firebase.firestore().runTransaction(async t => { - t.set( - docRef, - { - bar: 'foo', - }, - { - merge: true, - }, - ); - }); - - const snapshot = await docRef.get(); - snapshot.data().should.eql(jet.contextify(expected)); + describe('transaction.delete()', function () { + it('should throw if not providing a document reference', async function () { + const { getFirestore, runTransaction } = firestoreModular; + try { + await runTransaction(getFirestore(), async t => { + t.delete(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should delete documents', async function () { + const { getFirestore, runTransaction, doc, setDoc, getDocs } = firestoreModular; + const db = getFirestore(); + const docRef1 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete1`); + await setDoc(docRef1, {}); + + const docRef2 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete2`); + await setDoc(docRef2, {}); + + await runTransaction(db, async t => { + t.delete(docRef1); + t.delete(docRef2); + }); + + const snapshot1 = await getDocs(docRef1); + snapshot1.exists.should.eql(false); + + const snapshot2 = await getDocs(docRef2); + snapshot2.exists.should.eql(false); + }); }); - it('should set data with merge fields', async function () { - const docRef = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/set-mergefields`); - await docRef.set({ - foo: 'bar', - bar: 'baz', - baz: 'ben', - }); - const expected = { - foo: 'bar', - bar: 'foo', - baz: 'foo', - }; - - await firebase.firestore().runTransaction(async t => { - t.set( - docRef, - { - bar: 'foo', - baz: 'foo', - }, - { - mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], - }, - ); - }); - - const snapshot = await docRef.get(); - snapshot.data().should.eql(jet.contextify(expected)); + describe('transaction.update()', function () { + it('should throw if not providing a document reference', async function () { + const { getFirestore, runTransaction } = firestoreModular; + try { + await runTransaction(getFirestore(), async t => { + t.update(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); + + it('should throw if update args are invalid', async function () { + const { getFirestore, runTransaction, doc } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/foo`); + + try { + await runTransaction(db, async t => { + t.update(docRef, 123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('it must be an object'); + return Promise.resolve(); + } + }); + + it('should update documents', async function () { + const { getFirestore, runTransaction, doc, setDoc, getDocs } = firestoreModular; + const db = getFirestore(); + const value = Date.now(); + + const docRef1 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete1`); + await setDoc(docRef1, { + foo: 'bar', + bar: 'baz', + }); + + const docRef2 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete2`); + await setDoc(docRef2, { + foo: 'bar', + bar: 'baz', + }); + + await runTransaction(db, async t => { + t.update(docRef1, { + bar: value, + }); + t.update(docRef2, 'bar', value); + }); + + const expected = { + foo: 'bar', + bar: value, + }; + + const snapshot1 = await getDocs(docRef1); + snapshot1.exists.should.eql(true); + snapshot1.data().should.eql(jet.contextify(expected)); + + const snapshot2 = await getDocs(docRef2); + snapshot2.exists.should.eql(true); + snapshot2.data().should.eql(jet.contextify(expected)); + }); }); - it('should roll back any updates that failed', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/transactions/transaction/rollback`); + describe('transaction.set()', function () { + it('should throw if not providing a document reference', async function () { + const { getFirestore, runTransaction } = firestoreModular; + try { + await runTransaction(getFirestore(), async t => { + t.set(123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } + }); - await docRef.set({ - turn: 0, + it('should throw if set data is invalid', async function () { + const { getFirestore, runTransaction, doc } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/foo`); + + try { + await runTransaction(db, async t => { + t.set(docRef, 123); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'data' must be an object."); + return Promise.resolve(); + } }); - const prop1 = 'prop1'; - const prop2 = 'prop2'; - const turn = 0; - const errorMessage = 'turn cannot exceed 1'; + it('should throw if set options are invalid', async function () { + const { getFirestore, runTransaction, doc } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/foo`); + + try { + await runTransaction(db, async t => { + t.set( + docRef, + {}, + { + merge: true, + mergeFields: [], + }, + ); + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' must not contain both 'merge' & 'mergeFields'", + ); + return Promise.resolve(); + } + }); - const createTransaction = prop => { - return firebase.firestore().runTransaction(async transaction => { - const doc = await transaction.get(docRef); - const data = doc.data(); + it('should set data', async function () { + const { getFirestore, runTransaction, doc, getDocs, setDoc } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/transactions/transaction/set`); + await setDoc(docRef, { + foo: 'bar', + }); + const expected = { + foo: 'baz', + }; - if (data.turn !== turn) { - throw new Error(errorMessage); - } + await runTransaction(db, async t => { + t.set(docRef, expected); + }); - const update = { - turn: turn + 1, - [prop]: 1, - }; + const snapshot = await getDocs(docRef); + snapshot.data().should.eql(jet.contextify(expected)); + }); - transaction.update(docRef, update); + it('should set data with merge', async function () { + const { getFirestore, runTransaction, doc, getDocs, setDoc } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/transactions/transaction/set-merge`); + await setDoc(docRef, { + foo: 'bar', + bar: 'baz', }); - }; + const expected = { + foo: 'bar', + bar: 'foo', + }; - const promises = [createTransaction(prop1), createTransaction(prop2)]; + await runTransaction(db, async t => { + t.set( + docRef, + { + bar: 'foo', + }, + { + merge: true, + }, + ); + }); - try { - await Promise.all(promises); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql(errorMessage); - } - const result = await docRef.get(); - should(result.data()).not.have.properties([prop1, prop2]); + const snapshot = await getDocs(docRef); + snapshot.data().should.eql(jet.contextify(expected)); + }); + + it('should set data with merge fields', async function () { + const { getFirestore, runTransaction, doc, getDocs, setDoc } = firestoreModular; + const db = getFirestore(); + + const docRef = doc(db, `${COLLECTION}/transactions/transaction/set-mergefields`); + await setDoc(docRef, { + foo: 'bar', + bar: 'baz', + baz: 'ben', + }); + const expected = { + foo: 'bar', + bar: 'foo', + baz: 'foo', + }; + + await runTransaction(db, async t => { + t.set( + docRef, + { + bar: 'foo', + baz: 'foo', + }, + { + mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], + }, + ); + }); + + const snapshot = await getDocs(docRef); + snapshot.data().should.eql(jet.contextify(expected)); + }); + + it('should roll back any updates that failed', async function () { + const { getFirestore, runTransaction, doc, getDocs, setDoc } = firestoreModular; + const db = getFirestore(); + + const docRef = doc(db, `${COLLECTION}/transactions/transaction/rollback`); + + await setDoc(docRef, { + turn: 0, + }); + + const prop1 = 'prop1'; + const prop2 = 'prop2'; + const turn = 0; + const errorMessage = 'turn cannot exceed 1'; + + const createTransaction = prop => { + return runTransaction(db, async transaction => { + const doc = await transaction.get(docRef); + const data = doc.data(); + + if (data.turn !== turn) { + throw new Error(errorMessage); + } + + const update = { + turn: turn + 1, + [prop]: 1, + }; + + transaction.update(docRef, update); + }); + }; + + const promises = [createTransaction(prop1), createTransaction(prop2)]; + + try { + await Promise.all(promises); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql(errorMessage); + } + const result = await getDocs(docRef); + should(result.data()).not.have.properties([prop1, prop2]); + }); }); }); }); diff --git a/packages/firestore/e2e/WriteBatch/commit.e2e.js b/packages/firestore/e2e/WriteBatch/commit.e2e.js index a07d995781..2e161579bf 100644 --- a/packages/firestore/e2e/WriteBatch/commit.e2e.js +++ b/packages/firestore/e2e/WriteBatch/commit.e2e.js @@ -21,187 +21,407 @@ describe('firestore.WriteBatch.commit()', function () { before(function () { return wipe(); }); - it('returns a Promise', function () { - const commit = firebase.firestore().batch().commit(); - commit.should.be.a.Promise(); - }); - // FIXME this started to fail with dependency updates 20230628 - // firebase-tools firestore emulator allows more than 500 with an update? - // official docs still indicate that 500 is the limit, so this is likely - // an upstream bug in firestore emulator. - xit('throws if committing more than 500 writes', async function () { - const filledArray = new Array(501).fill({ foo: 'bar' }); - const batch = firebase.firestore().batch(); - - for (let i = 0; i < filledArray.length; i++) { - const doc = firebase.firestore().collection(COLLECTION).doc(i.toString()); - const filledArrayElement = filledArray[i]; - batch.set(doc, filledArrayElement); - } - - try { - await batch.commit(); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.code.should.containEql('firestore/invalid-argument'); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('returns a Promise', function () { + const commit = firebase.firestore().batch().commit(); + commit.should.be.a.Promise(); + }); + + // FIXME this started to fail with dependency updates 20230628 + // firebase-tools firestore emulator allows more than 500 with an update? + // official docs still indicate that 500 is the limit, so this is likely + // an upstream bug in firestore emulator. + xit('throws if committing more than 500 writes', async function () { + const filledArray = new Array(501).fill({ foo: 'bar' }); + const batch = firebase.firestore().batch(); + + for (let i = 0; i < filledArray.length; i++) { + const doc = firebase.firestore().collection(COLLECTION).doc(i.toString()); + const filledArrayElement = filledArray[i]; + batch.set(doc, filledArrayElement); + } + + try { + await batch.commit(); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.code.should.containEql('firestore/invalid-argument'); + return Promise.resolve(); + } + }); + + it('throws if already committed', async function () { + try { + const batch = firebase.firestore().batch(); + await batch.commit(); + await batch.commit(); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.message.should.containEql('A write batch can no longer be used'); + return Promise.resolve(); + } + }); + + it('should set & commit', async function () { + const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); + const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); + const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); - it('throws if already committed', async function () { - try { const batch = firebase.firestore().batch(); + + batch.set(lRef, { name: 'London' }); + batch.set(nycRef, { name: 'New York' }); + batch.set(sfRef, { name: 'San Francisco' }); + await batch.commit(); + + const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); + + lDoc.data().name.should.eql('London'); + nycDoc.data().name.should.eql('New York'); + sDoc.data().name.should.eql('San Francisco'); + await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); + }); + + it('should set/merge & commit', async function () { + const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); + const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); + const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); + + await Promise.all([ + lRef.set({ name: 'London' }), + nycRef.set({ name: 'New York' }), + sfRef.set({ name: 'San Francisco' }), + ]); + + const batch = firebase.firestore().batch(); + + batch.set(lRef, { country: 'UK' }, { merge: true }); + batch.set(nycRef, { country: 'USA' }, { merge: true }); + batch.set(sfRef, { country: 'USA' }, { merge: true }); + await batch.commit(); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql('A write batch can no longer be used'); - return Promise.resolve(); - } - }); - it('should set & commit', async function () { - const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); - const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); - const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); + const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); - const batch = firebase.firestore().batch(); + lDoc.data().name.should.eql('London'); + lDoc.data().country.should.eql('UK'); + nycDoc.data().name.should.eql('New York'); + nycDoc.data().country.should.eql('USA'); + sDoc.data().name.should.eql('San Francisco'); + sDoc.data().country.should.eql('USA'); - batch.set(lRef, { name: 'London' }); - batch.set(nycRef, { name: 'New York' }); - batch.set(sfRef, { name: 'San Francisco' }); + await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); + }); - await batch.commit(); + it('should set/mergeFields & commit', async function () { + const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); + const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); + const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); - const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); + await Promise.all([ + lRef.set({ name: 'London' }), + nycRef.set({ name: 'New York' }), + sfRef.set({ name: 'San Francisco' }), + ]); - lDoc.data().name.should.eql('London'); - nycDoc.data().name.should.eql('New York'); - sDoc.data().name.should.eql('San Francisco'); - await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); - }); + const batch = firebase.firestore().batch(); - it('should set/merge & commit', async function () { - const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); - const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); - const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); + batch.set(lRef, { name: 'LON', country: 'UK' }, { mergeFields: ['country'] }); + batch.set(nycRef, { name: 'NYC', country: 'USA' }, { mergeFields: ['country'] }); + batch.set( + sfRef, + { name: 'SF', country: 'USA' }, + { mergeFields: [new firebase.firestore.FieldPath('country')] }, + ); - await Promise.all([ - lRef.set({ name: 'London' }), - nycRef.set({ name: 'New York' }), - sfRef.set({ name: 'San Francisco' }), - ]); + await batch.commit(); - const batch = firebase.firestore().batch(); + const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); - batch.set(lRef, { country: 'UK' }, { merge: true }); - batch.set(nycRef, { country: 'USA' }, { merge: true }); - batch.set(sfRef, { country: 'USA' }, { merge: true }); + lDoc.data().name.should.eql('London'); + lDoc.data().country.should.eql('UK'); + nycDoc.data().name.should.eql('New York'); + nycDoc.data().country.should.eql('USA'); + sDoc.data().name.should.eql('San Francisco'); + sDoc.data().country.should.eql('USA'); - await batch.commit(); + await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); + }); - const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); + it('should delete & commit', async function () { + const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); + const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); + const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); - lDoc.data().name.should.eql('London'); - lDoc.data().country.should.eql('UK'); - nycDoc.data().name.should.eql('New York'); - nycDoc.data().country.should.eql('USA'); - sDoc.data().name.should.eql('San Francisco'); - sDoc.data().country.should.eql('USA'); + await Promise.all([ + lRef.set({ name: 'London' }), + nycRef.set({ name: 'New York' }), + sfRef.set({ name: 'San Francisco' }), + ]); - await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); - }); + const batch = firebase.firestore().batch(); - it('should set/mergeFields & commit', async function () { - const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); - const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); - const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); + batch.delete(lRef); + batch.delete(nycRef); + batch.delete(sfRef); + + await batch.commit(); - await Promise.all([ - lRef.set({ name: 'London' }), - nycRef.set({ name: 'New York' }), - sfRef.set({ name: 'San Francisco' }), - ]); + const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); - const batch = firebase.firestore().batch(); + lDoc.exists.should.be.False(); + nycDoc.exists.should.be.False(); + sDoc.exists.should.be.False(); + }); - batch.set(lRef, { name: 'LON', country: 'UK' }, { mergeFields: ['country'] }); - batch.set(nycRef, { name: 'NYC', country: 'USA' }, { mergeFields: ['country'] }); - batch.set( - sfRef, - { name: 'SF', country: 'USA' }, - { mergeFields: [new firebase.firestore.FieldPath('country')] }, - ); + it('should update & commit', async function () { + const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); + const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); + const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); - await batch.commit(); + await Promise.all([ + lRef.set({ name: 'London' }), + nycRef.set({ name: 'New York' }), + sfRef.set({ name: 'San Francisco' }), + ]); - const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); + const batch = firebase.firestore().batch(); + + batch.update(lRef, { name: 'LON', country: 'UK' }); + batch.update(nycRef, { name: 'NYC', country: 'USA' }); + batch.update(sfRef, 'name', 'SF', 'country', 'USA'); + + await batch.commit(); - lDoc.data().name.should.eql('London'); - lDoc.data().country.should.eql('UK'); - nycDoc.data().name.should.eql('New York'); - nycDoc.data().country.should.eql('USA'); - sDoc.data().name.should.eql('San Francisco'); - sDoc.data().country.should.eql('USA'); + const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); - await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); + lDoc.data().name.should.eql('LON'); + lDoc.data().country.should.eql('UK'); + nycDoc.data().name.should.eql('NYC'); + nycDoc.data().country.should.eql('USA'); + sDoc.data().name.should.eql('SF'); + sDoc.data().country.should.eql('USA'); + + await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); + }); }); - it('should delete & commit', async function () { - const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); - const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); - const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); + describe('modular', function () { + it('returns a Promise', function () { + const { getFirestore, writeBatch } = firestoreModular; + const commit = writeBatch(getFirestore()).commit(); + commit.should.be.a.Promise(); + }); + + // FIXME this started to fail with dependency updates 20230628 + // firebase-tools firestore emulator allows more than 500 with an update? + // official docs still indicate that 500 is the limit, so this is likely + // an upstream bug in firestore emulator. + xit('throws if committing more than 500 writes', async function () { + const { getFirestore, writeBatch, collection, doc, setDoc } = firestoreModular; + const db = getFirestore(); + const filledArray = new Array(501).fill({ foo: 'bar' }); + const batch = writeBatch(db); + + for (let i = 0; i < filledArray.length; i++) { + const docRef = doc(collection(db, COLLECTION), i.toString()); + const filledArrayElement = filledArray[i]; + setDoc(batch, docRef, filledArrayElement); + } + + try { + await batch.commit(); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.code.should.containEql('firestore/invalid-argument'); + return Promise.resolve(); + } + }); + + it('throws if already committed', async function () { + const { getFirestore, writeBatch } = firestoreModular; + try { + const batch = writeBatch(getFirestore()); + await batch.commit(); + await batch.commit(); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.message.should.containEql('A write batch can no longer be used'); + return Promise.resolve(); + } + }); + + it('should set & commit', async function () { + const { getFirestore, writeBatch, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const db = getFirestore(); + const lRef = doc(db, `${COLLECTION}/LON`); + const nycRef = doc(db, `${COLLECTION}/NYC`); + const sfRef = doc(db, `${COLLECTION}/SF`); + + const batch = writeBatch(db); + + setDoc(batch, lRef, { name: 'London' }); + setDoc(batch, nycRef, { name: 'New York' }); + setDoc(batch, sfRef, { name: 'San Francisco' }); + + await batch.commit(); - await Promise.all([ - lRef.set({ name: 'London' }), - nycRef.set({ name: 'New York' }), - sfRef.set({ name: 'San Francisco' }), - ]); + const [lDoc, nycDoc, sDoc] = await Promise.all([ + getDocs(lRef), + getDocs(nycRef), + getDocs(sfRef), + ]); + + lDoc.data().name.should.eql('London'); + nycDoc.data().name.should.eql('New York'); + sDoc.data().name.should.eql('San Francisco'); + await Promise.all([deleteDoc(lRef), deleteDoc(nycRef), deleteDoc(sfRef)]); + }); + + it('should set/merge & commit', async function () { + const { getFirestore, writeBatch, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const db = getFirestore(); + const lRef = doc(db, `${COLLECTION}/LON`); + const nycRef = doc(db, `${COLLECTION}/NYC`); + const sfRef = doc(db, `${COLLECTION}/SF`); + + await Promise.all([ + setDoc(lRef, { name: 'London' }), + setDoc(nycRef, { name: 'New York' }), + setDoc(sfRef, { name: 'San Francisco' }), + ]); + + const batch = writeBatch(db); + + batch.set(lRef, { country: 'UK' }, { merge: true }); + batch.set(nycRef, { country: 'USA' }, { merge: true }); + batch.set(sfRef, { country: 'USA' }, { merge: true }); - const batch = firebase.firestore().batch(); + await batch.commit(); - batch.delete(lRef); - batch.delete(nycRef); - batch.delete(sfRef); + const [lDoc, nycDoc, sDoc] = await Promise.all([ + getDocs(lRef), + getDocs(nycRef), + getDocs(sfRef), + ]); + + lDoc.data().name.should.eql('London'); + lDoc.data().country.should.eql('UK'); + nycDoc.data().name.should.eql('New York'); + nycDoc.data().country.should.eql('USA'); + sDoc.data().name.should.eql('San Francisco'); + sDoc.data().country.should.eql('USA'); + + await Promise.all([deleteDoc(lRef), deleteDoc(nycRef), deleteDoc(sfRef)]); + }); + + it('should set/mergeFields & commit', async function () { + const { getFirestore, writeBatch, doc, setDoc, FieldPath, getDocs, deleteDoc } = + firestoreModular; + const db = getFirestore(); + const lRef = doc(db, `${COLLECTION}/LON`); + const nycRef = doc(db, `${COLLECTION}/NYC`); + const sfRef = doc(db, `${COLLECTION}/SF`); + + await Promise.all([ + setDoc(lRef, { name: 'London' }), + setDoc(nycRef, { name: 'New York' }), + setDoc(sfRef, { name: 'San Francisco' }), + ]); + + const batch = writeBatch(db); + + batch.set(lRef, { name: 'LON', country: 'UK' }, { mergeFields: ['country'] }); + batch.set(nycRef, { name: 'NYC', country: 'USA' }, { mergeFields: ['country'] }); + batch.set(sfRef, { name: 'SF', country: 'USA' }, { mergeFields: [new FieldPath('country')] }); - await batch.commit(); + await batch.commit(); - const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); + const [lDoc, nycDoc, sDoc] = await Promise.all([ + getDocs(lRef), + getDocs(nycRef), + getDocs(sfRef), + ]); + + lDoc.data().name.should.eql('London'); + lDoc.data().country.should.eql('UK'); + nycDoc.data().name.should.eql('New York'); + nycDoc.data().country.should.eql('USA'); + sDoc.data().name.should.eql('San Francisco'); + sDoc.data().country.should.eql('USA'); + + await Promise.all([deleteDoc(lRef), deleteDoc(nycRef), deleteDoc(sfRef)]); + }); + + it('should delete & commit', async function () { + const { getFirestore, writeBatch, doc, setDoc, getDocs } = firestoreModular; + const db = getFirestore(); + const lRef = doc(db, `${COLLECTION}/LON`); + const nycRef = doc(db, `${COLLECTION}/NYC`); + const sfRef = doc(db, `${COLLECTION}/SF`); + + await Promise.all([ + setDoc(lRef, { name: 'London' }), + setDoc(nycRef, { name: 'New York' }), + setDoc(sfRef, { name: 'San Francisco' }), + ]); + + const batch = writeBatch(db); + + batch.delete(lRef); + batch.delete(nycRef); + batch.delete(sfRef); - lDoc.exists.should.be.False(); - nycDoc.exists.should.be.False(); - sDoc.exists.should.be.False(); - }); + await batch.commit(); - it('should update & commit', async function () { - const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); - const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); - const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); + const [lDoc, nycDoc, sDoc] = await Promise.all([ + getDocs(lRef), + getDocs(nycRef), + getDocs(sfRef), + ]); - await Promise.all([ - lRef.set({ name: 'London' }), - nycRef.set({ name: 'New York' }), - sfRef.set({ name: 'San Francisco' }), - ]); + lDoc.exists.should.be.False(); + nycDoc.exists.should.be.False(); + sDoc.exists.should.be.False(); + }); - const batch = firebase.firestore().batch(); + it('should update & commit', async function () { + const { getFirestore, writeBatch, doc, setDoc, getDocs, deleteDoc } = firestoreModular; + const db = getFirestore(); + const lRef = doc(db, `${COLLECTION}/LON`); + const nycRef = doc(db, `${COLLECTION}/NYC`); + const sfRef = doc(db, `${COLLECTION}/SF`); - batch.update(lRef, { name: 'LON', country: 'UK' }); - batch.update(nycRef, { name: 'NYC', country: 'USA' }); - batch.update(sfRef, 'name', 'SF', 'country', 'USA'); + await Promise.all([ + setDoc(lRef, { name: 'London' }), + setDoc(nycRef, { name: 'New York' }), + setDoc(sfRef, { name: 'San Francisco' }), + ]); - await batch.commit(); + const batch = writeBatch(db); - const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); + batch.update(lRef, { name: 'LON', country: 'UK' }); + batch.update(nycRef, { name: 'NYC', country: 'USA' }); + batch.update(sfRef, 'name', 'SF', 'country', 'USA'); - lDoc.data().name.should.eql('LON'); - lDoc.data().country.should.eql('UK'); - nycDoc.data().name.should.eql('NYC'); - nycDoc.data().country.should.eql('USA'); - sDoc.data().name.should.eql('SF'); - sDoc.data().country.should.eql('USA'); + await batch.commit(); - await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); + const [lDoc, nycDoc, sDoc] = await Promise.all([ + getDocs(lRef), + getDocs(nycRef), + getDocs(sfRef), + ]); + + lDoc.data().name.should.eql('LON'); + lDoc.data().country.should.eql('UK'); + nycDoc.data().name.should.eql('NYC'); + nycDoc.data().country.should.eql('USA'); + sDoc.data().name.should.eql('SF'); + sDoc.data().country.should.eql('USA'); + + await Promise.all([deleteDoc(lRef), deleteDoc(nycRef), deleteDoc(sfRef)]); + }); }); }); diff --git a/packages/firestore/e2e/WriteBatch/delete.e2e.js b/packages/firestore/e2e/WriteBatch/delete.e2e.js index 5de04c83cd..21d717bb3f 100644 --- a/packages/firestore/e2e/WriteBatch/delete.e2e.js +++ b/packages/firestore/e2e/WriteBatch/delete.e2e.js @@ -17,39 +17,83 @@ const COLLECTION = 'firestore'; describe('firestore.WriteBatch.delete()', function () { - it('throws if a DocumentReference instance is not provided', function () { - try { - firebase.firestore().batch().delete(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if a DocumentReference instance is not provided', function () { + try { + firebase.firestore().batch().delete(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); + return Promise.resolve(); + } + }); + + it('throws if a DocumentReference firestore instance is different', function () { + try { + const app2 = firebase.app('secondaryFromNative'); + const docRef = firebase.firestore(app2).doc(`${COLLECTION}/foo`); + + firebase.firestore().batch().delete(docRef); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'documentRef' provided DocumentReference is from a different Firestore instance", + ); + return Promise.resolve(); + } + }); - it('throws if a DocumentReference firestore instance is different', function () { - try { - const app2 = firebase.app('secondaryFromNative'); - const docRef = firebase.firestore(app2).doc(`${COLLECTION}/foo`); - - firebase.firestore().batch().delete(docRef); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'documentRef' provided DocumentReference is from a different Firestore instance", - ); - return Promise.resolve(); - } + it('adds the DocumentReference to the internal writes', function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + const wb = firebase.firestore().batch().delete(docRef); + wb._writes.length.should.eql(1); + const expected = { + path: `${COLLECTION}/foo`, + type: 'DELETE', + }; + wb._writes[0].should.eql(jet.contextify(expected)); + }); }); - it('adds the DocumentReference to the internal writes', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - const wb = firebase.firestore().batch().delete(docRef); - wb._writes.length.should.eql(1); - const expected = { - path: `${COLLECTION}/foo`, - type: 'DELETE', - }; - wb._writes[0].should.eql(jet.contextify(expected)); + describe('modular', function () { + it('throws if a DocumentReference instance is not provided', function () { + const { getFirestore, writeBatch } = firestoreModular; + try { + writeBatch(getFirestore()).delete(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); + return Promise.resolve(); + } + }); + + it('throws if a DocumentReference firestore instance is different', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + try { + const app2 = firebase.app('secondaryFromNative'); + const docRef = doc(getFirestore(app2), `${COLLECTION}/foo`); + + writeBatch(getFirestore()).delete(docRef); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'documentRef' provided DocumentReference is from a different Firestore instance", + ); + return Promise.resolve(); + } + }); + + it('adds the DocumentReference to the internal writes', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/foo`); + const wb = writeBatch(db).delete(docRef); + wb._writes.length.should.eql(1); + const expected = { + path: `${COLLECTION}/foo`, + type: 'DELETE', + }; + wb._writes[0].should.eql(jet.contextify(expected)); + }); }); }); diff --git a/packages/firestore/e2e/WriteBatch/set.e2e.js b/packages/firestore/e2e/WriteBatch/set.e2e.js index 1acd12f5b6..801bfe6dae 100644 --- a/packages/firestore/e2e/WriteBatch/set.e2e.js +++ b/packages/firestore/e2e/WriteBatch/set.e2e.js @@ -17,112 +17,156 @@ const COLLECTION = 'firestore'; describe('firestore.WriteBatch.set()', function () { - it('throws if a DocumentReference instance is not provided', function () { - try { - firebase.firestore().batch().set(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if a DocumentReference instance is not provided', function () { + try { + firebase.firestore().batch().set(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); + return Promise.resolve(); + } + }); - it('throws if a DocumentReference firestore instance is different', function () { - try { - const app2 = firebase.app('secondaryFromNative'); - const docRef = firebase.firestore(app2).doc(`${COLLECTION}/foo`); + it('throws if a DocumentReference firestore instance is different', function () { + try { + const app2 = firebase.app('secondaryFromNative'); + const docRef = firebase.firestore(app2).doc(`${COLLECTION}/foo`); - firebase.firestore().batch().set(docRef); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'documentRef' provided DocumentReference is from a different Firestore instance", - ); - return Promise.resolve(); - } - }); + firebase.firestore().batch().set(docRef); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'documentRef' provided DocumentReference is from a different Firestore instance", + ); + return Promise.resolve(); + } + }); - it('throws if a data is not an object', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + it('throws if a data is not an object', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().set(docRef, 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'data' must be an object"); - return Promise.resolve(); - } - }); + firebase.firestore().batch().set(docRef, 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'data' must be an object"); + return Promise.resolve(); + } + }); - it('throws if a options is not an object', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + it('throws if a options is not an object', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().set(docRef, {}, 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must be an object"); - return Promise.resolve(); - } - }); + firebase.firestore().batch().set(docRef, {}, 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' must be an object"); + return Promise.resolve(); + } + }); - it('throws if merge and mergeFields is provided', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase - .firestore() - .batch() - .set( + it('throws if merge and mergeFields is provided', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + firebase + .firestore() + .batch() + .set( + docRef, + {}, + { + merge: true, + mergeFields: ['123'], + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' must not contain both 'merge' & 'mergeFields'"); + return Promise.resolve(); + } + }); + + it('throws if merge is not a boolean', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + firebase.firestore().batch().set( docRef, {}, { - merge: true, - mergeFields: ['123'], + merge: 'true', }, ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must not contain both 'merge' & 'mergeFields'"); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.merge' must be a boolean value"); + return Promise.resolve(); + } + }); - it('throws if merge is not a boolean', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().set( - docRef, - {}, - { - merge: 'true', - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.merge' must be a boolean value"); - return Promise.resolve(); - } - }); + it('throws if mergeFields is not an array', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + firebase.firestore().batch().set( + docRef, + {}, + { + mergeFields: '[]', + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.mergeFields' must be an array"); + return Promise.resolve(); + } + }); - it('throws if mergeFields is not an array', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().set( - docRef, - {}, - { - mergeFields: '[]', - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.mergeFields' must be an array"); - return Promise.resolve(); - } - }); + it('throws if mergeFields item is not valid', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + + firebase + .firestore() + .batch() + .set( + docRef, + {}, + { + mergeFields: [123], + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options.mergeFields' all fields must be of type string or FieldPath", + ); + return Promise.resolve(); + } + }); + + it('throws if string fieldpath is invalid', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - it('throws if mergeFields item is not valid', function () { - try { + firebase + .firestore() + .batch() + .set( + docRef, + {}, + { + mergeFields: ['.foo.bar'], + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.mergeFields' Invalid field path"); + return Promise.resolve(); + } + }); + + it('accepts string fieldpath & FieldPath', function () { const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); firebase @@ -132,73 +176,223 @@ describe('firestore.WriteBatch.set()', function () { docRef, {}, { - mergeFields: [123], + mergeFields: ['foo.bar', new firebase.firestore.FieldPath('foo', 'bar')], }, ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options.mergeFields' all fields must be of type string or FieldPath", - ); - return Promise.resolve(); - } - }); + }); - it('throws if string fieldpath is invalid', function () { - try { + it('adds the DocumentReference to the internal writes', function () { const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase + const wb = firebase .firestore() .batch() .set( + docRef, + { foo: 'bar' }, + { mergeFields: [new firebase.firestore.FieldPath('foo', 'bar')] }, + ); + wb._writes.length.should.eql(1); + const expected = { + path: `${COLLECTION}/foo`, + type: 'SET', + options: jet.contextify({ + mergeFields: jet.contextify(['foo.bar']), + }), + }; + wb._writes[0].should.containEql(jet.contextify(expected)); + }); + }); + + describe('modular', function () { + it('throws if a DocumentReference instance is not provided', function () { + const { getFirestore, writeBatch } = firestoreModular; + try { + writeBatch(getFirestore()).set(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); + return Promise.resolve(); + } + }); + + it('throws if a DocumentReference firestore instance is different', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + try { + const app2 = firebase.app('secondaryFromNative'); + const docRef = doc(getFirestore(app2), `${COLLECTION}/foo`); + + writeBatch(getFirestore()).set(docRef); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'documentRef' provided DocumentReference is from a different Firestore instance", + ); + return Promise.resolve(); + } + }); + + it('throws if a data is not an object', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + + writeBatch(db).set(docRef, 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'data' must be an object"); + return Promise.resolve(); + } + }); + + it('throws if a options is not an object', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + + writeBatch(db).set(docRef, {}, 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' must be an object"); + return Promise.resolve(); + } + }); + + it('throws if merge and mergeFields is provided', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + writeBatch(db).set( + docRef, + {}, + { + merge: true, + mergeFields: ['123'], + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options' must not contain both 'merge' & 'mergeFields'"); + return Promise.resolve(); + } + }); + + it('throws if merge is not a boolean', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + writeBatch(db).set( + docRef, + {}, + { + merge: 'true', + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.merge' must be a boolean value"); + return Promise.resolve(); + } + }); + + it('throws if mergeFields is not an array', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + writeBatch(db).set( + docRef, + {}, + { + mergeFields: '[]', + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.mergeFields' must be an array"); + return Promise.resolve(); + } + }); + + it('throws if mergeFields item is not valid', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + + writeBatch(db).set( + docRef, + {}, + { + mergeFields: [123], + }, + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options.mergeFields' all fields must be of type string or FieldPath", + ); + return Promise.resolve(); + } + }); + + it('throws if string fieldpath is invalid', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + + writeBatch(db).set( docRef, {}, { mergeFields: ['.foo.bar'], }, ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.mergeFields' Invalid field path"); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'options.mergeFields' Invalid field path"); + return Promise.resolve(); + } + }); - it('accepts string fieldpath & FieldPath', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + it('accepts string fieldpath & FieldPath', function () { + const { getFirestore, doc, writeBatch, FieldPath } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/foo`); - firebase - .firestore() - .batch() - .set( + writeBatch(db).set( docRef, {}, { - mergeFields: ['foo.bar', new firebase.firestore.FieldPath('foo', 'bar')], + mergeFields: ['foo.bar', new FieldPath('foo', 'bar')], }, ); - }); + }); - it('adds the DocumentReference to the internal writes', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + it('adds the DocumentReference to the internal writes', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/foo`); - const wb = firebase - .firestore() - .batch() - .set( + const wb = writeBatch(db).set( docRef, { foo: 'bar' }, { mergeFields: [new firebase.firestore.FieldPath('foo', 'bar')] }, ); - wb._writes.length.should.eql(1); - const expected = { - path: `${COLLECTION}/foo`, - type: 'SET', - options: jet.contextify({ - mergeFields: jet.contextify(['foo.bar']), - }), - }; - wb._writes[0].should.containEql(jet.contextify(expected)); + wb._writes.length.should.eql(1); + const expected = { + path: `${COLLECTION}/foo`, + type: 'SET', + options: jet.contextify({ + mergeFields: jet.contextify(['foo.bar']), + }), + }; + wb._writes[0].should.containEql(jet.contextify(expected)); + }); }); }); diff --git a/packages/firestore/e2e/WriteBatch/update.e2e.js b/packages/firestore/e2e/WriteBatch/update.e2e.js index 46957d066c..6ac6ea8404 100644 --- a/packages/firestore/e2e/WriteBatch/update.e2e.js +++ b/packages/firestore/e2e/WriteBatch/update.e2e.js @@ -18,83 +18,179 @@ const COLLECTION = 'firestore'; describe('firestore.WriteBatch.update()', function () { - it('throws if a DocumentReference instance is not provided', function () { - try { - firebase.firestore().batch().update(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); - return Promise.resolve(); - } - }); + describe('v8 compatibility', function () { + it('throws if a DocumentReference instance is not provided', function () { + try { + firebase.firestore().batch().update(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); + return Promise.resolve(); + } + }); - it('throws if a DocumentReference firestore instance is different', function () { - try { - const app2 = firebase.app('secondaryFromNative'); - const docRef = firebase.firestore(app2).doc(`${COLLECTION}/foo`); - - firebase.firestore().batch().update(docRef); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'documentRef' provided DocumentReference is from a different Firestore instance", - ); - return Promise.resolve(); - } - }); + it('throws if a DocumentReference firestore instance is different', function () { + try { + const app2 = firebase.app('secondaryFromNative'); + const docRef = firebase.firestore(app2).doc(`${COLLECTION}/foo`); - it('throws if update args are not provided', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().update(docRef); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Expected update object or list of key/value pairs'); - return Promise.resolve(); - } - }); + firebase.firestore().batch().update(docRef); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'documentRef' provided DocumentReference is from a different Firestore instance", + ); + return Promise.resolve(); + } + }); - it('throws if update arg is not an object', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().update(docRef, 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('if using a single update argument, it must be an object'); - return Promise.resolve(); - } - }); + it('throws if update args are not provided', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + firebase.firestore().batch().update(docRef); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Expected update object or list of key/value pairs'); + return Promise.resolve(); + } + }); - it('throws if update key/values are invalid', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().update(docRef, 'foo', 'bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('equal numbers of key/value pairs'); - return Promise.resolve(); - } - }); + it('throws if update arg is not an object', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + firebase.firestore().batch().update(docRef, 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('if using a single update argument, it must be an object'); + return Promise.resolve(); + } + }); + + it('throws if update key/values are invalid', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + firebase.firestore().batch().update(docRef, 'foo', 'bar', 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('equal numbers of key/value pairs'); + return Promise.resolve(); + } + }); - it('throws if update keys are invalid', function () { - try { + it('throws if update keys are invalid', function () { + try { + const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); + firebase.firestore().batch().update(docRef, 'foo', 'bar', 123, 'ben'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('argument at index 2 must be a string or FieldPath'); + return Promise.resolve(); + } + }); + + it('adds the DocumentReference to the internal writes', function () { const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().update(docRef, 'foo', 'bar', 123, 'ben'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('argument at index 2 must be a string or FieldPath'); - return Promise.resolve(); - } + const wb = firebase.firestore().batch().update(docRef, { foo: 'bar' }); + wb._writes.length.should.eql(1); + const expected = { + path: `${COLLECTION}/foo`, + type: 'UPDATE', + }; + wb._writes[0].should.containEql(jet.contextify(expected)); + }); }); - it('adds the DocumentReference to the internal writes', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - const wb = firebase.firestore().batch().update(docRef, { foo: 'bar' }); - wb._writes.length.should.eql(1); - const expected = { - path: `${COLLECTION}/foo`, - type: 'UPDATE', - }; - wb._writes[0].should.containEql(jet.contextify(expected)); + describe('modular', function () { + it('throws if a DocumentReference instance is not provided', function () { + const { getFirestore, writeBatch } = firestoreModular; + try { + writeBatch(getFirestore()).update(123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); + return Promise.resolve(); + } + }); + + it('throws if a DocumentReference firestore instance is different', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + try { + const app2 = firebase.app('secondaryFromNative'); + const docRef = doc(getFirestore(app2), `${COLLECTION}/foo`); + + writeBatch(getFirestore()).update(docRef); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'documentRef' provided DocumentReference is from a different Firestore instance", + ); + return Promise.resolve(); + } + }); + + it('throws if update args are not provided', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + writeBatch(db).update(docRef); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Expected update object or list of key/value pairs'); + return Promise.resolve(); + } + }); + + it('throws if update arg is not an object', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + writeBatch(db).update(docRef, 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('if using a single update argument, it must be an object'); + return Promise.resolve(); + } + }); + + it('throws if update key/values are invalid', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + writeBatch(db).update(docRef, 'foo', 'bar', 'baz'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('equal numbers of key/value pairs'); + return Promise.resolve(); + } + }); + + it('throws if update keys are invalid', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + try { + const docRef = doc(db, `${COLLECTION}/foo`); + writeBatch(db).update(docRef, 'foo', 'bar', 123, 'ben'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('argument at index 2 must be a string or FieldPath'); + return Promise.resolve(); + } + }); + + it('adds the DocumentReference to the internal writes', function () { + const { getFirestore, doc, writeBatch } = firestoreModular; + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/foo`); + const wb = writeBatch(db).update(docRef, { foo: 'bar' }); + wb._writes.length.should.eql(1); + const expected = { + path: `${COLLECTION}/foo`, + type: 'UPDATE', + }; + wb._writes[0].should.containEql(jet.contextify(expected)); + }); }); }); diff --git a/packages/firestore/e2e/firestore.e2e.js b/packages/firestore/e2e/firestore.e2e.js index da31545ddc..9347a654ad 100644 --- a/packages/firestore/e2e/firestore.e2e.js +++ b/packages/firestore/e2e/firestore.e2e.js @@ -18,279 +18,643 @@ const COLLECTION = 'firestore'; const COLLECTION_GROUP = 'collectionGroup'; describe('firestore()', function () { - describe('namespace', function () { - // removing as pending if module.options.hasMultiAppSupport = true - it('supports multiple apps', async function () { - firebase.firestore().app.name.should.equal('[DEFAULT]'); + describe('v8 compatibility', function () { + describe('namespace', function () { + // removing as pending if module.options.hasMultiAppSupport = true + it('supports multiple apps', async function () { + firebase.firestore().app.name.should.equal('[DEFAULT]'); - firebase - .firestore(firebase.app('secondaryFromNative')) - .app.name.should.equal('secondaryFromNative'); + firebase + .firestore(firebase.app('secondaryFromNative')) + .app.name.should.equal('secondaryFromNative'); - firebase.app('secondaryFromNative').firestore().app.name.should.equal('secondaryFromNative'); + firebase + .app('secondaryFromNative') + .firestore() + .app.name.should.equal('secondaryFromNative'); + }); }); - }); - describe('batch()', function () {}); + describe('batch()', function () {}); - describe('clearPersistence()', function () {}); + describe('clearPersistence()', function () {}); - describe('collection()', function () {}); + describe('collection()', function () {}); - describe('collectionGroup()', function () { - it('performs a collection group query', async function () { - const docRef1 = firebase.firestore().doc(`${COLLECTION}/collectionGroup1`); - const docRef2 = firebase.firestore().doc(`${COLLECTION}/collectionGroup2`); - const docRef3 = firebase.firestore().doc(`${COLLECTION}/collectionGroup3`); - const subRef1 = docRef1.collection(COLLECTION_GROUP).doc('ref'); - const subRef2 = docRef1.collection(COLLECTION_GROUP).doc('ref2'); - const subRef3 = docRef2.collection(COLLECTION_GROUP).doc('ref'); - const subRef4 = docRef2.collection(COLLECTION_GROUP).doc('ref2'); - const subRef5 = docRef3.collection(COLLECTION_GROUP).doc('ref'); - const subRef6 = docRef3.collection(COLLECTION_GROUP).doc('ref2'); + describe('collectionGroup()', function () { + it('performs a collection group query', async function () { + const docRef1 = firebase.firestore().doc(`${COLLECTION}/collectionGroup1`); + const docRef2 = firebase.firestore().doc(`${COLLECTION}/collectionGroup2`); + const docRef3 = firebase.firestore().doc(`${COLLECTION}/collectionGroup3`); + const subRef1 = docRef1.collection(COLLECTION_GROUP).doc('ref'); + const subRef2 = docRef1.collection(COLLECTION_GROUP).doc('ref2'); + const subRef3 = docRef2.collection(COLLECTION_GROUP).doc('ref'); + const subRef4 = docRef2.collection(COLLECTION_GROUP).doc('ref2'); + const subRef5 = docRef3.collection(COLLECTION_GROUP).doc('ref'); + const subRef6 = docRef3.collection(COLLECTION_GROUP).doc('ref2'); - await Promise.all([ - subRef1.set({ value: 1 }), - subRef2.set({ value: 2 }), + await Promise.all([ + subRef1.set({ value: 1 }), + subRef2.set({ value: 2 }), - subRef3.set({ value: 1 }), - subRef4.set({ value: 2 }), + subRef3.set({ value: 1 }), + subRef4.set({ value: 2 }), - subRef5.set({ value: 1 }), - subRef6.set({ value: 2 }), - ]); + subRef5.set({ value: 1 }), + subRef6.set({ value: 2 }), + ]); + + const querySnapshot = await firebase + .firestore() + .collectionGroup(COLLECTION_GROUP) + .where('value', '==', 2) + .get(); + + querySnapshot.forEach(ds => { + ds.data().value.should.eql(2); + }); - const querySnapshot = await firebase - .firestore() - .collectionGroup(COLLECTION_GROUP) - .where('value', '==', 2) - .get(); + querySnapshot.size.should.eql(3); - querySnapshot.forEach(ds => { - ds.data().value.should.eql(2); + await Promise.all([ + subRef1.delete(), + subRef2.delete(), + + subRef3.delete(), + subRef4.delete(), + + subRef5.delete(), + subRef6.delete(), + ]); }); - querySnapshot.size.should.eql(3); + it('performs a collection group query with cursor queries', async function () { + const docRef = firebase.firestore().doc(`${COLLECTION}/collectionGroupCursor`); - await Promise.all([ - subRef1.delete(), - subRef2.delete(), + const ref1 = await docRef.collection(COLLECTION_GROUP).add({ number: 1 }); + const startAt = await docRef.collection(COLLECTION_GROUP).add({ number: 2 }); + const ref3 = await docRef.collection(COLLECTION_GROUP).add({ number: 3 }); - subRef3.delete(), - subRef4.delete(), + const ds = await startAt.get(); + + const querySnapshot = await firebase + .firestore() + .collectionGroup(COLLECTION_GROUP) + .orderBy('number') + .startAt(ds) + .get(); + + querySnapshot.size.should.eql(2); + querySnapshot.forEach((d, i) => { + d.data().number.should.eql(i + 2); + }); + await Promise.all([ref1.delete(), ref3.delete(), startAt.delete()]); + }); + }); - subRef5.delete(), - subRef6.delete(), - ]); + describe('disableNetwork() & enableNetwork()', function () { + it('disables and enables with no errors', async function () { + await firebase.firestore().disableNetwork(); + await firebase.firestore().enableNetwork(); + }); }); - it('performs a collection group query with cursor queries', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/collectionGroupCursor`); + describe('Clear cached data persistence', function () { + // NOTE: removed as it breaks emulator tests + xit('should clear any cached data', async function () { + const db = firebase.firestore(); + const id = 'foobar'; + const ref = db.doc(`${COLLECTION}/${id}`); + await ref.set({ foo: 'bar' }); + try { + await db.clearPersistence(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.code.should.equal('firestore/failed-precondition'); + } + const doc = await ref.get({ source: 'cache' }); + should(doc.id).equal(id); + await db.terminate(); + await db.clearPersistence(); + try { + await ref.get({ source: 'cache' }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.code.should.equal('firestore/unavailable'); + return Promise.resolve(); + } + }); + }); - const ref1 = await docRef.collection(COLLECTION_GROUP).add({ number: 1 }); - const startAt = await docRef.collection(COLLECTION_GROUP).add({ number: 2 }); - const ref3 = await docRef.collection(COLLECTION_GROUP).add({ number: 3 }); + describe('wait for pending writes', function () { + xit('waits for pending writes', async function () { + const waitForPromiseMs = 500; + const testTimeoutMs = 10000; + + await firebase.firestore().disableNetwork(); + + //set up a pending write + + const db = firebase.firestore(); + const id = 'foobar'; + const ref = db.doc(`v6/${id}`); + ref.set({ foo: 'bar' }); + + //waitForPendingWrites should never resolve, but unfortunately we can only + //test that this is not returning within X ms + + let rejected = false; + const timedOutWithNetworkDisabled = await Promise.race([ + firebase + .firestore() + .waitForPendingWrites() + .then( + () => false, + () => { + rejected = true; + }, + ), + Utils.sleep(waitForPromiseMs).then(() => true), + ]); + + should(timedOutWithNetworkDisabled).equal(true); + should(rejected).equal(false); + + //if we sign in as a different user then it should reject the promise + try { + await firebase.auth().signOut(); + } catch (e) {} + await firebase.auth().signInAnonymously(); + should(rejected).equal(true); + + //now if we enable the network then waitForPendingWrites should return immediately + await firebase.firestore().enableNetwork(); + + const timedOutWithNetworkEnabled = await Promise.race([ + firebase + .firestore() + .waitForPendingWrites() + .then(() => false), + Utils.sleep(testTimeoutMs).then(() => true), + ]); + + should(timedOutWithNetworkEnabled).equal(false); + }); + }); - const ds = await startAt.get(); + describe('settings', function () { + describe('serverTimestampBehavior', function () { + it("handles 'estimate'", async function () { + firebase.firestore().settings({ serverTimestampBehavior: 'estimate' }); + const ref = firebase.firestore().doc(`${COLLECTION}/serverTimestampEstimate`); + + const promise = new Promise((resolve, reject) => { + const subscription = ref.onSnapshot(snapshot => { + try { + should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); + subscription(); + resolve(); + } catch (e) { + reject(e); + } + }, reject); + }); - const querySnapshot = await firebase - .firestore() - .collectionGroup(COLLECTION_GROUP) - .orderBy('number') - .startAt(ds) - .get(); + await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await promise; + await ref.delete(); + }); + it("handles 'previous'", async function () { + firebase.firestore().settings({ serverTimestampBehavior: 'previous' }); + const ref = firebase.firestore().doc(`${COLLECTION}/serverTimestampPrevious`); + + const promise = new Promise((resolve, reject) => { + let counter = 0; + let previous = null; + const subscription = ref.onSnapshot(snapshot => { + try { + switch (counter++) { + case 0: + should(snapshot.get('timestamp')).equal(null); + break; + case 1: + should(snapshot.get('timestamp')).be.an.instanceOf( + firebase.firestore.Timestamp, + ); + break; + case 2: + should(snapshot.get('timestamp')).be.an.instanceOf( + firebase.firestore.Timestamp, + ); + should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal( + true, + ); + break; + case 3: + should(snapshot.get('timestamp')).be.an.instanceOf( + firebase.firestore.Timestamp, + ); + should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal( + false, + ); + subscription(); + resolve(); + break; + } + } catch (e) { + reject(e); + } + previous = snapshot; + }, reject); + }); + + await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await new Promise(resolve => setTimeout(resolve, 100)); + await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await promise; + await ref.delete(); + }); + it("handles 'none'", async function () { + firebase.firestore().settings({ serverTimestampBehavior: 'none' }); + const ref = firebase.firestore().doc(`${COLLECTION}/serverTimestampNone`); + + const promise = new Promise((resolve, reject) => { + let counter = 0; + const subscription = ref.onSnapshot(snapshot => { + try { + switch (counter++) { + case 0: + // The initial callback snapshot should have no value for the timestamp, it has not been set at all + should(snapshot.get('timestamp')).equal(null); + break; + case 1: + should(snapshot.get('timestamp')).be.an.instanceOf( + firebase.firestore.Timestamp, + ); + subscription(); + resolve(); + break; + default: + // there should only be initial callback and set callback, any other callbacks are a fail + reject(new Error('too many callbacks')); + } + } catch (e) { + reject(e); + } + }, reject); + }); - querySnapshot.size.should.eql(2); - querySnapshot.forEach((d, i) => { - d.data().number.should.eql(i + 2); + await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await promise; + await ref.delete(); + }); }); - await Promise.all([ref1.delete(), ref3.delete(), startAt.delete()]); }); }); - describe('disableNetwork() & enableNetwork()', function () { - it('disables and enables with no errors', async function () { - await firebase.firestore().disableNetwork(); - await firebase.firestore().enableNetwork(); + describe('modular', function () { + describe('getFirestore', function () { + // removing as pending if module.options.hasMultiAppSupport = true + it('supports multiple apps', async function () { + const { getFirestore } = firestoreModular; + const db1 = await getFirestore(); + const db2 = await getFirestore(firebase.app('secondaryFromNative')); + + db1.app.name.should.equal('[DEFAULT]'); + db2.app.name.should.equal('secondaryFromNative'); + }); }); - }); - describe('Clear cached data persistence', function () { - // NOTE: removed as it breaks emulator tests - xit('should clear any cached data', async function () { - const db = firebase.firestore(); - const id = 'foobar'; - const ref = db.doc(`${COLLECTION}/${id}`); - await ref.set({ foo: 'bar' }); - try { - await db.clearPersistence(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.code.should.equal('firestore/failed-precondition'); - } - const doc = await ref.get({ source: 'cache' }); - should(doc.id).equal(id); - await db.terminate(); - await db.clearPersistence(); - try { - await ref.get({ source: 'cache' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.code.should.equal('firestore/unavailable'); - return Promise.resolve(); - } + /* + FIXME: these empty describe blocks exist for v8 as well. They should be removed. + These are tested in separate tests. + */ + describe('batch()', function () {}); + + describe('clearPersistence()', function () {}); + + describe('collection()', function () {}); + + describe('collectionGroup()', function () { + it('performs a collection group query', async function () { + const { + getFirestore, + setDoc, + doc, + collection, + collectionGroup, + where, + getDocs, + query, + deleteDoc, + } = firestoreModular; + const db = getFirestore(); + + const docRef1 = doc(db, `${COLLECTION}/collectionGroup1`); + const docRef2 = doc(db, `${COLLECTION}/collectionGroup2`); + const docRef3 = doc(db, `${COLLECTION}/collectionGroup3`); + const subRef1 = doc(collection(docRef1, COLLECTION_GROUP), 'ref'); + const subRef2 = doc(collection(docRef1, COLLECTION_GROUP), 'ref2'); + const subRef3 = doc(collection(docRef2, COLLECTION_GROUP), 'ref'); + const subRef4 = doc(collection(docRef2, COLLECTION_GROUP), 'ref2'); + const subRef5 = doc(collection(docRef3, COLLECTION_GROUP), 'ref'); + const subRef6 = doc(collection(docRef3, COLLECTION_GROUP), 'ref2'); + + await Promise.all([ + setDoc(subRef1, { value: 1 }), + setDoc(subRef2, { value: 2 }), + + setDoc(subRef3, { value: 1 }), + setDoc(subRef4, { value: 2 }), + + setDoc(subRef5, { value: 1 }), + setDoc(subRef6, { value: 2 }), + ]); + + const querySnapshot = await getDocs( + query(collectionGroup(db, COLLECTION_GROUP), where('value', '==', 2)), + ); + + querySnapshot.forEach(ds => { + ds.data().value.should.eql(2); + }); + + querySnapshot.size.should.eql(3); + + await Promise.all([ + deleteDoc(subRef1), + deleteDoc(subRef2), + + deleteDoc(subRef3), + deleteDoc(subRef4), + + deleteDoc(subRef5), + deleteDoc(subRef6), + ]); + }); + + it('performs a collection group query with cursor queries', async function () { + const { + getFirestore, + doc, + collection, + collectionGroup, + addDoc, + getDocs, + query, + orderBy, + startAt, + deleteDoc, + } = firestoreModular; + + const db = getFirestore(); + const docRef = doc(db, `${COLLECTION}/collectionGroupCursor`); + + const ref1 = await addDoc(collection(docRef, COLLECTION_GROUP), { number: 1 }); + const startAtRef = await addDoc(collection(docRef, COLLECTION_GROUP), { number: 2 }); + const ref3 = await addDoc(collection(docRef, COLLECTION_GROUP), { number: 3 }); + + const ds = await getDocs(startAtRef); + + const querySnapshot = await getDocs( + query(collectionGroup(db, COLLECTION_GROUP), orderBy('number'), startAt(ds)), + ); + + querySnapshot.size.should.eql(2); + querySnapshot.forEach((d, i) => { + d.data().number.should.eql(i + 2); + }); + await Promise.all([deleteDoc(ref1), deleteDoc(ref3), deleteDoc(startAtRef)]); + }); }); - }); - describe('wait for pending writes', function () { - xit('waits for pending writes', async function () { - const waitForPromiseMs = 500; - const testTimeoutMs = 10000; + describe('disableNetwork() & enableNetwork()', function () { + it('disables and enables with no errors', async function () { + const { getFirestore, disableNetwork, enableNetwork } = firestoreModular; + const db = getFirestore(); - await firebase.firestore().disableNetwork(); + await disableNetwork(db); + await enableNetwork(db); + }); + }); - //set up a pending write + describe('Clear cached data persistence', function () { + // NOTE: removed as it breaks emulator tests + xit('should clear any cached data', async function () { + const { getFirestore, doc, setDoc, getDocsFromCache, terminate, clearPersistence } = + firestoreModular; + + const db = getFirestore(); + const id = 'foobar'; + const ref = doc(db, `${COLLECTION}/${id}`); + await setDoc(ref, { foo: 'bar' }); + try { + await clearPersistence(db); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.code.should.equal('firestore/failed-precondition'); + } + const docRef = await getDocsFromCache(ref); + should(docRef.id).equal(id); + await terminate(db); + await clearPersistence(db); + try { + await getDocsFromCache(ref); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.code.should.equal('firestore/unavailable'); + return Promise.resolve(); + } + }); + }); - const db = firebase.firestore(); - const id = 'foobar'; - const ref = db.doc(`v6/${id}`); - ref.set({ foo: 'bar' }); + describe('wait for pending writes', function () { + xit('waits for pending writes', async function () { + const { getFirestore, disableNetwork, doc, setDoc, waitForPendingWrites, enableNetwork } = + firestoreModular; - //waitForPendingWrites should never resolve, but unfortunately we can only - //test that this is not returning within X ms + const waitForPromiseMs = 500; + const testTimeoutMs = 10000; + const db = getFirestore(); - let rejected = false; - const timedOutWithNetworkDisabled = await Promise.race([ - firebase - .firestore() - .waitForPendingWrites() - .then( + await disableNetwork(db); + + //set up a pending write + + const id = 'foobar'; + const ref = doc(db, `v6/${id}`); + setDoc(ref, { foo: 'bar' }); + + //waitForPendingWrites should never resolve, but unfortunately we can only + //test that this is not returning within X ms + + let rejected = false; + const timedOutWithNetworkDisabled = await Promise.race([ + waitForPendingWrites(db).then( () => false, () => { rejected = true; }, ), - Utils.sleep(waitForPromiseMs).then(() => true), - ]); + Utils.sleep(waitForPromiseMs).then(() => true), + ]); - should(timedOutWithNetworkDisabled).equal(true); - should(rejected).equal(false); + should(timedOutWithNetworkDisabled).equal(true); + should(rejected).equal(false); - //if we sign in as a different user then it should reject the promise - try { - await firebase.auth().signOut(); - } catch (e) {} - await firebase.auth().signInAnonymously(); - should(rejected).equal(true); + //if we sign in as a different user then it should reject the promise + try { + await firebase.auth().signOut(); + } catch (e) {} + await firebase.auth().signInAnonymously(); + should(rejected).equal(true); - //now if we enable the network then waitForPendingWrites should return immediately - await firebase.firestore().enableNetwork(); + //now if we enable the network then waitForPendingWrites should return immediately + await enableNetwork(db); - const timedOutWithNetworkEnabled = await Promise.race([ - firebase - .firestore() - .waitForPendingWrites() - .then(() => false), - Utils.sleep(testTimeoutMs).then(() => true), - ]); + const timedOutWithNetworkEnabled = await Promise.race([ + waitForPendingWrites(db).then(() => false), + Utils.sleep(testTimeoutMs).then(() => true), + ]); - should(timedOutWithNetworkEnabled).equal(false); + should(timedOutWithNetworkEnabled).equal(false); + }); }); - }); - describe('settings', function () { - describe('serverTimestampBehavior', function () { - it("handles 'estimate'", async function () { - firebase.firestore().settings({ serverTimestampBehavior: 'estimate' }); - const ref = firebase.firestore().doc(`${COLLECTION}/serverTimestampEstimate`); - - const promise = new Promise((resolve, reject) => { - const subscription = ref.onSnapshot(snapshot => { - try { - should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); - subscription(); - resolve(); - } catch (e) { - reject(e); - } - }, reject); - }); - - await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); - await promise; - await ref.delete(); - }); - it("handles 'previous'", async function () { - firebase.firestore().settings({ serverTimestampBehavior: 'previous' }); - const ref = firebase.firestore().doc(`${COLLECTION}/serverTimestampPrevious`); - - const promise = new Promise((resolve, reject) => { - let counter = 0; - let previous = null; - const subscription = ref.onSnapshot(snapshot => { - try { - switch (counter++) { - case 0: - should(snapshot.get('timestamp')).equal(null); - break; - case 1: - should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); - break; - case 2: + describe('settings', function () { + describe('serverTimestampBehavior', function () { + it("handles 'estimate'", async function () { + const { initializeFirestore, doc, onSnapshot, setDoc, deleteDoc } = firestoreModular; + + const db = await initializeFirestore(firebase.app(), { + serverTimestampBehavior: 'estimate', + }); + const ref = doc(db, `${COLLECTION}/serverTimestampEstimate`); + + const promise = new Promise((resolve, reject) => { + const subscription = onSnapshot( + ref, + snapshot => { + try { should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); - should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal(true); - break; - case 3: - should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); - should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal(false); subscription(); resolve(); - break; - } - } catch (e) { - reject(e); - } - previous = snapshot; - }, reject); + } catch (e) { + reject(e); + } + }, + reject, + ); + }); + + await setDoc(ref, { timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await promise; + await deleteDoc(ref); }); - await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); - await new Promise(resolve => setTimeout(resolve, 100)); - await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); - await promise; - await ref.delete(); - }); - it("handles 'none'", async function () { - firebase.firestore().settings({ serverTimestampBehavior: 'none' }); - const ref = firebase.firestore().doc(`${COLLECTION}/serverTimestampNone`); - - const promise = new Promise((resolve, reject) => { - let counter = 0; - const subscription = ref.onSnapshot(snapshot => { - try { - switch (counter++) { - case 0: - // The initial callback snapshot should have no value for the timestamp, it has not been set at all - should(snapshot.get('timestamp')).equal(null); - break; - case 1: - should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); - subscription(); - resolve(); - break; - default: - // there should only be initial callback and set callback, any other callbacks are a fail - reject(new Error('too many callbacks')); - } - } catch (e) { - reject(e); - } - }, reject); + // FIXME: works in isolation but not in suite + xit("handles 'previous'", async function () { + const { initializeFirestore, doc, onSnapshot, setDoc, deleteDoc } = firestoreModular; + + const db = await initializeFirestore(firebase.app(), { + serverTimestampBehavior: 'previous', + }); + const ref = doc(db, `${COLLECTION}/serverTimestampPrevious`); + + const promise = new Promise((resolve, reject) => { + let counter = 0; + let previous = null; + const subscription = onSnapshot( + ref, + snapshot => { + try { + switch (counter++) { + case 0: + should(snapshot.get('timestamp')).equal(null); + break; + case 1: + should(snapshot.get('timestamp')).be.an.instanceOf( + firebase.firestore.Timestamp, + ); + break; + case 2: + should(snapshot.get('timestamp')).be.an.instanceOf( + firebase.firestore.Timestamp, + ); + should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal( + true, + ); + break; + case 3: + should(snapshot.get('timestamp')).be.an.instanceOf( + firebase.firestore.Timestamp, + ); + should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal( + false, + ); + subscription(); + resolve(); + break; + } + } catch (e) { + reject(e); + } + previous = snapshot; + }, + reject, + ); + }); + + await setDoc(ref, { timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await new Promise(resolve => setTimeout(resolve, 100)); + await setDoc(ref, { timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await promise; + await deleteDoc(ref); }); - await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); - await promise; - await ref.delete(); + // FIXME: works in isolation but not in suite + xit("handles 'none'", async function () { + const { initializeFirestore, doc, onSnapshot, setDoc, deleteDoc } = firestoreModular; + + const db = await initializeFirestore(firebase.app(), { + serverTimestampBehavior: 'none', + }); + const ref = doc(db, `${COLLECTION}/serverTimestampNone`); + + const promise = new Promise((resolve, reject) => { + let counter = 0; + const subscription = onSnapshot( + ref, + snapshot => { + try { + switch (counter++) { + case 0: + // The initial callback snapshot should have no value for the timestamp, it has not been set at all + should(snapshot.get('timestamp')).equal(null); + break; + case 1: + should(snapshot.get('timestamp')).be.an.instanceOf( + firebase.firestore.Timestamp, + ); + subscription(); + resolve(); + break; + default: + // there should only be initial callback and set callback, any other callbacks are a fail + reject(new Error('too many callbacks')); + } + } catch (e) { + reject(e); + } + }, + reject, + ); + }); + + await setDoc(ref, { timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await promise; + await deleteDoc(ref); + }); }); }); }); diff --git a/packages/firestore/e2e/issues.e2e.js b/packages/firestore/e2e/issues.e2e.js index a5f5982cbe..1d36e0371f 100644 --- a/packages/firestore/e2e/issues.e2e.js +++ b/packages/firestore/e2e/issues.e2e.js @@ -20,6 +20,9 @@ const { getE2eEmulatorHost } = require('@react-native-firebase/app/e2e/helpers') const jsFirebase = require('firebase/compat/app'); require('firebase/compat/firestore'); +const jsFirebaseModular = require('firebase/app'); +const jsFirestoreModular = require('firebase/firestore'); + const testNumbers = { zero: 0, // int negativeZero: -0, // double @@ -34,125 +37,302 @@ const testNumbers = { }; describe('firestore()', function () { - describe('issues', function () { - before(async function () { - await Promise.all([ - firebase.firestore().doc(`${COLLECTION}/wbXwyLJheRfYXXWlY46j`).set({ index: 2, number: 2 }), - firebase.firestore().doc(`${COLLECTION}/kGC5cYPN1nKnZCcAb9oQ`).set({ index: 6, number: 2 }), - firebase.firestore().doc(`${COLLECTION}/8Ek8iWCDQPPJ5s2n8PiQ`).set({ index: 4, number: 2 }), - firebase.firestore().doc(`${COLLECTION}/mr7MdAygvuheF6AUtWma`).set({ index: 1, number: 1 }), - firebase.firestore().doc(`${COLLECTION}/RCO5SvNn4fdoE49OKrIV`).set({ index: 3, number: 1 }), - firebase.firestore().doc(`${COLLECTION}/CvVG7VP1hXTtcfdUaeNl`).set({ index: 5, number: 1 }), - ]); - }); + describe('v8 compatibility', function () { + describe('issues', function () { + before(async function () { + await Promise.all([ + firebase + .firestore() + .doc(`${COLLECTION}/wbXwyLJheRfYXXWlY46j`) + .set({ index: 2, number: 2 }), + firebase + .firestore() + .doc(`${COLLECTION}/kGC5cYPN1nKnZCcAb9oQ`) + .set({ index: 6, number: 2 }), + firebase + .firestore() + .doc(`${COLLECTION}/8Ek8iWCDQPPJ5s2n8PiQ`) + .set({ index: 4, number: 2 }), + firebase + .firestore() + .doc(`${COLLECTION}/mr7MdAygvuheF6AUtWma`) + .set({ index: 1, number: 1 }), + firebase + .firestore() + .doc(`${COLLECTION}/RCO5SvNn4fdoE49OKrIV`) + .set({ index: 3, number: 1 }), + firebase + .firestore() + .doc(`${COLLECTION}/CvVG7VP1hXTtcfdUaeNl`) + .set({ index: 5, number: 1 }), + ]); + }); - it('returns all results', async function () { - const db = firebase.firestore(); - const ref = db.collection(COLLECTION).orderBy('number', 'desc'); - const allResultsSnapshot = await ref.get(); - allResultsSnapshot.forEach((doc, i) => { - if (i === 0) { - doc.id.should.equal('wbXwyLJheRfYXXWlY46j'); - } - if (i === 1) { - doc.id.should.equal('kGC5cYPN1nKnZCcAb9oQ'); - } - if (i === 2) { - doc.id.should.equal('8Ek8iWCDQPPJ5s2n8PiQ'); - } - if (i === 3) { - doc.id.should.equal('mr7MdAygvuheF6AUtWma'); - } - if (i === 4) { - doc.id.should.equal('RCO5SvNn4fdoE49OKrIV'); - } - if (i === 5) { - doc.id.should.equal('CvVG7VP1hXTtcfdUaeNl'); - } + it('returns all results', async function () { + const db = firebase.firestore(); + const ref = db.collection(COLLECTION).orderBy('number', 'desc'); + const allResultsSnapshot = await ref.get(); + allResultsSnapshot.forEach((doc, i) => { + if (i === 0) { + doc.id.should.equal('wbXwyLJheRfYXXWlY46j'); + } + if (i === 1) { + doc.id.should.equal('kGC5cYPN1nKnZCcAb9oQ'); + } + if (i === 2) { + doc.id.should.equal('8Ek8iWCDQPPJ5s2n8PiQ'); + } + if (i === 3) { + doc.id.should.equal('mr7MdAygvuheF6AUtWma'); + } + if (i === 4) { + doc.id.should.equal('RCO5SvNn4fdoE49OKrIV'); + } + if (i === 5) { + doc.id.should.equal('CvVG7VP1hXTtcfdUaeNl'); + } + }); }); - }); - it('returns first page', async function () { - const db = firebase.firestore(); - const ref = db.collection(COLLECTION).orderBy('number', 'desc'); - const firstPageSnapshot = await ref.limit(2).get(); - should.equal(firstPageSnapshot.docs.length, 2); - firstPageSnapshot.forEach((doc, i) => { - if (i === 0) { - doc.id.should.equal('wbXwyLJheRfYXXWlY46j'); - } - if (i === 1) { - doc.id.should.equal('kGC5cYPN1nKnZCcAb9oQ'); - } + it('returns first page', async function () { + const db = firebase.firestore(); + const ref = db.collection(COLLECTION).orderBy('number', 'desc'); + const firstPageSnapshot = await ref.limit(2).get(); + should.equal(firstPageSnapshot.docs.length, 2); + firstPageSnapshot.forEach((doc, i) => { + if (i === 0) { + doc.id.should.equal('wbXwyLJheRfYXXWlY46j'); + } + if (i === 1) { + doc.id.should.equal('kGC5cYPN1nKnZCcAb9oQ'); + } + }); + }); + + it('returns second page', async function () { + const db = firebase.firestore(); + const ref = db.collection(COLLECTION).orderBy('number', 'desc'); + const firstPageSnapshot = await ref.limit(2).get(); + let lastDocument; + firstPageSnapshot.forEach(doc => { + lastDocument = doc; + }); + + const secondPageSnapshot = await ref.startAfter(lastDocument).limit(2).get(); + should.equal(secondPageSnapshot.docs.length, 2); + secondPageSnapshot.forEach((doc, i) => { + if (i === 0) { + doc.id.should.equal('8Ek8iWCDQPPJ5s2n8PiQ'); + } + if (i === 1) { + doc.id.should.equal('mr7MdAygvuheF6AUtWma'); + } + }); }); }); - it('returns second page', async function () { - const db = firebase.firestore(); - const ref = db.collection(COLLECTION).orderBy('number', 'desc'); - const firstPageSnapshot = await ref.limit(2).get(); - let lastDocument; - firstPageSnapshot.forEach(doc => { - lastDocument = doc; + describe('number type consistency', function () { + before(async function () { + jsFirebase.initializeApp(FirebaseHelpers.app.config()); + jsFirebase.firestore().useEmulator(getE2eEmulatorHost(), 8080); + + // Put one example of each number in our collection using JS SDK + await Promise.all( + Object.entries(testNumbers).map(([testName, testValue]) => { + return jsFirebase + .firestore() + .doc(`${COLLECTION}/numberTestsJS/cases/${testName}`) + .set({ number: testValue }); + }), + ); + + // Put one example of each number in our collection using Native SDK + await Promise.all( + Object.entries(testNumbers).map(([testName, testValue]) => { + return firebase + .firestore() + .doc(`${COLLECTION}/numberTestsNative/cases/${testName}`) + .set({ number: testValue }); + }), + ); }); - const secondPageSnapshot = await ref.startAfter(lastDocument).limit(2).get(); - should.equal(secondPageSnapshot.docs.length, 2); - secondPageSnapshot.forEach((doc, i) => { - if (i === 0) { - doc.id.should.equal('8Ek8iWCDQPPJ5s2n8PiQ'); - } - if (i === 1) { - doc.id.should.equal('mr7MdAygvuheF6AUtWma'); - } + it('types inserted by JS may be queried by native with filters', async function () { + const testValues = Object.values(testNumbers); + const ref = firebase + .firestore() + .collection(`${COLLECTION}/numberTestsJS/cases`) + .where('number', 'in', testValues); + typesSnap = await ref.get(); + should.deepEqual(typesSnap.docs.map(d => d.id).sort(), Object.keys(testNumbers).sort()); + }); + + it('types inserted by native may be queried by JS with filters', async function () { + const testValues = Object.values(testNumbers); + const ref = jsFirebase + .firestore() + .collection(`${COLLECTION}/numberTestsNative/cases`) + .where('number', 'in', testValues); + typesSnap = await ref.get(); + should.deepEqual(typesSnap.docs.map(d => d.id).sort(), Object.keys(testNumbers).sort()); }); }); }); - describe('number type consistency', function () { - before(async function () { - jsFirebase.initializeApp(FirebaseHelpers.app.config()); - jsFirebase.firestore().useEmulator(getE2eEmulatorHost(), 8080); + describe('modular', function () { + describe('issues', function () { + before(async function () { + const { getFirestore, doc, setDoc } = firestoreModular; + const db = getFirestore(); - // Put one example of each number in our collection using JS SDK - await Promise.all( - Object.entries(testNumbers).map(([testName, testValue]) => { - return jsFirebase - .firestore() - .doc(`${COLLECTION}/numberTestsJS/cases/${testName}`) - .set({ number: testValue }); - }), - ); - - // Put one example of each number in our collection using Native SDK - await Promise.all( - Object.entries(testNumbers).map(([testName, testValue]) => { - return firebase - .firestore() - .doc(`${COLLECTION}/numberTestsNative/cases/${testName}`) - .set({ number: testValue }); - }), - ); - }); + await Promise.all([ + setDoc(doc(db, `${COLLECTION}/wbXwyLJheRfYXXWlY46j`), { index: 2, number: 2 }), + setDoc(doc(db, `${COLLECTION}/kGC5cYPN1nKnZCcAb9oQ`), { index: 6, number: 2 }), + setDoc(doc(db, `${COLLECTION}/8Ek8iWCDQPPJ5s2n8PiQ`), { index: 4, number: 2 }), + setDoc(doc(db, `${COLLECTION}/mr7MdAygvuheF6AUtWma`), { index: 1, number: 1 }), + setDoc(doc(db, `${COLLECTION}/RCO5SvNn4fdoE49OKrIV`), { index: 3, number: 1 }), + setDoc(doc(db, `${COLLECTION}/CvVG7VP1hXTtcfdUaeNl`), { index: 5, number: 1 }), + ]); + }); + + it('returns all results', async function () { + const { getFirestore, collection, query, orderBy, getDocs } = firestoreModular; + const db = getFirestore(); + + const ref = query(collection(db, COLLECTION), orderBy('number', 'desc')); + const allResultsSnapshot = await getDocs(ref); + allResultsSnapshot.forEach((doc, i) => { + if (i === 0) { + doc.id.should.equal('wbXwyLJheRfYXXWlY46j'); + } + if (i === 1) { + doc.id.should.equal('kGC5cYPN1nKnZCcAb9oQ'); + } + if (i === 2) { + doc.id.should.equal('8Ek8iWCDQPPJ5s2n8PiQ'); + } + if (i === 3) { + doc.id.should.equal('mr7MdAygvuheF6AUtWma'); + } + if (i === 4) { + doc.id.should.equal('RCO5SvNn4fdoE49OKrIV'); + } + if (i === 5) { + doc.id.should.equal('CvVG7VP1hXTtcfdUaeNl'); + } + }); + }); + + it('returns first page', async function () { + const { getFirestore, collection, query, orderBy, limit, getDocs } = firestoreModular; + const db = getFirestore(); + + const ref = query(collection(db, COLLECTION), orderBy('number', 'desc')); + const firstPageSnapshot = await getDocs(query(ref, limit(2))); + should.equal(firstPageSnapshot.docs.length, 2); + firstPageSnapshot.forEach((doc, i) => { + if (i === 0) { + doc.id.should.equal('wbXwyLJheRfYXXWlY46j'); + } + if (i === 1) { + doc.id.should.equal('kGC5cYPN1nKnZCcAb9oQ'); + } + }); + }); + + it('returns second page', async function () { + const { getFirestore, collection, query, orderBy, limit, startAfter, getDocs } = + firestoreModular; + const db = getFirestore(); + + const ref = query(collection(db, COLLECTION), orderBy('number', 'desc')); + const firstPageSnapshot = await getDocs(query(ref, limit(2))); + let lastDocument; + firstPageSnapshot.forEach(doc => { + lastDocument = doc; + }); - it('types inserted by JS may be queried by native with filters', async function () { - const testValues = Object.values(testNumbers); - const ref = firebase - .firestore() - .collection(`${COLLECTION}/numberTestsJS/cases`) - .where('number', 'in', testValues); - typesSnap = await ref.get(); - should.deepEqual(typesSnap.docs.map(d => d.id).sort(), Object.keys(testNumbers).sort()); + const secondPageSnapshot = await getDocs(query(ref, startAfter(lastDocument), limit(2))); + should.equal(secondPageSnapshot.docs.length, 2); + secondPageSnapshot.forEach((doc, i) => { + if (i === 0) { + doc.id.should.equal('8Ek8iWCDQPPJ5s2n8PiQ'); + } + if (i === 1) { + doc.id.should.equal('mr7MdAygvuheF6AUtWma'); + } + }); + }); }); - it('types inserted by native may be queried by JS with filters', async function () { - const testValues = Object.values(testNumbers); - const ref = jsFirebase - .firestore() - .collection(`${COLLECTION}/numberTestsNative/cases`) - .where('number', 'in', testValues); - typesSnap = await ref.get(); - should.deepEqual(typesSnap.docs.map(d => d.id).sort(), Object.keys(testNumbers).sort()); + describe('number type consistency', function () { + before(async function () { + // FIXME: + // This only throws an error in the suite since this is already initialized in the v8 tests above. + // It throws the following error: + // + // FirebaseError: Firestore has already been started and its settings can no longer be changed. + // You can only modify settings before calling any other methods on a Firestore object. + try { + jsFirebaseModular.initializeApp(FirebaseHelpers.app.config()); + jsFirestoreModular.connectFirestoreEmulator( + jsFirestoreModular.getFirestore(), + getE2eEmulatorHost(), + 8080, + ); + } catch (e) {} + + // Put one example of each number in our collection using JS SDK + await Promise.all( + Object.entries(testNumbers).map(([testName, testValue]) => { + return jsFirestoreModular.setDoc( + jsFirestoreModular.doc( + jsFirestoreModular.getFirestore(), + `${COLLECTION}/numberTestsJS/cases/${testName}`, + ), + { number: testValue }, + ); + }), + ); + + const { getFirestore, doc, setDoc } = firestoreModular; + + // Put one example of each number in our collection using Native SDK + await Promise.all( + Object.entries(testNumbers).map(([testName, testValue]) => { + return setDoc( + doc(getFirestore(), `${COLLECTION}/numberTestsNative/cases/${testName}`), + { + number: testValue, + }, + ); + }), + ); + }); + + it('types inserted by JS may be queried by native with filters', async function () { + const { getFirestore, collection, query, where, getDocs } = firestoreModular; + const testValues = Object.values(testNumbers); + const ref = query( + collection(getFirestore(), `${COLLECTION}/numberTestsJS/cases`), + where('number', 'in', testValues), + ); + const typesSnap = await getDocs(ref); + should.deepEqual(typesSnap.docs.map(d => d.id).sort(), Object.keys(testNumbers).sort()); + }); + + it('types inserted by native may be queried by JS with filters', async function () { + const testValues = Object.values(testNumbers); + const ref = jsFirestoreModular.query( + jsFirestoreModular.collection( + jsFirestoreModular.getFirestore(), + `${COLLECTION}/numberTestsNative/cases`, + ), + jsFirestoreModular.where('number', 'in', testValues), + ); + typesSnap = await jsFirestoreModular.getDocs(ref); + should.deepEqual(typesSnap.docs.map(d => d.id).sort(), Object.keys(testNumbers).sort()); + }); }); }); }); diff --git a/packages/firestore/lib/index.js b/packages/firestore/lib/index.js index 7b7e8167ff..8c19b95b4f 100644 --- a/packages/firestore/lib/index.js +++ b/packages/firestore/lib/index.js @@ -359,6 +359,8 @@ class FirebaseFirestoreModule extends FirebaseModule { // import { SDK_VERSION } from '@react-native-firebase/firestore'; export const SDK_VERSION = version; +export * from './modular'; + // import firestore from '@react-native-firebase/firestore'; // firestore().X(...); export default createModuleNamespace({ diff --git a/packages/firestore/lib/modular/Bytes.d.ts b/packages/firestore/lib/modular/Bytes.d.ts new file mode 100644 index 0000000000..1bda348f9a --- /dev/null +++ b/packages/firestore/lib/modular/Bytes.d.ts @@ -0,0 +1,11 @@ +export declare class Bytes { + static fromBase64String(base64: string): Bytes; + + static fromUint8Array(array: Uint8Array): Bytes; + + toBase64(): string; + + toUint8Array(): Uint8Array; + + isEqual(other: Bytes): boolean; +} diff --git a/packages/firestore/lib/modular/Bytes.js b/packages/firestore/lib/modular/Bytes.js new file mode 100644 index 0000000000..f1f82bb436 --- /dev/null +++ b/packages/firestore/lib/modular/Bytes.js @@ -0,0 +1,59 @@ +import { firebase } from '../index'; + +/** + * An immutable object representing an array of bytes. + */ +export class Bytes { + /** + * @hideconstructor + * @param {firebase.firestore.Blob} blob + */ + constructor(blob) { + this._blob = blob; + } + + /** + * @param {string} base64 + * @returns {Bytes} + */ + static fromBase64String(base64) { + return new Bytes(firebase.firestore.Blob.fromBase64String(base64)); + } + + /** + * @param {Uint8Array} array + * @returns {Bytes} + */ + static fromUint8Array(array) { + return new Bytes(firebase.firestore.Blob.fromUint8Array(array)); + } + + /** + * @returns {string} + */ + toBase64() { + return this._blob.toBase64(); + } + + /** + * @returns {Uint8Array} + */ + toUint8Array() { + return this._blob.toUint8Array(); + } + + /** + * @returns {string} + */ + toString() { + return 'Bytes(base64: ' + this.toBase64() + ')'; + } + + /** + * @param {Bytes} other + * @returns {boolean} + */ + isEqual(other) { + return this._blob.isEqual(other._blob); + } +} diff --git a/packages/firestore/lib/modular/FieldPath.d.ts b/packages/firestore/lib/modular/FieldPath.d.ts new file mode 100644 index 0000000000..f00fe5d5e3 --- /dev/null +++ b/packages/firestore/lib/modular/FieldPath.d.ts @@ -0,0 +1,13 @@ +/** + * A `FieldPath` refers to a field in a document. The path may consist of a + * single field name (referring to a top-level field in the document), or a + * list of field names (referring to a nested field in the document). + * + * Create a `FieldPath` by providing field names. If more than one field + * name is provided, the path will point to a nested field in a document. + */ +export declare class FieldPath { + constructor(...fieldNames: string[]); + + isEqual(other: FieldPath): boolean; +} diff --git a/packages/firestore/lib/modular/FieldPath.js b/packages/firestore/lib/modular/FieldPath.js new file mode 100644 index 0000000000..93792b6454 --- /dev/null +++ b/packages/firestore/lib/modular/FieldPath.js @@ -0,0 +1,3 @@ +import FirestoreFieldPath from '../FirestoreFieldPath'; + +export const FieldPath = FirestoreFieldPath; diff --git a/packages/firestore/lib/modular/FieldValue.d.ts b/packages/firestore/lib/modular/FieldValue.d.ts new file mode 100644 index 0000000000..bd560e6a4d --- /dev/null +++ b/packages/firestore/lib/modular/FieldValue.d.ts @@ -0,0 +1,67 @@ +/** + * Sentinel values that can be used when writing document fields with `set()` + * or `update()`. + */ +export declare class FieldValue { + isEqual(other: FieldValue): boolean; +} + +/** + * Returns a sentinel for use with {@link @firebase/firestore#(updateDoc:1)} or + * {@link @firebase/firestore/lite#(setDoc:1)} with `{merge: true}` to mark a field for deletion. + */ +export function deleteField(): FieldValue; + +/** + * Returns a sentinel used with {@link @firebase/firestore#(setDoc:1)} or {@link @firebase/firestore/lite#(updateDoc:1)} to + * include a server-generated timestamp in the written data. + */ +export function serverTimestamp(): FieldValue; + +/** + * Returns a special value that can be used with {@link @firebase/firestore#(setDoc:1)} or {@link + * @firebase/firestore/lite#(updateDoc:1)} that tells the server to union the given elements with any array + * value that already exists on the server. Each specified element that doesn't + * already exist in the array will be added to the end. If the field being + * modified is not already an array it will be overwritten with an array + * containing exactly the specified elements. + * + * @param elements - The elements to union into the array. + * @returns The `FieldValue` sentinel for use in a call to `setDoc()` or + * `updateDoc()`. + */ +export function arrayUnion(...elements: unknown[]): FieldValue; + +/** + * Returns a special value that can be used with {@link (setDoc:1)} or {@link + * updateDoc:1} that tells the server to remove the given elements from any + * array value that already exists on the server. All instances of each element + * specified will be removed from the array. If the field being modified is not + * already an array it will be overwritten with an empty array. + * + * @param elements - The elements to remove from the array. + * @returns The `FieldValue` sentinel for use in a call to `setDoc()` or + * `updateDoc()` + */ +export function arrayRemove(...elements: unknown[]): FieldValue; + +/** + * Returns a special value that can be used with {@link @firebase/firestore#(setDoc:1)} or {@link + * @firebase/firestore/lite#(updateDoc:1)} that tells the server to increment the field's current value by + * the given value. + * + * If either the operand or the current field value uses floating point + * precision, all arithmetic follows IEEE 754 semantics. If both values are + * integers, values outside of JavaScript's safe number range + * (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`) are also subject to + * precision loss. Furthermore, once processed by the Firestore backend, all + * integer operations are capped between -2^63 and 2^63-1. + * + * If the current field value is not of type `number`, or if the field does not + * yet exist, the transformation sets the field to the given value. + * + * @param n - The value to increment by. + * @returns The `FieldValue` sentinel for use in a call to `setDoc()` or + * `updateDoc()` + */ +export function increment(n: number): FieldValue; diff --git a/packages/firestore/lib/modular/FieldValue.js b/packages/firestore/lib/modular/FieldValue.js new file mode 100644 index 0000000000..ef43c349d5 --- /dev/null +++ b/packages/firestore/lib/modular/FieldValue.js @@ -0,0 +1,41 @@ +import FirestoreFieldValue from '../FirestoreFieldValue'; + +export const FieldValue = FirestoreFieldValue; + +/** + * @returns {FieldValue} + */ +export function deleteField() { + return FieldValue.delete(); +} + +/** + * @returns {FieldValue} + */ +export function serverTimestamp() { + return FieldValue.serverTimestamp(); +} + +/** + * @param {unknown} elements + * @returns {FieldValue} + */ +export function arrayUnion(...elements) { + return FieldValue.arrayUnion(...elements); +} + +/** + * @param {unknown} elements + * @returns {FieldValue} + */ +export function arrayRemove(...elements) { + return FieldValue.arrayRemove(...elements); +} + +/** + * @param {number} n + * @returns {FieldValue} + */ +export function increment(n) { + return FieldValue.increment(n); +} diff --git a/packages/firestore/lib/modular/GeoPoint.d.ts b/packages/firestore/lib/modular/GeoPoint.d.ts new file mode 100644 index 0000000000..b9b3ce4618 --- /dev/null +++ b/packages/firestore/lib/modular/GeoPoint.d.ts @@ -0,0 +1,17 @@ +/** + * An immutable object representing a geographic location in Firestore. The + * location is represented as latitude/longitude pair. + * + * Latitude values are in the range of [-90, 90]. + * Longitude values are in the range of [-180, 180]. + */ +export declare class GeoPoint { + readonly latitude: number; + readonly longitude: number; + + constructor(latitude: number, longitude: number); + + isEqual(other: GeoPoint): boolean; + + toJSON(): { latitude: number; longitude: number }; +} diff --git a/packages/firestore/lib/modular/GeoPoint.js b/packages/firestore/lib/modular/GeoPoint.js new file mode 100644 index 0000000000..7a527ecf7d --- /dev/null +++ b/packages/firestore/lib/modular/GeoPoint.js @@ -0,0 +1,3 @@ +import FirestoreGeoPoint from '../FirestoreGeoPoint'; + +export const GeoPoint = FirestoreGeoPoint; diff --git a/packages/firestore/lib/modular/Timestamp.d.ts b/packages/firestore/lib/modular/Timestamp.d.ts new file mode 100644 index 0000000000..3ff58ff154 --- /dev/null +++ b/packages/firestore/lib/modular/Timestamp.d.ts @@ -0,0 +1,85 @@ +/** + * A `Timestamp` represents a point in time independent of any time zone or + * calendar, represented as seconds and fractions of seconds at nanosecond + * resolution in UTC Epoch time. + * + * It is encoded using the Proleptic Gregorian Calendar which extends the + * Gregorian calendar backwards to year one. It is encoded assuming all minutes + * are 60 seconds long, i.e. leap seconds are "smeared" so that no leap second + * table is needed for interpretation. Range is from 0001-01-01T00:00:00Z to + * 9999-12-31T23:59:59.999999999Z. + * + * For examples and further specifications, refer to the + * {@link https://github.com/google/protobuf/blob/master/src/google/protobuf/timestamp.proto | Timestamp definition}. + */ +export declare class Timestamp { + readonly seconds: number; + readonly nanoseconds: number; + + constructor(seconds: number, nanoseconds: number); + + /** + * Creates a new timestamp with the current date, with millisecond precision. + * + * @returns a new timestamp representing the current date. + */ + static now(): Timestamp; + + /** + * Creates a new timestamp from the given date. + * + * @param date - The date to initialize the `Timestamp` from. + * @returns A new `Timestamp` representing the same point in time as the given + * date. + */ + static fromDate(date: Date): Timestamp; + + /** + * Creates a new timestamp from the given number of milliseconds. + * + * @param milliseconds - Number of milliseconds since Unix epoch + * 1970-01-01T00:00:00Z. + * @returns A new `Timestamp` representing the same point in time as the given + * number of milliseconds. + */ + static fromMillis(milliseconds: number): Timestamp; + + /** + * Converts a `Timestamp` to a JavaScript `Date` object. This conversion + * causes a loss of precision since `Date` objects only support millisecond + * precision. + * + * @returns JavaScript `Date` object representing the same point in time as + * this `Timestamp`, with millisecond precision. + */ + toDate(): Date; + + /** + * Converts a `Timestamp` to a numeric timestamp (in milliseconds since + * epoch). This operation causes a loss of precision. + * + * @returns The point in time corresponding to this timestamp, represented as + * the number of milliseconds since Unix epoch 1970-01-01T00:00:00Z. + */ + toMillis(): number; + + /** + * Returns true if this `Timestamp` is equal to the provided one. + * + * @param other - The `Timestamp` to compare against. + * @returns true if this `Timestamp` is equal to the provided one. + */ + isEqual(other: Timestamp): boolean; + + /** Returns a JSON-serializable representation of this `Timestamp`. */ + toJSON(): { seconds: number; nanoseconds: number }; + + /** Returns a textual representation of this `Timestamp`. */ + toString(): string; + + /** + * Converts this object to a primitive string, which allows `Timestamp` objects + * to be compared using the `>`, `<=`, `>=` and `>` operators. + */ + valueOf(): string; +} diff --git a/packages/firestore/lib/modular/Timestamp.js b/packages/firestore/lib/modular/Timestamp.js new file mode 100644 index 0000000000..6510afee17 --- /dev/null +++ b/packages/firestore/lib/modular/Timestamp.js @@ -0,0 +1,3 @@ +import FirestoreTimestamp from '../FirestoreTimestamp'; + +export const Timestamp = FirestoreTimestamp; diff --git a/packages/firestore/lib/modular/index.d.ts b/packages/firestore/lib/modular/index.d.ts new file mode 100644 index 0000000000..352e5e43ba --- /dev/null +++ b/packages/firestore/lib/modular/index.d.ts @@ -0,0 +1,535 @@ +import { ReactNativeFirebase } from '@react-native-firebase/app'; +import { FirebaseFirestoreTypes } from '../index'; + +import FirebaseApp = ReactNativeFirebase.FirebaseApp; +import Firestore = FirebaseFirestoreTypes.Module; +import CollectionReference = FirebaseFirestoreTypes.CollectionReference; +import DocumentReference = FirebaseFirestoreTypes.DocumentReference; +import DocumentData = FirebaseFirestoreTypes.DocumentData; +import Query = FirebaseFirestoreTypes.Query; +import FieldValue = FirebaseFirestoreTypes.FieldValue; +import FieldPath = FirebaseFirestoreTypes.FieldPath; + +/** Primitive types. */ +export type Primitive = string | number | boolean | undefined | null; + +/** + * Similar to Typescript's `Partial`, but allows nested fields to be + * omitted and FieldValues to be passed in as property values. + */ +export type PartialWithFieldValue = + | Partial + | (T extends Primitive + ? T + : T extends object + ? { [K in keyof T]?: PartialWithFieldValue | FieldValue } + : never); + +/** + * Given a union type `U = T1 | T2 | ...`, returns an intersected type (`T1 & T2 & ...`). + * + * Uses distributive conditional types and inference from conditional types. + * This works because multiple candidates for the same type variable in contra-variant positions + * causes an intersection type to be inferred. + * https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-inference-in-conditional-types + * https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type + */ +export declare type UnionToIntersection = (U extends unknown ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I + : never; + +/** + * Returns a new map where every key is prefixed with the outer key appended to a dot. + */ +export declare type AddPrefixToKeys> = { + [K in keyof T & string as `${Prefix}.${K}`]+?: T[K]; +}; + +/** + * Helper for calculating the nested fields for a given type `T1`. This is needed to distribute + * union types such as `undefined | {...}` (happens for optional props) or `{a: A} | {b: B}`. + * + * In this use case, `V` is used to distribute the union types of `T[K]` on Record, since `T[K]` + * is evaluated as an expression and not distributed. + * + * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types + */ +export declare type ChildUpdateFields = V extends Record + ? AddPrefixToKeys> + : never; + +/** + * For each field (e.g. 'bar'), find all nested keys (e.g. {'bar.baz': T1, 'bar.qux': T2}). + * Intersect them together to make a single map containing all possible keys that are all marked as optional + */ +export declare type NestedUpdateFields> = UnionToIntersection< + { + [K in keyof T & string]: ChildUpdateFields; + }[keyof T & string] +>; + +/** + * Update data (for use with {@link updateDoc}) that consists of field paths (e.g. 'foo' or 'foo.baz') + * mapped to values. Fields that contain dots reference nested fields within the document. + * FieldValues can be passed in as property values. + */ +export declare type UpdateData = T extends Primitive + ? T + : T extends object + ? { + [K in keyof T]?: UpdateData | FieldValue; + } & NestedUpdateFields + : Partial; + +/** + * Allows FieldValues to be passed in as a property value while maintaining + * type safety. + */ +export type WithFieldValue = + | T + | (T extends Primitive + ? T + : T extends object + ? { [K in keyof T]: WithFieldValue | FieldValue } + : never); + +/** + * Returns the existing default {@link Firestore} instance that is associated with the + * default {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new + * instance with default settings. + * + * @returns The {@link Firestore} instance of the provided app. + */ +export declare function getFirestore(): Firestore; + +/** + * Returns the existing default {@link Firestore} instance that is associated with the + * provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new + * instance with default settings. + * + * @param app - The {@link @firebase/app#FirebaseApp} instance that the returned {@link Firestore} + * instance is associated with. + * @returns The {@link Firestore} instance of the provided app. + * @internal + */ +export declare function getFirestore(app: FirebaseApp): Firestore; + +export function getFirestore(app?: FirebaseApp): Firestore; + +/** + * Gets a `DocumentReference` instance that refers to the document at the + * specified absolute path. + * + * @param firestore - A reference to the root `Firestore` instance. + * @param path - A slash-separated path to a document. + * @param pathSegments - Additional path segments that will be applied relative + * to the first argument. + * @throws If the final path has an odd number of segments and does not point to + * a document. + * @returns The `DocumentReference` instance. + */ +export function doc( + firestore: Firestore, + path: string, + ...pathSegments: string[] +): DocumentReference; + +/** + * Gets a `DocumentReference` instance that refers to a document within + * `reference` at the specified relative path. If no path is specified, an + * automatically-generated unique ID will be used for the returned + * `DocumentReference`. + * + * @param reference - A reference to a collection. + * @param path - A slash-separated path to a document. Has to be omitted to use + * auto-generated IDs. + * @param pathSegments - Additional path segments that will be applied relative + * to the first argument. + * @throws If the final path has an odd number of segments and does not point to + * a document. + * @returns The `DocumentReference` instance. + */ +export function doc( + reference: CollectionReference, + path?: string, + ...pathSegments: string[] +): DocumentReference; + +/** + * Gets a `DocumentReference` instance that refers to a document within + * `reference` at the specified relative path. + * + * @param reference - A reference to a Firestore document. + * @param path - A slash-separated path to a document. + * @param pathSegments - Additional path segments that will be applied relative + * to the first argument. + * @throws If the final path has an odd number of segments and does not point to + * a document. + * @returns The `DocumentReference` instance. + */ +export function doc( + reference: DocumentReference, + path: string, + ...pathSegments: string[] +): DocumentReference; + +export function doc( + parent: Firestore | CollectionReference | DocumentReference, + path?: string, + ...pathSegments: string[] +): DocumentReference; + +/** + * Gets a `CollectionReference` instance that refers to the collection at + * the specified absolute path. + * + * @param firestore - A reference to the root `Firestore` instance. + * @param path - A slash-separated path to a collection. + * @param pathSegments - Additional path segments to apply relative to the first + * argument. + * @throws If the final path has an even number of segments and does not point + * to a collection. + * @returns The `CollectionReference` instance. + */ +export function collection( + firestore: Firestore, + path: string, + ...pathSegments: string[] +): CollectionReference; + +/** + * Gets a `CollectionReference` instance that refers to a subcollection of + * `reference` at the specified relative path. + * + * @param reference - A reference to a collection. + * @param path - A slash-separated path to a collection. + * @param pathSegments - Additional path segments to apply relative to the first + * argument. + * @throws If the final path has an even number of segments and does not point + * to a collection. + * @returns The `CollectionReference` instance. + */ +export function collection( + reference: CollectionReference, + path: string, + ...pathSegments: string[] +): CollectionReference; + +/** + * Gets a `CollectionReference` instance that refers to a subcollection of + * `reference` at the specified relative path. + * + * @param reference - A reference to a Firestore document. + * @param path - A slash-separated path to a collection. + * @param pathSegments - Additional path segments that will be applied relative + * to the first argument. + * @throws If the final path has an even number of segments and does not point + * to a collection. + * @returns The `CollectionReference` instance. + */ +export function collection( + reference: DocumentReference, + path: string, + ...pathSegments: string[] +): CollectionReference; + +export function collection( + parent: Firestore | DocumentReference | CollectionReference, + path: string, + ...pathSegments: string[] +): CollectionReference; + +/** + * Creates and returns a new `Query` instance that includes all documents in the + * database that are contained in a collection or subcollection with the + * given `collectionId`. + * + * @param firestore - A reference to the root `Firestore` instance. + * @param collectionId - Identifies the collections to query over. Every + * collection or subcollection with this ID as the last segment of its path + * will be included. Cannot contain a slash. + * @returns The created `Query`. + */ +export function collectionGroup(firestore: Firestore, collectionId: string): Query; + +/** + * Writes to the document referred to by this `DocumentReference`. If the + * document does not yet exist, it will be created. + * + * @param reference - A reference to the document to write. + * @param data - A map of the fields and values for the document. + * @returns A `Promise` resolved once the data has been successfully written + * to the backend (note that it won't resolve while you're offline). + */ +export function setDoc(reference: DocumentReference, data: WithFieldValue): Promise; + +/** + * Writes to the document referred to by the specified `DocumentReference`. If + * the document does not yet exist, it will be created. If you provide `merge` + * or `mergeFields`, the provided data can be merged into an existing document. + * + * @param reference - A reference to the document to write. + * @param data - A map of the fields and values for the document. + * @param options - An object to configure the set behavior. + * @returns A Promise resolved once the data has been successfully written + * to the backend (note that it won't resolve while you're offline). + */ +export function setDoc( + reference: DocumentReference, + data: PartialWithFieldValue, + options: FirebaseFirestoreTypes.SetOptions, +): Promise; + +export function setDoc( + reference: DocumentReference, + data: PartialWithFieldValue, + options?: FirebaseFirestoreTypes.SetOptions, +): Promise; + +/** + * Updates fields in the document referred to by the specified + * `DocumentReference`. The update will fail if applied to a document that does + * not exist. + * + * @param reference - A reference to the document to update. + * @param data - An object containing the fields and values with which to + * update the document. Fields can contain dots to reference nested fields + * within the document. + * @returns A `Promise` resolved once the data has been successfully written + * to the backend (note that it won't resolve while you're offline). + */ +export function updateDoc(reference: DocumentReference, data: UpdateData): Promise; +/** + * Updates fields in the document referred to by the specified + * `DocumentReference` The update will fail if applied to a document that does + * not exist. + * + * Nested fields can be updated by providing dot-separated field path + * strings or by providing `FieldPath` objects. + * + * @param reference - A reference to the document to update. + * @param field - The first field to update. + * @param value - The first value. + * @param moreFieldsAndValues - Additional key value pairs. + * @returns A `Promise` resolved once the data has been successfully written + * to the backend (note that it won't resolve while you're offline). + */ +export function updateDoc( + reference: DocumentReference, + field: string | FieldPath, + value: unknown, + ...moreFieldsAndValues: unknown[] +): Promise; + +/** + * Add a new document to specified `CollectionReference` with the given data, + * assigning it a document ID automatically. + * + * @param reference - A reference to the collection to add this document to. + * @param data - An Object containing the data for the new document. + * @returns A `Promise` resolved with a `DocumentReference` pointing to the + * newly created document after it has been written to the backend (Note that it + * won't resolve while you're offline). + */ +export function addDoc( + reference: CollectionReference, + data: WithFieldValue, +): Promise>; + +/** + * Re-enables use of the network for this {@link Firestore} instance after a prior + * call to {@link disableNetwork}. + * + * @returns A `Promise` that is resolved once the network has been enabled. + */ +export function enableNetwork(firestore: Firestore): Promise; + +/** + * Disables network usage for this instance. It can be re-enabled via {@link + * enableNetwork}. While the network is disabled, any snapshot listeners, + * `getDoc()` or `getDocs()` calls will return results from cache, and any write + * operations will be queued until the network is restored. + * + * @returns A `Promise` that is resolved once the network has been disabled. + */ +export function disableNetwork(firestore: Firestore): Promise; + +/** + * Aimed primarily at clearing up any data cached from running tests. Needs to be executed before any database calls + * are made. + * + * @param firestore - A reference to the root `Firestore` instance. + */ +export function clearPersistence(firestore: Firestore): Promise; + +/** + * Terminates the provided {@link Firestore} instance. + * + * To restart after termination, create a new instance of FirebaseFirestore with + * {@link (getFirestore:1)}. + * + * Termination does not cancel any pending writes, and any promises that are + * awaiting a response from the server will not be resolved. If you have + * persistence enabled, the next time you start this instance, it will resume + * sending these writes to the server. + * + * Note: Under normal circumstances, calling `terminate()` is not required. This + * function is useful only when you want to force this instance to release all + * of its resources or in combination with `clearIndexedDbPersistence()` to + * ensure that all local state is destroyed between test runs. + * + * @returns A `Promise` that is resolved when the instance has been successfully + * terminated. + */ +export function terminate(firestore: Firestore): Promise; + +/** + * Waits until all currently pending writes for the active user have been + * acknowledged by the backend. + * + * The returned promise resolves immediately if there are no outstanding writes. + * Otherwise, the promise waits for all previously issued writes (including + * those written in a previous app session), but it does not wait for writes + * that were added after the function is called. If you want to wait for + * additional writes, call `waitForPendingWrites()` again. + * + * Any outstanding `waitForPendingWrites()` promises are rejected during user + * changes. + * + * @returns A `Promise` which resolves when all currently pending writes have been + * acknowledged by the backend. + */ +export function waitForPendingWrites(firestore: Firestore): Promise; + +/* + * @param app - The {@link @firebase/app#FirebaseApp} with which the {@link Firestore} instance will + * be associated. + * @param settings - A settings object to configure the {@link Firestore} instance. + * @param databaseId - The name of database. + * @returns A newly initialized {@link Firestore} instance. + */ +export function initializeFirestore( + app: FirebaseApp, + settings: FirestoreSettings, + databaseId?: string, +): Promise; + +/** + * The verbosity you set for activity and error logging. Can be any of the following values: + * - debug for the most verbose logging level, primarily for debugging. + * - error to log errors only. + * - silent to turn off logging. + */ +type LogLevel = 'debug' | 'error' | 'silent'; + +/** + * Sets the verbosity of Cloud Firestore logs (debug, error, or silent). + * @param logLevel - The verbosity you set for activity and error logging. + */ +export function setLogLevel(logLevel: LogLevel): void; + +/** + * Executes the given `updateFunction` and then attempts to commit the changes + * applied within the transaction. If any document read within the transaction + * has changed, Cloud Firestore retries the `updateFunction`. If it fails to + * commit after 5 attempts, the transaction fails. + * + * The maximum number of writes allowed in a single transaction is 500. + * + * @param firestore - A reference to the Firestore database to run this + * transaction against. + * @param updateFunction - The function to execute within the transaction + * context. + * @returns If the transaction completed successfully or was explicitly aborted + * (the `updateFunction` returned a failed promise), the promise returned by the + * `updateFunction `is returned here. Otherwise, if the transaction failed, a + * rejected promise with the corresponding failure error is returned. + */ +export function runTransaction( + firestore: Firestore, + updateFunction: (transaction: FirebaseFirestoreTypes.Transaction) => Promise, +): Promise; + +/** + * Calculates the number of documents in the result set of the given query, + * without actually downloading the documents. + * + * Using this function to count the documents is efficient because only the + * final count, not the documents' data, is downloaded. This function can even + * count the documents if the result set would be prohibitively large to + * download entirely (e.g. thousands of documents). + * + * The result received from the server is presented, unaltered, without + * considering any local state. That is, documents in the local cache are not + * taken into consideration, neither are local modifications not yet + * synchronized with the server. Previously-downloaded results, if any, are not + * used: every request using this source necessarily involves a round trip to + * the server. + * + * @param query - The query whose result set size to calculate. + * @returns A Promise that will be resolved with the count; the count can be + * retrieved from `snapshot.data().count`, where `snapshot` is the + * `AggregateQuerySnapshot` to which the returned Promise resolves. + */ +export function getCountFromServer( + query: Query, +): Promise< + FirebaseFirestoreTypes.AggregateQuerySnapshot< + { count: FirebaseFirestoreTypes.AggregateField }, + AppModelType, + DbModelType + > +>; + +/** + * Represents the task of loading a Firestore bundle. + * It provides progress of bundle loading, as well as task completion and error events. + */ +type LoadBundleTask = Promise; + +/** + * Loads a Firestore bundle into the local cache. + * + * @param firestore - The {@link Firestore} instance to load bundles for. + * @param bundleData - An object representing the bundle to be loaded. Valid + * objects are `ArrayBuffer`, `ReadableStream` or `string`. + * + * @returns A `LoadBundleTask` object, which notifies callers with progress + * updates, and completion or error events. It can be used as a + * `Promise`. + */ +export function loadBundle( + firestore: Firestore, + bundleData: ReadableStream | ArrayBuffer | string, +): LoadBundleTask; + +/** + * Reads a Firestore {@link Query} from local cache, identified by the given + * name. + * + * The named queries are packaged into bundles on the server side (along + * with resulting documents), and loaded to local cache using `loadBundle`. Once + * in local cache, use this method to extract a {@link Query} by name. + * + * @param firestore - The {@link Firestore} instance to read the query from. + * @param name - The name of the query. + * @returns A named Query. + */ +export function namedQuery(firestore: Firestore, name: string): Query; + +/** + * Creates a write batch, used for performing multiple writes as a single + * atomic operation. The maximum number of writes allowed in a single WriteBatch + * is 500. + * + * The result of these writes will only be reflected in document reads that + * occur after the returned promise resolves. If the client is offline, the + * write fails. If you would like to see local modifications or buffer writes + * until the client is online, use the full Firestore SDK. + * + * @returns A `WriteBatch` that can be used to atomically execute multiple + * writes. + */ +export function writeBatch(firestore: Firestore): FirebaseFirestoreTypes.WriteBatch; diff --git a/packages/firestore/lib/modular/index.js b/packages/firestore/lib/modular/index.js new file mode 100644 index 0000000000..c4296fec52 --- /dev/null +++ b/packages/firestore/lib/modular/index.js @@ -0,0 +1,218 @@ +/** + * @typedef {import('..').FirebaseFirestoreTypes} FirebaseFirestoreTypes + * @typedef {import('..').FirebaseFirestoreTypes.CollectionReference} CollectionReference + * @typedef {import('..').FirebaseFirestoreTypes.DocumentData} DocumentData + * @typedef {import('..').FirebaseFirestoreTypes.DocumentReference} DocumentReference + * @typedef {import('..').FirebaseFirestoreTypes.FieldPath} FieldPath + * @typedef {import('..').FirebaseFirestoreTypes.Module} Firestore + * @typedef {import('..').FirebaseFirestoreTypes.Query} Query + * @typedef {import('..').FirebaseFirestoreTypes.SetOptions} SetOptions + * @typedef {import('..').FirebaseFirestoreTypes.Settings} FirestoreSettings + * @typedef {import('@firebase/app').FirebaseApp} FirebaseApp + */ + +import { firebase } from '../index'; + +/** + * @param {FirebaseApp?} app + * @returns {Firestore} + */ +export function getFirestore(app) { + if (app) { + return firebase.firestore(app); + } + + return firebase.firestore(); +} + +/** + * @param {Firestore | CollectionReference | DocumentReference} parent + * @param {string?} path + * @param {string?} pathSegments + * @returns {DocumentReference} + */ +export function doc(parent, path, ...pathSegments) { + if (pathSegments && pathSegments.length) { + path = path + '/' + pathSegments.map(e => e.replace(/^\/|\/$/g, '')).join('/'); + } + + return parent.doc(path); +} + +/** + * @param {Firestore | DocumentReference | CollectionReference} parent + * @param {string} path + * @param {string?} pathSegments + * @returns {CollectionReference} + */ +export function collection(parent, path, ...pathSegments) { + if (pathSegments && pathSegments.length) { + path = path + '/' + pathSegments.map(e => e.replace(/^\/|\/$/g, '')).join('/'); + } + + return parent.collection(path); +} + +/** + * @param {Firestore} firestore + * @param {string} collectionId + * @returns {Query} + */ +export function collectionGroup(firestore, collectionId) { + return firestore.collectionGroup(collectionId); +} + +/** + * @param {DocumentReference} reference + * @param {import('.').PartialWithFieldValue} data + * @param {SetOptions?} options + * @returns {Promise} + */ +export function setDoc(reference, data, options) { + return reference.set(data, options); +} + +/** + * @param {DocumentReference} reference + * @param {string | FieldPath | import('.').UpdateData} fieldOrUpdateData + * @param {unknown?} value + * @param {unknown} moreFieldsAndValues + * @returns {Promise} + */ +export function updateDoc(reference, fieldOrUpdateData, value, ...moreFieldsAndValues) { + if (!fieldOrUpdateData) { + // @ts-ignore + return reference.update(); + } + + if (!value) { + return reference.update(fieldOrUpdateData); + } + + if (!moreFieldsAndValues || !Array.isArray(moreFieldsAndValues)) { + return reference.update(fieldOrUpdateData, value); + } + + return reference.update(fieldOrUpdateData, value, ...moreFieldsAndValues); +} + +/** + * @param {CollectionReference} reference + * @param {WithFieldValue} data + * @returns {Promise} + */ +export function addDoc(reference, data) { + return reference.add(data); +} + +/** + * @param {Firestore} firestore + * @returns {Promise} + */ +export function enableNetwork(firestore) { + return firestore.enableNetwork(); +} + +/** + * @param {Firestore} firestore + * @returns {Promise} + */ +export function disableNetwork(firestore) { + return firestore.disableNetwork(); +} + +/** + * @param {Firestore} firestore + * @returns {Promise} + */ +export function clearPersistence(firestore) { + return firestore.clearPersistence(); +} + +/** + * @param {Firestore} firestore + * @returns {Promise} + */ +export function terminate(firestore) { + return firestore.terminate(); +} + +/** + * @param {Firestore} firestore + * @returns {Promise} + */ +export function waitForPendingWrites(firestore) { + return firestore.waitForPendingWrites(); +} + +/** + * @param {FirebaseApp} app + * @param {FirestoreSettings} settings + * @param {string?} databaseId + * @returns {Promise} + */ +export async function initializeFirestore(app, settings /* databaseId */) { + // TODO(exaby73): implement 2nd database once it's supported + const firestore = firebase.firestore(app); + await firestore.settings(settings); + return firestore; +} + +/** + * @param {import('./').LogLevel} logLevel + * @returns {void} + */ +export function setLogLevel(logLevel) { + return firebase.firestore.setLogLevel(logLevel); +} + +/** + * @param {Firestore} firestore + * @param {(transaction: FirebaseFirestoreTypes.Transaction) => Promise} updateFunction + * @returns {Promise} + */ +export function runTransaction(firestore, updateFunction) { + return firestore.runTransaction(updateFunction); +} + +/** + * @param {Query} query + * @returns {Promise} + */ +export function getCountFromServer(query) { + return query.count().get(); +} + +/** + * @param {Firestore} firestore + * @param {ReadableStream | ArrayBuffer | string} bundleData + * @returns {import('.').LoadBundleTask} + */ +export function loadBundle(firestore, bundleData) { + return firestore.loadBundle(bundleData); +} + +/** + * @param {Firestore} firestore + * @param {string} name + * @returns {Query} + */ +export function namedQuery(firestore, name) { + return firestore.namedQuery(name); +} + +/** + * @param {Firestore} firestore + * @returns {FirebaseFirestoreTypes.WriteBatch} + */ +export function writeBatch(firestore) { + return firestore.batch(); +} + +export * from './query'; +export * from './snapshot'; +export * from './Bytes'; +export * from './FieldPath'; +export * from './FieldValue'; +export * from './GeoPoint'; +export * from './Timestamp'; diff --git a/packages/firestore/lib/modular/query.d.ts b/packages/firestore/lib/modular/query.d.ts new file mode 100644 index 0000000000..a9b85bf3f9 --- /dev/null +++ b/packages/firestore/lib/modular/query.d.ts @@ -0,0 +1,344 @@ +import { FirebaseFirestoreTypes } from '../..'; + +import Query = FirebaseFirestoreTypes.Query; +import QueryCompositeFilterConstraint = FirebaseFirestoreTypes.QueryCompositeFilterConstraint; +import WhereFilterOp = FirebaseFirestoreTypes.WhereFilterOp; +import FieldPath = FirebaseFirestoreTypes.FieldPath; +import QuerySnapshot = FirebaseFirestoreTypes.QuerySnapshot; +import DocumentReference = FirebaseFirestoreTypes.DocumentReference; +import DocumentSnapshot = FirebaseFirestoreTypes.DocumentSnapshot; +import DocumentData = FirebaseFirestoreTypes.DocumentData; + +/** Describes the different query constraints available in this SDK. */ +export type QueryConstraintType = + | 'where' + | 'orderBy' + | 'limit' + | 'limitToLast' + | 'startAt' + | 'startAfter' + | 'endAt' + | 'endBefore'; + +/** + * An `AppliableConstraint` is an abstraction of a constraint that can be applied + * to a Firestore query. + */ +export interface AppliableConstraint { + /** + * Takes the provided {@link Query} and returns a copy of the {@link Query} with this + * {@link AppliableConstraint} applied. + */ + _apply(query: Query): Query; +} + +/** + * A `QueryConstraint` is used to narrow the set of documents returned by a + * Firestore query. `QueryConstraint`s are created by invoking {@link where}, + * {@link orderBy}, {@link (startAt:1)}, {@link (startAfter:1)}, {@link + * (endBefore:1)}, {@link (endAt:1)}, {@link limit}, {@link limitToLast} and + * can then be passed to {@link (query:1)} to create a new query instance that + * also contains this `QueryConstraint`. + */ +export interface IQueryConstraint extends AppliableConstraint { + /** The type of this query constraint */ + readonly type: QueryConstraintType; + + /** + * Takes the provided {@link Query} and returns a copy of the {@link Query} with this + * {@link AppliableConstraint} applied. + */ + _apply(query: Query): Query; +} + +export class QueryOrderByConstraint extends QueryConstraint { + readonly type: QueryConstraintType = 'orderBy'; +} + +export class QueryLimitConstraint extends QueryConstraint { + readonly type: QueryConstraintType = 'limit'; +} + +export class QueryStartAtConstraint extends QueryConstraint { + readonly type: QueryConstraintType = 'startAt'; +} + +export class QueryEndAtConstraint extends QueryConstraint { + readonly type: QueryConstraintType = 'endAt'; +} + +export class QueryFieldFilterConstraint extends QueryConstraint { + readonly type: QueryConstraintType = 'where'; +} + +/** + * `QueryNonFilterConstraint` is a helper union type that represents + * QueryConstraints which are used to narrow or order the set of documents, + * but that do not explicitly filter on a document field. + * `QueryNonFilterConstraint`s are created by invoking {@link orderBy}, + * {@link (startAt:1)}, {@link (startAfter:1)}, {@link (endBefore:1)}, {@link (endAt:1)}, + * {@link limit} or {@link limitToLast} and can then be passed to {@link (query:1)} + * to create a new query instance that also contains the `QueryConstraint`. + */ +export type QueryNonFilterConstraint = + | QueryOrderByConstraint + | QueryLimitConstraint + | QueryStartAtConstraint + | QueryEndAtConstraint; + +/** + * Creates a new immutable instance of {@link Query} that is extended to also + * include additional query constraints. + * + * @param query - The {@link Query} instance to use as a base for the new + * constraints. + * @param compositeFilter - The {@link QueryCompositeFilterConstraint} to + * apply. Create {@link QueryCompositeFilterConstraint} using {@link and} or + * {@link or}. + * @param queryConstraints - Additional {@link QueryNonFilterConstraint}s to + * apply (e.g. {@link orderBy}, {@link limit}). + * @throws if any of the provided query constraints cannot be combined with the + * existing or new constraints. + */ +export function query( + query: Query, + compositeFilter: QueryCompositeFilterConstraint, + ...queryConstraints: QueryNonFilterConstraint[] +): Query; + +/** + * Creates a new immutable instance of {@link Query} that is extended to also + * include additional query constraints. + * + * @param query - The {@link Query} instance to use as a base for the new + * constraints. + * @param queryConstraints - The list of {@link IQueryConstraint}s to apply. + * @throws if any of the provided query constraints cannot be combined with the + * existing or new constraints. + */ +export function query(query: Query, ...queryConstraints: IQueryConstraint[]): Query; + +export function query( + query: Query, + queryConstraint: QueryCompositeFilterConstraint | IQueryConstraint | undefined, + ...additionalQueryConstraints: Array +): Query; + +/** + * Creates a {@link QueryFieldFilterConstraint} that enforces that documents + * must contain the specified field and that the value should satisfy the + * relation constraint provided. + * + * @param fieldPath - The path to compare + * @param opStr - The operation string (e.g "<", "<=", "==", "<", + * "<=", "!="). + * @param value - The value for comparison + * @returns The created {@link QueryFieldFilterConstraint}. + */ +export function where( + fieldPath: string | FieldPath, + opStr: WhereFilterOp, + value: unknown, +): QueryFieldFilterConstraint; + +/** + * The or() function used to generate a logical OR query. + * e.g. or(where('name', '==', 'Ada'), where('name', '==', 'Bob')) + */ +export function or(...queries: QueryFilterConstraint[]): QueryCompositeFilterConstraint; + +/** + * The and() function used to generate a logical AND query. + * e.g. and(where('name', '==', 'Ada'), where('name', '==', 'Bob')) + */ +export function and(...queries: QueryFilterConstraint[]): QueryCompositeFilterConstraint; + +/** + * The direction of a {@link orderBy} clause is specified as 'desc' or 'asc' + * (descending or ascending). + */ +export type OrderByDirection = 'desc' | 'asc'; + +/** + * Creates a {@link QueryOrderByConstraint} that sorts the query result by the + * specified field, optionally in descending order instead of ascending. + * + * Note: Documents that do not contain the specified field will not be present + * in the query result. + * + * @param fieldPath - The field to sort by. + * @param directionStr - Optional direction to sort by ('asc' or 'desc'). If + * not specified, order will be ascending. + * @returns The created {@link QueryOrderByConstraint}. + */ +export function orderBy( + fieldPath: string | FieldPath, + directionStr: OrderByDirection = 'asc', +): QueryOrderByConstraint; + +/** + * Creates a {@link QueryStartAtConstraint} that modifies the result set to + * start at the provided document (inclusive). The starting position is relative + * to the order of the query. The document must contain all of the fields + * provided in the `orderBy` of this query. + * + * @param snapshot - The snapshot of the document to start at. + * @returns A {@link QueryStartAtConstraint} to pass to `query()`. + */ +export function startAt(snapshot: DocumentSnapshot): QueryStartAtConstraint; +/** + * + * Creates a {@link QueryStartAtConstraint} that modifies the result set to + * start at the provided fields relative to the order of the query. The order of + * the field values must match the order of the order by clauses of the query. + * + * @param fieldValues - The field values to start this query at, in order + * of the query's order by. + * @returns A {@link QueryStartAtConstraint} to pass to `query()`. + */ +export function startAt(...fieldValues: unknown[]): QueryStartAtConstraint; + +export function startAt( + ...docOrFields: Array> +): QueryStartAtConstraint; + +/** + * Creates a {@link QueryStartAtConstraint} that modifies the result set to + * start after the provided document (exclusive). The starting position is + * relative to the order of the query. The document must contain all of the + * fields provided in the orderBy of the query. + * + * @param snapshot - The snapshot of the document to start after. + * @returns A {@link QueryStartAtConstraint} to pass to `query()` + */ +export function startAfter( + snapshot: DocumentSnapshot, +): QueryStartAtConstraint; + +/** + * Creates a {@link QueryStartAtConstraint} that modifies the result set to + * start after the provided fields relative to the order of the query. The order + * of the field values must match the order of the order by clauses of the query. + * + * @param fieldValues - The field values to start this query after, in order + * of the query's order by. + * @returns A {@link QueryStartAtConstraint} to pass to `query()` + */ +export function startAfter(...fieldValues: unknown[]): QueryStartAtConstraint; + +export function startAfter( + ...docOrFields: Array> +): QueryStartAtConstraint; + +/** + * Creates a {@link QueryLimitConstraint} that only returns the first matching + * documents. + * + * @param limit - The maximum number of items to return. + * @returns The created {@link QueryLimitConstraint}. + */ +export function limit(limit: number): QueryLimitConstraint; + +/** + * Executes the query and returns the results as a `QuerySnapshot`. + * + * Note: `getDocs()` attempts to provide up-to-date data when possible by + * waiting for data from the server, but it may return cached data or fail if + * you are offline and the server cannot be reached. To specify this behavior, + * invoke {@link getDocsFromCache} or {@link getDocsFromServer}. + * + * @returns A `Promise` that will be resolved with the results of the query. + */ +export function getDocs(query: Query): Promise>; + +/** + * Executes the query and returns the results as a `QuerySnapshot` from cache. + * Returns an empty result set if no documents matching the query are currently + * cached. + * + * @returns A `Promise` that will be resolved with the results of the query. + */ +export function getDocsFromCache(query: Query): Promise>; + +/** + * Executes the query and returns the results as a `QuerySnapshot` from the + * server. Returns an error if the network is not available. + * + * @returns A `Promise` that will be resolved with the results of the query. + */ +export function getDocsFromServer(query: Query): Promise>; + +/** + * Deletes the document referred to by the specified `DocumentReference`. + * + * @param reference - A reference to the document to delete. + * @returns A Promise resolved once the document has been successfully + * deleted from the backend (note that it won't resolve while you're offline). + */ +export function deleteDoc(reference: DocumentReference): Promise; + +/** + * Creates a `QueryConstraint` with the specified ending point. + * + * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()` + * allows you to choose arbitrary starting and ending points for your queries. + * + * The ending point is inclusive, so children with exactly the specified value + * will be included in the query. The optional key argument can be used to + * further limit the range of the query. If it is specified, then children that + * have exactly the specified value must also have a key name less than or equal + * to the specified key. + * + * You can read more about `endAt()` in + * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}. + * + * @param value - The value to end at. The argument type depends on which + * `orderBy*()` function was used in this query. Specify a value that matches + * the `orderBy*()` type. When used in combination with `orderByKey()`, the + * value must be a string. + * @param key - The child key to end at, among the children with the previously + * specified priority. This argument is only allowed if ordering by child, + * value, or priority. + */ +export function endAt(value: number | string | boolean | null, key?: string): QueryConstraint; + +/** + * Creates a `QueryConstraint` with the specified ending point (exclusive). + * + * Using `startAt()`, `startAfter()`, `endBefore()`, `endAt()` and `equalTo()` + * allows you to choose arbitrary starting and ending points for your queries. + * + * The ending point is exclusive. If only a value is provided, children + * with a value less than the specified value will be included in the query. + * If a key is specified, then children must have a value less than or equal + * to the specified value and a key name less than the specified key. + * + * @param value - The value to end before. The argument type depends on which + * `orderBy*()` function was used in this query. Specify a value that matches + * the `orderBy*()` type. When used in combination with `orderByKey()`, the + * value must be a string. + * @param key - The child key to end before, among the children with the + * previously specified priority. This argument is only allowed if ordering by + * child, value, or priority. + */ +export function endBefore(value: number | string | boolean | null, key?: string): QueryConstraint; + +/** + * Creates a new `QueryConstraint` that is limited to return only the last + * specified number of children. + * + * The `limitToLast()` method is used to set a maximum number of children to be + * synced for a given callback. If we set a limit of 100, we will initially only + * receive up to 100 `child_added` events. If we have fewer than 100 messages + * stored in our Database, a `child_added` event will fire for each message. + * However, if we have over 100 messages, we will only receive a `child_added` + * event for the last 100 ordered messages. As items change, we will receive + * `child_removed` events for each item that drops out of the active list so + * that the total number stays at 100. + * + * You can read more about `limitToLast()` in + * {@link https://firebase.google.com/docs/database/web/lists-of-data#filtering_data | Filtering data}. + * + * @param limit - The maximum number of nodes to include in this query. + */ +export function limitToLast(limit: number): QueryConstraint; diff --git a/packages/firestore/lib/modular/query.js b/packages/firestore/lib/modular/query.js new file mode 100644 index 0000000000..1ab0b3c4d0 --- /dev/null +++ b/packages/firestore/lib/modular/query.js @@ -0,0 +1,202 @@ +/** + * @typedef {import('..').FirebaseFirestoreTypes.DocumentReference} DocumentReference + * @typedef {import('..').FirebaseFirestoreTypes.DocumentSnapshot} DocumentSnapshot + * @typedef {import('..').FirebaseFirestoreTypes.FieldPath} FieldPath + * @typedef {import('..').FirebaseFirestoreTypes.QueryCompositeFilterConstraint} QueryCompositeFilterConstraint + * @typedef {import('..').FirebaseFirestoreTypes.QuerySnapshot} QuerySnapshot + * @typedef {import('..').FirebaseFirestoreTypes.Query} Query + * @typedef {import('..').FirebaseFirestoreTypes.WhereFilterOp} WhereFilterOp + * @typedef {import('../FirestoreFilter')._Filter} _Filter + * @typedef {import('./query').IQueryConstraint} IQueryConstraint + * @typedef {import('./query').OrderByDirection} OrderByDirection + * @typedef {import('./query').QueryFieldFilterConstraint} QueryFieldFilterConstraint + * @typedef {import('./query').QueryLimitConstraint} QueryLimitConstraint + * @typedef {import('./query').QueryNonFilterConstraint} QueryNonFilterConstraint + * @typedef {import('./query').QueryOrderByConstraint} QueryOrderByConstraint + * @typedef {import('./query').QueryStartAtConstraint} QueryStartAtConstraint + */ + +import { _Filter, Filter } from '../FirestoreFilter'; + +/** + * @implements {IQueryConstraint} + */ +class QueryConstraint { + constructor(type, ...args) { + this.type = type; + this._args = args; + } + + _apply(query) { + return query[this.type].apply(query, this._args); + } +} + +/** + * @param {Query} query + * @param {QueryCompositeFilterConstraint | QueryConstraint | undefined} queryConstraint + * @param {(QueryConstraint | QueryNonFilterConstraint)[]} additionalQueryConstraints + * @returns {Query} + */ +export function query(query, queryConstraint, ...additionalQueryConstraints) { + const queryConstraints = [queryConstraint, ...additionalQueryConstraints].filter( + constraint => constraint !== undefined, + ); + let q = query; + for (const queryConstraint of queryConstraints) { + q = queryConstraint._apply(q); + } + return q; +} + +/** + * @param {string | FieldPath} fieldPath + * @param {WhereFilterOp} opStr + * @param {unknown} value + * @returns {QueryFieldFilterConstraint} + */ +export function where(fieldPath, opStr, value) { + return new QueryConstraint('where', fieldPath, opStr, value); +} + +/** + * @param {QueryFieldFilterConstraint[]} queries + * @returns {_Filter[]} + */ +function getFilterOps(queries) { + const ops = []; + for (const query of queries) { + if (query.type !== 'where') { + throw 'Not where'; // FIXME: Better error message + } + + const args = query._args; + if (!args.length) { + throw 'No args'; // FIXME: Better error message + } + + if (args[0] instanceof _Filter) { + ops.push(args[0]); + continue; + } + + const [fieldPath, opStr, value] = args; + ops.push(Filter(fieldPath, opStr, value)); + } + return ops; +} + +/** + * @param {QueryFieldFilterConstraint[]} queries + * @returns {QueryCompositeFilterConstraint} + */ +export function or(...queries) { + const ops = getFilterOps(queries); + return new QueryConstraint('where', Filter.or(...ops)); +} + +/** + * @param {QueryFieldFilterConstraint[]} queries + * @returns {QueryCompositeFilterConstraint} + */ +export function and(...queries) { + const ops = getFilterOps(queries); + return new QueryConstraint('where', Filter.and(...ops)); +} + +/** + * @param {string | FieldPath} fieldPath + * @param {OrderByDirection} directionStr + * @returns {QueryOrderByConstraint} + */ +export function orderBy(fieldPath, directionStr) { + return new QueryConstraint('orderBy', fieldPath, directionStr); +} + +/** + * @param {(unknown | DocumentSnapshot)} docOrFields + * @returns {QueryStartAtConstraint} + */ +export function startAt(...docOrFields) { + return new QueryConstraint('startAt', ...docOrFields); +} + +/** + * @param {(unknown | DocumentSnapshot)} docOrFields + * @returns {QueryStartAtConstraint} + */ +export function startAfter(...docOrFields) { + return new QueryConstraint('startAfter', ...docOrFields); +} + +/** + * @param {number | string | boolean | null} value + * @param {string?} key + * @returns {QueryConstraint} + */ +export function endAt(value, key) { + if (!key) { + return new QueryConstraint('endAt', value); + } + return new QueryConstraint('endAt', value, key); +} + +/** + * @param {number | string | boolean | null} value + * @param {string?} key + * @returns {QueryConstraint} + */ +export function endBefore(value, key) { + if (!key) { + return new QueryConstraint('endBefore', value); + } + return new QueryConstraint('endBefore', value, key); +} + +/** + * @param {number} limit + * @returns {QueryLimitConstraint} + */ +export function limit(limit) { + return new QueryConstraint('limit', limit); +} + +/** + * @param {number} limit + * @returns {QueryConstraint} + */ +export function limitToLast(limit) { + return new QueryConstraint('limitToLast', limit); +} + +/** + * @param {Query} query + * @returns {Promise} + */ +export function getDocs(query) { + return query.get({ source: 'default' }); +} + +/** + * @param {Query} query + * @returns {Promise} + */ +export function getDocsFromCache(query) { + return query.get({ source: 'cache' }); +} + +/** + * @param {Query} query + * @returns {Promise} + */ +export function getDocsFromServer(query) { + return query.get({ source: 'server' }); +} + +/** + * @param {DocumentReference} reference + * @returns {Promise} + */ +export function deleteDoc(reference) { + return reference.delete(); +} diff --git a/packages/firestore/lib/modular/snapshot.d.ts b/packages/firestore/lib/modular/snapshot.d.ts new file mode 100644 index 0000000000..ba00c3f7dc --- /dev/null +++ b/packages/firestore/lib/modular/snapshot.d.ts @@ -0,0 +1,203 @@ +import { FirebaseFirestoreTypes } from '../index'; + +import DocumentReference = FirebaseFirestoreTypes.DocumentReference; +import DocumentSnapshot = FirebaseFirestoreTypes.DocumentSnapshot; +import SnapshotListenOptions = FirebaseFirestoreTypes.SnapshotListenOptions; +import QuerySnapshot = FirebaseFirestoreTypes.QuerySnapshot; +import Query = FirebaseFirestoreTypes.Query; + +export type Unsubscribe = () => void; +export type FirestoreError = Error; + +/** + * Attaches a listener for `DocumentSnapshot` events. You may either pass + * individual `onNext` and `onError` callbacks or pass a single observer + * object with `next` and `error` callbacks. + * + * NOTE: Although an `onCompletion` callback can be provided, it will + * never be called because the snapshot stream is never-ending. + * + * @param reference - A reference to the document to listen to. + * @param observer - A single object containing `next` and `error` callbacks. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + reference: DocumentReference, + observer: { + next?: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + }, +): Unsubscribe; +/** + * Attaches a listener for `DocumentSnapshot` events. You may either pass + * individual `onNext` and `onError` callbacks or pass a single observer + * object with `next` and `error` callbacks. + * + * NOTE: Although an `onCompletion` callback can be provided, it will + * never be called because the snapshot stream is never-ending. + * + * @param reference - A reference to the document to listen to. + * @param options - Options controlling the listen behavior. + * @param observer - A single object containing `next` and `error` callbacks. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + reference: DocumentReference, + options: SnapshotListenOptions, + observer: { + next?: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + }, +): Unsubscribe; +/** + * Attaches a listener for `DocumentSnapshot` events. You may either pass + * individual `onNext` and `onError` callbacks or pass a single observer + * object with `next` and `error` callbacks. + * + * NOTE: Although an `onCompletion` callback can be provided, it will + * never be called because the snapshot stream is never-ending. + * + * @param reference - A reference to the document to listen to. + * @param onNext - A callback to be called every time a new `DocumentSnapshot` + * is available. + * @param onError - A callback to be called if the listen fails or is + * cancelled. No further callbacks will occur. + * @param onCompletion - Can be provided, but will not be called since streams are + * never ending. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + reference: DocumentReference, + onNext: (snapshot: DocumentSnapshot) => void, + onError?: (error: FirestoreError) => void, + onCompletion?: () => void, +): Unsubscribe; +/** + * Attaches a listener for `DocumentSnapshot` events. You may either pass + * individual `onNext` and `onError` callbacks or pass a single observer + * object with `next` and `error` callbacks. + * + * NOTE: Although an `onCompletion` callback can be provided, it will + * never be called because the snapshot stream is never-ending. + * + * @param reference - A reference to the document to listen to. + * @param options - Options controlling the listen behavior. + * @param onNext - A callback to be called every time a new `DocumentSnapshot` + * is available. + * @param onError - A callback to be called if the listen fails or is + * cancelled. No further callbacks will occur. + * @param onCompletion - Can be provided, but will not be called since streams are + * never ending. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + reference: DocumentReference, + options: SnapshotListenOptions, + onNext: (snapshot: DocumentSnapshot) => void, + onError?: (error: FirestoreError) => void, + onCompletion?: () => void, +): Unsubscribe; +/** + * Attaches a listener for `QuerySnapshot` events. You may either pass + * individual `onNext` and `onError` callbacks or pass a single observer + * object with `next` and `error` callbacks. The listener can be cancelled by + * calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will + * never be called because the snapshot stream is never-ending. + * + * @param query - The query to listen to. + * @param observer - A single object containing `next` and `error` callbacks. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + query: Query, + observer: { + next?: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + }, +): Unsubscribe; +/** + * Attaches a listener for `QuerySnapshot` events. You may either pass + * individual `onNext` and `onError` callbacks or pass a single observer + * object with `next` and `error` callbacks. The listener can be cancelled by + * calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will + * never be called because the snapshot stream is never-ending. + * + * @param query - The query to listen to. + * @param options - Options controlling the listen behavior. + * @param observer - A single object containing `next` and `error` callbacks. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + query: Query, + options: SnapshotListenOptions, + observer: { + next?: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + }, +): Unsubscribe; +/** + * Attaches a listener for `QuerySnapshot` events. You may either pass + * individual `onNext` and `onError` callbacks or pass a single observer + * object with `next` and `error` callbacks. The listener can be cancelled by + * calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will + * never be called because the snapshot stream is never-ending. + * + * @param query - The query to listen to. + * @param onNext - A callback to be called every time a new `QuerySnapshot` + * is available. + * @param onCompletion - Can be provided, but will not be called since streams are + * never ending. + * @param onError - A callback to be called if the listen fails or is + * cancelled. No further callbacks will occur. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + query: Query, + onNext: (snapshot: QuerySnapshot) => void, + onError?: (error: FirestoreError) => void, + onCompletion?: () => void, +): Unsubscribe; +/** + * Attaches a listener for `QuerySnapshot` events. You may either pass + * individual `onNext` and `onError` callbacks or pass a single observer + * object with `next` and `error` callbacks. The listener can be cancelled by + * calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will + * never be called because the snapshot stream is never-ending. + * + * @param query - The query to listen to. + * @param options - Options controlling the listen behavior. + * @param onNext - A callback to be called every time a new `QuerySnapshot` + * is available. + * @param onCompletion - Can be provided, but will not be called since streams are + * never ending. + * @param onError - A callback to be called if the listen fails or is + * cancelled. No further callbacks will occur. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + query: Query, + options: SnapshotListenOptions, + onNext: (snapshot: QuerySnapshot) => void, + onError?: (error: FirestoreError) => void, + onCompletion?: () => void, +): Unsubscribe; diff --git a/packages/firestore/lib/modular/snapshot.js b/packages/firestore/lib/modular/snapshot.js new file mode 100644 index 0000000000..3293ac0980 --- /dev/null +++ b/packages/firestore/lib/modular/snapshot.js @@ -0,0 +1,14 @@ +/** + * @typedef {import('../..').FirebaseFirestoreTypes.Query} Query + * @typedef {import('../..').FirebaseFirestoreTypes.DocumentReference} DocumentReference + * @typedef {import('snapshot').Unsubscribe} Unsubscribe + */ + +/** + * @param {Query | DocumentReference} reference + * @param {unknown} args + * @returns {Promise} + */ +export function onSnapshot(reference, ...args) { + return reference.onSnapshot(...args); +} diff --git a/packages/firestore/lib/modular/utils/observer.js b/packages/firestore/lib/modular/utils/observer.js new file mode 100644 index 0000000000..e83195a208 --- /dev/null +++ b/packages/firestore/lib/modular/utils/observer.js @@ -0,0 +1,16 @@ +/** + * @param {unknown} obj + * @returns {boolean} + */ +export function isPartialObserver(obj) { + const observerMethods = ['next', 'error', 'complete']; + if (typeof obj !== 'object' || obj == null) return false; + + for (const method of observerMethods) { + if (method in obj && typeof obj[method] === 'function') { + return true; + } + } + + return false; +} diff --git a/tests/app.js b/tests/app.js index 3cc8e04eba..0dbd658c5e 100644 --- a/tests/app.js +++ b/tests/app.js @@ -30,6 +30,7 @@ import '@react-native-firebase/crashlytics'; import '@react-native-firebase/database'; import '@react-native-firebase/dynamic-links'; import '@react-native-firebase/firestore'; +import * as firestoreModular from '@react-native-firebase/firestore'; import * as functionsModular from '@react-native-firebase/functions'; import '@react-native-firebase/in-app-messaging'; import '@react-native-firebase/installations'; @@ -70,6 +71,7 @@ jet.exposeContextProperty('installationsModular', installationsModular); jet.exposeContextProperty('crashlyticsModular', crashlyticsModular); jet.exposeContextProperty('dynamicLinksModular', dynamicLinksModular); jet.exposeContextProperty('databaseModular', databaseModular); +jet.exposeContextProperty('firestoreModular', firestoreModular); firebase.database().useEmulator('localhost', 9000); firebase.auth().useEmulator('http://localhost:9099'); diff --git a/tests/e2e/globals.js b/tests/e2e/globals.js index 2bca260d1e..9c4e589c1b 100644 --- a/tests/e2e/globals.js +++ b/tests/e2e/globals.js @@ -155,4 +155,10 @@ Object.defineProperty(global, 'databaseModular', { }, }); +Object.defineProperty(global, 'firestoreModular', { + get() { + return jet.firestoreModular; + }, +}); + global.isCI = !!process.env.CI;