Skip to content

Commit

Permalink
feat(firestore): add support to update using FieldPath (#10388)
Browse files Browse the repository at this point in the history
* feat(firestore): add support to update using FieldPath

* feat(firestore): add support to update using FieldPath on android

* feat(firestore): add support to update using FieldPath on Web

* feat(firestore): add support to update using FieldPath

* feat(firestore): add support to update using FieldPath on android

* feat(firestore): add support to update using FieldPath on android

* feat(firestore): add support to update using FieldPath on android

* feat(firestore): add support to update using FieldPath on android
  • Loading branch information
Lyokone committed Feb 9, 2023
1 parent 24d2f94 commit 538090f
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 23 deletions.
Expand Up @@ -43,6 +43,7 @@
import io.flutter.plugins.firebase.firestore.streamhandler.TransactionStreamHandler;
import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter;
import io.flutter.plugins.firebase.firestore.utils.ServerTimestampBehaviorConverter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -420,10 +421,25 @@ private Task<Void> documentUpdate(Map<String, Object> arguments) {
DocumentReference documentReference =
(DocumentReference) Objects.requireNonNull(arguments.get("reference"));
@SuppressWarnings("unchecked")
Map<String, Object> data =
(Map<String, Object>) Objects.requireNonNull(arguments.get("data"));

taskCompletionSource.setResult(Tasks.await(documentReference.update(data)));
Map<FieldPath, Object> data =
(Map<FieldPath, Object>) Objects.requireNonNull(arguments.get("data"));

// Due to the signature of the function, I extract the first element of the map and
// pass the rest of the map as an array of alternating keys and values.
FieldPath firstFieldPath = data.keySet().iterator().next();
Object firstObject = data.get(firstFieldPath);

ArrayList<Object> flattenData = new ArrayList<>();
for (FieldPath fieldPath : data.keySet()) {
if (fieldPath.equals(firstFieldPath)) {
continue;
}
flattenData.add(fieldPath);
flattenData.add(data.get(fieldPath));
}
taskCompletionSource.setResult(
Tasks.await(
documentReference.update(firstFieldPath, firstObject, flattenData.toArray())));
} catch (Exception e) {
taskCompletionSource.setException(e);
}
Expand Down
Expand Up @@ -4,9 +4,9 @@

import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

void runDocumentReferenceTests() {
group('$DocumentReference', () {
Expand Down Expand Up @@ -403,6 +403,76 @@ void runDocumentReferenceTests() {
expect(snapshot2.data(), equals({'foo': 'bar', 'bar': 'baz'}));
});

test('updates nested data using dots', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-update-field-path');
await document.set({
'foo': {'bar': 'baz'}
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
expect(
snapshot.data(),
equals({
'foo': {'bar': 'baz'}
}),
);

await document.update({'foo.bar': 'toto'});
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await document.get();
expect(
snapshot2.data(),
equals({
'foo': {'bar': 'toto'}
}),
);
});

test('updates nested data using FieldPath', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-update-field-path');
await document.set({
'foo': {'bar': 'baz'}
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
expect(
snapshot.data(),
equals({
'foo': {'bar': 'baz'}
}),
);

await document.update({
FieldPath(const ['foo', 'bar']): 'toto'
});
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await document.get();
expect(
snapshot2.data(),
equals({
'foo': {'bar': 'toto'}
}),
);
});

test('updates nested data containing a dot using FieldPath', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-update-field-path');
await document.set({'foo.bar': 'baz'});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
expect(
snapshot.data(),
equals({'foo.bar': 'baz'}),
);

await document.update({
FieldPath(const ['foo.bar']): 'toto'
});
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await document.get();
expect(
snapshot2.data(),
equals({'foo.bar': 'toto'}),
);
});

test('throws if document does not exist', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-update-not-exists');
Expand Down
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -221,6 +221,7 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down Expand Up @@ -252,6 +253,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down
Expand Up @@ -45,5 +45,7 @@
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
Expand Up @@ -38,8 +38,10 @@ abstract class DocumentReference<T extends Object?> {
/// Updates data on the document. Data will be merged with any existing
/// document data.
///
/// Objects key can be a String or a FieldPath.
///
/// If no document exists yet, the update will fail.
Future<void> update(Map<String, Object?> data);
Future<void> update(Map<Object, Object?> data);

/// Reads the document referenced by this [DocumentReference].
///
Expand Down Expand Up @@ -170,9 +172,9 @@ class _JsonDocumentReference
}

@override
Future<void> update(Map<String, Object?> data) {
Future<void> update(Map<Object, Object?> data) {
return _delegate
.update(_CodecUtility.replaceValueWithDelegatesInMap(data)!);
.update(_CodecUtility.replaceValueWithDelegatesInMapFieldPath(data)!);
}

@override
Expand Down Expand Up @@ -282,7 +284,7 @@ class _WithConverterDocumentReference<T extends Object?>
}

@override
Future<void> update(Map<String, Object?> data) {
Future<void> update(Map<Object, Object?> data) {
return _originalDocumentReference.update(data);
}

Expand Down
Expand Up @@ -16,6 +16,27 @@ class _CodecUtility {
return output;
}

static Map<FieldPath, dynamic>? replaceValueWithDelegatesInMapFieldPath(
Map<Object, dynamic>? data,
) {
if (data == null) {
return null;
}
Map<FieldPath, dynamic> output = <FieldPath, dynamic>{};
data.forEach((key, value) {
if (key is FieldPath) {
output[key] = valueEncode(value);
} else if (key is String) {
output[FieldPath.fromString(key)] = valueEncode(value);
} else {
throw StateError(
'Invalid key type for map. Expected String or FieldPath, but got $key: ${key.runtimeType}.',
);
}
});
return output;
}

static List<dynamic>? replaceValueWithDelegatesInArray(List<dynamic>? data) {
if (data == null) {
return null;
Expand Down
Expand Up @@ -47,7 +47,7 @@ class MethodChannelDocumentReference extends DocumentReferencePlatform {
}

@override
Future<void> update(Map<String, dynamic> data) async {
Future<void> update(Map<FieldPath, dynamic> data) async {
try {
await MethodChannelFirebaseFirestore.channel.invokeMethod<void>(
'DocumentReference#update',
Expand Down
Expand Up @@ -92,7 +92,7 @@ abstract class DocumentReferencePlatform extends PlatformInterface {
/// special sentinel [FieldValuePlatform] type.
///
/// If no document exists yet, the update will fail.
Future<void> update(Map<String, dynamic> data) {
Future<void> update(Map<FieldPath, dynamic> data) {
throw UnimplementedError('update() is not implemented');
}

Expand Down
Expand Up @@ -47,9 +47,9 @@ void main() {

test('update', () async {
bool isMethodCalled = false;
final Map<String, dynamic> data = {
'test': 'test',
'fieldValue': mockFieldValue
final Map<FieldPath, dynamic> data = {
FieldPath.fromString('test'): 'test',
FieldPath.fromString('fieldValue'): mockFieldValue
};
handleMethodCall((call) {
if (call.method == 'DocumentReference#update') {
Expand Down
Expand Up @@ -3,14 +3,13 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart';
import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_firestore.dart';
import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_query.dart';
import 'package:cloud_firestore_platform_interface/src/method_channel/utils/firestore_message_codec.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';

import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart';
import 'package:cloud_firestore_platform_interface/src/method_channel/utils/firestore_message_codec.dart';

/// This codec is able to decode FieldValues.
/// This ability is only required in tests, hence why
/// those values are only decoded in tests.
Expand All @@ -24,6 +23,7 @@ class TestFirestoreMessageCodec extends FirestoreMessageCodec {
static const int _kDelete = 134;
static const int _kServerTimestamp = 135;
static const int _kFirestoreInstance = 144;
static const int _kFieldPath = 140;
static const int _kFirestoreQuery = 145;
static const int _kFirestoreSettings = 146;

Expand Down Expand Up @@ -77,6 +77,13 @@ class TestFirestoreMessageCodec extends FirestoreMessageCodec {
readValue(buffer)! as MethodChannelFirebaseFirestore;
String path = readValue(buffer)! as String;
return firestore.doc(path);
case _kFieldPath:
final int size = readSize(buffer);
final List<String> segments = <String>[];
for (int i = 0; i < size; i++) {
segments.add(readValue(buffer)! as String);
}
return FieldPath(segments);
default:
return super.readValueOfType(type, buffer);
}
Expand Down
Expand Up @@ -38,9 +38,9 @@ class DocumentReferenceWeb extends DocumentReferencePlatform {
}

@override
Future<void> update(Map<String, dynamic> data) {
Future<void> update(Map<Object, dynamic> data) {
return convertWebExceptions(
() => _delegate.update(EncodeUtility.encodeMapData(data)!));
() => _delegate.update(EncodeUtility.encodeMapDataFieldPath(data)!));
}

@override
Expand Down
Expand Up @@ -366,7 +366,7 @@ class DocumentReference
return handleThenable(jsObjectSet);
}

Future<void> update(Map<String, dynamic> data) =>
Future<void> update(Map<FieldPath, dynamic> data) =>
handleThenable(firestore_interop.updateDoc(jsObject, jsify(data)));
}

Expand Down
Expand Up @@ -5,14 +5,14 @@

import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart';

import '../interop/firestore.dart' as firestore_interop;
import '../document_reference_web.dart';
import '../field_value_web.dart';
import '../interop/firestore.dart' as firestore_interop;

/// Class containing static utility methods to encode/decode firestore data.
class EncodeUtility {
/// Encodes a Map of values from their proper types to a serialized version.
static Map<String, dynamic>? encodeMapData(Map<String, dynamic>? data) {
static Map<String, dynamic>? encodeMapData(Map<Object, dynamic>? data) {
if (data == null) {
return null;
}
Expand All @@ -21,6 +21,18 @@ class EncodeUtility {
return output;
}

static Map<FieldPath, dynamic>? encodeMapDataFieldPath(
Map<Object, dynamic>? data) {
if (data == null) {
return null;
}
Map<FieldPath, dynamic> output = <FieldPath, dynamic>{};
data.forEach((key, value) {
output[valueEncode(key)] = valueEncode(value);
});
return output;
}

/// Encodes an Array of values from their proper types to a serialized version.
static List<dynamic>? encodeArrayData(List<dynamic>? data) {
if (data == null) {
Expand Down

0 comments on commit 538090f

Please sign in to comment.