mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
3139 lines
102 KiB
TypeScript
3139 lines
102 KiB
TypeScript
/**
|
||
* @license
|
||
* Copyright 2017 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 { deleteApp } from '@firebase/app';
|
||
import { Deferred, isNode } from '@firebase/util';
|
||
import { expect, use } from 'chai';
|
||
import chaiAsPromised from 'chai-as-promised';
|
||
|
||
import { it } from '../../util/mocha_extensions';
|
||
import { EventsAccumulator } from '../util/events_accumulator';
|
||
import {
|
||
addDoc,
|
||
clearIndexedDbPersistence,
|
||
collection,
|
||
collectionGroup,
|
||
deleteDoc,
|
||
deleteField,
|
||
disableNetwork,
|
||
doc,
|
||
DocumentData,
|
||
documentId,
|
||
DocumentSnapshot,
|
||
documentSnapshotFromJSON,
|
||
enableIndexedDbPersistence,
|
||
enableNetwork,
|
||
getDoc,
|
||
getDocFromCache,
|
||
getDocFromServer,
|
||
getDocs,
|
||
initializeFirestore,
|
||
limit,
|
||
onSnapshot,
|
||
onSnapshotResume,
|
||
onSnapshotsInSync,
|
||
orderBy,
|
||
query,
|
||
queryEqual,
|
||
refEqual,
|
||
serverTimestamp,
|
||
setDoc,
|
||
SetOptions,
|
||
terminate,
|
||
updateDoc,
|
||
waitForPendingWrites,
|
||
where,
|
||
writeBatch,
|
||
QueryDocumentSnapshot,
|
||
DocumentReference,
|
||
runTransaction,
|
||
WithFieldValue,
|
||
Timestamp,
|
||
FieldPath,
|
||
newTestFirestore,
|
||
SnapshotOptions,
|
||
newTestApp,
|
||
FirestoreError,
|
||
QuerySnapshot,
|
||
querySnapshotFromJSON,
|
||
vector,
|
||
getDocsFromServer
|
||
} from '../util/firebase_export';
|
||
import {
|
||
apiDescribe,
|
||
withTestCollection,
|
||
withTestDbsSettings,
|
||
withTestDb,
|
||
withTestDbs,
|
||
withTestDoc,
|
||
withTestDocAndInitialData,
|
||
withNamedTestDbsOrSkipUnlessUsingEmulator,
|
||
toDataArray,
|
||
checkOnlineAndOfflineResultsMatch,
|
||
toIds
|
||
} from '../util/helpers';
|
||
import { DEFAULT_SETTINGS, DEFAULT_PROJECT_ID } from '../util/settings';
|
||
|
||
use(chaiAsPromised);
|
||
|
||
apiDescribe('Database', persistence => {
|
||
it('can set a document', () => {
|
||
return withTestDoc(persistence, docRef => {
|
||
return setDoc(docRef, {
|
||
desc: 'Stuff related to Firestore project...',
|
||
owner: {
|
||
name: 'Jonny',
|
||
title: 'scallywag'
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
it('doc() will auto generate an ID', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const ref = doc(collection(db, 'foo'));
|
||
// Auto IDs are 20 characters long
|
||
expect(ref.id.length).to.equal(20);
|
||
});
|
||
});
|
||
|
||
it('can delete a document', () => {
|
||
// TODO(#1865): This test fails with node:persistence against Prod
|
||
return withTestDoc(persistence, docRef => {
|
||
return setDoc(docRef, { foo: 'bar' })
|
||
.then(() => getDoc(docRef))
|
||
.then(doc => {
|
||
expect(doc.data()).to.deep.equal({ foo: 'bar' });
|
||
return deleteDoc(docRef);
|
||
})
|
||
.then(() => getDoc(docRef))
|
||
.then(doc => {
|
||
expect(doc.exists()).to.equal(false);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can update existing document', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
const initialData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Jonny', email: '[email protected]' }
|
||
};
|
||
const updateData = {
|
||
desc: 'NewDescription',
|
||
'owner.email': '[email protected]'
|
||
};
|
||
const finalData = {
|
||
desc: 'NewDescription',
|
||
owner: { name: 'Jonny', email: '[email protected]' }
|
||
};
|
||
return setDoc(doc, initialData)
|
||
.then(() => updateDoc(doc, updateData))
|
||
.then(() => getDoc(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.exists).to.be.ok;
|
||
expect(docSnapshot.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can retrieve document that does not exist', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
return getDoc(doc).then(snapshot => {
|
||
expect(snapshot.exists()).to.equal(false);
|
||
expect(snapshot.data()).to.equal(undefined);
|
||
expect(snapshot.get('foo')).to.equal(undefined);
|
||
});
|
||
});
|
||
});
|
||
|
||
// eslint-disable-next-line no-restricted-properties
|
||
(persistence.gc === 'lru' ? it : it.skip)(
|
||
'can update an unknown document',
|
||
() => {
|
||
return withTestDbs(persistence, 2, async ([reader, writer]) => {
|
||
const writerRef = doc(collection(writer, 'collection'));
|
||
const readerRef = doc(collection(reader, 'collection'), writerRef.id);
|
||
await setDoc(writerRef, { a: 'a' });
|
||
await updateDoc(readerRef, { b: 'b' });
|
||
await getDocFromCache(writerRef).then(
|
||
doc => expect(doc.exists()).to.be.true
|
||
);
|
||
await getDocFromCache(readerRef).then(
|
||
() => {
|
||
expect.fail('Expected cache miss');
|
||
},
|
||
err => expect(err.code).to.be.equal('unavailable')
|
||
);
|
||
await getDoc(writerRef).then(doc =>
|
||
expect(doc.data()).to.deep.equal({ a: 'a', b: 'b' })
|
||
);
|
||
await getDoc(readerRef).then(doc =>
|
||
expect(doc.data()).to.deep.equal({ a: 'a', b: 'b' })
|
||
);
|
||
});
|
||
}
|
||
);
|
||
|
||
it('can merge data with an existing document using set', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
const initialData = {
|
||
desc: 'description',
|
||
'owner.data': { name: 'Jonny', email: '[email protected]' }
|
||
};
|
||
const mergeData = {
|
||
updated: true,
|
||
'owner.data': { name: 'Sebastian' }
|
||
};
|
||
const finalData = {
|
||
updated: true,
|
||
desc: 'description',
|
||
'owner.data': { name: 'Sebastian', email: '[email protected]' }
|
||
};
|
||
return setDoc(doc, initialData)
|
||
.then(() => setDoc(doc, mergeData, { merge: true }))
|
||
.then(() => getDoc(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.exists).to.be.ok;
|
||
expect(docSnapshot.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can merge server timestamps', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
const initialData = {
|
||
updated: false
|
||
};
|
||
const mergeData = {
|
||
time: serverTimestamp(),
|
||
nested: { time: serverTimestamp() }
|
||
};
|
||
return setDoc(doc, initialData)
|
||
.then(() => setDoc(doc, mergeData, { merge: true }))
|
||
.then(() => getDoc(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.exists).to.be.ok;
|
||
expect(docSnapshot.get('updated')).to.be.false;
|
||
expect(docSnapshot.get('time')).to.be.an.instanceof(Timestamp);
|
||
expect(docSnapshot.get('nested.time')).to.be.an.instanceof(Timestamp);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can merge empty object', async () => {
|
||
await withTestDoc(persistence, async doc => {
|
||
const accumulator = new EventsAccumulator<DocumentSnapshot>();
|
||
const unsubscribe = onSnapshot(doc, accumulator.storeEvent);
|
||
await accumulator
|
||
.awaitEvent()
|
||
.then(() => setDoc(doc, {}))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(docSnapshot => expect(docSnapshot.data()).to.be.deep.equal({}))
|
||
.then(() => setDoc(doc, { a: {} }, { mergeFields: ['a'] }))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(docSnapshot =>
|
||
expect(docSnapshot.data()).to.be.deep.equal({ a: {} })
|
||
)
|
||
.then(() => setDoc(doc, { b: {} }, { merge: true }))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(docSnapshot =>
|
||
expect(docSnapshot.data()).to.be.deep.equal({ a: {}, b: {} })
|
||
)
|
||
.then(() => getDocFromServer(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.data()).to.be.deep.equal({ a: {}, b: {} });
|
||
});
|
||
|
||
unsubscribe();
|
||
});
|
||
});
|
||
|
||
it('update with empty object replaces all fields', () => {
|
||
return withTestDoc(persistence, async doc => {
|
||
await setDoc(doc, { a: 'a' });
|
||
await updateDoc(doc, 'a', {});
|
||
const docSnapshot = await getDoc(doc);
|
||
expect(docSnapshot.data()).to.be.deep.equal({ a: {} });
|
||
});
|
||
});
|
||
|
||
it('merge with empty object replaces all fields', () => {
|
||
return withTestDoc(persistence, async doc => {
|
||
await setDoc(doc, { a: 'a' });
|
||
await setDoc(doc, { 'a': {} }, { merge: true });
|
||
const docSnapshot = await getDoc(doc);
|
||
expect(docSnapshot.data()).to.be.deep.equal({ a: {} });
|
||
});
|
||
});
|
||
|
||
it('can delete field using merge', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
const initialData = {
|
||
untouched: true,
|
||
foo: 'bar',
|
||
nested: { untouched: true, foo: 'bar' }
|
||
};
|
||
const mergeData = {
|
||
foo: deleteField(),
|
||
nested: { foo: deleteField() }
|
||
};
|
||
const finalData = {
|
||
untouched: true,
|
||
nested: { untouched: true }
|
||
};
|
||
return setDoc(doc, initialData)
|
||
.then(() => setDoc(doc, mergeData, { merge: true }))
|
||
.then(() => getDoc(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.exists).to.be.ok;
|
||
expect(docSnapshot.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can delete field using mergeFields', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
const initialData = {
|
||
untouched: true,
|
||
foo: 'bar',
|
||
inner: { removed: true, foo: 'bar' },
|
||
nested: { untouched: true, foo: 'bar' }
|
||
};
|
||
const mergeData = {
|
||
foo: deleteField(),
|
||
inner: { foo: deleteField() },
|
||
nested: {
|
||
untouched: deleteField(),
|
||
foo: deleteField()
|
||
}
|
||
};
|
||
const finalData = {
|
||
untouched: true,
|
||
inner: {},
|
||
nested: { untouched: true }
|
||
};
|
||
return setDoc(doc, initialData)
|
||
.then(() =>
|
||
setDoc(doc, mergeData, {
|
||
mergeFields: ['foo', 'inner', 'nested.foo']
|
||
})
|
||
)
|
||
.then(() => getDoc(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.exists).to.be.ok;
|
||
expect(docSnapshot.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can set server timestamps using mergeFields', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
const initialData = {
|
||
untouched: true,
|
||
foo: 'bar',
|
||
nested: { untouched: true, foo: 'bar' }
|
||
};
|
||
const mergeData = {
|
||
foo: serverTimestamp(),
|
||
inner: { foo: serverTimestamp() },
|
||
nested: { foo: serverTimestamp() }
|
||
};
|
||
return setDoc(doc, initialData)
|
||
.then(() =>
|
||
setDoc(doc, mergeData, {
|
||
mergeFields: ['foo', 'inner', 'nested.foo']
|
||
})
|
||
)
|
||
.then(() => getDoc(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.exists).to.be.ok;
|
||
expect(docSnapshot.get('foo')).to.be.instanceof(Timestamp);
|
||
expect(docSnapshot.get('inner.foo')).to.be.instanceof(Timestamp);
|
||
expect(docSnapshot.get('nested.foo')).to.be.instanceof(Timestamp);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can replace an array by merging using set', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
const initialData = {
|
||
untouched: true,
|
||
data: 'old',
|
||
topLevel: ['old', 'old'],
|
||
mapInArray: [{ data: 'old' }]
|
||
};
|
||
const mergeData = {
|
||
data: 'new',
|
||
topLevel: ['new'],
|
||
mapInArray: [{ data: 'new' }]
|
||
};
|
||
const finalData = {
|
||
untouched: true,
|
||
data: 'new',
|
||
topLevel: ['new'],
|
||
mapInArray: [{ data: 'new' }]
|
||
};
|
||
return setDoc(doc, initialData)
|
||
.then(() => setDoc(doc, mergeData, { merge: true }))
|
||
.then(() => getDoc(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.exists).to.be.ok;
|
||
expect(docSnapshot.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
});
|
||
|
||
it("can't specify a field mask for a missing field using set", () => {
|
||
return withTestDoc(persistence, async docRef => {
|
||
expect(() => {
|
||
void setDoc(
|
||
docRef,
|
||
{ desc: 'NewDescription' },
|
||
{ mergeFields: ['desc', 'owner'] }
|
||
);
|
||
}).to.throw(
|
||
"Field 'owner' is specified in your field mask but missing from your input data."
|
||
);
|
||
});
|
||
});
|
||
|
||
it('can set a subset of fields using a field mask', () => {
|
||
const initialData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Jonny', email: '[email protected]' }
|
||
};
|
||
const finalData = { desc: 'Description', owner: 'Sebastian' };
|
||
return withTestDocAndInitialData(persistence, initialData, async docRef => {
|
||
await setDoc(
|
||
docRef,
|
||
{ desc: 'NewDescription', owner: 'Sebastian' },
|
||
{ mergeFields: ['owner'] }
|
||
);
|
||
const result = await getDoc(docRef);
|
||
expect(result.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
|
||
it("doesn't apply field delete outside of mask", () => {
|
||
const initialData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Jonny', email: '[email protected]' }
|
||
};
|
||
const finalData = { desc: 'Description', owner: 'Sebastian' };
|
||
return withTestDocAndInitialData(persistence, initialData, async docRef => {
|
||
await setDoc(
|
||
docRef,
|
||
{ desc: deleteField(), owner: 'Sebastian' },
|
||
{ mergeFields: ['owner'] }
|
||
);
|
||
const result = await getDoc(docRef);
|
||
expect(result.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
|
||
it("doesn't apply field transform outside of mask", () => {
|
||
const initialData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Jonny', email: '[email protected]' }
|
||
};
|
||
const finalData = { desc: 'Description', owner: 'Sebastian' };
|
||
return withTestDocAndInitialData(persistence, initialData, async docRef => {
|
||
await setDoc(
|
||
docRef,
|
||
{
|
||
desc: serverTimestamp(),
|
||
owner: 'Sebastian'
|
||
},
|
||
{ mergeFields: ['owner'] }
|
||
);
|
||
const result = await getDoc(docRef);
|
||
expect(result.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
|
||
it('can set an empty field mask', () => {
|
||
const initialData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Jonny', email: '[email protected]' }
|
||
};
|
||
const finalData = initialData;
|
||
return withTestDocAndInitialData(persistence, initialData, async docRef => {
|
||
await setDoc(
|
||
docRef,
|
||
{ desc: 'NewDescription', owner: 'Sebastian' },
|
||
{ mergeFields: [] }
|
||
);
|
||
const result = await getDoc(docRef);
|
||
expect(result.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
|
||
it('can specify fields multiple times in a field mask', () => {
|
||
const initialData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Jonny', email: '[email protected]' }
|
||
};
|
||
const finalData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Sebastian', email: '[email protected]' }
|
||
};
|
||
return withTestDocAndInitialData(persistence, initialData, async docRef => {
|
||
await setDoc(
|
||
docRef,
|
||
{
|
||
desc: 'NewDescription',
|
||
owner: { name: 'Sebastian', email: '[email protected]' }
|
||
},
|
||
{ mergeFields: ['owner.name', 'owner', 'owner'] }
|
||
);
|
||
const result = await getDoc(docRef);
|
||
expect(result.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
|
||
it('cannot update nonexistent document', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
return updateDoc(doc, { owner: 'abc' })
|
||
.then(
|
||
() => Promise.reject('update should have failed.'),
|
||
err => {
|
||
expect(err.message).to.exist;
|
||
expect(err.code).to.equal('not-found');
|
||
}
|
||
)
|
||
.then(() => getDoc(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.exists()).to.equal(false);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can delete a field with an update', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
const initialData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Jonny', email: '[email protected]' }
|
||
};
|
||
const updateData = {
|
||
'owner.email': deleteField()
|
||
};
|
||
const finalData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Jonny' }
|
||
};
|
||
return setDoc(doc, initialData)
|
||
.then(() => updateDoc(doc, updateData))
|
||
.then(() => getDoc(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.exists).to.be.ok;
|
||
expect(docSnapshot.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can update nested fields', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
const initialData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Jonny' },
|
||
'is.admin': false
|
||
};
|
||
const finalData = {
|
||
desc: 'Description',
|
||
owner: { name: 'Sebastian' },
|
||
'is.admin': true
|
||
};
|
||
return setDoc(doc, initialData)
|
||
.then(() =>
|
||
updateDoc(
|
||
doc,
|
||
'owner.name',
|
||
'Sebastian',
|
||
new FieldPath('is.admin'),
|
||
true
|
||
)
|
||
)
|
||
.then(() => getDoc(doc))
|
||
.then(docSnapshot => {
|
||
expect(docSnapshot.exists).to.be.ok;
|
||
expect(docSnapshot.data()).to.deep.equal(finalData);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can specify updated field multiple times', () => {
|
||
return withTestDoc(persistence, doc => {
|
||
return setDoc(doc, {})
|
||
.then(() => updateDoc(doc, 'field', 100, new FieldPath('field'), 200))
|
||
.then(() => getDoc(doc))
|
||
.then(docSnap => {
|
||
expect(docSnap.data()).to.deep.equal({ field: 200 });
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('vector embeddings', () => {
|
||
it('can write and read vector embeddings', async () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
const ref = await addDoc(coll, {
|
||
vector0: vector([0.0]),
|
||
vector1: vector([1, 2, 3.99])
|
||
});
|
||
await setDoc(ref, {
|
||
vector0: vector([0.0]),
|
||
vector1: vector([1, 2, 3.99]),
|
||
vector2: vector([0, 0, 0])
|
||
});
|
||
await updateDoc(ref, {
|
||
vector3: vector([-1, -200, -999])
|
||
});
|
||
|
||
const snap1 = await getDoc(ref);
|
||
expect(snap1.get('vector0').isEqual(vector([0.0]))).to.be.true;
|
||
expect(snap1.get('vector1').isEqual(vector([1, 2, 3.99]))).to.be.true;
|
||
expect(snap1.get('vector2').isEqual(vector([0, 0, 0]))).to.be.true;
|
||
expect(snap1.get('vector3').isEqual(vector([-1, -200, -999]))).to.be
|
||
.true;
|
||
});
|
||
});
|
||
|
||
it('can listen to documents with vectors', async () => {
|
||
return withTestCollection(persistence, {}, async randomCol => {
|
||
const ref = doc(randomCol);
|
||
const initialDeferred = new Deferred<void>();
|
||
const createDeferred = new Deferred<void>();
|
||
const setDeferred = new Deferred<void>();
|
||
const updateDeferred = new Deferred<void>();
|
||
const deleteDeferred = new Deferred<void>();
|
||
|
||
const expected = [
|
||
initialDeferred,
|
||
createDeferred,
|
||
setDeferred,
|
||
updateDeferred,
|
||
deleteDeferred
|
||
];
|
||
let idx = 0;
|
||
let document: DocumentSnapshot | null = null;
|
||
|
||
const q = query(randomCol, where('purpose', '==', 'vector tests'));
|
||
const unlisten = onSnapshot(q, (snap: QuerySnapshot<DocumentData>) => {
|
||
expected[idx].resolve();
|
||
idx += 1;
|
||
if (snap.docs.length > 0) {
|
||
document = snap.docs[0];
|
||
} else {
|
||
document = null;
|
||
}
|
||
});
|
||
|
||
await initialDeferred.promise;
|
||
expect(document).to.be.null;
|
||
|
||
await setDoc(ref, {
|
||
purpose: 'vector tests',
|
||
vector0: vector([0.0]),
|
||
vector1: vector([1, 2, 3.99])
|
||
});
|
||
|
||
await createDeferred.promise;
|
||
expect(document).to.be.not.null;
|
||
expect(document!.get('vector0').isEqual(vector([0.0]))).to.be.true;
|
||
expect(document!.get('vector1').isEqual(vector([1, 2, 3.99]))).to.be
|
||
.true;
|
||
|
||
await setDoc(ref, {
|
||
purpose: 'vector tests',
|
||
vector0: vector([0.0]),
|
||
vector1: vector([1, 2, 3.99]),
|
||
vector2: vector([0, 0, 0])
|
||
});
|
||
await setDeferred.promise;
|
||
expect(document).to.be.not.null;
|
||
expect(document!.get('vector0').isEqual(vector([0.0]))).to.be.true;
|
||
expect(document!.get('vector1').isEqual(vector([1, 2, 3.99]))).to.be
|
||
.true;
|
||
expect(document!.get('vector2').isEqual(vector([0, 0, 0]))).to.be.true;
|
||
|
||
await updateDoc(ref, {
|
||
vector3: vector([-1, -200, -999])
|
||
});
|
||
await updateDeferred.promise;
|
||
expect(document).to.be.not.null;
|
||
expect(document!.get('vector0').isEqual(vector([0.0]))).to.be.true;
|
||
expect(document!.get('vector1').isEqual(vector([1, 2, 3.99]))).to.be
|
||
.true;
|
||
expect(document!.get('vector2').isEqual(vector([0, 0, 0]))).to.be.true;
|
||
expect(document!.get('vector3').isEqual(vector([-1, -200, -999]))).to.be
|
||
.true;
|
||
|
||
await deleteDoc(ref);
|
||
await deleteDeferred.promise;
|
||
expect(document).to.be.null;
|
||
|
||
unlisten();
|
||
});
|
||
});
|
||
|
||
it('SDK orders vector field same way as backend', async () => {
|
||
// Test data in the order that we expect the backend to sort it.
|
||
const docsInOrder = [
|
||
{ embedding: [1, 2, 3, 4, 5, 6] },
|
||
{ embedding: [100] },
|
||
{ embedding: vector([Number.NEGATIVE_INFINITY]) },
|
||
{ embedding: vector([-100]) },
|
||
{ embedding: vector([100]) },
|
||
{ embedding: vector([Number.POSITIVE_INFINITY]) },
|
||
{ embedding: vector([1, 2]) },
|
||
{ embedding: vector([2, 2]) },
|
||
{ embedding: vector([1, 2, 3]) },
|
||
{ embedding: vector([1, 2, 3, 4]) },
|
||
{ embedding: vector([1, 2, 3, 4, 5]) },
|
||
{ embedding: vector([1, 2, 100, 4, 4]) },
|
||
{ embedding: vector([100, 2, 3, 4, 5]) },
|
||
{ embedding: { HELLO: 'WORLD' } },
|
||
{ embedding: { hello: 'world' } }
|
||
];
|
||
|
||
const docs = docsInOrder.reduce((obj, doc) => {
|
||
obj[Math.random().toString()] = doc;
|
||
return obj;
|
||
}, {} as { [i: string]: DocumentData });
|
||
|
||
return withTestCollection(persistence, docs, async randomCol => {
|
||
// We validate that the SDK orders the vector field the same way as the backend
|
||
// by comparing the sort order of vector fields from getDocsFromServer and
|
||
// onSnapshot. onSnapshot will return sort order of the SDK,
|
||
// and getDocsFromServer will return sort order of the backend.
|
||
|
||
const orderedQuery = query(randomCol, orderBy('embedding'));
|
||
const gotInitialSnapshot = new Deferred<QuerySnapshot>();
|
||
|
||
const unsubscribe = onSnapshot(
|
||
orderedQuery,
|
||
snapshot => {
|
||
gotInitialSnapshot.resolve(snapshot);
|
||
},
|
||
err => {
|
||
gotInitialSnapshot.reject!(err);
|
||
}
|
||
);
|
||
|
||
const watchSnapshot = await gotInitialSnapshot.promise;
|
||
unsubscribe();
|
||
|
||
const getSnapshot = await getDocsFromServer(orderedQuery);
|
||
|
||
// Compare the snapshot (including sort order) of a snapshot
|
||
// from Query.onSnapshot() to an actual snapshot from Query.get()
|
||
expect(toDataArray(watchSnapshot)).to.deep.equal(
|
||
toDataArray(getSnapshot)
|
||
);
|
||
|
||
// Compare the snapshot (including sort order) of a snapshot
|
||
// from Query.onSnapshot() to the expected sort order from
|
||
// the backend.
|
||
expect(toDataArray(watchSnapshot)).to.deep.equal(docsInOrder);
|
||
});
|
||
});
|
||
|
||
// eslint-disable-next-line no-restricted-properties
|
||
(persistence.gc === 'lru' ? describe : describe.skip)('From Cache', () => {
|
||
it('SDK orders vector field the same way online and offline', async () => {
|
||
// Test data in the order that we expect the SDK to sort it.
|
||
const docsInOrder = [
|
||
{ embedding: [1, 2, 3, 4, 5, 6] },
|
||
{ embedding: [100] },
|
||
{ embedding: vector([Number.NEGATIVE_INFINITY]) },
|
||
{ embedding: vector([-100]) },
|
||
{ embedding: vector([100]) },
|
||
{ embedding: vector([Number.POSITIVE_INFINITY]) },
|
||
{ embedding: vector([1, 2]) },
|
||
{ embedding: vector([2, 2]) },
|
||
{ embedding: vector([1, 2, 3]) },
|
||
{ embedding: vector([1, 2, 3, 4]) },
|
||
{ embedding: vector([1, 2, 3, 4, 5]) },
|
||
{ embedding: vector([1, 2, 100, 4, 4]) },
|
||
{ embedding: vector([100, 2, 3, 4, 5]) },
|
||
{ embedding: { HELLO: 'WORLD' } },
|
||
{ embedding: { hello: 'world' } }
|
||
];
|
||
|
||
const documentIds: string[] = [];
|
||
const docs = docsInOrder.reduce((obj, doc, index) => {
|
||
const documentId = index.toString();
|
||
documentIds.push(documentId);
|
||
obj[documentId] = doc;
|
||
return obj;
|
||
}, {} as { [i: string]: DocumentData });
|
||
|
||
return withTestCollection(persistence, docs, async randomCol => {
|
||
const orderedQuery = query(randomCol, orderBy('embedding'));
|
||
await checkOnlineAndOfflineResultsMatch(
|
||
randomCol,
|
||
orderedQuery,
|
||
...documentIds
|
||
);
|
||
|
||
const orderedQueryLessThan = query(
|
||
randomCol,
|
||
orderBy('embedding'),
|
||
where('embedding', '<', vector([1, 2, 100, 4, 4]))
|
||
);
|
||
await checkOnlineAndOfflineResultsMatch(
|
||
randomCol,
|
||
orderedQueryLessThan,
|
||
...documentIds.slice(2, 11)
|
||
);
|
||
|
||
const orderedQueryGreaterThan = query(
|
||
randomCol,
|
||
orderBy('embedding'),
|
||
where('embedding', '>', vector([1, 2, 100, 4, 4]))
|
||
);
|
||
await checkOnlineAndOfflineResultsMatch(
|
||
randomCol,
|
||
orderedQueryGreaterThan,
|
||
...documentIds.slice(12, 13)
|
||
);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('documents: ', () => {
|
||
const invalidDocValues = [undefined, null, 0, 'foo', ['a'], new Date()];
|
||
for (const val of invalidDocValues) {
|
||
it('set/update should reject: ' + val, () => {
|
||
return withTestDoc(persistence, async doc => {
|
||
// Intentionally passing bad types.
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
expect(() => setDoc(doc, val as any)).to.throw();
|
||
// Intentionally passing bad types.
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
expect(() => updateDoc(doc, val as any)).to.throw();
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
it('CollectionRef.add() resolves with resulting DocumentRef.', () => {
|
||
return withTestCollection(persistence, {}, coll => {
|
||
return addDoc(coll, { foo: 1 })
|
||
.then(docRef => getDoc(docRef))
|
||
.then(docSnap => {
|
||
expect(docSnap.data()).to.deep.equal({ foo: 1 });
|
||
});
|
||
});
|
||
});
|
||
|
||
it('onSnapshotsInSync fires after listeners are in sync', () => {
|
||
const testDocs = {
|
||
a: { foo: 1 }
|
||
};
|
||
return withTestCollection(persistence, testDocs, async (coll, db) => {
|
||
let events: string[] = [];
|
||
const gotInitialSnapshot = new Deferred<void>();
|
||
const docA = doc(coll, 'a');
|
||
|
||
onSnapshot(docA, snap => {
|
||
events.push('doc');
|
||
gotInitialSnapshot.resolve();
|
||
});
|
||
await gotInitialSnapshot.promise;
|
||
events = [];
|
||
|
||
const done = new Deferred<void>();
|
||
onSnapshotsInSync(db, () => {
|
||
events.push('snapshots-in-sync');
|
||
if (events.length === 3) {
|
||
// We should have an initial snapshots-in-sync event, then a snapshot
|
||
// event for set(), then another event to indicate we're in sync
|
||
// again.
|
||
expect(events).to.deep.equal([
|
||
'snapshots-in-sync',
|
||
'doc',
|
||
'snapshots-in-sync'
|
||
]);
|
||
done.resolve();
|
||
}
|
||
});
|
||
|
||
await setDoc(docA, { foo: 3 });
|
||
await done.promise;
|
||
});
|
||
});
|
||
|
||
apiDescribe('Queries are validated client-side', persistence => {
|
||
// NOTE: Failure cases are validated in validation_test.ts
|
||
|
||
it('same inequality fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, where('x', '>=', 32), where('x', '<=', 'cat'))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('inequality and equality on different fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, where('x', '>=', 32), where('y', '==', 'cat'))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('inequality on multiple fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, where('x', '>=', 32), where('y', '!=', 'cat'))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('inequality with key fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, where(documentId(), '>=', 'aa'), where('x', '>=', 32))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('inequality and array-contains on different fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, where('x', '>=', 32), where('y', 'array-contains', 'cat'))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('inequality and array-contains-any on different fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(
|
||
coll,
|
||
where('x', '>=', 32),
|
||
where('y', 'array-contains-any', [1, 2])
|
||
)
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('multiple inequality and array-contains on different fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(
|
||
coll,
|
||
where('x', '>=', 32),
|
||
where('y', 'array-contains', 'cat'),
|
||
where('z', '<=', 42)
|
||
)
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('multiple inequality and array-contains-any on different fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(
|
||
coll,
|
||
where('x', '>=', 32),
|
||
where('y', 'array-contains-any', [1, 2]),
|
||
where('z', '<=', 42)
|
||
)
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('inequality and IN on different fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, where('x', '>=', 32), where('y', 'in', [1, 2]))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('multiple inequality and IN on different fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(
|
||
coll,
|
||
where('x', '>=', 32),
|
||
where('y', 'in', [1, 2]),
|
||
where('z', '<=', 42)
|
||
)
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('inequality and NOT IN on different fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, where('x', '>=', 32), where('y', 'not-in', [1, 2]))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('multiple inequality and NOT IN on different fields works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(
|
||
coll,
|
||
where('x', '>=', 32),
|
||
where('y', 'not-in', [1, 2]),
|
||
where('z', '<=', 42)
|
||
)
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('inequality same as orderBy works.', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, where('x', '>', 32), orderBy('x'))
|
||
).not.to.throw();
|
||
expect(() =>
|
||
query(coll, orderBy('x'), where('x', '>', 32))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('!= same as orderBy works.', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, where('x', '!=', 32), orderBy('x'))
|
||
).not.to.throw();
|
||
expect(() =>
|
||
query(coll, orderBy('x'), where('x', '!=', 32))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('equality different than orderBy works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, orderBy('x'), where('y', '==', 'cat'))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('inequality different than orderBy works.', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, where('x', '>', 32), orderBy('y'))
|
||
).not.to.throw();
|
||
expect(() =>
|
||
query(coll, orderBy('y'), where('x', '>', 32))
|
||
).not.to.throw();
|
||
expect(() =>
|
||
query(coll, where('x', '>', 32), orderBy('y'), orderBy('z'))
|
||
).not.to.throw();
|
||
expect(() =>
|
||
query(coll, orderBy('y'), where('x', '>', 32), orderBy('x'))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('multiple inequality different from orderBy works.', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(
|
||
coll,
|
||
where('x', '>', 32),
|
||
where('y', '!=', 'cat'),
|
||
orderBy('z')
|
||
)
|
||
).not.to.throw();
|
||
expect(() =>
|
||
query(
|
||
coll,
|
||
orderBy('z'),
|
||
where('x', '>', 32),
|
||
where('y', '!=', 'cat')
|
||
)
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('array-contains different than orderBy works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, orderBy('x'), where('y', 'array-contains', 'cat'))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('IN different than orderBy works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, orderBy('x'), where('y', 'in', [1, 2]))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
|
||
it('array-contains-any different than orderBy works', () => {
|
||
return withTestCollection(persistence, {}, async coll => {
|
||
expect(() =>
|
||
query(coll, orderBy('x'), where('y', 'array-contains-any', [1, 2]))
|
||
).not.to.throw();
|
||
});
|
||
});
|
||
});
|
||
|
||
it('DocumentSnapshot events for nonexistent document', () => {
|
||
return withTestCollection(persistence, {}, col => {
|
||
const docA = doc(col);
|
||
const storeEvent = new EventsAccumulator<DocumentSnapshot>();
|
||
onSnapshot(docA, storeEvent.storeEvent);
|
||
return storeEvent.awaitEvent().then(snap => {
|
||
expect(snap.exists()).to.be.false;
|
||
expect(snap.data()).to.equal(undefined);
|
||
return storeEvent.assertNoAdditionalEvents();
|
||
});
|
||
});
|
||
});
|
||
|
||
it('DocumentSnapshot events for add data to document', () => {
|
||
return withTestCollection(persistence, {}, col => {
|
||
const docA = doc(col);
|
||
const storeEvent = new EventsAccumulator<DocumentSnapshot>();
|
||
onSnapshot(docA, { includeMetadataChanges: true }, storeEvent.storeEvent);
|
||
return storeEvent
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.false;
|
||
expect(snap.data()).to.equal(undefined);
|
||
})
|
||
.then(() => setDoc(docA, { a: 1 }))
|
||
.then(() => storeEvent.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal({ a: 1 });
|
||
expect(snap.metadata.hasPendingWrites).to.be.true;
|
||
})
|
||
.then(() => storeEvent.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal({ a: 1 });
|
||
// This event could be a metadata change for fromCache as well.
|
||
// We comment this line out to reduce flakiness.
|
||
// TODO(b/295872012): Figure out a way to check for all scenarios.
|
||
// expect(snap.metadata.hasPendingWrites).to.be.false;
|
||
});
|
||
});
|
||
});
|
||
|
||
it('DocumentSnapshot events for change data in document', () => {
|
||
const initialData = { a: 1 };
|
||
const changedData = { b: 2 };
|
||
|
||
return withTestCollection(persistence, { key1: initialData }, col => {
|
||
const doc1 = doc(col, 'key1');
|
||
const storeEvent = new EventsAccumulator<DocumentSnapshot>();
|
||
onSnapshot(doc1, { includeMetadataChanges: true }, storeEvent.storeEvent);
|
||
return storeEvent
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
expect(snap.data()).to.deep.equal(initialData);
|
||
expect(snap.metadata.hasPendingWrites).to.be.false;
|
||
})
|
||
.then(() => setDoc(doc1, changedData))
|
||
.then(() => storeEvent.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.data()).to.deep.equal(changedData);
|
||
expect(snap.metadata.hasPendingWrites).to.be.true;
|
||
})
|
||
.then(() => storeEvent.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.data()).to.deep.equal(changedData);
|
||
// This event could be a metadata change for fromCache as well.
|
||
// We comment this line out to reduce flakiness.
|
||
// TODO(b/295872012): Figure out a way to check for all scenarios.
|
||
// expect(snap.metadata.hasPendingWrites).to.be.false;
|
||
});
|
||
});
|
||
});
|
||
|
||
it('DocumentSnapshot events for delete data in document', () => {
|
||
const initialData = { a: 1 };
|
||
|
||
return withTestCollection(persistence, { key1: initialData }, col => {
|
||
const doc1 = doc(col, 'key1');
|
||
const storeEvent = new EventsAccumulator<DocumentSnapshot>();
|
||
onSnapshot(doc1, { includeMetadataChanges: true }, storeEvent.storeEvent);
|
||
return storeEvent
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(initialData);
|
||
expect(snap.metadata.hasPendingWrites).to.be.false;
|
||
})
|
||
.then(() => deleteDoc(doc1))
|
||
.then(() => storeEvent.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.false;
|
||
expect(snap.data()).to.equal(undefined);
|
||
expect(snap.metadata.hasPendingWrites).to.be.false;
|
||
})
|
||
.then(() => storeEvent.assertNoAdditionalEvents());
|
||
});
|
||
});
|
||
|
||
it('Listen can be called multiple times', () => {
|
||
return withTestCollection(persistence, {}, coll => {
|
||
const docA = doc(coll);
|
||
const deferred1 = new Deferred<void>();
|
||
const deferred2 = new Deferred<void>();
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
setDoc(docA, { foo: 'bar' }).then(() => {
|
||
onSnapshot(docA, () => {
|
||
deferred1.resolve();
|
||
onSnapshot(docA, () => deferred2.resolve());
|
||
});
|
||
});
|
||
return Promise.all([deferred1.promise, deferred2.promise]).then(() => {});
|
||
});
|
||
});
|
||
|
||
it('DocumentSnapshot events for snapshot created by a bundle', async () => {
|
||
if (isNode()) {
|
||
const initialData = { a: 1 };
|
||
const finalData = { a: 2 };
|
||
await withTestDocAndInitialData(
|
||
persistence,
|
||
initialData,
|
||
async (docRef, db) => {
|
||
const doc = await getDoc(docRef);
|
||
const accumulator = new EventsAccumulator<DocumentSnapshot>();
|
||
const unsubscribe = onSnapshotResume(
|
||
db,
|
||
doc.toJSON(),
|
||
accumulator.storeEvent
|
||
);
|
||
await accumulator
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
console.error('DEDB accumulator event 1');
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(initialData);
|
||
})
|
||
.then(() => setDoc(docRef, finalData))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(finalData);
|
||
});
|
||
unsubscribe();
|
||
}
|
||
);
|
||
}
|
||
});
|
||
|
||
it('DocumentSnapshot updated doc events in snapshot created by a bundle accumulator', async () => {
|
||
if (isNode()) {
|
||
const initialData = { a: 1 };
|
||
const finalData = { a: 2 };
|
||
await withTestDocAndInitialData(
|
||
persistence,
|
||
initialData,
|
||
async (docRef, db) => {
|
||
const doc = await getDoc(docRef);
|
||
const accumulator = new EventsAccumulator<DocumentSnapshot>();
|
||
const unsubscribe = onSnapshotResume(
|
||
db,
|
||
doc.toJSON(),
|
||
accumulator.storeEvent
|
||
);
|
||
await accumulator
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(initialData);
|
||
})
|
||
.then(() => setDoc(docRef, finalData))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(finalData);
|
||
});
|
||
unsubscribe();
|
||
}
|
||
);
|
||
}
|
||
});
|
||
|
||
it('DocumentSnapshot observer events for snapshot created by a bundle', async () => {
|
||
if (isNode()) {
|
||
const initialData = { a: 1 };
|
||
const finalData = { a: 2 };
|
||
await withTestDocAndInitialData(
|
||
persistence,
|
||
initialData,
|
||
async (docRef, db) => {
|
||
const doc = await getDoc(docRef);
|
||
const accumulator = new EventsAccumulator<DocumentSnapshot>();
|
||
const unsubscribe = onSnapshotResume(db, doc.toJSON(), {
|
||
next: accumulator.storeEvent
|
||
});
|
||
await accumulator
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(initialData);
|
||
})
|
||
.then(() => setDoc(docRef, finalData))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(finalData);
|
||
});
|
||
unsubscribe();
|
||
}
|
||
);
|
||
}
|
||
});
|
||
|
||
it('DocumentSnapshot error events for snapshot created by a bundle', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const json = {
|
||
bundle: 'BadData',
|
||
bundleName: 'bundleName',
|
||
bundleSource: 'DocumentSnapshot'
|
||
};
|
||
const deferred = new Deferred();
|
||
const unsubscribe = onSnapshotResume(
|
||
db,
|
||
json,
|
||
ds => {
|
||
expect(ds).to.not.exist;
|
||
deferred.resolve();
|
||
},
|
||
err => {
|
||
expect(err.name).to.exist;
|
||
expect(err.message).to.exist;
|
||
deferred.resolve();
|
||
}
|
||
);
|
||
await deferred.promise;
|
||
unsubscribe();
|
||
});
|
||
});
|
||
|
||
it('DocumentSnapshot observer error events for snapshot created by a bundle', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const json = {
|
||
bundle: 'BadData',
|
||
bundleName: 'bundleName',
|
||
bundleSource: 'QuerySnapshot'
|
||
};
|
||
const deferred = new Deferred();
|
||
const unsubscribe = onSnapshotResume(db, json, {
|
||
next: ds => {
|
||
expect(ds).to.not.exist;
|
||
deferred.resolve();
|
||
},
|
||
error: err => {
|
||
expect(err.name).to.exist;
|
||
expect(err.message).to.exist;
|
||
deferred.resolve();
|
||
}
|
||
});
|
||
await deferred.promise;
|
||
unsubscribe();
|
||
});
|
||
});
|
||
|
||
it('DocumentSnapshot updated doc events in snapshot created by fromJSON bundle', async () => {
|
||
if (isNode()) {
|
||
const initialData = { a: 1 };
|
||
const finalData = { a: 2 };
|
||
await withTestDocAndInitialData(
|
||
persistence,
|
||
initialData,
|
||
async (docRef, db) => {
|
||
const doc = await getDoc(docRef);
|
||
const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON());
|
||
const accumulator = new EventsAccumulator<DocumentSnapshot>();
|
||
const unsubscribe = onSnapshotResume(
|
||
db,
|
||
fromJsonDoc.toJSON(),
|
||
accumulator.storeEvent
|
||
);
|
||
await accumulator
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(initialData);
|
||
})
|
||
.then(() => setDoc(docRef, finalData))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(finalData);
|
||
});
|
||
unsubscribe();
|
||
}
|
||
);
|
||
}
|
||
});
|
||
|
||
it('DocumentSnapshot updated doc events in snapshot created by fromJSON doc ref', async () => {
|
||
if (isNode()) {
|
||
const initialData = { a: 1 };
|
||
const finalData = { a: 2 };
|
||
await withTestDocAndInitialData(
|
||
persistence,
|
||
initialData,
|
||
async (docRef, db) => {
|
||
const doc = await getDoc(docRef);
|
||
const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON());
|
||
const accumulator = new EventsAccumulator<DocumentSnapshot>();
|
||
const unsubscribe = onSnapshot(
|
||
fromJsonDoc.ref,
|
||
accumulator.storeEvent
|
||
);
|
||
await accumulator
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(initialData);
|
||
})
|
||
.then(() => setDoc(docRef, finalData))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.exists()).to.be.true;
|
||
expect(snap.data()).to.deep.equal(finalData);
|
||
});
|
||
unsubscribe();
|
||
}
|
||
);
|
||
}
|
||
});
|
||
|
||
it('Querysnapshot events for snapshot created by a bundle', async () => {
|
||
if (isNode()) {
|
||
const testDocs = {
|
||
a: { foo: 1 },
|
||
b: { bar: 2 }
|
||
};
|
||
await withTestCollection(persistence, testDocs, async (coll, db) => {
|
||
const querySnap = await getDocs(query(coll, orderBy(documentId())));
|
||
const accumulator = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshotResume(
|
||
db,
|
||
querySnap.toJSON(),
|
||
accumulator.storeEvent
|
||
);
|
||
await accumulator.awaitEvent().then(snap => {
|
||
expect(snap.docs).not.to.be.null;
|
||
expect(snap.docs.length).to.equal(2);
|
||
expect(snap.docs[0].data()).to.deep.equal(testDocs.a);
|
||
expect(snap.docs[1].data()).to.deep.equal(testDocs.b);
|
||
});
|
||
unsubscribe();
|
||
});
|
||
}
|
||
});
|
||
|
||
it('Querysnapshot observer events for snapshot created by a bundle', async () => {
|
||
if (isNode()) {
|
||
const testDocs = {
|
||
a: { foo: 1 },
|
||
b: { bar: 2 }
|
||
};
|
||
await withTestCollection(persistence, testDocs, async (coll, db) => {
|
||
const querySnap = await getDocs(query(coll, orderBy(documentId())));
|
||
const accumulator = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshotResume(db, querySnap.toJSON(), {
|
||
next: accumulator.storeEvent
|
||
});
|
||
await accumulator.awaitEvent().then(snap => {
|
||
expect(snap.docs).not.to.be.null;
|
||
expect(snap.docs.length).to.equal(2);
|
||
expect(snap.docs[0].data()).to.deep.equal(testDocs.a);
|
||
expect(snap.docs[1].data()).to.deep.equal(testDocs.b);
|
||
});
|
||
unsubscribe();
|
||
});
|
||
}
|
||
});
|
||
|
||
it('QuerySnapshot error events for snapshot created by a bundle', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const json = {
|
||
bundle: 'BadData',
|
||
bundleName: 'bundleName',
|
||
bundleSource: 'QuerySnapshot'
|
||
};
|
||
const deferred = new Deferred();
|
||
const unsubscribe = onSnapshotResume(
|
||
db,
|
||
json,
|
||
qs => {
|
||
expect(qs).to.not.exist;
|
||
deferred.resolve();
|
||
},
|
||
err => {
|
||
expect(err.name).to.exist;
|
||
expect(err.message).to.exist;
|
||
deferred.resolve();
|
||
}
|
||
);
|
||
await deferred.promise;
|
||
unsubscribe();
|
||
});
|
||
});
|
||
|
||
it('QuerySnapshot observer error events for snapshot created by a bundle', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const json = {
|
||
bundle: 'BadData',
|
||
bundleName: 'bundleName',
|
||
bundleSource: 'QuerySnapshot'
|
||
};
|
||
const deferred = new Deferred();
|
||
const unsubscribe = onSnapshotResume(db, json, {
|
||
next: qs => {
|
||
expect(qs).to.not.exist;
|
||
deferred.resolve();
|
||
},
|
||
error: err => {
|
||
expect(err.name).to.exist;
|
||
expect(err.message).to.exist;
|
||
deferred.resolve();
|
||
}
|
||
});
|
||
await deferred.promise;
|
||
unsubscribe();
|
||
});
|
||
});
|
||
|
||
it('QuerySnapshot updated doc events in snapshot created by a bundle', async () => {
|
||
if (isNode()) {
|
||
const testDocs = {
|
||
a: { foo: 1 },
|
||
b: { bar: 2 }
|
||
};
|
||
await withTestCollection(persistence, testDocs, async (coll, db) => {
|
||
const querySnap = await getDocs(query(coll, orderBy(documentId())));
|
||
const refForDocA = querySnap.docs[0].ref;
|
||
const accumulator = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshotResume(
|
||
db,
|
||
querySnap.toJSON(),
|
||
accumulator.storeEvent
|
||
);
|
||
await accumulator
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
expect(snap.docs).not.to.be.null;
|
||
expect(snap.docs.length).to.equal(2);
|
||
expect(snap.docs[0].data()).to.deep.equal(testDocs.a);
|
||
expect(snap.docs[1].data()).to.deep.equal(testDocs.b);
|
||
})
|
||
.then(() => setDoc(refForDocA, { foo: 0 }))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.docs).not.to.be.null;
|
||
expect(snap.docs.length).to.equal(2);
|
||
expect(snap.docs[0].data()).to.deep.equal({ foo: 0 });
|
||
expect(snap.docs[1].data()).to.deep.equal(testDocs.b);
|
||
});
|
||
unsubscribe();
|
||
});
|
||
}
|
||
});
|
||
|
||
it('QuerySnapshot updated doc events in snapshot created by fromJSON ', async () => {
|
||
if (isNode()) {
|
||
const testDocs = {
|
||
a: { foo: 1 },
|
||
b: { bar: 2 }
|
||
};
|
||
await withTestCollection(persistence, testDocs, async (coll, db) => {
|
||
const querySnap = await getDocs(query(coll, orderBy(documentId())));
|
||
const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON());
|
||
const refForDocA = querySnapFromJson.docs[0].ref;
|
||
const accumulator = new EventsAccumulator<QuerySnapshot>();
|
||
|
||
const unsubscribe = onSnapshotResume(
|
||
db,
|
||
querySnapFromJson.toJSON(),
|
||
accumulator.storeEvent
|
||
);
|
||
await accumulator
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
expect(snap.docs).not.to.be.null;
|
||
expect(snap.docs.length).to.equal(2);
|
||
expect(snap.docs[0].data()).to.deep.equal(testDocs.a);
|
||
expect(snap.docs[1].data()).to.deep.equal(testDocs.b);
|
||
})
|
||
.then(() => setDoc(refForDocA, { foo: 0 }))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.docs).not.to.be.null;
|
||
expect(snap.docs.length).to.equal(2);
|
||
expect(snap.docs[0].data()).to.deep.equal({ foo: 0 });
|
||
expect(snap.docs[1].data()).to.deep.equal(testDocs.b);
|
||
});
|
||
unsubscribe();
|
||
});
|
||
}
|
||
});
|
||
|
||
it('QuerySnapshot updated doc events in snapshot created by fromJSON query ref', async () => {
|
||
if (isNode()) {
|
||
const testDocs = {
|
||
a: { foo: 1 },
|
||
b: { bar: 2 }
|
||
};
|
||
await withTestCollection(persistence, testDocs, async (coll, db) => {
|
||
const querySnap = await getDocs(query(coll, orderBy(documentId())));
|
||
const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON());
|
||
const refForDocA = querySnapFromJson.docs[0].ref;
|
||
const accumulator = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshot(
|
||
querySnapFromJson.query,
|
||
accumulator.storeEvent
|
||
);
|
||
await accumulator
|
||
.awaitEvent()
|
||
.then(snap => {
|
||
expect(snap.docs).not.to.be.null;
|
||
expect(snap.docs.length).to.equal(2);
|
||
expect(snap.docs[0].data()).to.deep.equal(testDocs.a);
|
||
expect(snap.docs[1].data()).to.deep.equal(testDocs.b);
|
||
})
|
||
.then(() => setDoc(refForDocA, { foo: 0 }))
|
||
.then(() => accumulator.awaitEvent())
|
||
.then(snap => {
|
||
expect(snap.docs).not.to.be.null;
|
||
expect(snap.docs.length).to.equal(2);
|
||
expect(snap.docs[0].data()).to.deep.equal({ foo: 0 });
|
||
expect(snap.docs[1].data()).to.deep.equal(testDocs.b);
|
||
});
|
||
unsubscribe();
|
||
});
|
||
}
|
||
});
|
||
|
||
it('Metadata only changes are not fired when no options provided', () => {
|
||
return withTestDoc(persistence, docRef => {
|
||
const secondUpdateFound = new Deferred();
|
||
let count = 0;
|
||
const unlisten = onSnapshot(docRef, doc => {
|
||
if (doc) {
|
||
count++;
|
||
if (count === 1) {
|
||
expect(doc.data()).to.deep.equal({ a: 1 });
|
||
} else {
|
||
expect(doc.data()).to.deep.equal({ b: 1 });
|
||
secondUpdateFound.resolve();
|
||
}
|
||
}
|
||
});
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
setDoc(docRef, { a: 1 }).then(() => {
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
setDoc(docRef, { b: 1 });
|
||
});
|
||
return secondUpdateFound.promise.then(() => {
|
||
unlisten();
|
||
});
|
||
});
|
||
});
|
||
|
||
// TODO(mikelehen): We need a way to create a query that will pass
|
||
// client-side validation but fail remotely. May need to wait until we
|
||
// have security rules support or something?
|
||
// eslint-disable-next-line no-restricted-properties
|
||
describe('Listens are rejected remotely:', () => {
|
||
it('will reject listens', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const deferred = new Deferred();
|
||
const queryForRejection = collection(db, 'a/__badpath__/b');
|
||
onSnapshot(
|
||
queryForRejection,
|
||
() => {},
|
||
(err: Error) => {
|
||
expect(err.name).to.exist;
|
||
expect(err.message).to.exist;
|
||
deferred.resolve();
|
||
}
|
||
);
|
||
await deferred.promise;
|
||
});
|
||
});
|
||
|
||
it('will reject same listens twice in a row', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const deferred = new Deferred();
|
||
const queryForRejection = collection(db, 'a/__badpath__/b');
|
||
onSnapshot(
|
||
queryForRejection,
|
||
() => {},
|
||
(err: Error) => {
|
||
expect(err.name).to.exist;
|
||
expect(err.message).to.exist;
|
||
onSnapshot(
|
||
queryForRejection,
|
||
() => {},
|
||
(err2: Error) => {
|
||
expect(err2.name).to.exist;
|
||
expect(err2.message).to.exist;
|
||
deferred.resolve();
|
||
}
|
||
);
|
||
}
|
||
);
|
||
await deferred.promise;
|
||
});
|
||
});
|
||
|
||
it('will reject gets', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const queryForRejection = collection(db, 'a/__badpath__/b');
|
||
await getDocs(queryForRejection).then(
|
||
() => {
|
||
expect.fail('Promise resolved even though error was expected.');
|
||
},
|
||
err => {
|
||
expect(err.name).to.exist;
|
||
expect(err.message).to.exist;
|
||
}
|
||
);
|
||
});
|
||
});
|
||
|
||
it('will reject gets twice in a row', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const queryForRejection = collection(db, 'a/__badpath__/b');
|
||
return getDocs(queryForRejection)
|
||
.then(
|
||
() => {
|
||
expect.fail('Promise resolved even though error was expected.');
|
||
},
|
||
err => {
|
||
expect(err.name).to.exist;
|
||
expect(err.message).to.exist;
|
||
}
|
||
)
|
||
.then(() => getDocs(queryForRejection))
|
||
.then(
|
||
() => {
|
||
expect.fail('Promise resolved even though error was expected.');
|
||
},
|
||
err => {
|
||
expect(err.name).to.exist;
|
||
expect(err.message).to.exist;
|
||
}
|
||
);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('exposes "firestore" on document references.', () => {
|
||
return withTestDb(persistence, async db => {
|
||
expect(doc(db, 'foo/bar').firestore).to.equal(db);
|
||
});
|
||
});
|
||
|
||
it('exposes "firestore" on query references.', () => {
|
||
return withTestDb(persistence, async db => {
|
||
expect(query(collection(db, 'foo'), limit(5)).firestore).to.equal(db);
|
||
});
|
||
});
|
||
|
||
it('can compare DocumentReference instances with isEqual().', () => {
|
||
return withTestDb(persistence, firestore => {
|
||
return withTestDb(persistence, async otherFirestore => {
|
||
const docRef = doc(firestore, 'foo/bar');
|
||
expect(refEqual(docRef, doc(firestore, 'foo/bar'))).to.be.true;
|
||
expect(refEqual(collection(docRef, 'baz').parent!, docRef)).to.be.true;
|
||
|
||
expect(refEqual(doc(firestore, 'foo/BAR'), docRef)).to.be.false;
|
||
|
||
expect(refEqual(doc(otherFirestore, 'foo/bar'), docRef)).to.be.false;
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can compare Query instances with isEqual().', () => {
|
||
return withTestDb(persistence, firestore => {
|
||
return withTestDb(persistence, async otherFirestore => {
|
||
const query1 = query(
|
||
collection(firestore, 'foo'),
|
||
orderBy('bar'),
|
||
where('baz', '==', 42)
|
||
);
|
||
const query2 = query(
|
||
collection(firestore, 'foo'),
|
||
orderBy('bar'),
|
||
where('baz', '==', 42)
|
||
);
|
||
expect(queryEqual(query1, query2)).to.be.true;
|
||
|
||
const query3 = query(
|
||
collection(firestore, 'foo'),
|
||
orderBy('BAR'),
|
||
where('baz', '==', 42)
|
||
);
|
||
expect(queryEqual(query1, query3)).to.be.false;
|
||
|
||
const query4 = query(
|
||
collection(otherFirestore, 'foo'),
|
||
orderBy('bar'),
|
||
where('baz', '==', 42)
|
||
);
|
||
expect(queryEqual(query4, query1)).to.be.false;
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can compare multiple inequality Query instances with isEqual().', () => {
|
||
return withTestDb(persistence, async firestore => {
|
||
const query1 = query(
|
||
collection(firestore, 'foo'),
|
||
where('x', '>=', 42),
|
||
where('y', '!=', 42),
|
||
orderBy('z')
|
||
);
|
||
const query2 = query(
|
||
collection(firestore, 'foo'),
|
||
where('x', '>=', 42),
|
||
where('y', '!=', 42),
|
||
orderBy('z')
|
||
);
|
||
expect(queryEqual(query1, query2)).to.be.true;
|
||
|
||
// Inequality fields in different order
|
||
const query3 = query(
|
||
collection(firestore, 'foo'),
|
||
where('y', '!=', 42),
|
||
where('x', '>=', 42),
|
||
orderBy('z')
|
||
);
|
||
expect(queryEqual(query1, query3)).to.be.false;
|
||
});
|
||
});
|
||
|
||
it('can traverse collections and documents.', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const expected = 'a/b/c/d';
|
||
// doc path from root Firestore.
|
||
expect(doc(db, 'a/b/c/d').path).to.deep.equal(expected);
|
||
// collection path from root Firestore.
|
||
expect(doc(collection(db, 'a/b/c'), 'd').path).to.deep.equal(expected);
|
||
// doc path from CollectionReference.
|
||
expect(doc(collection(db, 'a'), 'b/c/d').path).to.deep.equal(expected);
|
||
// collection path from DocumentReference.
|
||
expect(collection(doc(db, 'a/b'), 'c/d/e').path).to.deep.equal(
|
||
expected + '/e'
|
||
);
|
||
});
|
||
});
|
||
|
||
it('can traverse collection and document parents.', () => {
|
||
return withTestDb(persistence, async db => {
|
||
let coll = collection(db, 'a/b/c');
|
||
expect(coll.path).to.deep.equal('a/b/c');
|
||
|
||
const doc = coll.parent!;
|
||
expect(doc.path).to.deep.equal('a/b');
|
||
|
||
coll = doc.parent;
|
||
expect(coll.path).to.equal('a');
|
||
|
||
const nullDoc = coll.parent;
|
||
expect(nullDoc).to.equal(null);
|
||
});
|
||
});
|
||
|
||
it('can queue writes while offline', () => {
|
||
return withTestDoc(persistence, (docRef, firestore) => {
|
||
return disableNetwork(firestore)
|
||
.then(() =>
|
||
Promise.all([
|
||
setDoc(docRef, { foo: 'bar' }),
|
||
enableNetwork(firestore)
|
||
])
|
||
)
|
||
.then(() => getDoc(docRef))
|
||
.then(doc => {
|
||
expect(doc.data()).to.deep.equal({ foo: 'bar' });
|
||
});
|
||
});
|
||
});
|
||
|
||
// eslint-disable-next-line no-restricted-properties
|
||
(persistence.storage === 'indexeddb' ? it : it.skip)(
|
||
'offline writes are sent after restart',
|
||
() => {
|
||
return withTestDoc(persistence, async (docRef, firestore) => {
|
||
const app = firestore.app;
|
||
const name = app.name;
|
||
const options = app.options;
|
||
|
||
await disableNetwork(firestore);
|
||
|
||
// We are merely adding to the cache.
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
setDoc(docRef, { foo: 'bar' });
|
||
|
||
await deleteApp(app);
|
||
|
||
const firestore2 = newTestFirestore(
|
||
newTestApp(options.projectId!, name),
|
||
DEFAULT_SETTINGS,
|
||
// @ts-ignore internal API usage
|
||
firestore._databaseId.database
|
||
);
|
||
await enableIndexedDbPersistence(firestore2);
|
||
await waitForPendingWrites(firestore2);
|
||
const doc2 = await getDoc(doc(firestore2, docRef.path));
|
||
|
||
expect(doc2.exists()).to.be.true;
|
||
expect(doc2.metadata.hasPendingWrites).to.be.false;
|
||
});
|
||
}
|
||
);
|
||
|
||
it('rejects subsequent method calls after terminate() is called', async () => {
|
||
return withTestDb(persistence, db => {
|
||
return terminate(db).then(() => {
|
||
expect(() => {
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
disableNetwork(db);
|
||
}).to.throw('The client has already been terminated.');
|
||
});
|
||
});
|
||
});
|
||
|
||
it('can call terminate() multiple times', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
await terminate(db);
|
||
await terminate(db);
|
||
});
|
||
});
|
||
|
||
// eslint-disable-next-line no-restricted-properties
|
||
(persistence.storage === 'indexeddb' ? it : it.skip)(
|
||
'maintains persistence after restarting app',
|
||
async () => {
|
||
await withTestDoc(persistence, async docRef => {
|
||
await setDoc(docRef, { foo: 'bar' });
|
||
const app = docRef.firestore.app;
|
||
const name = app.name;
|
||
const options = app.options;
|
||
|
||
await deleteApp(app);
|
||
|
||
const firestore2 = newTestFirestore(
|
||
newTestApp(options.projectId!, name),
|
||
undefined,
|
||
// @ts-ignore internal API usage
|
||
docRef.firestore._databaseId.database
|
||
);
|
||
await enableIndexedDbPersistence(firestore2);
|
||
const docRef2 = doc(firestore2, docRef.path);
|
||
const docSnap2 = await getDocFromCache(docRef2);
|
||
expect(docSnap2.exists()).to.be.true;
|
||
});
|
||
}
|
||
);
|
||
|
||
// eslint-disable-next-line no-restricted-properties
|
||
(persistence.storage === 'indexeddb' ? it : it.skip)(
|
||
'can clear persistence if the client has been terminated',
|
||
async () => {
|
||
await withTestDoc(persistence, async (docRef, firestore) => {
|
||
await setDoc(docRef, { foo: 'bar' });
|
||
const app = docRef.firestore.app;
|
||
const name = app.name;
|
||
const options = app.options;
|
||
|
||
await deleteApp(app);
|
||
await clearIndexedDbPersistence(firestore);
|
||
const firestore2 = newTestFirestore(
|
||
newTestApp(options.projectId!, name)
|
||
);
|
||
await enableIndexedDbPersistence(firestore2);
|
||
const docRef2 = doc(firestore2, docRef.path);
|
||
await expect(getDocFromCache(docRef2)).to.eventually.be.rejectedWith(
|
||
'Failed to get document from cache.'
|
||
);
|
||
});
|
||
}
|
||
);
|
||
|
||
// eslint-disable-next-line no-restricted-properties
|
||
(persistence.storage === 'indexeddb' ? it : it.skip)(
|
||
'can clear persistence if the client has not been initialized',
|
||
async () => {
|
||
await withTestDoc(persistence, async docRef => {
|
||
await setDoc(docRef, { foo: 'bar' });
|
||
const app = docRef.firestore.app;
|
||
const name = app.name;
|
||
const options = app.options;
|
||
|
||
await deleteApp(app);
|
||
const firestore2 = newTestFirestore(
|
||
newTestApp(options.projectId!, name)
|
||
);
|
||
await clearIndexedDbPersistence(firestore2);
|
||
await enableIndexedDbPersistence(firestore2);
|
||
const docRef2 = doc(firestore2, docRef.path);
|
||
await expect(getDocFromCache(docRef2)).to.eventually.be.rejectedWith(
|
||
'Failed to get document from cache.'
|
||
);
|
||
});
|
||
}
|
||
);
|
||
|
||
// eslint-disable-next-line no-restricted-properties
|
||
(persistence.storage === 'indexeddb' ? it : it.skip)(
|
||
'cannot clear persistence if the client has been initialized',
|
||
async () => {
|
||
await withTestDoc(persistence, async (docRef, firestore) => {
|
||
await setDoc(docRef, {});
|
||
const expectedError =
|
||
'Persistence can only be cleared before a Firestore instance is ' +
|
||
'initialized or after it is terminated.';
|
||
expect(() => clearIndexedDbPersistence(firestore)).to.throw(
|
||
expectedError
|
||
);
|
||
});
|
||
}
|
||
);
|
||
|
||
it('can get documents while offline', async () => {
|
||
await withTestDoc(persistence, async (docRef, firestore) => {
|
||
await disableNetwork(firestore);
|
||
await expect(getDoc(docRef)).to.eventually.be.rejectedWith(
|
||
'Failed to get document because the client is offline.'
|
||
);
|
||
|
||
const writePromise = setDoc(docRef, { foo: 'bar' });
|
||
const doc = await getDoc(docRef);
|
||
expect(doc.metadata.fromCache).to.be.true;
|
||
|
||
await enableNetwork(firestore);
|
||
await writePromise;
|
||
|
||
const doc2 = await getDoc(docRef);
|
||
expect(doc2.metadata.fromCache).to.be.false;
|
||
expect(doc2.data()).to.deep.equal({ foo: 'bar' });
|
||
});
|
||
});
|
||
|
||
it('can enable and disable networking', () => {
|
||
return withTestDb(persistence, async db => {
|
||
// There's not currently a way to check if networking is in fact disabled,
|
||
// so for now just test that the method is well-behaved and doesn't throw.
|
||
await enableNetwork(db);
|
||
await enableNetwork(db);
|
||
await disableNetwork(db);
|
||
await disableNetwork(db);
|
||
await enableNetwork(db);
|
||
});
|
||
});
|
||
|
||
it('can start a new instance after shut down', async () => {
|
||
return withTestDoc(persistence, async (docRef, firestore) => {
|
||
const app = firestore.app;
|
||
await terminate(firestore);
|
||
|
||
const newFirestore = initializeFirestore(app, DEFAULT_SETTINGS);
|
||
expect(newFirestore).to.not.equal(firestore);
|
||
|
||
// New instance functions.
|
||
const docRef2 = doc(newFirestore, docRef.path);
|
||
await setDoc(docRef2, { foo: 'bar' });
|
||
const docSnap = await getDoc(docRef2);
|
||
expect(docSnap.data()).to.deep.equal({ foo: 'bar' });
|
||
});
|
||
});
|
||
|
||
it('new operation after termination should throw', async () => {
|
||
await withTestDoc(persistence, async (docRef, firestore) => {
|
||
await terminate(firestore);
|
||
|
||
expect(() => {
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
setDoc(doc(firestore, docRef.path), { foo: 'bar' });
|
||
}).to.throw('The client has already been terminated.');
|
||
});
|
||
});
|
||
|
||
it('calling terminate multiple times should proceed', async () => {
|
||
await withTestDoc(persistence, async (docRef, firestore) => {
|
||
await terminate(firestore);
|
||
await terminate(firestore);
|
||
|
||
expect(() => {
|
||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||
setDoc(doc(firestore, docRef.path), { foo: 'bar' });
|
||
}).to.throw();
|
||
});
|
||
});
|
||
|
||
it('can unlisten queries after termination', async () => {
|
||
return withTestDoc(persistence, async (docRef, firestore) => {
|
||
const accumulator = new EventsAccumulator<DocumentSnapshot>();
|
||
const unsubscribe = onSnapshot(docRef, accumulator.storeEvent);
|
||
await accumulator.awaitEvent();
|
||
await terminate(firestore);
|
||
|
||
// This should proceed without error.
|
||
unsubscribe();
|
||
// Multiple calls should proceed as well.
|
||
unsubscribe();
|
||
});
|
||
});
|
||
|
||
it('can query after firestore restart', async () => {
|
||
return withTestDoc(persistence, async (docRef, firestore) => {
|
||
const deferred: Deferred<FirestoreError> = new Deferred();
|
||
const unsubscribe = onSnapshot(docRef, snapshot => {}, deferred.resolve);
|
||
|
||
// @ts-ignore internal API usage
|
||
await firestore._restart();
|
||
|
||
await expect(deferred.promise)
|
||
.to.eventually.haveOwnProperty('message')
|
||
.equal('Firestore shutting down');
|
||
|
||
// Call should proceed without error.
|
||
unsubscribe();
|
||
|
||
await setDoc(docRef, { foo: 'bar' });
|
||
const docSnap = await getDoc(docRef);
|
||
expect(docSnap.data()).to.deep.equal({ foo: 'bar' });
|
||
});
|
||
});
|
||
|
||
it('query listener throws error on termination', async () => {
|
||
return withTestDoc(persistence, async (docRef, firestore) => {
|
||
const deferred: Deferred<FirestoreError> = new Deferred();
|
||
const unsubscribe = onSnapshot(docRef, snapshot => {}, deferred.resolve);
|
||
|
||
await terminate(firestore);
|
||
|
||
await expect(deferred.promise)
|
||
.to.eventually.haveOwnProperty('message')
|
||
.equal('Firestore shutting down');
|
||
|
||
// Call should proceed without error.
|
||
unsubscribe();
|
||
});
|
||
});
|
||
|
||
it('can wait for pending writes', async () => {
|
||
await withTestDoc(persistence, async (docRef, firestore) => {
|
||
// Prevent pending writes receiving acknowledgement.
|
||
await disableNetwork(firestore);
|
||
|
||
const pendingWrites = setDoc(docRef, { foo: 'bar' });
|
||
const awaitPendingWrites = waitForPendingWrites(firestore);
|
||
|
||
// pending writes can receive acknowledgements now.
|
||
await enableNetwork(firestore);
|
||
await pendingWrites;
|
||
await awaitPendingWrites;
|
||
});
|
||
});
|
||
|
||
it('waiting for pending writes resolves immediately when offline and no pending writes', async () => {
|
||
await withTestDoc(persistence, async (docRef, firestore) => {
|
||
// Prevent pending writes receiving acknowledgement.
|
||
await disableNetwork(firestore);
|
||
|
||
// `awaitsPendingWrites` is created when there is no pending writes, it will resolve
|
||
// immediately even if we are offline.
|
||
await waitForPendingWrites(firestore);
|
||
});
|
||
});
|
||
|
||
// PORTING NOTE: These tests are for FirestoreDataConverter support and apply
|
||
// only to web.
|
||
apiDescribe('withConverter() support', persistence => {
|
||
class Post {
|
||
constructor(
|
||
readonly title: string,
|
||
readonly author: string,
|
||
readonly ref: DocumentReference | null = null
|
||
) {}
|
||
byline(): string {
|
||
return this.title + ', by ' + this.author;
|
||
}
|
||
}
|
||
|
||
const postConverter = {
|
||
toFirestore(post: Post): DocumentData {
|
||
return { title: post.title, author: post.author };
|
||
},
|
||
fromFirestore(snapshot: QueryDocumentSnapshot): Post {
|
||
expect(snapshot).to.be.an.instanceof(QueryDocumentSnapshot);
|
||
const data = snapshot.data();
|
||
return new Post(data.title, data.author, snapshot.ref);
|
||
}
|
||
};
|
||
|
||
const postConverterMerge = {
|
||
toFirestore(
|
||
post: WithFieldValue<Post>,
|
||
options?: SetOptions
|
||
): DocumentData {
|
||
if (options && ('merge' in options || 'mergeFields' in options)) {
|
||
expect(post).to.not.be.an.instanceof(Post);
|
||
} else {
|
||
expect(post).to.be.an.instanceof(Post);
|
||
}
|
||
const result: DocumentData = {};
|
||
if (post.title) {
|
||
result.title = post.title;
|
||
}
|
||
if (post.author) {
|
||
result.author = post.author;
|
||
}
|
||
return result;
|
||
},
|
||
fromFirestore(snapshot: QueryDocumentSnapshot): Post {
|
||
const data = snapshot.data();
|
||
return new Post(data.title, data.author, snapshot.ref);
|
||
}
|
||
};
|
||
|
||
it('for DocumentReference.withConverter()', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const docRef = doc(collection(db, 'posts')).withConverter(
|
||
postConverter
|
||
);
|
||
|
||
await setDoc(docRef, new Post('post', 'author'));
|
||
const postData = await getDoc(docRef);
|
||
const post = postData.data();
|
||
expect(post).to.not.equal(undefined);
|
||
expect(post!.byline()).to.equal('post, by author');
|
||
});
|
||
});
|
||
|
||
it('for DocumentReference.withConverter(null) ', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const docRef = doc(collection(db, 'posts'))
|
||
.withConverter(postConverter)
|
||
.withConverter(null);
|
||
|
||
expect(() => setDoc(docRef, new Post('post', 'author'))).to.throw();
|
||
});
|
||
});
|
||
|
||
it('for CollectionReference.withConverter()', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const coll = collection(db, 'posts').withConverter(postConverter);
|
||
|
||
const docRef = await addDoc(coll, new Post('post', 'author'));
|
||
const postData = await getDoc(docRef);
|
||
const post = postData.data();
|
||
expect(post).to.not.equal(undefined);
|
||
expect(post!.byline()).to.equal('post, by author');
|
||
});
|
||
});
|
||
|
||
it('for CollectionReference.withConverter(null)', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const coll = collection(db, 'posts')
|
||
.withConverter(postConverter)
|
||
.withConverter(null);
|
||
|
||
expect(() => addDoc(coll, new Post('post', 'author'))).to.throw();
|
||
});
|
||
});
|
||
|
||
it('for Query.withConverter()', () => {
|
||
return withTestDb(persistence, async db => {
|
||
await setDoc(doc(db, 'postings/post1'), {
|
||
title: 'post1',
|
||
author: 'author1'
|
||
});
|
||
await setDoc(doc(db, 'postings/post2'), {
|
||
title: 'post2',
|
||
author: 'author2'
|
||
});
|
||
const posts = await getDocs(
|
||
collectionGroup(db, 'postings').withConverter(postConverter)
|
||
);
|
||
expect(posts.size).to.equal(2);
|
||
expect(posts.docs[0].data()!.byline()).to.equal('post1, by author1');
|
||
});
|
||
});
|
||
|
||
it('for Query.withConverter(null)', () => {
|
||
return withTestDb(persistence, async db => {
|
||
await setDoc(doc(db, 'postings/post1'), {
|
||
title: 'post1',
|
||
author: 'author1'
|
||
});
|
||
const posts = await getDocs(
|
||
collectionGroup(db, 'postings')
|
||
.withConverter(postConverter)
|
||
.withConverter(null)
|
||
);
|
||
expect(posts.docs[0].data()).to.not.be.an.instanceof(Post);
|
||
});
|
||
});
|
||
|
||
it('requires the correct converter for Partial usage', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const ref = doc(db, 'posts', 'some-post').withConverter(postConverter);
|
||
await setDoc(ref, new Post('walnut', 'author'));
|
||
const batch = writeBatch(db);
|
||
expect(() =>
|
||
batch.set(ref, { title: 'olive' }, { merge: true })
|
||
).to.throw(
|
||
'Function WriteBatch.set() called with invalid ' +
|
||
'data (via `toFirestore()`). Unsupported field value: undefined ' +
|
||
'(found in field author in document posts/some-post)'
|
||
);
|
||
});
|
||
});
|
||
|
||
it('WriteBatch.set() supports partials with merge', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const ref = doc(collection(db, 'posts')).withConverter(
|
||
postConverterMerge
|
||
);
|
||
await setDoc(ref, new Post('walnut', 'author'));
|
||
const batch = writeBatch(db);
|
||
batch.set(ref, { title: 'olive' }, { merge: true });
|
||
await batch.commit();
|
||
const docSnap = await getDoc(ref);
|
||
expect(docSnap.get('title')).to.equal('olive');
|
||
expect(docSnap.get('author')).to.equal('author');
|
||
});
|
||
});
|
||
|
||
it('WriteBatch.set() supports partials with mergeFields', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const ref = doc(collection(db, 'posts')).withConverter(
|
||
postConverterMerge
|
||
);
|
||
await setDoc(ref, new Post('walnut', 'author'));
|
||
const batch = writeBatch(db);
|
||
batch.set(
|
||
ref,
|
||
{ title: 'olive', author: 'writer' },
|
||
{ mergeFields: ['title'] }
|
||
);
|
||
await batch.commit();
|
||
const docSnap = await getDoc(ref);
|
||
expect(docSnap.get('title')).to.equal('olive');
|
||
expect(docSnap.get('author')).to.equal('author');
|
||
});
|
||
});
|
||
|
||
it('Transaction.set() supports partials with merge', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const ref = doc(collection(db, 'posts')).withConverter(
|
||
postConverterMerge
|
||
);
|
||
await setDoc(ref, new Post('walnut', 'author'));
|
||
await runTransaction(db, async tx => {
|
||
tx.set(ref, { title: 'olive' }, { merge: true });
|
||
});
|
||
const docSnap = await getDoc(ref);
|
||
expect(docSnap.get('title')).to.equal('olive');
|
||
expect(docSnap.get('author')).to.equal('author');
|
||
});
|
||
});
|
||
|
||
it('Transaction.set() supports partials with mergeFields', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const ref = doc(collection(db, 'posts')).withConverter(
|
||
postConverterMerge
|
||
);
|
||
await setDoc(ref, new Post('walnut', 'author'));
|
||
await runTransaction(db, async tx => {
|
||
tx.set(
|
||
ref,
|
||
{ title: 'olive', author: 'person' },
|
||
{ mergeFields: ['title'] }
|
||
);
|
||
});
|
||
const docSnap = await getDoc(ref);
|
||
expect(docSnap.get('title')).to.equal('olive');
|
||
expect(docSnap.get('author')).to.equal('author');
|
||
});
|
||
});
|
||
|
||
it('DocumentReference.set() supports partials with merge', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const ref = doc(collection(db, 'posts')).withConverter(
|
||
postConverterMerge
|
||
);
|
||
await setDoc(ref, new Post('walnut', 'author'));
|
||
await setDoc(ref, { title: 'olive' }, { merge: true });
|
||
const docSnap = await getDoc(ref);
|
||
expect(docSnap.get('title')).to.equal('olive');
|
||
expect(docSnap.get('author')).to.equal('author');
|
||
});
|
||
});
|
||
|
||
it('DocumentReference.set() supports partials with mergeFields', async () => {
|
||
return withTestDb(persistence, async db => {
|
||
const ref = doc(collection(db, 'posts')).withConverter(
|
||
postConverterMerge
|
||
);
|
||
await setDoc(ref, new Post('walnut', 'author'));
|
||
await setDoc(
|
||
ref,
|
||
{ title: 'olive', author: 'writer' },
|
||
{ mergeFields: ['title'] }
|
||
);
|
||
const docSnap = await getDoc(ref);
|
||
expect(docSnap.get('title')).to.equal('olive');
|
||
expect(docSnap.get('author')).to.equal('author');
|
||
});
|
||
});
|
||
|
||
it('calls DocumentSnapshot.data() with specified SnapshotOptions', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const docRef = doc(db, 'some/doc').withConverter({
|
||
toFirestore(post: Post): DocumentData {
|
||
return { title: post.title, author: post.author };
|
||
},
|
||
fromFirestore(snapshot: QueryDocumentSnapshot): Post {
|
||
// Hack: Due to our build setup, TypeScript thinks this is a
|
||
// firestore-lite converter which does not have options
|
||
const options = arguments[1] as SnapshotOptions;
|
||
// Check that options were passed in properly.
|
||
expect(options).to.deep.equal({ serverTimestamps: 'estimate' });
|
||
|
||
const data = snapshot.data(options);
|
||
return new Post(data.title, data.author, snapshot.ref);
|
||
}
|
||
});
|
||
|
||
await setDoc(docRef, new Post('post', 'author'));
|
||
const postData = await getDoc(docRef);
|
||
postData.data({ serverTimestamps: 'estimate' });
|
||
});
|
||
});
|
||
|
||
it('drops the converter when calling CollectionReference<T>.parent()', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const postsCollection = collection(
|
||
db,
|
||
'users/user1/posts'
|
||
).withConverter(postConverter);
|
||
|
||
const usersCollection = postsCollection.parent;
|
||
expect(refEqual(usersCollection!, doc(db, 'users/user1'))).to.be.true;
|
||
});
|
||
});
|
||
|
||
it('checks converter when comparing with isEqual()', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const postConverter2 = { ...postConverter };
|
||
|
||
const postsCollection = collection(
|
||
db,
|
||
'users/user1/posts'
|
||
).withConverter(postConverter);
|
||
const postsCollection2 = collection(
|
||
db,
|
||
'users/user1/posts'
|
||
).withConverter(postConverter2);
|
||
expect(refEqual(postsCollection, postsCollection2)).to.be.false;
|
||
|
||
const docRef = doc(db, 'some/doc').withConverter(postConverter);
|
||
const docRef2 = doc(db, 'some/doc').withConverter(postConverter2);
|
||
expect(refEqual(docRef, docRef2)).to.be.false;
|
||
});
|
||
});
|
||
|
||
it('Correct snapshot specified to fromFirestore() when registered with DocumentReference', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const untypedDocRef = doc(collection(db, '/models'));
|
||
const docRef = untypedDocRef.withConverter(postConverter);
|
||
await setDoc(docRef, new Post('post', 'author'));
|
||
const docSnapshot = await getDoc(docRef);
|
||
const ref = docSnapshot.data()!.ref!;
|
||
expect(ref).to.be.an.instanceof(DocumentReference);
|
||
expect(refEqual(untypedDocRef, ref)).to.be.true;
|
||
});
|
||
});
|
||
|
||
it('Correct snapshot specified to fromFirestore() when registered with CollectionReference', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const untypedCollection = collection(
|
||
doc(collection(db, '/models')),
|
||
'sub'
|
||
);
|
||
const typedCollection = untypedCollection.withConverter(postConverter);
|
||
const docRef = doc(typedCollection);
|
||
await setDoc(docRef, new Post('post', 'author', docRef));
|
||
const querySnapshot = await getDocs(typedCollection);
|
||
expect(querySnapshot.size).to.equal(1);
|
||
const ref = querySnapshot.docs[0].data().ref!;
|
||
expect(ref).to.be.an.instanceof(DocumentReference);
|
||
const untypedDocRef = doc(untypedCollection, docRef.id);
|
||
expect(refEqual(untypedDocRef, ref)).to.be.true;
|
||
});
|
||
});
|
||
|
||
it('Correct snapshot specified to fromFirestore() when registered with Query', () => {
|
||
return withTestDb(persistence, async db => {
|
||
const untypedCollection = collection(db, '/models');
|
||
const untypedDocRef = doc(untypedCollection);
|
||
const docRef = untypedDocRef.withConverter(postConverter);
|
||
await setDoc(docRef, new Post('post', 'author', docRef));
|
||
const filteredQuery = query(
|
||
untypedCollection,
|
||
where(documentId(), '==', docRef.id)
|
||
).withConverter(postConverter);
|
||
const querySnapshot = await getDocs(filteredQuery);
|
||
expect(querySnapshot.size).to.equal(1);
|
||
const ref = querySnapshot.docs[0].data().ref!;
|
||
expect(ref).to.be.an.instanceof(DocumentReference);
|
||
expect(refEqual(untypedDocRef, ref)).to.be.true;
|
||
});
|
||
});
|
||
|
||
it('DocumentReference.withConverter() default DbModelType', () => {
|
||
const converter = {
|
||
toFirestore: (value: number) => {
|
||
return { value };
|
||
},
|
||
fromFirestore: (snapshot: QueryDocumentSnapshot) => {
|
||
return snapshot.data()['value'] as number;
|
||
}
|
||
};
|
||
return withTestDoc(persistence, async docRef => {
|
||
// The line below should compile since the DbModelType type parameter of
|
||
// DocumentReference.withConverter() has a default value.
|
||
const typedDocRef = docRef.withConverter<number>(converter);
|
||
await setDoc(typedDocRef, 42);
|
||
const snapshot = await getDoc(typedDocRef);
|
||
expect(snapshot.data()).to.equal(42);
|
||
});
|
||
});
|
||
|
||
it('CollectionReference.withConverter() default DbModelType', () => {
|
||
const converter = {
|
||
toFirestore: (value: number) => {
|
||
return { value };
|
||
},
|
||
fromFirestore: (snapshot: QueryDocumentSnapshot) => {
|
||
return snapshot.data()['value'] as number;
|
||
}
|
||
};
|
||
const testDocs = { doc1: { value: 42 } };
|
||
return withTestCollection(persistence, testDocs, async collectionRef => {
|
||
// The line below should compile since the DbModelType type parameter of
|
||
// CollectionReference.withConverter() has a default value.
|
||
const typedCollectionRef =
|
||
collectionRef.withConverter<number>(converter);
|
||
const snapshot = await getDocs(typedCollectionRef);
|
||
expect(snapshot.size).to.equal(1);
|
||
expect(snapshot.docs[0].data()).to.equal(42);
|
||
});
|
||
});
|
||
|
||
it('Query.withConverter() default DbModelType', () => {
|
||
const converter = {
|
||
toFirestore: (value: number) => {
|
||
return { value };
|
||
},
|
||
fromFirestore: (snapshot: QueryDocumentSnapshot) => {
|
||
return snapshot.data()['value'] as number;
|
||
}
|
||
};
|
||
const testDocs = { doc1: { value: 42 } };
|
||
return withTestCollection(persistence, testDocs, async collectionRef => {
|
||
const query_ = query(collectionRef, where('value', '==', 42));
|
||
// The line below should compile since the DbModelType type parameter of
|
||
// Query.withConverter() has a default value.
|
||
const typedQuery = query_.withConverter<number>(converter);
|
||
const snapshot = await getDocs(typedQuery);
|
||
expect(snapshot.size).to.equal(1);
|
||
expect(snapshot.docs[0].data()).to.equal(42);
|
||
});
|
||
});
|
||
});
|
||
|
||
// TODO(b/196858864): This test regularly times out on CI.
|
||
// eslint-disable-next-line no-restricted-properties
|
||
it.skip('can set and get data with auto detect long polling enabled', () => {
|
||
const settings = {
|
||
...DEFAULT_SETTINGS,
|
||
experimentalAutoDetectLongPolling: true
|
||
};
|
||
|
||
return withTestDbsSettings(
|
||
persistence,
|
||
DEFAULT_PROJECT_ID,
|
||
settings,
|
||
1,
|
||
async ([db]) => {
|
||
const data = { name: 'Rafi', email: '[email protected]' };
|
||
const ref = await doc(collection(db, 'users'));
|
||
|
||
return setDoc(ref, data)
|
||
.then(() => getDoc(ref))
|
||
.then(snapshot => {
|
||
expect(snapshot.exists()).to.be.ok;
|
||
expect(snapshot.data()).to.deep.equal(data);
|
||
});
|
||
}
|
||
);
|
||
});
|
||
|
||
it('can keep docs separate with multi-db when online', () => {
|
||
return withNamedTestDbsOrSkipUnlessUsingEmulator(
|
||
persistence,
|
||
['db1', 'db2'],
|
||
async ([db1, db2]) => {
|
||
const data = { name: 'Rafi', email: '[email protected]' };
|
||
|
||
const ref1 = await doc(collection(db1, 'users'), 'doc1');
|
||
await setDoc(ref1, data);
|
||
const snapshot1 = await getDoc(ref1);
|
||
expect(snapshot1.exists()).to.be.ok;
|
||
expect(snapshot1.data()).to.be.deep.equals(data);
|
||
|
||
const ref2 = await doc(collection(db2, 'users'), 'doc1');
|
||
const snapshot2 = await getDocFromServer(ref2);
|
||
expect(snapshot2.exists()).to.not.be.ok;
|
||
}
|
||
);
|
||
});
|
||
|
||
it('can keep docs separate with multi-db when offline', () => {
|
||
return withNamedTestDbsOrSkipUnlessUsingEmulator(
|
||
persistence,
|
||
['db1', 'db2'],
|
||
async ([db1, db2]) => {
|
||
await disableNetwork(db1);
|
||
await disableNetwork(db2);
|
||
const data = { name: 'Rafi', email: '[email protected]' };
|
||
|
||
const ref1 = await doc(collection(db1, 'users'));
|
||
void setDoc(ref1, data);
|
||
const snapshot = await getDocFromCache(ref1);
|
||
expect(snapshot.exists()).to.be.ok;
|
||
expect(snapshot.data()).to.be.deep.equals(data);
|
||
|
||
const ref2 = await doc(collection(db2, 'users'));
|
||
await expect(getDocFromCache(ref2)).to.eventually.rejectedWith(
|
||
'Failed to get document from cache.'
|
||
);
|
||
}
|
||
);
|
||
});
|
||
|
||
it('Cannot get document from cache with eager GC enabled.', () => {
|
||
const initialData = { key: 'value' };
|
||
return withTestDb(persistence.toEagerGc(), async db => {
|
||
const docRef = doc(collection(db, 'test-collection'));
|
||
await setDoc(docRef, initialData);
|
||
await expect(getDocFromCache(docRef)).to.be.rejectedWith('Failed to get');
|
||
});
|
||
});
|
||
|
||
it('Can get document from cache with Lru GC enabled.', () => {
|
||
const initialData = { key: 'value' };
|
||
return withTestDb(persistence.toLruGc(), async db => {
|
||
const docRef = doc(collection(db, 'test-collection'));
|
||
await setDoc(docRef, initialData);
|
||
return getDocFromCache(docRef).then(doc => {
|
||
expect(doc.exists()).to.be.true;
|
||
expect(doc.metadata.fromCache).to.be.true;
|
||
expect(doc.data()).to.deep.equal(initialData);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('Lru GC is enabled by default.', () => {
|
||
const initialData = { key: 'value' };
|
||
return withTestDb(persistence, async db => {
|
||
const docRef = doc(collection(db, 'test-collection'));
|
||
await setDoc(docRef, initialData);
|
||
return getDocFromCache(docRef).then(doc => {
|
||
expect(doc.exists()).to.be.true;
|
||
expect(doc.metadata.fromCache).to.be.true;
|
||
expect(doc.data()).to.deep.equal(initialData);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('sort documents by DocumentId', () => {
|
||
it('snapshot listener sorts query by DocumentId same way as get query', async () => {
|
||
const testDocs = {
|
||
'A': { a: 1 },
|
||
'a': { a: 1 },
|
||
'Aa': { a: 1 },
|
||
'7': { a: 1 },
|
||
'12': { a: 1 },
|
||
'__id7__': { a: 1 },
|
||
'__id12__': { a: 1 },
|
||
'__id-2__': { a: 1 },
|
||
'_id1__': { a: 1 },
|
||
'__id1_': { a: 1 },
|
||
'__id': { a: 1 },
|
||
// largest long numbers
|
||
'__id9223372036854775807__': { a: 1 },
|
||
'__id9223372036854775806__': { a: 1 },
|
||
// smallest long numbers
|
||
'__id-9223372036854775808__': { a: 1 },
|
||
'__id-9223372036854775807__': { a: 1 }
|
||
};
|
||
|
||
return withTestCollection(persistence, testDocs, async collectionRef => {
|
||
const orderedQuery = query(collectionRef, orderBy(documentId()));
|
||
const expectedDocs = [
|
||
'__id-9223372036854775808__',
|
||
'__id-9223372036854775807__',
|
||
'__id-2__',
|
||
'__id7__',
|
||
'__id12__',
|
||
'__id9223372036854775806__',
|
||
'__id9223372036854775807__',
|
||
'12',
|
||
'7',
|
||
'A',
|
||
'Aa',
|
||
'__id',
|
||
'__id1_',
|
||
'_id1__',
|
||
'a'
|
||
];
|
||
|
||
const getSnapshot = await getDocsFromServer(orderedQuery);
|
||
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
|
||
|
||
const storeEvent = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
|
||
const watchSnapshot = await storeEvent.awaitEvent();
|
||
expect(toIds(watchSnapshot)).to.deep.equal(expectedDocs);
|
||
|
||
unsubscribe();
|
||
});
|
||
});
|
||
|
||
// Enterprise does not sort numeric IDs before string
|
||
it.skipEnterprise(
|
||
'snapshot listener sorts filtered query by DocumentId same way as get query',
|
||
async () => {
|
||
const testDocs = {
|
||
'A': { a: 1 },
|
||
'a': { a: 1 },
|
||
'Aa': { a: 1 },
|
||
'7': { a: 1 },
|
||
'12': { a: 1 },
|
||
'__id7__': { a: 1 },
|
||
'__id12__': { a: 1 },
|
||
'__id-2__': { a: 1 },
|
||
'_id1__': { a: 1 },
|
||
'__id1_': { a: 1 },
|
||
'__id': { a: 1 },
|
||
// largest long numbers
|
||
'__id9223372036854775807__': { a: 1 },
|
||
'__id9223372036854775806__': { a: 1 },
|
||
// smallest long numbers
|
||
'__id-9223372036854775808__': { a: 1 },
|
||
'__id-9223372036854775807__': { a: 1 }
|
||
};
|
||
|
||
return withTestCollection(
|
||
persistence,
|
||
testDocs,
|
||
async collectionRef => {
|
||
const filteredQuery = query(
|
||
collectionRef,
|
||
orderBy(documentId()),
|
||
where(documentId(), '>', '__id7__'),
|
||
where(documentId(), '<=', 'Aa')
|
||
);
|
||
const expectedDocs = [
|
||
'__id12__',
|
||
'__id9223372036854775806__',
|
||
'__id9223372036854775807__',
|
||
'12',
|
||
'7',
|
||
'A',
|
||
'Aa'
|
||
];
|
||
|
||
const getSnapshot = await getDocsFromServer(filteredQuery);
|
||
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
|
||
|
||
const storeEvent = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshot(
|
||
filteredQuery,
|
||
storeEvent.storeEvent
|
||
);
|
||
const watchSnapshot = await storeEvent.awaitEvent();
|
||
expect(toIds(watchSnapshot)).to.deep.equal(expectedDocs);
|
||
unsubscribe();
|
||
}
|
||
);
|
||
}
|
||
);
|
||
|
||
// eslint-disable-next-line no-restricted-properties
|
||
(persistence.gc === 'lru' ? describe : describe.skip)('offline', () => {
|
||
it.skipEnterprise(
|
||
'SDK orders query the same way online and offline',
|
||
async () => {
|
||
const testDocs = {
|
||
'A': { a: 1 },
|
||
'a': { a: 1 },
|
||
'Aa': { a: 1 },
|
||
'7': { a: 1 },
|
||
'12': { a: 1 },
|
||
'__id7__': { a: 1 },
|
||
'__id12__': { a: 1 },
|
||
'__id-2__': { a: 1 },
|
||
'_id1__': { a: 1 },
|
||
'__id1_': { a: 1 },
|
||
'__id': { a: 1 },
|
||
// largest long numbers
|
||
'__id9223372036854775807__': { a: 1 },
|
||
'__id9223372036854775806__': { a: 1 },
|
||
// smallest long numbers
|
||
'__id-9223372036854775808__': { a: 1 },
|
||
'__id-9223372036854775807__': { a: 1 }
|
||
};
|
||
|
||
return withTestCollection(
|
||
persistence,
|
||
testDocs,
|
||
async collectionRef => {
|
||
const orderedQuery = query(collectionRef, orderBy(documentId()));
|
||
let expectedDocs = [
|
||
'__id-9223372036854775808__',
|
||
'__id-9223372036854775807__',
|
||
'__id-2__',
|
||
'__id7__',
|
||
'__id12__',
|
||
'__id9223372036854775806__',
|
||
'__id9223372036854775807__',
|
||
'12',
|
||
'7',
|
||
'A',
|
||
'Aa',
|
||
'__id',
|
||
'__id1_',
|
||
'_id1__',
|
||
'a'
|
||
];
|
||
await checkOnlineAndOfflineResultsMatch(
|
||
collectionRef,
|
||
orderedQuery,
|
||
...expectedDocs
|
||
);
|
||
|
||
const filteredQuery = query(
|
||
collectionRef,
|
||
orderBy(documentId()),
|
||
where(documentId(), '>', '__id7__'),
|
||
where(documentId(), '<=', 'Aa')
|
||
);
|
||
expectedDocs = [
|
||
'__id12__',
|
||
'__id9223372036854775806__',
|
||
'__id9223372036854775807__',
|
||
'12',
|
||
'7',
|
||
'A',
|
||
'Aa'
|
||
];
|
||
await checkOnlineAndOfflineResultsMatch(
|
||
collectionRef,
|
||
filteredQuery,
|
||
...expectedDocs
|
||
);
|
||
}
|
||
);
|
||
}
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('Sort unicode strings', () => {
|
||
const expectedDocs = [
|
||
'b',
|
||
'a',
|
||
'h',
|
||
'i',
|
||
'c',
|
||
'f',
|
||
'e',
|
||
'd',
|
||
'g',
|
||
'k',
|
||
'j'
|
||
];
|
||
it('snapshot listener sorts unicode strings the same as server', async () => {
|
||
const testDocs = {
|
||
'a': { value: 'Łukasiewicz' },
|
||
'b': { value: 'Sierpiński' },
|
||
'c': { value: '岩澤' },
|
||
'd': { value: '🄟' },
|
||
'e': { value: 'P' },
|
||
'f': { value: '︒' },
|
||
'g': { value: '🐵' },
|
||
'h': { value: '你好' },
|
||
'i': { value: '你顥' },
|
||
'j': { value: '😁' },
|
||
'k': { value: '😀' }
|
||
};
|
||
|
||
return withTestCollection(persistence, testDocs, async collectionRef => {
|
||
const orderedQuery = query(collectionRef, orderBy('value'));
|
||
|
||
const getSnapshot = await getDocsFromServer(orderedQuery);
|
||
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
|
||
|
||
const storeEvent = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
|
||
const watchSnapshot = await storeEvent.awaitEvent();
|
||
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
|
||
|
||
unsubscribe();
|
||
|
||
await checkOnlineAndOfflineResultsMatch(
|
||
collectionRef,
|
||
orderedQuery,
|
||
...expectedDocs
|
||
);
|
||
});
|
||
});
|
||
|
||
it('snapshot listener sorts unicode strings in array the same as server', async () => {
|
||
const testDocs = {
|
||
'a': { value: ['Łukasiewicz'] },
|
||
'b': { value: ['Sierpiński'] },
|
||
'c': { value: ['岩澤'] },
|
||
'd': { value: ['🄟'] },
|
||
'e': { value: ['P'] },
|
||
'f': { value: ['︒'] },
|
||
'g': { value: ['🐵'] },
|
||
'h': { value: ['你好'] },
|
||
'i': { value: ['你顥'] },
|
||
'j': { value: ['😁'] },
|
||
'k': { value: ['😀'] }
|
||
};
|
||
|
||
return withTestCollection(persistence, testDocs, async collectionRef => {
|
||
const orderedQuery = query(collectionRef, orderBy('value'));
|
||
|
||
const getSnapshot = await getDocsFromServer(orderedQuery);
|
||
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
|
||
|
||
const storeEvent = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
|
||
const watchSnapshot = await storeEvent.awaitEvent();
|
||
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
|
||
|
||
unsubscribe();
|
||
|
||
await checkOnlineAndOfflineResultsMatch(
|
||
collectionRef,
|
||
orderedQuery,
|
||
...expectedDocs
|
||
);
|
||
});
|
||
});
|
||
|
||
it('snapshot listener sorts unicode strings in map the same as server', async () => {
|
||
const testDocs = {
|
||
'a': { value: { foo: 'Łukasiewicz' } },
|
||
'b': { value: { foo: 'Sierpiński' } },
|
||
'c': { value: { foo: '岩澤' } },
|
||
'd': { value: { foo: '🄟' } },
|
||
'e': { value: { foo: 'P' } },
|
||
'f': { value: { foo: '︒' } },
|
||
'g': { value: { foo: '🐵' } },
|
||
'h': { value: { foo: '你好' } },
|
||
'i': { value: { foo: '你顥' } },
|
||
'j': { value: { foo: '😁' } },
|
||
'k': { value: { foo: '😀' } }
|
||
};
|
||
|
||
return withTestCollection(persistence, testDocs, async collectionRef => {
|
||
const orderedQuery = query(collectionRef, orderBy('value'));
|
||
|
||
const getSnapshot = await getDocsFromServer(orderedQuery);
|
||
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
|
||
|
||
const storeEvent = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
|
||
const watchSnapshot = await storeEvent.awaitEvent();
|
||
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
|
||
|
||
unsubscribe();
|
||
|
||
await checkOnlineAndOfflineResultsMatch(
|
||
collectionRef,
|
||
orderedQuery,
|
||
...expectedDocs
|
||
);
|
||
});
|
||
});
|
||
|
||
it('snapshot listener sorts unicode strings in map key the same as server', async () => {
|
||
const testDocs = {
|
||
'a': { value: { 'Łukasiewicz': true } },
|
||
'b': { value: { 'Sierpiński': true } },
|
||
'c': { value: { '岩澤': true } },
|
||
'd': { value: { '🄟': true } },
|
||
'e': { value: { 'P': true } },
|
||
'f': { value: { '︒': true } },
|
||
'g': { value: { '🐵': true } },
|
||
'h': { value: { '你好': true } },
|
||
'i': { value: { '你顥': true } },
|
||
'j': { value: { '😁': true } },
|
||
'k': { value: { '😀': true } }
|
||
};
|
||
|
||
return withTestCollection(persistence, testDocs, async collectionRef => {
|
||
const orderedQuery = query(collectionRef, orderBy('value'));
|
||
|
||
const getSnapshot = await getDocsFromServer(orderedQuery);
|
||
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
|
||
|
||
const storeEvent = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
|
||
const watchSnapshot = await storeEvent.awaitEvent();
|
||
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
|
||
|
||
unsubscribe();
|
||
|
||
await checkOnlineAndOfflineResultsMatch(
|
||
collectionRef,
|
||
orderedQuery,
|
||
...expectedDocs
|
||
);
|
||
});
|
||
});
|
||
|
||
it('snapshot listener sorts unicode strings in document key the same as server', async () => {
|
||
const testDocs = {
|
||
'Łukasiewicz': { value: true },
|
||
'Sierpiński': { value: true },
|
||
'岩澤': { value: true },
|
||
'🄟': { value: true },
|
||
'P': { value: true },
|
||
'︒': { value: true },
|
||
'🐵': { value: true },
|
||
'你好': { value: true },
|
||
'你顥': { value: true },
|
||
'😁': { value: true },
|
||
'😀': { value: true }
|
||
};
|
||
|
||
return withTestCollection(persistence, testDocs, async collectionRef => {
|
||
const orderedQuery = query(collectionRef, orderBy(documentId()));
|
||
|
||
const getSnapshot = await getDocsFromServer(orderedQuery);
|
||
const expectedDocs = [
|
||
'Sierpiński',
|
||
'Łukasiewicz',
|
||
'你好',
|
||
'你顥',
|
||
'岩澤',
|
||
'︒',
|
||
'P',
|
||
'🄟',
|
||
'🐵',
|
||
'😀',
|
||
'😁'
|
||
];
|
||
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
|
||
|
||
const storeEvent = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
|
||
const watchSnapshot = await storeEvent.awaitEvent();
|
||
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
|
||
|
||
unsubscribe();
|
||
|
||
await checkOnlineAndOfflineResultsMatch(
|
||
collectionRef,
|
||
orderedQuery,
|
||
...expectedDocs
|
||
);
|
||
});
|
||
});
|
||
|
||
// eslint-disable-next-line no-restricted-properties
|
||
(persistence.storage === 'indexeddb' ? it.skip : it)(
|
||
'snapshot listener sorts unicode strings in document key the same as server with persistence',
|
||
async () => {
|
||
const testDocs = {
|
||
'Łukasiewicz': { value: true },
|
||
'Sierpiński': { value: true },
|
||
'岩澤': { value: true },
|
||
'🄟': { value: true },
|
||
'P': { value: true },
|
||
'︒': { value: true },
|
||
'🐵': { value: true },
|
||
'你好': { value: true },
|
||
'你顥': { value: true },
|
||
'😁': { value: true },
|
||
'😀': { value: true }
|
||
};
|
||
|
||
return withTestCollection(
|
||
persistence,
|
||
testDocs,
|
||
async collectionRef => {
|
||
const orderedQuery = query(collectionRef, orderBy('value'));
|
||
|
||
const getSnapshot = await getDocsFromServer(orderedQuery);
|
||
expect(toIds(getSnapshot)).to.deep.equal([
|
||
'Sierpiński',
|
||
'Łukasiewicz',
|
||
'你好',
|
||
'你顥',
|
||
'岩澤',
|
||
'︒',
|
||
'P',
|
||
'🄟',
|
||
'🐵',
|
||
'😀',
|
||
'😁'
|
||
]);
|
||
|
||
const storeEvent = new EventsAccumulator<QuerySnapshot>();
|
||
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
|
||
const watchSnapshot = await storeEvent.awaitEvent();
|
||
// TODO: IndexedDB sorts string lexicographically, and misses the document with ID '🄟','🐵'
|
||
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
|
||
|
||
unsubscribe();
|
||
}
|
||
);
|
||
}
|
||
);
|
||
});
|
||
});
|