Files

1217 lines
38 KiB
TypeScript

/**
* @license
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file 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.
*/
import * as firestore from '@firebase/firestore-types';
import { expect } from 'chai';
import { addEqualityMatcher } from './util/equality_matcher';
import { EventsAccumulator } from './util/events_accumulator';
import * as firebaseExport from './util/firebase_export';
import {
apiDescribe,
toChangesArray,
toDataArray,
withTestCollection,
withTestDb
} from './util/helpers';
import { Deferred } from './util/promise';
const Blob = firebaseExport.Blob;
const FieldPath = firebaseExport.FieldPath;
const GeoPoint = firebaseExport.GeoPoint;
const Timestamp = firebaseExport.Timestamp;
apiDescribe('Queries', (persistence: boolean) => {
addEqualityMatcher();
it('can issue limit queries', () => {
const testDocs = {
a: { k: 'a' },
b: { k: 'b' },
c: { k: 'c' }
};
return withTestCollection(persistence, testDocs, collection => {
return collection
.limit(2)
.get()
.then(docs => {
expect(toDataArray(docs)).to.deep.equal([{ k: 'a' }, { k: 'b' }]);
});
});
});
it('cannot issue limitToLast queries without explicit order-by', () => {
return withTestCollection(persistence, {}, async collection => {
const expectedError =
'limitToLast() queries require specifying at least one orderBy() clause';
expect(() => collection.limitToLast(2).get()).to.throw(expectedError);
});
});
it('can issue limit queries using descending sort order', () => {
const testDocs = {
a: { k: 'a', sort: 0 },
b: { k: 'b', sort: 1 },
c: { k: 'c', sort: 1 },
d: { k: 'd', sort: 2 }
};
return withTestCollection(persistence, testDocs, collection => {
return collection
.orderBy('sort', 'desc')
.limit(2)
.get()
.then(docs => {
expect(toDataArray(docs)).to.deep.equal([
{ k: 'd', sort: 2 },
{ k: 'c', sort: 1 }
]);
});
});
});
it('can issue limitToLast queries using descending sort order', () => {
const testDocs = {
a: { k: 'a', sort: 0 },
b: { k: 'b', sort: 1 },
c: { k: 'c', sort: 1 },
d: { k: 'd', sort: 2 }
};
return withTestCollection(persistence, testDocs, collection => {
return collection
.orderBy('sort', 'desc')
.limitToLast(2)
.get()
.then(docs => {
expect(toDataArray(docs)).to.deep.equal([
{ k: 'b', sort: 1 },
{ k: 'a', sort: 0 }
]);
});
});
});
it('can listen to limitToLast queries', () => {
const testDocs = {
a: { k: 'a', sort: 0 },
b: { k: 'b', sort: 1 },
c: { k: 'c', sort: 1 },
d: { k: 'd', sort: 2 }
};
return withTestCollection(persistence, testDocs, async collection => {
const storeEvent = new EventsAccumulator<firestore.QuerySnapshot>();
collection
.orderBy('sort', 'desc')
.limitToLast(2)
.onSnapshot(storeEvent.storeEvent);
let snapshot = await storeEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'b', sort: 1 },
{ k: 'a', sort: 0 }
]);
await collection.add({ k: 'e', sort: -1 });
snapshot = await storeEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'a', sort: 0 },
{ k: 'e', sort: -1 }
]);
});
});
// Two queries that mapped to the same target ID are referred to as
// "mirror queries". An example for a mirror query is a limitToLast()
// query and a limit() query that share the same backend Target ID.
// Since limitToLast() queries are sent to the backend with a modified
// orderBy() clause, they can map to the same target representation as
// limit() query, even if both queries appear separate to the user.
it('can listen/unlisten/relisten to mirror queries', () => {
const testDocs = {
a: { k: 'a', sort: 0 },
b: { k: 'b', sort: 1 },
c: { k: 'c', sort: 1 },
d: { k: 'd', sort: 2 }
};
return withTestCollection(persistence, testDocs, async collection => {
// Setup `limit` query
const storeLimitEvent = new EventsAccumulator<firestore.QuerySnapshot>();
let limitUnlisten = collection
.orderBy('sort', 'asc')
.limit(2)
.onSnapshot(storeLimitEvent.storeEvent);
// Setup mirroring `limitToLast` query
const storeLimitToLastEvent =
new EventsAccumulator<firestore.QuerySnapshot>();
let limitToLastUnlisten = collection
.orderBy('sort', 'desc')
.limitToLast(2)
.onSnapshot(storeLimitToLastEvent.storeEvent);
// Verify both queries get expected results.
let snapshot = await storeLimitEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'a', sort: 0 },
{ k: 'b', sort: 1 }
]);
snapshot = await storeLimitToLastEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'b', sort: 1 },
{ k: 'a', sort: 0 }
]);
// Unlisten then relisten limit query.
limitUnlisten();
limitUnlisten = collection
.orderBy('sort', 'asc')
.limit(2)
.onSnapshot(storeLimitEvent.storeEvent);
// Verify `limit` query still works.
snapshot = await storeLimitEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'a', sort: 0 },
{ k: 'b', sort: 1 }
]);
// Add a document that would change the result set.
await collection.add({ k: 'e', sort: -1 });
// Verify both queries get expected results.
snapshot = await storeLimitEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'e', sort: -1 },
{ k: 'a', sort: 0 }
]);
snapshot = await storeLimitToLastEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'a', sort: 0 },
{ k: 'e', sort: -1 }
]);
// Unlisten to limitToLast, update a doc, then relisten limitToLast.
limitToLastUnlisten();
await collection.doc('a').update({ k: 'a', sort: -2 });
limitToLastUnlisten = collection
.orderBy('sort', 'desc')
.limitToLast(2)
.onSnapshot(storeLimitToLastEvent.storeEvent);
// Verify both queries get expected results.
snapshot = await storeLimitEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'a', sort: -2 },
{ k: 'e', sort: -1 }
]);
snapshot = await storeLimitToLastEvent.awaitEvent();
expect(toDataArray(snapshot)).to.deep.equal([
{ k: 'e', sort: -1 },
{ k: 'a', sort: -2 }
]);
});
});
it('key order is descending for descending inequality', () => {
const testDocs = {
a: {
foo: 42
},
b: {
foo: 42.0
},
c: {
foo: 42
},
d: {
foo: 21
},
e: {
foo: 21
},
f: {
foo: 66
},
g: {
foo: 66
}
};
return withTestCollection(persistence, testDocs, coll => {
return coll
.where('foo', '>', 21.0)
.orderBy('foo', 'desc')
.get()
.then(docs => {
expect(docs.docs.map(d => d.id)).to.deep.equal([
'g',
'f',
'c',
'b',
'a'
]);
});
});
});
it('can use unary filters', () => {
const testDocs = {
a: { null: null, nan: NaN },
b: { null: null, nan: 0 },
c: { null: false, nan: NaN }
};
return withTestCollection(persistence, testDocs, coll => {
return coll
.where('null', '==', null)
.where('nan', '==', NaN)
.get()
.then(docs => {
expect(toDataArray(docs)).to.deep.equal([{ null: null, nan: NaN }]);
});
});
});
it('can filter on infinity', () => {
const testDocs = {
a: { inf: Infinity },
b: { inf: -Infinity }
};
return withTestCollection(persistence, testDocs, coll => {
return coll
.where('inf', '==', Infinity)
.get()
.then(docs => {
expect(toDataArray(docs)).to.deep.equal([{ inf: Infinity }]);
});
});
});
it('will not get metadata only updates', () => {
const testDocs = { a: { v: 'a' }, b: { v: 'b' } };
return withTestCollection(persistence, testDocs, coll => {
const storeEvent = new EventsAccumulator<firestore.QuerySnapshot>();
let unlisten: (() => void) | null = null;
return Promise.all([
coll.doc('a').set({ v: 'a' }),
coll.doc('b').set({ v: 'b' })
])
.then(() => {
unlisten = coll.onSnapshot(storeEvent.storeEvent);
return storeEvent.awaitEvent();
})
.then(querySnap => {
expect(toDataArray(querySnap)).to.deep.equal([
{ v: 'a' },
{ v: 'b' }
]);
return coll.doc('a').set({ v: 'a1' });
})
.then(() => {
return storeEvent.awaitEvent();
})
.then(querySnap => {
expect(toDataArray(querySnap)).to.deep.equal([
{ v: 'a1' },
{ v: 'b' }
]);
return storeEvent.assertNoAdditionalEvents();
})
.then(() => {
unlisten!();
});
});
});
it('maintains correct DocumentChange indices', async () => {
const testDocs = {
'a': { order: 1 },
'b': { order: 2 },
'c': { 'order': 3 }
};
await withTestCollection(persistence, testDocs, async coll => {
const accumulator = new EventsAccumulator<firestore.QuerySnapshot>();
const unlisten = coll.orderBy('order').onSnapshot(accumulator.storeEvent);
await accumulator
.awaitEvent()
.then(querySnapshot => {
const changes = querySnapshot.docChanges();
expect(changes.length).to.equal(3);
verifyDocumentChange(changes[0], 'a', -1, 0, 'added');
verifyDocumentChange(changes[1], 'b', -1, 1, 'added');
verifyDocumentChange(changes[2], 'c', -1, 2, 'added');
})
.then(() => coll.doc('b').set({ order: 4 }))
.then(() => accumulator.awaitEvent())
.then(querySnapshot => {
const changes = querySnapshot.docChanges();
expect(changes.length).to.equal(1);
verifyDocumentChange(changes[0], 'b', 1, 2, 'modified');
})
.then(() => coll.doc('c').delete())
.then(() => accumulator.awaitEvent())
.then(querySnapshot => {
const changes = querySnapshot.docChanges();
expect(changes.length).to.equal(1);
verifyDocumentChange(changes[0], 'c', 1, -1, 'removed');
});
unlisten();
});
});
it('can listen for the same query with different options', () => {
const testDocs = { a: { v: 'a' }, b: { v: 'b' } };
return withTestCollection(persistence, testDocs, coll => {
const storeEvent = new EventsAccumulator<firestore.QuerySnapshot>();
const storeEventFull = new EventsAccumulator<firestore.QuerySnapshot>();
const unlisten1 = coll.onSnapshot(storeEvent.storeEvent);
const unlisten2 = coll.onSnapshot(
{ includeMetadataChanges: true },
storeEventFull.storeEvent
);
return storeEvent
.awaitEvent()
.then(querySnap => {
expect(toDataArray(querySnap)).to.deep.equal([
{ v: 'a' },
{ v: 'b' }
]);
return storeEventFull.awaitEvent();
})
.then(async querySnap => {
expect(toDataArray(querySnap)).to.deep.equal([
{ v: 'a' },
{ v: 'b' }
]);
if (querySnap.metadata.fromCache) {
// We might receive an additional event if the first query snapshot
// was served from cache.
await storeEventFull.awaitEvent();
}
return coll.doc('a').set({ v: 'a1' });
})
.then(() => {
return storeEventFull.awaitEvents(2);
})
.then(events => {
// Expect two events for the write, once from latency compensation
// and once from the acknowledgment from the server.
expect(toDataArray(events[0])).to.deep.equal([
{ v: 'a1' },
{ v: 'b' }
]);
expect(toDataArray(events[1])).to.deep.equal([
{ v: 'a1' },
{ v: 'b' }
]);
const localResult = events[0].docs;
expect(localResult[0].metadata.hasPendingWrites).to.equal(true);
const syncedResults = events[1].docs;
expect(syncedResults[0].metadata.hasPendingWrites).to.equal(false);
return storeEvent.awaitEvent();
})
.then(querySnap => {
// Expect only one event for the write.
expect(toDataArray(querySnap)).to.deep.equal([
{ v: 'a1' },
{ v: 'b' }
]);
return storeEvent.assertNoAdditionalEvents();
})
.then(() => {
storeEvent.allowAdditionalEvents();
return coll.doc('b').set({ v: 'b1' });
})
.then(() => {
return storeEvent.awaitEvent();
})
.then(querySnap => {
// Expect only one event from the second write
expect(toDataArray(querySnap)).to.deep.equal([
{ v: 'a1' },
{ v: 'b1' }
]);
return storeEventFull.awaitEvents(2);
})
.then(events => {
// Expect 2 events from the second write.
expect(toDataArray(events[0])).to.deep.equal([
{ v: 'a1' },
{ v: 'b1' }
]);
expect(toDataArray(events[1])).to.deep.equal([
{ v: 'a1' },
{ v: 'b1' }
]);
const localResults = events[0].docs;
expect(localResults[1].metadata.hasPendingWrites).to.equal(true);
const syncedResults = events[1].docs;
expect(syncedResults[1].metadata.hasPendingWrites).to.equal(false);
return storeEvent.assertNoAdditionalEvents();
})
.then(() => {
return storeEventFull.assertNoAdditionalEvents();
})
.then(() => {
unlisten1!();
unlisten2!();
});
});
});
it('can issue queries with Dates differing in milliseconds', () => {
const date1 = new Date();
date1.setMilliseconds(0);
const date2 = new Date(date1.getTime());
date2.setMilliseconds(1);
const date3 = new Date(date1.getTime());
date3.setMilliseconds(2);
const testDocs = {
'1': { id: '1', date: date1 },
'2': { id: '2', date: date2 },
'3': { id: '3', date: date3 }
};
return withTestCollection(persistence, testDocs, coll => {
// Make sure to issue the queries in parallel
const docs1Promise = coll.where('date', '>', date1).get();
const docs2Promise = coll.where('date', '>', date2).get();
return Promise.all([docs1Promise, docs2Promise]).then(results => {
const docs1 = results[0];
const docs2 = results[1];
expect(toDataArray(docs1)).to.deep.equal([
{ id: '2', date: Timestamp.fromDate(date2) },
{ id: '3', date: Timestamp.fromDate(date3) }
]);
expect(toDataArray(docs2)).to.deep.equal([
{ id: '3', date: Timestamp.fromDate(date3) }
]);
});
});
});
it('can listen for QueryMetadata changes', () => {
const testDocs = {
'1': { sort: 1, filter: true, key: '1' },
'2': { sort: 2, filter: true, key: '2' },
'3': { sort: 2, filter: true, key: '3' },
'4': { sort: 3, filter: false, key: '4' }
};
return withTestCollection(persistence, testDocs, coll => {
const query = coll.where('key', '<', '4');
const accum = new EventsAccumulator<firestore.QuerySnapshot>();
let unlisten2: () => void;
const unlisten1 = query.onSnapshot(result => {
expect(toDataArray(result)).to.deep.equal([
testDocs[1],
testDocs[2],
testDocs[3]
]);
const query2 = coll.where('filter', '==', true);
unlisten2 = query2.onSnapshot(
{
includeMetadataChanges: true
},
accum.storeEvent
);
});
return accum.awaitEvents(2).then(events => {
const results1 = events[0];
const results2 = events[1];
expect(toDataArray(results1)).to.deep.equal([
testDocs[1],
testDocs[2],
testDocs[3]
]);
expect(toDataArray(results1)).to.deep.equal(toDataArray(results2));
expect(results1.metadata.fromCache).to.equal(true);
expect(results2.metadata.fromCache).to.equal(false);
unlisten1();
unlisten2();
});
});
});
it('can listen for metadata changes', () => {
const initialDoc = {
foo: { a: 'b', v: 1 }
};
const modifiedDoc = {
foo: { a: 'b', v: 2 }
};
return withTestCollection(persistence, initialDoc, async coll => {
const accum = new EventsAccumulator<firestore.QuerySnapshot>();
const unlisten = coll.onSnapshot(
{ includeMetadataChanges: true },
accum.storeEvent
);
await accum.awaitEvents(1).then(events => {
const results1 = events[0];
expect(toDataArray(results1)).to.deep.equal([initialDoc['foo']]);
});
// eslint-disable-next-line @typescript-eslint/no-floating-promises
coll.doc('foo').set(modifiedDoc['foo']);
await accum.awaitEvents(2).then(events => {
const results1 = events[0];
expect(toDataArray(results1)).to.deep.equal([modifiedDoc['foo']]);
expect(toChangesArray(results1)).to.deep.equal([modifiedDoc['foo']]);
const results2 = events[1];
expect(toDataArray(results2)).to.deep.equal([modifiedDoc['foo']]);
expect(toChangesArray(results2)).to.deep.equal([]);
expect(
toChangesArray(results2, { includeMetadataChanges: true })
).to.deep.equal([modifiedDoc['foo']]);
});
unlisten();
});
});
it('can explicitly sort by document ID', () => {
const testDocs = {
a: { key: 'a' },
b: { key: 'b' },
c: { key: 'c' }
};
return withTestCollection(persistence, testDocs, coll => {
// Ideally this would be descending to validate it's different than
// the default, but that requires an extra index
return coll
.orderBy(FieldPath.documentId())
.get()
.then(docs => {
expect(toDataArray(docs)).to.deep.equal([
testDocs['a'],
testDocs['b'],
testDocs['c']
]);
});
});
});
it('can query by document ID', () => {
const testDocs = {
aa: { key: 'aa' },
ab: { key: 'ab' },
ba: { key: 'ba' },
bb: { key: 'bb' }
};
return withTestCollection(persistence, testDocs, coll => {
return coll
.where(FieldPath.documentId(), '==', 'ab')
.get()
.then(docs => {
expect(toDataArray(docs)).to.deep.equal([testDocs['ab']]);
return coll
.where(FieldPath.documentId(), '>', 'aa')
.where(FieldPath.documentId(), '<=', 'ba')
.get();
})
.then(docs => {
expect(toDataArray(docs)).to.deep.equal([
testDocs['ab'],
testDocs['ba']
]);
});
});
});
it('can query by document ID using refs', () => {
const testDocs = {
aa: { key: 'aa' },
ab: { key: 'ab' },
ba: { key: 'ba' },
bb: { key: 'bb' }
};
return withTestCollection(persistence, testDocs, coll => {
return coll
.where(FieldPath.documentId(), '==', coll.doc('ab'))
.get()
.then(docs => {
expect(toDataArray(docs)).to.deep.equal([testDocs['ab']]);
return coll
.where(FieldPath.documentId(), '>', coll.doc('aa'))
.where(FieldPath.documentId(), '<=', coll.doc('ba'))
.get();
})
.then(docs => {
expect(toDataArray(docs)).to.deep.equal([
testDocs['ab'],
testDocs['ba']
]);
});
});
});
it('can query while reconnecting to network', () => {
return withTestCollection(persistence, /* docs= */ {}, coll => {
const deferred = new Deferred<void>();
const unregister = coll.onSnapshot(
{ includeMetadataChanges: true },
snapshot => {
if (!snapshot.empty && !snapshot.metadata.fromCache) {
deferred.resolve();
}
}
);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
coll.firestore.disableNetwork().then(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
coll.doc().set({ a: 1 });
// eslint-disable-next-line @typescript-eslint/no-floating-promises
coll.firestore.enableNetwork();
});
return deferred.promise.then(unregister);
});
});
it('trigger with isFromCache=true when offline', () => {
return withTestCollection(persistence, { a: { foo: 1 } }, coll => {
const firestore = coll.firestore;
const accum = new EventsAccumulator<firestore.QuerySnapshot>();
const unregister = coll.onSnapshot(
{ includeMetadataChanges: true },
accum.storeEvent
);
return accum
.awaitEvent()
.then(querySnap => {
// initial event
expect(querySnap.docs.map(doc => doc.data())).to.deep.equal([
{ foo: 1 }
]);
expect(querySnap.metadata.fromCache).to.be.false;
})
.then(() => firestore.disableNetwork())
.then(() => accum.awaitEvent())
.then(querySnap => {
// offline event with fromCache = true
expect(querySnap.metadata.fromCache).to.be.true;
})
.then(() => firestore.enableNetwork())
.then(() => accum.awaitEvent())
.then(querySnap => {
// back online event with fromCache = false
expect(querySnap.metadata.fromCache).to.be.false;
unregister();
});
});
});
it('can use != filters', async () => {
// These documents are ordered by value in "zip" since the '!=' filter is
// an inequality, which results in documents being sorted by value.
const testDocs = {
a: { zip: Number.NaN },
b: { zip: 91102 },
c: { zip: 98101 },
d: { zip: '98101' },
e: { zip: [98101] },
f: { zip: [98101, 98102] },
g: { zip: ['98101', { zip: 98101 }] },
h: { zip: { code: 500 } },
i: { code: 500 },
j: { zip: null }
};
await withTestCollection(persistence, testDocs, async coll => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let expected: { [name: string]: any } = { ...testDocs };
delete expected.c;
delete expected.i;
delete expected.j;
const snapshot = await coll.where('zip', '!=', 98101).get();
expect(toDataArray(snapshot)).to.deep.equal(Object.values(expected));
// With objects.
const snapshot2 = await coll.where('zip', '!=', { code: 500 }).get();
expected = { ...testDocs };
delete expected.h;
delete expected.i;
delete expected.j;
expect(toDataArray(snapshot2)).to.deep.equal(Object.values(expected));
// With null.
const snapshot3 = await coll.where('zip', '!=', null).get();
expected = { ...testDocs };
delete expected.i;
delete expected.j;
expect(toDataArray(snapshot3)).to.deep.equal(Object.values(expected));
// With NaN.
const snapshot4 = await coll.where('zip', '!=', Number.NaN).get();
expected = { ...testDocs };
delete expected.a;
delete expected.i;
delete expected.j;
expect(toDataArray(snapshot4)).to.deep.equal(Object.values(expected));
});
});
it('can use != filters by document ID', async () => {
const testDocs = {
aa: { key: 'aa' },
ab: { key: 'ab' },
ba: { key: 'ba' },
bb: { key: 'bb' }
};
await withTestCollection(persistence, testDocs, async coll => {
const snapshot = await coll
.where(FieldPath.documentId(), '!=', 'aa')
.get();
expect(toDataArray(snapshot)).to.deep.equal([
{ key: 'ab' },
{ key: 'ba' },
{ key: 'bb' }
]);
});
});
it('can use array-contains filters', async () => {
const testDocs = {
a: { array: [42] },
b: { array: ['a', 42, 'c'] },
c: { array: [41.999, '42', { a: [42] }] },
d: { array: [42], array2: ['bingo'] },
e: { array: [null] },
f: { array: [Number.NaN] }
};
await withTestCollection(persistence, testDocs, async coll => {
// Search for 42
const snapshot = await coll.where('array', 'array-contains', 42).get();
expect(toDataArray(snapshot)).to.deep.equal([
{ array: [42] },
{ array: ['a', 42, 'c'] },
{ array: [42], array2: ['bingo'] }
]);
// NOTE: The backend doesn't currently support null, NaN, objects, or
// arrays, so there isn't much of anything else interesting to test.
// With null.
const snapshot3 = await coll.where('zip', 'array-contains', null).get();
expect(toDataArray(snapshot3)).to.deep.equal([]);
// With NaN.
const snapshot4 = await coll
.where('zip', 'array-contains', Number.NaN)
.get();
expect(toDataArray(snapshot4)).to.deep.equal([]);
});
});
it('can use IN filters', async () => {
const testDocs = {
a: { zip: 98101 },
b: { zip: 91102 },
c: { zip: 98103 },
d: { zip: [98101] },
e: { zip: ['98101', { zip: 98101 }] },
f: { zip: { code: 500 } },
g: { zip: [98101, 98102] },
h: { zip: null },
i: { zip: Number.NaN }
};
await withTestCollection(persistence, testDocs, async coll => {
const snapshot = await coll
.where('zip', 'in', [98101, 98103, [98101, 98102]])
.get();
expect(toDataArray(snapshot)).to.deep.equal([
{ zip: 98101 },
{ zip: 98103 },
{ zip: [98101, 98102] }
]);
// With objects.
const snapshot2 = await coll.where('zip', 'in', [{ code: 500 }]).get();
expect(toDataArray(snapshot2)).to.deep.equal([{ zip: { code: 500 } }]);
// With null.
const snapshot3 = await coll.where('zip', 'in', [null]).get();
expect(toDataArray(snapshot3)).to.deep.equal([]);
// With null and a value.
const snapshot4 = await coll.where('zip', 'in', [98101, null]).get();
expect(toDataArray(snapshot4)).to.deep.equal([{ zip: 98101 }]);
// With NaN.
const snapshot5 = await coll.where('zip', 'in', [Number.NaN]).get();
expect(toDataArray(snapshot5)).to.deep.equal([]);
// With NaN and a value.
const snapshot6 = await coll
.where('zip', 'in', [98101, Number.NaN])
.get();
expect(toDataArray(snapshot6)).to.deep.equal([{ zip: 98101 }]);
});
});
it('can use IN filters by document ID', async () => {
const testDocs = {
aa: { key: 'aa' },
ab: { key: 'ab' },
ba: { key: 'ba' },
bb: { key: 'bb' }
};
await withTestCollection(persistence, testDocs, async coll => {
const snapshot = await coll
.where(FieldPath.documentId(), 'in', ['aa', 'ab'])
.get();
expect(toDataArray(snapshot)).to.deep.equal([
{ key: 'aa' },
{ key: 'ab' }
]);
});
});
it('can use NOT_IN filters', async () => {
// These documents are ordered by value in "zip" since the 'not-in' filter is
// an inequality, which results in documents being sorted by value.
const testDocs = {
a: { zip: Number.NaN },
b: { zip: 91102 },
c: { zip: 98101 },
d: { zip: 98103 },
e: { zip: [98101] },
f: { zip: [98101, 98102] },
g: { zip: ['98101', { zip: 98101 }] },
h: { zip: { code: 500 } },
i: { code: 500 },
j: { zip: null }
};
await withTestCollection(persistence, testDocs, async coll => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let expected: { [name: string]: any } = { ...testDocs };
delete expected.c;
delete expected.d;
delete expected.f;
delete expected.i;
delete expected.j;
const snapshot = await coll
.where('zip', 'not-in', [98101, 98103, [98101, 98102]])
.get();
expect(toDataArray(snapshot)).to.deep.equal(Object.values(expected));
// With objects.
const snapshot2 = await coll
.where('zip', 'not-in', [{ code: 500 }])
.get();
expected = { ...testDocs };
delete expected.h;
delete expected.i;
delete expected.j;
expect(toDataArray(snapshot2)).to.deep.equal(Object.values(expected));
// With null.
const snapshot3 = await coll.where('zip', 'not-in', [null]).get();
expect(toDataArray(snapshot3)).to.deep.equal([]);
// With NaN.
const snapshot4 = await coll.where('zip', 'not-in', [Number.NaN]).get();
expected = { ...testDocs };
delete expected.a;
delete expected.i;
delete expected.j;
expect(toDataArray(snapshot4)).to.deep.equal(Object.values(expected));
// With NaN and a number.
const snapshot5 = await coll
.where('zip', 'not-in', [Number.NaN, 98101])
.get();
expected = { ...testDocs };
delete expected.a;
delete expected.c;
delete expected.i;
delete expected.j;
expect(toDataArray(snapshot5)).to.deep.equal(Object.values(expected));
});
});
it('can use NOT_IN filters by document ID', async () => {
const testDocs = {
aa: { key: 'aa' },
ab: { key: 'ab' },
ba: { key: 'ba' },
bb: { key: 'bb' }
};
await withTestCollection(persistence, testDocs, async coll => {
const snapshot = await coll
.where(FieldPath.documentId(), 'not-in', ['aa', 'ab'])
.get();
expect(toDataArray(snapshot)).to.deep.equal([
{ key: 'ba' },
{ key: 'bb' }
]);
});
});
it('can use array-contains-any filters', async () => {
const testDocs = {
a: { array: [42] },
b: { array: ['a', 42, 'c'] },
c: { array: [41.999, '42', { a: [42] }] },
d: { array: [42], array2: ['bingo'] },
e: { array: [43] },
f: { array: [{ a: 42 }] },
g: { array: 42 },
h: { array: [null] },
i: { array: [Number.NaN] }
};
await withTestCollection(persistence, testDocs, async coll => {
const snapshot = await coll
.where('array', 'array-contains-any', [42, 43])
.get();
expect(toDataArray(snapshot)).to.deep.equal([
{ array: [42] },
{ array: ['a', 42, 'c'] },
{ array: [42], array2: ['bingo'] },
{ array: [43] }
]);
// With objects.
const snapshot2 = await coll
.where('array', 'array-contains-any', [{ a: 42 }])
.get();
expect(toDataArray(snapshot2)).to.deep.equal([{ array: [{ a: 42 }] }]);
// With null.
const snapshot3 = await coll
.where('array', 'array-contains-any', [null])
.get();
expect(toDataArray(snapshot3)).to.deep.equal([]);
// With null and a value.
const snapshot4 = await coll
.where('array', 'array-contains-any', [43, null])
.get();
expect(toDataArray(snapshot4)).to.deep.equal([{ array: [43] }]);
// With NaN.
const snapshot5 = await coll
.where('array', 'array-contains-any', [Number.NaN])
.get();
expect(toDataArray(snapshot5)).to.deep.equal([]);
// With NaN and a value.
const snapshot6 = await coll
.where('array', 'array-contains-any', [43, Number.NaN])
.get();
expect(toDataArray(snapshot6)).to.deep.equal([{ array: [43] }]);
});
});
it('can query collection groups', async () => {
await withTestDb(persistence, async db => {
// Use .doc() to get a random collection group name to use but ensure it starts with 'b' for
// predictable ordering.
const collectionGroup = 'b' + db.collection('foo').doc().id;
const docPaths = [
`abc/123/${collectionGroup}/cg-doc1`,
`abc/123/${collectionGroup}/cg-doc2`,
`${collectionGroup}/cg-doc3`,
`${collectionGroup}/cg-doc4`,
`def/456/${collectionGroup}/cg-doc5`,
`${collectionGroup}/virtual-doc/nested-coll/not-cg-doc`,
`x${collectionGroup}/not-cg-doc`,
`${collectionGroup}x/not-cg-doc`,
`abc/123/${collectionGroup}x/not-cg-doc`,
`abc/123/x${collectionGroup}/not-cg-doc`,
`abc/${collectionGroup}`
];
const batch = db.batch();
for (const docPath of docPaths) {
batch.set(db.doc(docPath), { x: 1 });
}
await batch.commit();
const querySnapshot = await db.collectionGroup(collectionGroup).get();
expect(querySnapshot.docs.map(d => d.id)).to.deep.equal([
'cg-doc1',
'cg-doc2',
'cg-doc3',
'cg-doc4',
'cg-doc5'
]);
});
});
it('can query collection groups with startAt / endAt by arbitrary documentId', async () => {
await withTestDb(persistence, async db => {
// Use .doc() to get a random collection group name to use but ensure it starts with 'b' for
// predictable ordering.
const collectionGroup = 'b' + db.collection('foo').doc().id;
const docPaths = [
`a/a/${collectionGroup}/cg-doc1`,
`a/b/a/b/${collectionGroup}/cg-doc2`,
`a/b/${collectionGroup}/cg-doc3`,
`a/b/c/d/${collectionGroup}/cg-doc4`,
`a/c/${collectionGroup}/cg-doc5`,
`${collectionGroup}/cg-doc6`,
`a/b/nope/nope`
];
const batch = db.batch();
for (const docPath of docPaths) {
batch.set(db.doc(docPath), { x: 1 });
}
await batch.commit();
let querySnapshot = await db
.collectionGroup(collectionGroup)
.orderBy(FieldPath.documentId())
.startAt(`a/b`)
.endAt('a/b0')
.get();
expect(querySnapshot.docs.map(d => d.id)).to.deep.equal([
'cg-doc2',
'cg-doc3',
'cg-doc4'
]);
querySnapshot = await db
.collectionGroup(collectionGroup)
.orderBy(FieldPath.documentId())
.startAfter('a/b')
.endBefore(`a/b/${collectionGroup}/cg-doc3`)
.get();
expect(querySnapshot.docs.map(d => d.id)).to.deep.equal(['cg-doc2']);
});
});
it('can query collection groups with where filters on arbitrary documentId', async () => {
await withTestDb(persistence, async db => {
// Use .doc() to get a random collection group name to use but ensure it starts with 'b' for
// predictable ordering.
const collectionGroup = 'b' + db.collection('foo').doc().id;
const docPaths = [
`a/a/${collectionGroup}/cg-doc1`,
`a/b/a/b/${collectionGroup}/cg-doc2`,
`a/b/${collectionGroup}/cg-doc3`,
`a/b/c/d/${collectionGroup}/cg-doc4`,
`a/c/${collectionGroup}/cg-doc5`,
`${collectionGroup}/cg-doc6`,
`a/b/nope/nope`
];
const batch = db.batch();
for (const docPath of docPaths) {
batch.set(db.doc(docPath), { x: 1 });
}
await batch.commit();
let querySnapshot = await db
.collectionGroup(collectionGroup)
.where(FieldPath.documentId(), '>=', `a/b`)
.where(FieldPath.documentId(), '<=', 'a/b0')
.get();
expect(querySnapshot.docs.map(d => d.id)).to.deep.equal([
'cg-doc2',
'cg-doc3',
'cg-doc4'
]);
querySnapshot = await db
.collectionGroup(collectionGroup)
.where(FieldPath.documentId(), '>', `a/b`)
.where(FieldPath.documentId(), '<', `a/b/${collectionGroup}/cg-doc3`)
.get();
expect(querySnapshot.docs.map(d => d.id)).to.deep.equal(['cg-doc2']);
});
});
it('can query custom types', () => {
return withTestCollection(persistence, {}, async ref => {
const data = {
ref: ref.firestore.doc('f/c'),
geoPoint: new GeoPoint(0, 0),
buffer: Blob.fromBase64String('Zm9v'),
time: Timestamp.now(),
array: [
ref.firestore.doc('f/c'),
new GeoPoint(0, 0),
Blob.fromBase64String('Zm9v'),
Timestamp.now()
]
};
await ref.add({ data });
// In https://github.com/firebase/firebase-js-sdk/issues/1524, a
// customer was not able to unlisten from a query that contained a
// nested object with a DocumentReference. The cause of it was that our
// serialization of nested references via JSON.stringify() was different
// for Queries created via the API layer versus Queries read from
// persistence. To simulate this issue, we have to listen and unlisten
// to the same query twice.
const query = ref.where('data', '==', data);
for (let i = 0; i < 2; ++i) {
const deferred = new Deferred<void>();
const unsubscribe = query.onSnapshot(snapshot => {
expect(snapshot.size).to.equal(1);
deferred.resolve();
});
await deferred.promise;
unsubscribe();
}
});
});
it('can use filter with nested field', () => {
// Reproduces https://github.com/firebase/firebase-js-sdk/issues/2204
const testDocs = {
a: {},
b: { map: {} },
c: { map: { nested: {} } },
d: { map: { nested: 'foo' } }
};
return withTestCollection(persistence, testDocs, async coll => {
await coll.get(); // Populate the cache
const snapshot = await coll.where('map.nested', '==', 'foo').get();
expect(toDataArray(snapshot)).to.deep.equal([{ map: { nested: 'foo' } }]);
});
});
});
function verifyDocumentChange<T>(
change: firestore.DocumentChange<T>,
id: string,
oldIndex: number,
newIndex: number,
type: firestore.DocumentChangeType
): void {
expect(change.doc.id).to.equal(id);
expect(change.type).to.equal(type);
expect(change.oldIndex).to.equal(oldIndex);
expect(change.newIndex).to.equal(newIndex);
}