mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
379 lines
12 KiB
TypeScript
379 lines
12 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2022 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import * as firestore from '@firebase/firestore-types';
|
|
import { expect } from 'chai';
|
|
|
|
import { EventsAccumulator } from './util/events_accumulator';
|
|
import * as firebaseExport from './util/firebase_export';
|
|
import * as integrationHelpers from './util/helpers';
|
|
|
|
const apiDescribe = integrationHelpers.apiDescribe;
|
|
const FieldPath = firebaseExport.FieldPath;
|
|
const FieldValue = firebaseExport.FieldValue;
|
|
const Timestamp = firebaseExport.Timestamp;
|
|
|
|
apiDescribe('Database batch writes', (persistence: boolean) => {
|
|
it('supports empty batches', () => {
|
|
return integrationHelpers.withTestDb(persistence, db => {
|
|
return db.batch().commit();
|
|
});
|
|
});
|
|
|
|
it('can set documents', () => {
|
|
return integrationHelpers.withTestDoc(persistence, doc => {
|
|
return doc.firestore
|
|
.batch()
|
|
.set(doc, { foo: 'bar' })
|
|
.commit()
|
|
.then(() => doc.get())
|
|
.then(snapshot => {
|
|
expect(snapshot.exists).to.equal(true);
|
|
expect(snapshot.data()).to.deep.equal({ foo: 'bar' });
|
|
});
|
|
});
|
|
});
|
|
|
|
it('can set documents with merge', () => {
|
|
return integrationHelpers.withTestDoc(persistence, doc => {
|
|
return doc.firestore
|
|
.batch()
|
|
.set(doc, { a: 'b', nested: { a: 'b' } }, { merge: true })
|
|
.commit()
|
|
.then(() => {
|
|
return doc.firestore
|
|
.batch()
|
|
.set(doc, { c: 'd', nested: { c: 'd' } }, { merge: true })
|
|
.commit();
|
|
})
|
|
.then(() => doc.get())
|
|
.then(snapshot => {
|
|
expect(snapshot.exists).to.equal(true);
|
|
expect(snapshot.data()).to.deep.equal({
|
|
a: 'b',
|
|
c: 'd',
|
|
nested: { a: 'b', c: 'd' }
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('can update documents', () => {
|
|
return integrationHelpers.withTestDoc(persistence, doc => {
|
|
return doc
|
|
.set({ foo: 'bar' })
|
|
.then(() => doc.firestore.batch().update(doc, { baz: 42 }).commit())
|
|
.then(() => doc.get())
|
|
.then(snapshot => {
|
|
expect(snapshot.exists).to.equal(true);
|
|
expect(snapshot.data()).to.deep.equal({ foo: 'bar', baz: 42 });
|
|
});
|
|
});
|
|
});
|
|
|
|
it('can update nested fields', () => {
|
|
const initialData = {
|
|
desc: 'Description',
|
|
owner: { name: 'Jonny' },
|
|
'is.admin': false
|
|
};
|
|
const finalData = {
|
|
desc: 'Description',
|
|
owner: { name: 'Sebastian' },
|
|
'is.admin': true
|
|
};
|
|
|
|
return integrationHelpers.withTestDb(persistence, db => {
|
|
const doc = db.collection('counters').doc();
|
|
return doc.firestore
|
|
.batch()
|
|
.set(doc, initialData)
|
|
.update(doc, 'owner.name', 'Sebastian', new FieldPath('is.admin'), true)
|
|
.commit()
|
|
.then(() => doc.get())
|
|
.then(docSnapshot => {
|
|
expect(docSnapshot.exists).to.be.ok;
|
|
expect(docSnapshot.data()).to.deep.equal(finalData);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('can delete documents', () => {
|
|
// TODO(#1865): This test fails with node:persistence against Prod
|
|
return integrationHelpers.withTestDoc(persistence, doc => {
|
|
return doc
|
|
.set({ foo: 'bar' })
|
|
.then(() => doc.get())
|
|
.then(snapshot => {
|
|
expect(snapshot.exists).to.equal(true);
|
|
})
|
|
.then(() => doc.firestore.batch().delete(doc).commit())
|
|
.then(() => doc.get())
|
|
.then(snapshot => {
|
|
expect(snapshot.exists).to.equal(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('commit atomically, raising correct events', () => {
|
|
return integrationHelpers.withTestCollection(
|
|
persistence,
|
|
{},
|
|
collection => {
|
|
const docA = collection.doc('a');
|
|
const docB = collection.doc('b');
|
|
const accumulator = new EventsAccumulator<firestore.QuerySnapshot>();
|
|
const unsubscribe = collection.onSnapshot(
|
|
{ includeMetadataChanges: true },
|
|
accumulator.storeEvent
|
|
);
|
|
return accumulator
|
|
.awaitEvent()
|
|
.then(initialSnap => {
|
|
expect(initialSnap.docs.length).to.equal(0);
|
|
|
|
// Atomically write two documents.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
collection.firestore
|
|
.batch()
|
|
.set(docA, { a: 1 })
|
|
.set(docB, { b: 2 })
|
|
.commit();
|
|
|
|
return accumulator.awaitEvent();
|
|
})
|
|
.then(localSnap => {
|
|
expect(localSnap.metadata.hasPendingWrites).to.equal(true);
|
|
expect(integrationHelpers.toDataArray(localSnap)).to.deep.equal([
|
|
{ a: 1 },
|
|
{ b: 2 }
|
|
]);
|
|
return accumulator.awaitEvent();
|
|
})
|
|
.then(serverSnap => {
|
|
expect(serverSnap.metadata.hasPendingWrites).to.equal(false);
|
|
expect(integrationHelpers.toDataArray(serverSnap)).to.deep.equal([
|
|
{ a: 1 },
|
|
{ b: 2 }
|
|
]);
|
|
unsubscribe();
|
|
});
|
|
}
|
|
);
|
|
});
|
|
|
|
it('fail atomically, raising correct events', () => {
|
|
return integrationHelpers.withTestCollection(
|
|
persistence,
|
|
{},
|
|
collection => {
|
|
const docA = collection.doc('a');
|
|
const docB = collection.doc('b');
|
|
const accumulator = new EventsAccumulator<firestore.QuerySnapshot>();
|
|
const unsubscribe = collection.onSnapshot(
|
|
{ includeMetadataChanges: true },
|
|
accumulator.storeEvent
|
|
);
|
|
let batchCommitPromise: Promise<void>;
|
|
return accumulator
|
|
.awaitEvent()
|
|
.then(initialSnap => {
|
|
expect(initialSnap.docs.length).to.equal(0);
|
|
|
|
// Atomically write 1 document and update a nonexistent
|
|
// document.
|
|
batchCommitPromise = collection.firestore
|
|
.batch()
|
|
.set(docA, { a: 1 })
|
|
.update(docB, { b: 2 })
|
|
.commit();
|
|
|
|
// Node logs warnings if you don't attach an error handler to a
|
|
// Promise before it fails, so attach a dummy one here (we handle
|
|
// the rejection for real below).
|
|
batchCommitPromise.catch(err => {});
|
|
|
|
return accumulator.awaitEvent();
|
|
})
|
|
.then(localSnap => {
|
|
// Local event with the set document.
|
|
expect(localSnap.metadata.hasPendingWrites).to.equal(true);
|
|
expect(integrationHelpers.toDataArray(localSnap)).to.deep.equal([
|
|
{ a: 1 }
|
|
]);
|
|
|
|
return accumulator.awaitEvent();
|
|
})
|
|
.then(serverSnap => {
|
|
// Server event with the set reverted.
|
|
expect(serverSnap.metadata.hasPendingWrites).to.equal(false);
|
|
expect(serverSnap.docs.length).to.equal(0);
|
|
|
|
return batchCommitPromise;
|
|
})
|
|
.then(
|
|
() => {
|
|
expect.fail('Batch commit should have failed.');
|
|
},
|
|
err => {
|
|
expect(err.message).to.exist;
|
|
expect(err.code).to.equal('not-found');
|
|
unsubscribe();
|
|
}
|
|
);
|
|
}
|
|
);
|
|
});
|
|
|
|
it('write the same server timestamp across writes', () => {
|
|
return integrationHelpers.withTestCollection(
|
|
persistence,
|
|
{},
|
|
collection => {
|
|
const docA = collection.doc('a');
|
|
const docB = collection.doc('b');
|
|
const accumulator = new EventsAccumulator<firestore.QuerySnapshot>();
|
|
const unsubscribe = collection.onSnapshot(
|
|
{ includeMetadataChanges: true },
|
|
accumulator.storeEvent
|
|
);
|
|
return accumulator
|
|
.awaitEvent()
|
|
.then(initialSnap => {
|
|
expect(initialSnap.docs.length).to.equal(0);
|
|
|
|
// Atomically write 2 documents with server timestamps.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
collection.firestore
|
|
.batch()
|
|
.set(docA, {
|
|
when: FieldValue.serverTimestamp()
|
|
})
|
|
.set(docB, {
|
|
when: FieldValue.serverTimestamp()
|
|
})
|
|
.commit();
|
|
|
|
return accumulator.awaitEvent();
|
|
})
|
|
.then(localSnap => {
|
|
expect(localSnap.metadata.hasPendingWrites).to.equal(true);
|
|
expect(localSnap.docs.length).to.equal(2);
|
|
expect(integrationHelpers.toDataArray(localSnap)).to.deep.equal([
|
|
{ when: null },
|
|
{ when: null }
|
|
]);
|
|
|
|
return accumulator.awaitEvent();
|
|
})
|
|
.then(serverSnap => {
|
|
expect(serverSnap.metadata.hasPendingWrites).to.equal(false);
|
|
expect(serverSnap.docs.length).to.equal(2);
|
|
const when = serverSnap.docs[0].data()['when'];
|
|
expect(when).to.be.an.instanceof(Timestamp);
|
|
expect(serverSnap.docs[1].data()['when']).to.deep.equal(when);
|
|
const docChanges = serverSnap.docChanges({
|
|
includeMetadataChanges: true
|
|
});
|
|
expect(docChanges[0].type).to.equal('modified');
|
|
unsubscribe();
|
|
});
|
|
}
|
|
);
|
|
});
|
|
|
|
it('can write the same document multiple times', () => {
|
|
return integrationHelpers.withTestDoc(persistence, doc => {
|
|
const accumulator = new EventsAccumulator<firestore.DocumentSnapshot>();
|
|
const unsubscribe = doc.onSnapshot(
|
|
{ includeMetadataChanges: true },
|
|
accumulator.storeEvent
|
|
);
|
|
return accumulator
|
|
.awaitEvent()
|
|
.then(initialSnap => {
|
|
expect(initialSnap.exists).to.equal(false);
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
doc.firestore
|
|
.batch()
|
|
.delete(doc)
|
|
.set(doc, { a: 1, b: 1, when: 'when' })
|
|
.update(doc, {
|
|
b: 2,
|
|
when: FieldValue.serverTimestamp()
|
|
})
|
|
.commit();
|
|
|
|
return accumulator.awaitEvent();
|
|
})
|
|
.then(localSnap => {
|
|
expect(localSnap.metadata.hasPendingWrites).to.equal(true);
|
|
expect(localSnap.data()).to.deep.equal({ a: 1, b: 2, when: null });
|
|
|
|
return accumulator.awaitEvent();
|
|
})
|
|
.then(serverSnap => {
|
|
expect(serverSnap.metadata.hasPendingWrites).to.equal(false);
|
|
const when = serverSnap.get('when');
|
|
expect(when).to.be.an.instanceof(Timestamp);
|
|
expect(serverSnap.data()).to.deep.equal({ a: 1, b: 2, when });
|
|
unsubscribe();
|
|
});
|
|
});
|
|
});
|
|
|
|
// PORTING NOTE: These tests are for FirestoreDataConverter support and apply
|
|
// only to web.
|
|
apiDescribe('withConverter() support', (persistence: boolean) => {
|
|
class Post {
|
|
constructor(readonly title: string, readonly author: string) {}
|
|
byline(): string {
|
|
return this.title + ', by ' + this.author;
|
|
}
|
|
}
|
|
|
|
it('for Writebatch.set<T>()', () => {
|
|
return integrationHelpers.withTestDb(persistence, db => {
|
|
const docRef = db
|
|
.collection('posts')
|
|
.doc()
|
|
.withConverter({
|
|
toFirestore(post: Post): firestore.DocumentData {
|
|
return { title: post.title, author: post.author };
|
|
},
|
|
fromFirestore(
|
|
snapshot: firestore.QueryDocumentSnapshot,
|
|
options: firestore.SnapshotOptions
|
|
): Post {
|
|
const data = snapshot.data(options);
|
|
return new Post(data.title, data.author);
|
|
}
|
|
});
|
|
return docRef.firestore
|
|
.batch()
|
|
.set(docRef, new Post('post', 'author'))
|
|
.commit()
|
|
.then(() => docRef.get())
|
|
.then(snapshot => {
|
|
expect(snapshot.exists).to.equal(true);
|
|
expect(snapshot.data()!.byline()).to.deep.equal('post, by author');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|