Skip to content
This repository was archived by the owner on Apr 4, 2023. It is now read-only.

Commit 029355f

Browse files
Batch and Transaction Operation in FireStore #667
1 parent 594c3e8 commit 029355f

7 files changed

Lines changed: 360 additions & 13 deletions

File tree

demo-ng/app/tabs/firestore/firestore.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,7 @@
2525
<Button text="Where, Order, Limit" (tap)="firestoreWhereOrderLimit()" class="button"></Button>
2626
<Button text="Where array_contains" (tap)="firestoreWhereCityHasALake()" class="button"></Button>
2727
<Button text="Delete" (tap)="firestoreDelete()" class="button"></Button>
28+
<!--<Button text="Transactional update" (tap)="transactionalUpdate()" class="button"></Button>-->
29+
<Button text="Write batch" (tap)="writeBatch()" class="button"></Button>
2830
</StackLayout>
2931
</ScrollView>

demo-ng/app/tabs/firestore/firestore.component.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,14 @@ export class FirestoreComponent {
4545
.set({
4646
name: "Woofie",
4747
last: "lastofwoofie",
48-
// note that this only works on iOS (there's a limitation in the Firestore Android SDK)
48+
// note that this only works on iOS (there's a limitation in the Firestore Android SDK)
4949
// updateTsSet: firebase.firestore().FieldValue().serverTimestamp()
5050
},
5151
{
5252
merge: true
5353
}
5454
)
55-
.then(() => {
56-
console.log("Woofie set");
57-
})
55+
.then(() => console.log("Woofie set"))
5856
.catch(err => console.log("Setting Woofie failed, error: " + err));
5957

6058

@@ -74,23 +72,26 @@ export class FirestoreComponent {
7472
state: "CA",
7573
country: "USA",
7674
capital: false,
77-
population: 3900000
75+
population: 3900000,
76+
landmarks: ["lake", "mountain"]
7877
});
7978

8079
citiesCollection.doc("SAC").set({
8180
name: "Sacramento",
8281
state: "CA",
8382
country: "USA",
8483
capital: true,
85-
population: 500000
84+
population: 500000,
85+
landmarks: ["mountain"]
8686
});
8787

8888
citiesCollection.doc("DC").set({
8989
name: "Washington, D.C.",
9090
state: "WA",
9191
country: "USA",
9292
capital: true,
93-
population: 680000
93+
population: 680000,
94+
landmarks: ["airport", "lake"]
9495
});
9596

9697
citiesCollection.doc("TOK").set({
@@ -290,4 +291,41 @@ export class FirestoreComponent {
290291
})
291292
.catch(error => console.log("doWebGetValueForCompanies error: " + error));
292293
}
294+
295+
/*
296+
public transactionalUpdate(): void {
297+
const sfDocRef: firestore.DocumentReference = firebase.firestore().collection("cities").doc("SF");
298+
299+
firebase.firestore().runTransaction(transaction => {
300+
const sfDoc = transaction.get(sfDocRef);
301+
if (!sfDoc.exists) {
302+
console.log("City SF doesn't exist");
303+
} else {
304+
const newPopulation = sfDoc.data().population + 1;
305+
console.log(`Updating city 'SF' to a new population of: ${newPopulation}, and flipping the 'capital' state to ${sfDoc.data().capital}.`);
306+
307+
// this should fail
308+
transaction
309+
.set(sfDocRef, {capital: !sfDoc.data().capital}, {merge: true})
310+
// .delete(sfDocRef) // with this line enabled, the next line will fail and the entire tx is rolled back 👍
311+
.update(sfDocRef, {population: newPopulation})
312+
}
313+
return null;
314+
})
315+
.then(() => console.log(`Transaction successfully committed`))
316+
.catch(error => console.log("doTransaction error: " + error));
317+
}
318+
*/
319+
320+
public writeBatch(): void {
321+
const sfDocRef: firestore.DocumentReference = firebase.firestore().collection("cities").doc("SF");
322+
323+
firebase.firestore().batch()
324+
.set(sfDocRef, {capital: false}, {merge: true})
325+
// .delete(sfDocRef) // Want to verify batches are atomic? With this line enabled, the next line will fail and the entire batch is rolled back correctly 👍
326+
.update(sfDocRef, {population: 5})
327+
.commit()
328+
.then(() => console.log(`Batch successfully committed`))
329+
.catch(error => console.log("Batch error: " + error));
330+
}
293331
}

docs/FIRESTORE.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,5 +237,29 @@ query
237237
});
238238
```
239239

240-
## Future work
241-
Need something else that's not supported yet? Please open an Issue or PR 😚
240+
### `batch()`
241+
To perform a (mixed) sequence of `set`, `update`, and/or `delete` operations in an atomic fashion
242+
(everything is rolled back if 1 operation fails), use the `batch` feature.
243+
244+
```typescript
245+
const sanFranciscoDocumentReference: firestore.DocumentReference = firebase.firestore().collection("cities").doc("SF");
246+
247+
firebase.firestore().batch()
248+
.set(sanFranciscoDocumentReference, {capital: false}, {merge: true})
249+
.update(sanFranciscoDocumentReference, {population: 5})
250+
.update(sanFranciscoDocumentReference, {population: 6})
251+
.commit()
252+
.then(() => console.log("Batch successfully committed"))
253+
.catch(error => console.log("Batch error: " + error));
254+
```
255+
256+
Need proof these batches are atomic? Try deleting and then updating a document 😉
257+
258+
```typescript
259+
firebase.firestore().batch()
260+
.delete(sanFranciscoDocumentReference)
261+
.update(sanFranciscoDocumentReference, {population: 7})
262+
.commit()
263+
.then(() => console.log("Batch successfully committed"))
264+
.catch(error => console.log("Batch error: " + error));
265+
```

src/app/firestore/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,13 @@ export module firestore {
1515
GeoPoint(latitude: number, longitude: number): firebase.firestore.GeoPoint {
1616
return firebase.firestore.GeoPoint(latitude, longitude);
1717
}
18+
19+
// runTransaction<T>(updateFunction: (transaction: firebase.firestore.Transaction) => Promise<any>): Promise<void> {
20+
// return firebase.firestore.runTransaction(updateFunction);
21+
// }
22+
23+
batch(): firebase.firestore.WriteBatch {
24+
return firebase.firestore.batch();
25+
}
1826
}
1927
}

src/firebase.android.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,6 +2115,159 @@ firebase.invites.getInvitation = () => {
21152115
});
21162116
};
21172117

2118+
class FirestoreWriteBatch implements firestore.WriteBatch {
2119+
2120+
public nativeWriteBatch: com.google.firebase.firestore.WriteBatch;
2121+
2122+
public set = (documentRef: firestore.DocumentReference, data: firestore.DocumentData, options?: firestore.SetOptions): firestore.WriteBatch => {
2123+
if (options && options.merge) {
2124+
this.nativeWriteBatch.set(documentRef.android, firebase.toValue(data), com.google.firebase.firestore.SetOptions.merge());
2125+
} else {
2126+
this.nativeWriteBatch.set(documentRef.android, firebase.toValue(data));
2127+
}
2128+
return this;
2129+
};
2130+
2131+
public update = (documentRef: firestore.DocumentReference, data: firestore.UpdateData): firestore.WriteBatch => {
2132+
this.nativeWriteBatch.update(documentRef.android, firebase.toValue(data));
2133+
return this;
2134+
};
2135+
2136+
public delete = (documentRef: firestore.DocumentReference): firestore.WriteBatch => {
2137+
this.nativeWriteBatch.delete(documentRef.android);
2138+
return this;
2139+
};
2140+
2141+
public commit(): Promise<void> {
2142+
return new Promise((resolve, reject) => {
2143+
const onCompleteListener = new com.google.android.gms.tasks.OnCompleteListener({
2144+
onComplete: task => {
2145+
if (!task.isSuccessful()) {
2146+
const ex = task.getException();
2147+
reject(ex && ex.getReason ? ex.getReason() : ex);
2148+
} else {
2149+
resolve();
2150+
}
2151+
}
2152+
});
2153+
this.nativeWriteBatch.commit().addOnCompleteListener(onCompleteListener);
2154+
});
2155+
};
2156+
}
2157+
2158+
firebase.firestore.batch = (): firestore.WriteBatch => {
2159+
const batch = new FirestoreWriteBatch();
2160+
batch.nativeWriteBatch = com.google.firebase.firestore.FirebaseFirestore.getInstance().batch();
2161+
return batch;
2162+
};
2163+
2164+
/*
2165+
class FirestoreTransaction implements firestore.Transaction {
2166+
2167+
public nativeTransaction: com.google.firebase.firestore.Transaction;
2168+
2169+
public get = (documentRef: firestore.DocumentReference): DocumentSnapshot => {
2170+
const docSnapshot: com.google.firebase.firestore.DocumentSnapshot = this.nativeTransaction.get(documentRef.android);
2171+
return new DocumentSnapshot(docSnapshot ? docSnapshot.getId() : null, docSnapshot.exists(), firebase.toJsObject(docSnapshot.getData()));
2172+
};
2173+
2174+
public set = (documentRef: firestore.DocumentReference, data: firestore.DocumentData, options?: firestore.SetOptions): firestore.Transaction => {
2175+
console.log(">>> in tx.set");
2176+
if (options && options.merge) {
2177+
this.nativeTransaction.set(documentRef.android, firebase.toValue(data), com.google.firebase.firestore.SetOptions.merge());
2178+
} else {
2179+
this.nativeTransaction.set(documentRef.android, firebase.toValue(data));
2180+
}
2181+
return this;
2182+
};
2183+
2184+
public update = (documentRef: firestore.DocumentReference, data: firestore.UpdateData): firestore.Transaction => {
2185+
console.log(">>> in tx.update");
2186+
this.nativeTransaction.update(documentRef.android, firebase.toValue(data));
2187+
return this;
2188+
};
2189+
2190+
public delete = (documentRef: firestore.DocumentReference): firestore.Transaction => {
2191+
console.log(">>> in tx.delete");
2192+
this.nativeTransaction.delete(documentRef.android);
2193+
return this;
2194+
}
2195+
};
2196+
2197+
firebase.firestore.Transaction = (nativeTransaction: com.google.firebase.firestore.Transaction): firestore.Transaction => {
2198+
const tx = new FirestoreTransaction();
2199+
tx.nativeTransaction = nativeTransaction;
2200+
return tx;
2201+
};
2202+
2203+
firebase.firestore.runTransaction = (updateFunction: (transaction: firestore.Transaction) => Promise<any>): Promise<void> => {
2204+
return new Promise((resolve, reject) => {
2205+
2206+
const onSuccessListenert = new com.google.android.gms.tasks.OnSuccessListener({
2207+
onSuccess: () => {
2208+
const i = 1;
2209+
}
2210+
});
2211+
2212+
const l = new java.util.ArrayList();
2213+
l.add("foooo");
2214+
l.add("barrr");
2215+
org.nativescript.plugins.firebase.FirebaseFirestore.STATE = onSuccessListenert; // for this assignment to 'stick', this needs to be a real Java object
2216+
org.nativescript.plugins.firebase.FirebaseFirestore.UPDATE_FUNCTION = updateFunction;
2217+
console.log(">>> STATE: " + org.nativescript.plugins.firebase.FirebaseFirestore.STATE);
2218+
console.log(">>> UPDATE_FUNCTION: " + org.nativescript.plugins.firebase.FirebaseFirestore.UPDATE_FUNCTION);
2219+
2220+
let worker;
2221+
if (global['TNS_WEBPACK']) {
2222+
const WorkerScript = require('nativescript-worker-loader!./android-firestoretx-worker.js');
2223+
worker = new WorkerScript();
2224+
} else {
2225+
worker = new Worker('./android-firestoretx-worker.js');
2226+
}
2227+
2228+
(<any>global).theUpdateFunction = updateFunction;
2229+
2230+
worker.onmessage = msg => {
2231+
console.log(">>> msg from worker, stringified: " + JSON.stringify(msg));
2232+
};
2233+
2234+
worker.onerror = err => {
2235+
console.log(">>> worker err: " + JSON.stringify(err));
2236+
};
2237+
2238+
worker.postMessage({
2239+
in: "innn",
2240+
someArray: l,
2241+
onSuccessListenert: onSuccessListenert
2242+
// updateFunction: updateFunction
2243+
});
2244+
2245+
const onSuccessListener = new com.google.android.gms.tasks.OnSuccessListener({
2246+
onSuccess: resolve // TODO does this syntax work? if so: refactor other instances... otherwise use: onSuccess: () => resolve()
2247+
});
2248+
2249+
const onFailureListener = new com.google.android.gms.tasks.OnFailureListener({
2250+
onFailure: exception => reject(exception.getMessage())
2251+
});
2252+
2253+
// TODO apply is called from java to js, so that makes it run on the main/UI thread.. so doesn't work
2254+
// .. can we share native objects via appcontext (for a worker)?
2255+
const txFunction = new com.google.firebase.firestore.Transaction.Function({
2256+
apply: (nativeTransaction: com.google.firebase.firestore.Transaction) => {
2257+
const tx = new firebase.firestore.Transaction(nativeTransaction);
2258+
return updateFunction(tx);
2259+
}
2260+
});
2261+
2262+
com.google.firebase.firestore.FirebaseFirestore.getInstance().runTransaction(txFunction)
2263+
.addOnSuccessListener(onSuccessListener)
2264+
.addOnFailureListener(onFailureListener);
2265+
2266+
// org.nativescript.plugins.firebase.FirebaseFirestore.runTransaction(txFunction, onSuccessListener, onFailureListener);
2267+
});
2268+
};
2269+
*/
2270+
21182271
firebase.firestore.collection = (collectionPath: string): firestore.CollectionReference => {
21192272
try {
21202273

src/firebase.d.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,42 @@ export namespace firestore {
753753
add(data: DocumentData): Promise<DocumentReference>;
754754
}
755755

756+
export type UpdateData = { [fieldPath: string]: any };
757+
758+
export class FieldPath {
759+
/**
760+
* Creates a FieldPath from the provided field names. If more than one field
761+
* name is provided, the path will point to a nested field in a document.
762+
*
763+
* @param fieldNames A list of field names.
764+
*/
765+
constructor(...fieldNames: string[]);
766+
767+
/**
768+
* Returns a special sentinel FieldPath to refer to the ID of a document.
769+
* It can be used in queries to sort or filter by the document ID.
770+
*/
771+
static documentId(): FieldPath;
772+
}
773+
774+
/*
775+
export interface Transaction {
776+
get(documentRef: DocumentReference): DocumentSnapshot;
777+
set(documentRef: DocumentReference, data: DocumentData, options?: SetOptions): Transaction;
778+
update(documentRef: DocumentReference, data: UpdateData): Transaction;
779+
update(documentRef: DocumentReference, field: string | FieldPath, value: any, ...moreFieldsAndValues: any[]): Transaction;
780+
delete(documentRef: DocumentReference): Transaction;
781+
}
782+
*/
783+
784+
export interface WriteBatch {
785+
set(documentRef: DocumentReference, data: DocumentData, options?: SetOptions): WriteBatch;
786+
update(documentRef: DocumentReference, data: UpdateData): WriteBatch;
787+
update(documentRef: DocumentReference, field: string | FieldPath, value: any, ...moreFieldsAndValues: any[]): WriteBatch;
788+
delete(documentRef: DocumentReference): WriteBatch;
789+
commit(): Promise<void>;
790+
}
791+
756792
export class FieldValue {
757793
static serverTimestamp: () => "SERVER_TIMESTAMP";
758794
}
@@ -776,6 +812,10 @@ export namespace firestore {
776812
function getDocument(collectionPath: string, documentPath: string): Promise<DocumentSnapshot>;
777813

778814
function update(collectionPath: string, documentPath: string, document: any): Promise<void>;
815+
816+
// function runTransaction(updateFunction: (transaction: firestore.Transaction) => Promise<any>): Promise<void>;
817+
818+
function batch(): firestore.WriteBatch;
779819
}
780820

781821
// Auth

0 commit comments

Comments
 (0)