cordova-plugin-firebasex-firestore 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +38 -0
- package/package.json +2 -1
- package/plugin.xml +2 -2
- package/src/android/FirebasexFirestorePlugin.java +130 -1
- package/src/ios/FirebasexFirestorePlugin.h +2 -0
- package/src/ios/FirebasexFirestorePlugin.m +139 -1
- package/types/index.d.ts +17 -0
- package/www/firebasex-firestore.js +31 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Version 2.0.4
|
|
2
|
+
feat: add `runTransactionOnFirestoreDocument` to atomically compare field conditions on an existing document and apply field-path updates when all conditions match.
|
|
3
|
+
|
|
4
|
+
# Version 2.0.3
|
|
5
|
+
- (ios) bugfix: fix boolean filter values in `fetchFirestoreCollection`/`listenToFirestoreCollection` being evaluated incorrectly (always `true`) due to an incorrect `NSNumber`-to-`BOOL` conversion in `getFilterValueAsType:valueIndex:typeIndex:`.
|
|
6
|
+
|
|
1
7
|
# Version 2.0.2
|
|
2
8
|
- fix: pin core plugin dependency to v2
|
|
3
9
|
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ This plugin wraps the [Firebase Firestore SDK](https://firebase.google.com/docs/
|
|
|
14
14
|
- [addDocumentToFirestoreCollection](#adddocumenttofirestorecollection)
|
|
15
15
|
- [setDocumentInFirestoreCollection](#setdocumentinfirestorecollection)
|
|
16
16
|
- [updateDocumentInFirestoreCollection](#updatedocumentinfirestorecollection)
|
|
17
|
+
- [runTransactionOnFirestoreDocument](#runtransactiononfirestoredocument)
|
|
17
18
|
- [deleteDocumentFromFirestoreCollection](#deletedocumentfromfirestorecollection)
|
|
18
19
|
- [documentExistsInFirestoreCollection](#documentexistsinfirestorecollection)
|
|
19
20
|
- [fetchDocumentInFirestoreCollection](#fetchdocumentinfirestorecollection)
|
|
@@ -231,6 +232,43 @@ FirebasexFirestore.updateDocumentInFirestoreCollection(
|
|
|
231
232
|
);
|
|
232
233
|
```
|
|
233
234
|
|
|
235
|
+
## runTransactionOnFirestoreDocument
|
|
236
|
+
|
|
237
|
+
Atomically compares field conditions on an existing document and applies field-path updates when
|
|
238
|
+
all conditions match. The operation runs inside a native Firestore transaction, so concurrent
|
|
239
|
+
claims cannot both pass the same preconditions. The success callback receives an object with a
|
|
240
|
+
`status` of `updated`, `conflict`, or `missing`; Firestore and transport errors use the error
|
|
241
|
+
callback.
|
|
242
|
+
|
|
243
|
+
**Parameters**:
|
|
244
|
+
|
|
245
|
+
- {string} documentId - document ID of the document to compare and update.
|
|
246
|
+
- {object[]} conditions - conditions with `path`, optional `exists`, and optional `value` properties.
|
|
247
|
+
- {object} updates - field-path updates to apply without replacing unrelated document data.
|
|
248
|
+
- {string} collection - name of top-level collection containing the document.
|
|
249
|
+
- {boolean} timestamp (optional) - Add `lastUpdate` to the update. Default `false`.
|
|
250
|
+
- {function} success (optional) - callback receiving `{status: "updated"|"conflict"|"missing"}`.
|
|
251
|
+
- {function} error (optional) - callback which will be passed a {string} error message.
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
FirebasexFirestore.runTransactionOnFirestoreDocument(
|
|
255
|
+
"my_doc",
|
|
256
|
+
[
|
|
257
|
+
{path: "some_property", exists: false}
|
|
258
|
+
],
|
|
259
|
+
{"id": [{id: "new-id", timestamp: new Date().toISOString()}]},
|
|
260
|
+
"my_collection",
|
|
261
|
+
function (result) {
|
|
262
|
+
if (result.status === "updated") {
|
|
263
|
+
console.log("Document updated atomically");
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
function (error) {
|
|
267
|
+
console.error("Error running transaction: " + error);
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
```
|
|
271
|
+
|
|
234
272
|
## deleteDocumentFromFirestoreCollection
|
|
235
273
|
|
|
236
274
|
Deletes an existing document with the given ID in a Firestore collection.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cordova-plugin-firebasex-firestore",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "Firestore module for cordova-plugin-firebasex",
|
|
5
5
|
"cordova": {
|
|
6
6
|
"id": "cordova-plugin-firebasex-firestore",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"keywords": ["cordova", "firebase", "firestore"],
|
|
23
23
|
"author": "Dave Alden",
|
|
24
24
|
"license": "MIT",
|
|
25
|
+
"types": "types/index.d.ts",
|
|
25
26
|
"dependencies": {
|
|
26
27
|
"cordova-plugin-firebasex-core": "^2.0.0"
|
|
27
28
|
},
|
package/plugin.xml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<?xml version='1.0' encoding='utf-8'?>
|
|
2
2
|
<plugin id="cordova-plugin-firebasex-firestore"
|
|
3
|
-
version="2.0.
|
|
3
|
+
version="2.0.4"
|
|
4
4
|
xmlns="http://apache.org/cordova/ns/plugins/1.0"
|
|
5
5
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
6
6
|
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
<source url="https://cdn.cocoapods.org/"/>
|
|
64
64
|
</config>
|
|
65
65
|
<pods use-frameworks="true">
|
|
66
|
-
<pod name="FirebaseFirestore"
|
|
66
|
+
<pod name="FirebaseFirestore" tag="12.14.0" git="https://github.com/invertase/firestore-ios-sdk-frameworks.git" nospm="true" />
|
|
67
67
|
</pods>
|
|
68
68
|
</podspec>
|
|
69
69
|
|
|
@@ -23,6 +23,7 @@ import com.google.firebase.firestore.Query;
|
|
|
23
23
|
import com.google.firebase.firestore.QueryDocumentSnapshot;
|
|
24
24
|
import com.google.firebase.firestore.QuerySnapshot;
|
|
25
25
|
import com.google.firebase.firestore.Query.Direction;
|
|
26
|
+
import com.google.firebase.firestore.Transaction;
|
|
26
27
|
import com.google.gson.Gson;
|
|
27
28
|
import com.google.gson.reflect.TypeToken;
|
|
28
29
|
|
|
@@ -36,6 +37,7 @@ import org.json.JSONObject;
|
|
|
36
37
|
import java.lang.reflect.Type;
|
|
37
38
|
import java.util.Date;
|
|
38
39
|
import java.util.HashMap;
|
|
40
|
+
import java.util.List;
|
|
39
41
|
import java.util.Map;
|
|
40
42
|
import java.util.Objects;
|
|
41
43
|
import java.util.Random;
|
|
@@ -75,7 +77,8 @@ public class FirebasexFirestorePlugin extends CordovaPlugin {
|
|
|
75
77
|
* Dispatches Cordova actions to plugin methods.
|
|
76
78
|
*
|
|
77
79
|
* <p>Supported actions: addDocumentToFirestoreCollection, setDocumentInFirestoreCollection,
|
|
78
|
-
|
|
80
|
+
* updateDocumentInFirestoreCollection, deleteDocumentFromFirestoreCollection,
|
|
81
|
+
* runTransactionOnFirestoreDocument,
|
|
79
82
|
* documentExistsInFirestoreCollection, fetchDocumentInFirestoreCollection,
|
|
80
83
|
* fetchFirestoreCollection, listenToDocumentInFirestoreCollection,
|
|
81
84
|
* listenToFirestoreCollection, removeFirestoreListener.
|
|
@@ -92,6 +95,9 @@ public class FirebasexFirestorePlugin extends CordovaPlugin {
|
|
|
92
95
|
case "updateDocumentInFirestoreCollection":
|
|
93
96
|
this.updateDocumentInFirestoreCollection(args, callbackContext);
|
|
94
97
|
return true;
|
|
98
|
+
case "runTransactionOnFirestoreDocument":
|
|
99
|
+
this.runTransactionOnFirestoreDocument(args, callbackContext);
|
|
100
|
+
return true;
|
|
95
101
|
case "deleteDocumentFromFirestoreCollection":
|
|
96
102
|
this.deleteDocumentFromFirestoreCollection(args, callbackContext);
|
|
97
103
|
return true;
|
|
@@ -144,6 +150,80 @@ public class FirebasexFirestorePlugin extends CordovaPlugin {
|
|
|
144
150
|
return gson.fromJson(jsonString, type);
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
/** Deserialises a JSON array of field conditions into maps. */
|
|
154
|
+
private List<Map<String, Object>> jsonStringToConditions(String jsonString) throws JSONException {
|
|
155
|
+
Type type = new TypeToken<List<Map<String, Object>>>() {}.getType();
|
|
156
|
+
return gson.fromJson(jsonString, type);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Marker returned when a field path does not exist in a document. */
|
|
160
|
+
private static final Object MISSING_FIELD = new Object();
|
|
161
|
+
|
|
162
|
+
/** Reads a nested field path from a Firestore document map. */
|
|
163
|
+
private Object getValueForFieldPath(Map<String, Object> document, String fieldPath) {
|
|
164
|
+
Object value = document;
|
|
165
|
+
String[] path = fieldPath.split("\\.");
|
|
166
|
+
for (String component : path) {
|
|
167
|
+
if (!(value instanceof Map) || !((Map<?, ?>) value).containsKey(component)) {
|
|
168
|
+
return MISSING_FIELD;
|
|
169
|
+
}
|
|
170
|
+
value = ((Map<?, ?>) value).get(component);
|
|
171
|
+
}
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Compares JSON-compatible Firestore values without depending on numeric runtime types. */
|
|
176
|
+
private boolean firestoreValuesEqual(Object actual, Object expected) {
|
|
177
|
+
if (actual == expected) return true;
|
|
178
|
+
if (actual == null || expected == null) return false;
|
|
179
|
+
if (actual instanceof Number && expected instanceof Number) {
|
|
180
|
+
return Double.compare(((Number) actual).doubleValue(), ((Number) expected).doubleValue()) == 0;
|
|
181
|
+
}
|
|
182
|
+
if (actual instanceof Map && expected instanceof Map) {
|
|
183
|
+
Map<?, ?> actualMap = (Map<?, ?>) actual;
|
|
184
|
+
Map<?, ?> expectedMap = (Map<?, ?>) expected;
|
|
185
|
+
if (!actualMap.keySet().equals(expectedMap.keySet())) return false;
|
|
186
|
+
for (Object key : actualMap.keySet()) {
|
|
187
|
+
if (!firestoreValuesEqual(actualMap.get(key), expectedMap.get(key))) return false;
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
if (actual instanceof List && expected instanceof List) {
|
|
192
|
+
List<?> actualList = (List<?>) actual;
|
|
193
|
+
List<?> expectedList = (List<?>) expected;
|
|
194
|
+
if (actualList.size() != expectedList.size()) return false;
|
|
195
|
+
for (int i = 0; i < actualList.size(); i++) {
|
|
196
|
+
if (!firestoreValuesEqual(actualList.get(i), expectedList.get(i))) return false;
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return actual.equals(expected);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Checks all declarative field conditions against a Firestore document. */
|
|
204
|
+
private boolean conditionsMatch(Map<String, Object> document, List<Map<String, Object>> conditions) {
|
|
205
|
+
for (Map<String, Object> condition : conditions) {
|
|
206
|
+
String fieldPath = (String) condition.get("path");
|
|
207
|
+
if (fieldPath == null || fieldPath.length() == 0) return false;
|
|
208
|
+
|
|
209
|
+
Object actual = getValueForFieldPath(document, fieldPath);
|
|
210
|
+
boolean actualExists = actual != MISSING_FIELD;
|
|
211
|
+
boolean expectedExists = !condition.containsKey("exists") || Boolean.TRUE.equals(condition.get("exists"));
|
|
212
|
+
if (actualExists != expectedExists) return false;
|
|
213
|
+
if (expectedExists && condition.containsKey("value") && !firestoreValuesEqual(actual, condition.get("value"))) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Creates the stable result returned by an atomic document transaction. */
|
|
221
|
+
private Map<String, Object> transactionResult(String status) {
|
|
222
|
+
Map<String, Object> result = new HashMap<String, Object>();
|
|
223
|
+
result.put("status", status);
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
147
227
|
/** Sanitises a Firestore data map and converts it to a JSONObject. */
|
|
148
228
|
private JSONObject mapFirestoreDataToJsonObject(Map<String, Object> map) throws JSONException {
|
|
149
229
|
map = sanitiseFirestoreHashMap(map);
|
|
@@ -430,6 +510,55 @@ public class FirebasexFirestorePlugin extends CordovaPlugin {
|
|
|
430
510
|
});
|
|
431
511
|
}
|
|
432
512
|
|
|
513
|
+
/** Atomically compares field conditions and updates a Firestore document when they match. */
|
|
514
|
+
private void runTransactionOnFirestoreDocument(JSONArray args, CallbackContext callbackContext) throws JSONException {
|
|
515
|
+
cordova.getThreadPool().execute(new Runnable() {
|
|
516
|
+
public void run() {
|
|
517
|
+
try {
|
|
518
|
+
String documentId = args.getString(0);
|
|
519
|
+
List<Map<String, Object>> conditions = jsonStringToConditions(args.getString(1));
|
|
520
|
+
Map<String, Object> updates = jsonStringToMap(args.getString(2));
|
|
521
|
+
String collection = args.getString(3);
|
|
522
|
+
boolean timestamp = args.getBoolean(4);
|
|
523
|
+
DocumentReference documentReference = firestore.collection(collection).document(documentId);
|
|
524
|
+
|
|
525
|
+
firestore.runTransaction(new Transaction.Function<Map<String, Object>>() {
|
|
526
|
+
@Override
|
|
527
|
+
public Map<String, Object> apply(@NonNull Transaction transaction) throws FirebaseFirestoreException {
|
|
528
|
+
DocumentSnapshot snapshot = transaction.get(documentReference);
|
|
529
|
+
if (!snapshot.exists()) return transactionResult("missing");
|
|
530
|
+
Map<String, Object> document = snapshot.getData();
|
|
531
|
+
if (document == null || !conditionsMatch(document, conditions)) {
|
|
532
|
+
return transactionResult("conflict");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
Map<String, Object> transactionUpdates = new HashMap<String, Object>(updates);
|
|
536
|
+
if (timestamp) transactionUpdates.put("lastUpdate", new Timestamp(new Date()));
|
|
537
|
+
transaction.update(documentReference, transactionUpdates);
|
|
538
|
+
return transactionResult("updated");
|
|
539
|
+
}
|
|
540
|
+
}).addOnSuccessListener(new OnSuccessListener<Map<String, Object>>() {
|
|
541
|
+
@Override
|
|
542
|
+
public void onSuccess(Map<String, Object> result) {
|
|
543
|
+
try {
|
|
544
|
+
callbackContext.success(new JSONObject(result));
|
|
545
|
+
} catch (Exception e) {
|
|
546
|
+
FirebasexCorePlugin.handleExceptionWithContext(e, callbackContext);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}).addOnFailureListener(new OnFailureListener() {
|
|
550
|
+
@Override
|
|
551
|
+
public void onFailure(@NonNull Exception e) {
|
|
552
|
+
FirebasexCorePlugin.handleExceptionWithContext(e, callbackContext);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
} catch (Exception e) {
|
|
556
|
+
FirebasexCorePlugin.handleExceptionWithContext(e, callbackContext);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
433
562
|
/** Deletes a document from a collection by its ID. */
|
|
434
563
|
private void deleteDocumentFromFirestoreCollection(JSONArray args, CallbackContext callbackContext) throws JSONException {
|
|
435
564
|
cordova.getThreadPool().execute(new Runnable() {
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
- (void)setDocumentInFirestoreCollection:(CDVInvokedUrlCommand *)command;
|
|
22
22
|
/** Updates fields of an existing document. @param command args[0]: documentId, args[1]: document, args[2]: collection, args[3]: timestamp. */
|
|
23
23
|
- (void)updateDocumentInFirestoreCollection:(CDVInvokedUrlCommand *)command;
|
|
24
|
+
/** Atomically compares field conditions and updates a document. @param command args[0]: documentId, args[1]: conditions, args[2]: updates, args[3]: collection, args[4]: timestamp. */
|
|
25
|
+
- (void)runTransactionOnFirestoreDocument:(CDVInvokedUrlCommand *)command;
|
|
24
26
|
/** Deletes a document by ID. @param command args[0]: documentId, args[1]: collection. */
|
|
25
27
|
- (void)deleteDocumentFromFirestoreCollection:(CDVInvokedUrlCommand *)command;
|
|
26
28
|
/** Checks if a document exists. @param command args[0]: documentId, args[1]: collection. */
|
|
@@ -120,6 +120,98 @@
|
|
|
120
120
|
return value;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/** Reads a nested field path and reports whether that path exists. */
|
|
124
|
+
- (id)valueForFieldPath:(NSString *)fieldPath inDocument:(NSDictionary *)document exists:(BOOL *)exists {
|
|
125
|
+
id value = document;
|
|
126
|
+
*exists = YES;
|
|
127
|
+
for (NSString *component in [fieldPath componentsSeparatedByString:@"."]) {
|
|
128
|
+
if (![value isKindOfClass:[NSDictionary class]] || [(NSDictionary *)value objectForKey:component] == nil) {
|
|
129
|
+
*exists = NO;
|
|
130
|
+
return nil;
|
|
131
|
+
}
|
|
132
|
+
value = [(NSDictionary *)value objectForKey:component];
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Compares JSON-compatible Firestore values, including nested dictionaries and arrays. */
|
|
138
|
+
- (BOOL)firestoreValuesEqual:(id)actual expected:(id)expected {
|
|
139
|
+
if (actual == expected) {
|
|
140
|
+
return YES;
|
|
141
|
+
}
|
|
142
|
+
if (actual == nil || expected == nil) {
|
|
143
|
+
return NO;
|
|
144
|
+
}
|
|
145
|
+
if ([actual isKindOfClass:[NSNumber class]] && [expected isKindOfClass:[NSNumber class]]) {
|
|
146
|
+
if ([self isBoolNumber:actual] != [self isBoolNumber:expected]) {
|
|
147
|
+
return NO;
|
|
148
|
+
}
|
|
149
|
+
return [actual isEqualToNumber:expected];
|
|
150
|
+
}
|
|
151
|
+
if ([actual isKindOfClass:[NSDictionary class]] && [expected isKindOfClass:[NSDictionary class]]) {
|
|
152
|
+
NSDictionary *actualDictionary = actual;
|
|
153
|
+
NSDictionary *expectedDictionary = expected;
|
|
154
|
+
if (actualDictionary.count != expectedDictionary.count) {
|
|
155
|
+
return NO;
|
|
156
|
+
}
|
|
157
|
+
for (id key in actualDictionary) {
|
|
158
|
+
if (![expectedDictionary objectForKey:key] ||
|
|
159
|
+
![self firestoreValuesEqual:[actualDictionary objectForKey:key]
|
|
160
|
+
expected:[expectedDictionary objectForKey:key]]) {
|
|
161
|
+
return NO;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return YES;
|
|
165
|
+
}
|
|
166
|
+
if ([actual isKindOfClass:[NSArray class]] && [expected isKindOfClass:[NSArray class]]) {
|
|
167
|
+
NSArray *actualArray = actual;
|
|
168
|
+
NSArray *expectedArray = expected;
|
|
169
|
+
if (actualArray.count != expectedArray.count) {
|
|
170
|
+
return NO;
|
|
171
|
+
}
|
|
172
|
+
for (NSUInteger index = 0; index < actualArray.count; index++) {
|
|
173
|
+
if (![self firestoreValuesEqual:[actualArray objectAtIndex:index]
|
|
174
|
+
expected:[expectedArray objectAtIndex:index]]) {
|
|
175
|
+
return NO;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return YES;
|
|
179
|
+
}
|
|
180
|
+
return [actual isEqual:expected];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Checks all declarative field conditions against a Firestore document. */
|
|
184
|
+
- (BOOL)conditionsMatch:(NSDictionary *)document conditions:(NSArray *)conditions {
|
|
185
|
+
for (NSDictionary *condition in conditions) {
|
|
186
|
+
if (![condition isKindOfClass:[NSDictionary class]]) {
|
|
187
|
+
return NO;
|
|
188
|
+
}
|
|
189
|
+
NSString *fieldPath = [condition objectForKey:@"path"];
|
|
190
|
+
if (![fieldPath isKindOfClass:[NSString class]] || fieldPath.length == 0) {
|
|
191
|
+
return NO;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
BOOL actualExists = NO;
|
|
195
|
+
id actual = [self valueForFieldPath:fieldPath inDocument:document exists:&actualExists];
|
|
196
|
+
id existsValue = [condition objectForKey:@"exists"];
|
|
197
|
+
BOOL expectedExists = existsValue == nil ||
|
|
198
|
+
([existsValue isKindOfClass:[NSNumber class]] && [existsValue boolValue]);
|
|
199
|
+
if (actualExists != expectedExists) {
|
|
200
|
+
return NO;
|
|
201
|
+
}
|
|
202
|
+
if (expectedExists && [condition objectForKey:@"value"] != nil &&
|
|
203
|
+
![self firestoreValuesEqual:actual expected:[condition objectForKey:@"value"]]) {
|
|
204
|
+
return NO;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return YES;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Creates the stable result dictionary returned by an atomic document transaction. */
|
|
211
|
+
- (NSDictionary *)transactionResult:(NSString *)status {
|
|
212
|
+
return @{ @"status": status };
|
|
213
|
+
}
|
|
214
|
+
|
|
123
215
|
#pragma mark - Query filters
|
|
124
216
|
|
|
125
217
|
/**
|
|
@@ -190,7 +282,7 @@
|
|
|
190
282
|
|
|
191
283
|
if ([type isEqual:@"boolean"]) {
|
|
192
284
|
if ([typedValue isKindOfClass:[NSNumber class]]) {
|
|
193
|
-
typedValue = [NSNumber numberWithBool:typedValue];
|
|
285
|
+
typedValue = [NSNumber numberWithBool:[typedValue boolValue]];
|
|
194
286
|
} else if ([typedValue isKindOfClass:[NSString class]]) {
|
|
195
287
|
bool boolValue = [typedValue boolValue];
|
|
196
288
|
typedValue = [NSNumber numberWithBool:boolValue];
|
|
@@ -321,6 +413,52 @@
|
|
|
321
413
|
}];
|
|
322
414
|
}
|
|
323
415
|
|
|
416
|
+
/** Atomically compares field conditions and updates a Firestore document when they match. */
|
|
417
|
+
- (void)runTransactionOnFirestoreDocument:(CDVInvokedUrlCommand *)command {
|
|
418
|
+
[self.commandDelegate runInBackground:^{
|
|
419
|
+
@try {
|
|
420
|
+
NSString *documentId = [command.arguments objectAtIndex:0];
|
|
421
|
+
NSArray *conditions = [command.arguments objectAtIndex:1];
|
|
422
|
+
NSDictionary *updates = [command.arguments objectAtIndex:2];
|
|
423
|
+
NSString *collection = [command.arguments objectAtIndex:3];
|
|
424
|
+
BOOL timestamp = [[command.arguments objectAtIndex:4] boolValue];
|
|
425
|
+
FIRDocumentReference *documentReference = [[self.firestore collectionWithPath:collection]
|
|
426
|
+
documentWithPath:documentId];
|
|
427
|
+
|
|
428
|
+
[self.firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
|
|
429
|
+
FIRDocumentSnapshot *snapshot = [transaction getDocument:documentReference error:error];
|
|
430
|
+
if (error != nil && *error != nil) {
|
|
431
|
+
return nil;
|
|
432
|
+
}
|
|
433
|
+
if (!snapshot.exists) {
|
|
434
|
+
return [self transactionResult:@"missing"];
|
|
435
|
+
}
|
|
436
|
+
if (![self conditionsMatch:snapshot.data conditions:conditions]) {
|
|
437
|
+
return [self transactionResult:@"conflict"];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
NSMutableDictionary *transactionUpdates = [updates mutableCopy];
|
|
441
|
+
if (timestamp) {
|
|
442
|
+
transactionUpdates[@"lastUpdate"] = [FIRTimestamp timestampWithDate:[NSDate date]];
|
|
443
|
+
}
|
|
444
|
+
[transaction updateData:transactionUpdates forDocument:documentReference];
|
|
445
|
+
return [self transactionResult:@"updated"];
|
|
446
|
+
} completion:^(id _Nullable result, NSError *_Nullable error) {
|
|
447
|
+
if (error != nil) {
|
|
448
|
+
[[FirebasexCorePlugin sharedInstance] sendPluginErrorWithMessage:error.localizedDescription :command];
|
|
449
|
+
} else {
|
|
450
|
+
[self.commandDelegate
|
|
451
|
+
sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK
|
|
452
|
+
messageAsDictionary:result]
|
|
453
|
+
callbackId:command.callbackId];
|
|
454
|
+
}
|
|
455
|
+
}];
|
|
456
|
+
} @catch (NSException *exception) {
|
|
457
|
+
[[FirebasexCorePlugin sharedInstance] handlePluginExceptionWithContext:exception :command];
|
|
458
|
+
}
|
|
459
|
+
}];
|
|
460
|
+
}
|
|
461
|
+
|
|
324
462
|
/** Deletes a document from a collection by its ID. */
|
|
325
463
|
- (void)deleteDocumentFromFirestoreCollection:(CDVInvokedUrlCommand *)command {
|
|
326
464
|
[self.commandDelegate runInBackground:^{
|
package/types/index.d.ts
CHANGED
|
@@ -22,6 +22,23 @@ interface FirebasexFirestore {
|
|
|
22
22
|
success: () => void,
|
|
23
23
|
error: (err: string) => void
|
|
24
24
|
): void;
|
|
25
|
+
runTransactionOnFirestoreDocument(
|
|
26
|
+
documentId: string | number,
|
|
27
|
+
conditions: object[],
|
|
28
|
+
updates: object,
|
|
29
|
+
collection: string,
|
|
30
|
+
timestamp: boolean,
|
|
31
|
+
success: (result: { status: 'updated' | 'conflict' | 'missing' }) => void,
|
|
32
|
+
error: (err: string) => void
|
|
33
|
+
): void;
|
|
34
|
+
runTransactionOnFirestoreDocument(
|
|
35
|
+
documentId: string | number,
|
|
36
|
+
conditions: object[],
|
|
37
|
+
updates: object,
|
|
38
|
+
collection: string,
|
|
39
|
+
success: (result: { status: 'updated' | 'conflict' | 'missing' }) => void,
|
|
40
|
+
error: (err: string) => void
|
|
41
|
+
): void;
|
|
25
42
|
deleteDocumentFromFirestoreCollection(
|
|
26
43
|
documentId: string,
|
|
27
44
|
collection: string,
|
|
@@ -40,7 +40,7 @@ exports.addDocumentToFirestoreCollection = function (document, collection, times
|
|
|
40
40
|
if (typeof collection !== 'string') return error("'collection' must be a string specifying the Firestore collection name");
|
|
41
41
|
if (typeof document !== 'object' || typeof document.length === 'number') return error("'document' must be an object specifying record data");
|
|
42
42
|
|
|
43
|
-
if (typeof timestamp !== "boolean"
|
|
43
|
+
if (typeof timestamp !== "boolean") {
|
|
44
44
|
error = success;
|
|
45
45
|
success = timestamp;
|
|
46
46
|
timestamp = false;
|
|
@@ -98,6 +98,36 @@ exports.updateDocumentInFirestoreCollection = function (documentId, document, co
|
|
|
98
98
|
exec(success, error, SERVICE, "updateDocumentInFirestoreCollection", [documentId.toString(), document, collection, timestamp || false]);
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Atomically compares fields on an existing document and updates it when all conditions match.
|
|
103
|
+
* Conditions use objects with a field path, an optional exists flag, and an optional value.
|
|
104
|
+
* Updates use Firestore field paths as keys, so nested fields can be changed without replacing
|
|
105
|
+
* unrelated data. The success callback receives {status: "updated"}, {status: "conflict"}, or
|
|
106
|
+
* {status: "missing"}.
|
|
107
|
+
*
|
|
108
|
+
* @param {string|number} documentId - The document identifier.
|
|
109
|
+
* @param {Array.<Object>} conditions - Field conditions to compare inside the transaction.
|
|
110
|
+
* @param {Object} updates - Field-path updates to apply when conditions match.
|
|
111
|
+
* @param {string} collection - The Firestore collection path.
|
|
112
|
+
* @param {boolean} [timestamp=false] - If true, updates the lastUpdate Timestamp field.
|
|
113
|
+
* @param {function} success - Called with the transaction result.
|
|
114
|
+
* @param {function} error - Called with an error message on failure.
|
|
115
|
+
*/
|
|
116
|
+
exports.runTransactionOnFirestoreDocument = function (documentId, conditions, updates, collection, timestamp, success, error) {
|
|
117
|
+
if (typeof documentId !== 'string' && typeof documentId !== 'number') return error("'documentId' must be a string or number specifying the Firestore document identifier");
|
|
118
|
+
if (!Array.isArray(conditions)) return error("'conditions' must be an array of field conditions");
|
|
119
|
+
if (typeof updates !== 'object' || updates === null || typeof updates.length === 'number') return error("'updates' must be an object specifying field-path updates");
|
|
120
|
+
if (typeof collection !== 'string') return error("'collection' must be a string specifying the Firestore collection name");
|
|
121
|
+
|
|
122
|
+
if (typeof timestamp !== "boolean" && typeof error === "undefined") {
|
|
123
|
+
error = success;
|
|
124
|
+
success = timestamp;
|
|
125
|
+
timestamp = false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
exec(success, error, SERVICE, "runTransactionOnFirestoreDocument", [documentId.toString(), conditions, updates, collection, timestamp || false]);
|
|
129
|
+
};
|
|
130
|
+
|
|
101
131
|
/**
|
|
102
132
|
* Deletes a document from a Firestore collection.
|
|
103
133
|
*
|