Skip to content

Commit 28b3b54

Browse files
authored
feat(datastore): Custom Error Handler (#1032)
1 parent bb04afb commit 28b3b54

File tree

9 files changed

+207
-24
lines changed

9 files changed

+207
-24
lines changed

packages/amplify_datastore/android/src/main/kotlin/com/amazonaws/amplify/amplify_datastore/AmplifyDataStorePlugin.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import com.amplifyframework.core.model.query.QueryOptions
4040
import com.amplifyframework.core.model.query.predicate.QueryPredicates
4141
import com.amplifyframework.datastore.AWSDataStorePlugin
4242
import com.amplifyframework.datastore.DataStoreConfiguration
43+
import com.amplifyframework.datastore.DataStoreErrorHandler
4344
import com.amplifyframework.datastore.DataStoreException
4445
import io.flutter.embedding.engine.plugins.FlutterPlugin
4546
import io.flutter.plugin.common.EventChannel
@@ -165,6 +166,24 @@ class AmplifyDataStorePlugin : FlutterPlugin, MethodCallHandler {
165166

166167
val dataStoreConfigurationBuilder = DataStoreConfiguration.builder()
167168

169+
var errorHandler : DataStoreErrorHandler;
170+
errorHandler = if( (request["hasErrorHandler"] as? Boolean? == true) ) {
171+
DataStoreErrorHandler {
172+
val args = hashMapOf(
173+
"errorCode" to "DataStoreException",
174+
"errorMessage" to ExceptionMessages.defaultFallbackExceptionMessage,
175+
"details" to createSerializedError(it)
176+
)
177+
channel.invokeMethod("errorHandler", args)
178+
}
179+
}
180+
else {
181+
DataStoreErrorHandler {
182+
LOG.error(it.toString())
183+
}
184+
}
185+
dataStoreConfigurationBuilder.errorHandler(errorHandler)
186+
168187
modelProvider.setVersion(request["modelProviderVersion"] as String)
169188
val defaultDataStoreConfiguration = DataStoreConfiguration.defaults()
170189
val syncInterval: Long =

packages/amplify_datastore/example/lib/main.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ class _MyAppState extends State<MyApp> {
8585
// Platform messages are asynchronous, so we initialize in an async method.
8686
Future<void> initPlatformState() async {
8787
try {
88-
datastorePlugin = AmplifyDataStore(modelProvider: ModelProvider.instance);
88+
datastorePlugin = AmplifyDataStore(
89+
modelProvider: ModelProvider.instance,
90+
errorHandler: ((error) =>
91+
{print("Custom ErrorHandler received: " + error.toString())}),
92+
);
8993
await Amplify.addPlugin(datastorePlugin);
9094

9195
// Configure

packages/amplify_datastore/ios/Classes/FlutterDataStoreErrorHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class FlutterDataStoreErrorHandler {
2626
details: FlutterDataStoreErrorHandler.createSerializedError(error: error))
2727
}
2828

29-
static func createSerializedError(error: DataStoreError) -> Dictionary<String, String> {
29+
static func createSerializedError(error: AmplifyError) -> Dictionary<String, String> {
3030
return createSerializedError(message: error.errorDescription,
3131
recoverySuggestion: error.recoverySuggestion,
3232
underlyingError: error.underlyingError?.localizedDescription)

packages/amplify_datastore/ios/Classes/SwiftAmplifyDataStorePlugin.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,29 @@ public class SwiftAmplifyDataStorePlugin: NSObject, FlutterPlugin {
122122

123123
flutterModelRegistration.version = modelProviderVersion
124124
let syncExpressions: [DataStoreSyncExpression] = try createSyncExpressions(syncExpressionList: syncExpressionList)
125-
125+
126126
self.dataStoreHubEventStreamHandler?.registerModelsForHub(flutterModelRegistration: flutterModelRegistration)
127+
128+
129+
var errorHandler: DataStoreErrorHandler
130+
if((args["hasErrorHandler"] as? Bool) == true) {
131+
errorHandler = { error in
132+
let map : [String:Any] = [
133+
"errorCode" : "DataStoreException",
134+
"errorMesage" : ErrorMessages.defaultFallbackErrorMessage,
135+
"details" : FlutterDataStoreErrorHandler.createSerializedError(error: error)
136+
]
137+
self.channel!.invokeMethod("errorHandler", arguments: args)
138+
}
139+
} else {
140+
errorHandler = { error in
141+
Amplify.Logging.error(error: error)
142+
}
143+
}
144+
127145
let dataStorePlugin = AWSDataStorePlugin(modelRegistration: flutterModelRegistration,
128146
configuration: .custom(
147+
errorHandler: errorHandler,
129148
syncInterval: syncInterval,
130149
syncMaxRecords: syncMaxRecords,
131150
syncPageSize: syncPageSize,

packages/amplify_datastore/lib/amplify_datastore.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import 'dart:async';
1717

18+
import 'package:amplify_core/types/exception/AmplifyException.dart';
1819
import 'package:amplify_datastore_plugin_interface/amplify_datastore_plugin_interface.dart';
1920
import 'package:meta/meta.dart';
2021
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
@@ -39,13 +40,15 @@ class AmplifyDataStore extends DataStorePluginInterface {
3940
/// [syncPageSize]: page size to sync
4041
AmplifyDataStore({
4142
required ModelProviderInterface modelProvider,
43+
Function(AmplifyException)? errorHandler,
4244
List<DataStoreSyncExpression> syncExpressions = const [],
4345
int? syncInterval,
4446
int? syncMaxRecords,
4547
int? syncPageSize,
4648
}) : super(
4749
token: _token,
4850
modelProvider: modelProvider,
51+
errorHandler: errorHandler,
4952
syncExpressions: syncExpressions,
5053
syncInterval: syncInterval,
5154
syncMaxRecords: syncMaxRecords,
@@ -71,6 +74,7 @@ class AmplifyDataStore extends DataStorePluginInterface {
7174
@override
7275
Future<void> configureDataStore({
7376
ModelProviderInterface? modelProvider,
77+
Function(AmplifyException)? errorHandler,
7478
List<DataStoreSyncExpression>? syncExpressions,
7579
int? syncInterval,
7680
int? syncMaxRecords,
@@ -85,6 +89,7 @@ class AmplifyDataStore extends DataStorePluginInterface {
8589
streamWrapper.registerModelsForHub(provider);
8690
return _instance.configureDataStore(
8791
modelProvider: provider,
92+
errorHandler: errorHandler ?? this.errorHandler,
8893
syncExpressions: this.syncExpressions,
8994
syncInterval: this.syncInterval,
9095
syncMaxRecords: this.syncMaxRecords,

packages/amplify_datastore/lib/method_channel_datastore.dart

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const MethodChannel _channel = MethodChannel('com.amazonaws.amplify/datastore');
2727
class AmplifyDataStoreMethodChannel extends AmplifyDataStore {
2828
dynamic _allModelsStreamFromMethodChannel = null;
2929

30+
List<DataStoreSyncExpression>? _syncExpressions;
31+
Function(AmplifyException)? _errorHandler;
32+
3033
ObserveQueryExecutor _observeQueryExecutor = ObserveQueryExecutor(
3134
dataStoreEventStream:
3235
AmplifyDataStore.streamWrapper.datastoreStreamController.stream,
@@ -35,39 +38,57 @@ class AmplifyDataStoreMethodChannel extends AmplifyDataStore {
3538
/// Internal use constructor
3639
AmplifyDataStoreMethodChannel() : super.tokenOnly();
3740

41+
// Receives calls from Native
42+
Future<dynamic> _methodCallHandler(MethodCall call) async {
43+
switch (call.method) {
44+
case 'resolveQueryPredicate':
45+
String? id = call.arguments;
46+
if (id == null) {
47+
throw ArgumentError(
48+
'resolveQueryPredicate must be called with an id');
49+
}
50+
return _syncExpressions!
51+
.firstWhere((syncExpression) => syncExpression.id == id)
52+
.resolveQueryPredicate()
53+
.serializeAsMap();
54+
55+
case 'errorHandler':
56+
Map<String, dynamic> arguments =
57+
Map<String, dynamic>.from(call.arguments);
58+
_errorHandler!(_deserializeExceptionFromMap(arguments));
59+
break;
60+
61+
case 'conflictHandler':
62+
break;
63+
64+
default:
65+
throw UnimplementedError('${call.method} has not been implemented.');
66+
}
67+
}
68+
3869
/// This method instantiates the native DataStore plugins with plugin
3970
/// configurations. This needs to happen before Amplify.configure() can be
4071
/// called.
4172
@override
4273
Future<void> configureDataStore({
4374
ModelProviderInterface? modelProvider,
75+
Function(AmplifyException)? errorHandler,
4476
List<DataStoreSyncExpression>? syncExpressions,
4577
int? syncInterval,
4678
int? syncMaxRecords,
4779
int? syncPageSize,
4880
}) async {
49-
_channel.setMethodCallHandler((MethodCall call) async {
50-
switch (call.method) {
51-
case 'resolveQueryPredicate':
52-
String? id = call.arguments;
53-
if (id == null) {
54-
throw ArgumentError(
55-
'resolveQueryPredicate must be called with an id');
56-
}
57-
return syncExpressions!
58-
.firstWhere((syncExpression) => syncExpression.id == id)
59-
.resolveQueryPredicate()
60-
.serializeAsMap();
61-
default:
62-
throw UnimplementedError('${call.method} has not been implemented.');
63-
}
64-
});
81+
_channel.setMethodCallHandler(_methodCallHandler);
6582
try {
83+
_syncExpressions = syncExpressions;
84+
_errorHandler = errorHandler;
85+
6686
return await _channel
6787
.invokeMethod('configureDataStore', <String, dynamic>{
6888
'modelSchemas': modelProvider?.modelSchemas
6989
.map((schema) => schema.toMap())
7090
.toList(),
91+
'hasErrorHandler': errorHandler != null,
7192
'modelProviderVersion': modelProvider?.version,
7293
'syncExpressions': syncExpressions!
7394
.map((syncExpression) => syncExpression.toMap())
@@ -231,6 +252,23 @@ class AmplifyDataStoreMethodChannel extends AmplifyDataStore {
231252
return serializedItem["modelName"] as String;
232253
}
233254

255+
AmplifyException _deserializeExceptionFromMap(Map<String, dynamic> e) {
256+
if (e['errorCode'] == 'DataStoreException') {
257+
return DataStoreException.fromMap(Map<String, String>.from(e['details']));
258+
} else if (e['errorCode'] == 'AmplifyAlreadyConfiguredException') {
259+
return AmplifyAlreadyConfiguredException.fromMap(
260+
Map<String, String>.from(e['details']));
261+
} else {
262+
// This shouldn't happen. All exceptions coming from platform for
263+
// amplify_datastore should have a known code. Throw an unknown error.
264+
return DataStoreException(
265+
AmplifyExceptionMessages.missingExceptionMessage,
266+
recoverySuggestion:
267+
AmplifyExceptionMessages.missingRecoverySuggestion,
268+
underlyingException: e.toString());
269+
}
270+
}
271+
234272
AmplifyException _deserializeException(PlatformException e) {
235273
if (e.code == 'DataStoreException') {
236274
return DataStoreException.fromMap(Map<String, String>.from(e.details));
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
import 'package:amplify_core/types/index.dart';
17+
import 'package:amplify_datastore/amplify_datastore.dart';
18+
import 'package:amplify_datastore_plugin_interface/amplify_datastore_plugin_interface.dart';
19+
import 'package:flutter/services.dart';
20+
import 'package:flutter_test/flutter_test.dart';
21+
import 'test_models/ModelProvider.dart';
22+
23+
// Utilized from: https://github.com/flutter/flutter/issues/63465 #CyrilHu
24+
// For mocking Native -> Dart
25+
extension MockMethodChannel on MethodChannel {
26+
Future<void> invokeMockMethod(String method, dynamic arguments) async {
27+
const codec = StandardMethodCodec();
28+
final data = codec.encodeMethodCall(MethodCall(method, arguments));
29+
30+
return ServicesBinding.instance?.defaultBinaryMessenger
31+
.handlePlatformMessage(
32+
name,
33+
data,
34+
(ByteData? data) {},
35+
);
36+
}
37+
}
38+
39+
void main() {
40+
const MethodChannel dataStoreChannel =
41+
MethodChannel('com.amazonaws.amplify/datastore');
42+
43+
AmplifyException? receivedException;
44+
45+
TestWidgetsFlutterBinding.ensureInitialized();
46+
47+
setUp(() {
48+
dataStoreChannel.setMockMethodCallHandler((MethodCall methodCall) async {});
49+
AmplifyDataStore dataStore = AmplifyDataStore(
50+
modelProvider: ModelProvider.instance,
51+
errorHandler: (exception) => {receivedException = exception});
52+
return dataStore.configureDataStore();
53+
});
54+
55+
test(
56+
'DataStoreException from MethodChannel is properly serialized and called',
57+
() async {
58+
await dataStoreChannel.invokeMockMethod("errorHandler", {
59+
'errorCode': 'DataStoreException',
60+
'errorMessage': 'ErrorMessage',
61+
'details': {
62+
'message': 'message',
63+
'recoverySuggestion': 'recoverySuggestion',
64+
'underlyingException': 'underlyingException'
65+
}
66+
});
67+
expect(
68+
receivedException,
69+
DataStoreException.fromMap({
70+
'message': 'message',
71+
'recoverySuggestion': 'recoverySuggestion',
72+
'underlyingException': 'underlyingException'
73+
}));
74+
});
75+
76+
test(
77+
'Unknown DataStoreException from MethodChannel is properly serialized and called',
78+
() async {
79+
await dataStoreChannel
80+
.invokeMockMethod("errorHandler", {'badErrorFormat': 'badErrorFormat'});
81+
expect(
82+
receivedException,
83+
DataStoreException(AmplifyExceptionMessages.missingExceptionMessage,
84+
recoverySuggestion:
85+
AmplifyExceptionMessages.missingRecoverySuggestion,
86+
underlyingException:
87+
{'badErrorFormat': 'badErrorFormat'}.toString()));
88+
});
89+
}

packages/amplify_datastore_plugin_interface/lib/amplify_datastore_plugin_interface.dart

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ abstract class DataStorePluginInterface extends AmplifyPluginInterface {
4747
/// modelProvider
4848
ModelProviderInterface? modelProvider;
4949

50+
// errorHandler
51+
Function(AmplifyException)? errorHandler;
52+
5053
/// list of sync expressions to filter datastore sync against
5154
List<DataStoreSyncExpression>? syncExpressions;
5255

@@ -63,6 +66,7 @@ abstract class DataStorePluginInterface extends AmplifyPluginInterface {
6366
DataStorePluginInterface({
6467
required Object token,
6568
required this.modelProvider,
69+
this.errorHandler,
6670
this.syncExpressions,
6771
this.syncInterval,
6872
this.syncMaxRecords,
@@ -80,15 +84,18 @@ abstract class DataStorePluginInterface extends AmplifyPluginInterface {
8084
}
8185

8286
/// Configure AmplifyDataStore plugin with mandatory [modelProvider]
83-
/// and optional datastore configuration properties including
87+
/// and optional DataStore configuration properties including
88+
///
89+
/// [errorHandler]: Custom error handler function that may receive an [AmplifyException] object when DataStore encounters an unhandled error during its background operations
8490
///
85-
/// [syncInterval]: datastore syncing interval (in seconds)
91+
/// [syncInterval]: DataStore syncing interval (in seconds)
8692
///
87-
/// [syncMaxRecords]: max number of records to sync
93+
/// [syncMaxRecords]: Max number of records to sync
8894
///
89-
/// [syncPageSize]: page size to sync
95+
/// [syncPageSize]: Page size to sync
9096
Future<void> configureDataStore(
9197
{required ModelProviderInterface modelProvider,
98+
Function(AmplifyException)? errorHandler,
9299
int? syncInterval,
93100
int? syncMaxRecords,
94101
int? syncPageSize}) {

packages/amplify_flutter/lib/categories/amplify_datastore_category.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ class DataStoreCategory {
3434
// Extra step to configure datastore specifically.
3535
// Note: The native datastore plugins are not added
3636
// in the `onAttachedToEngine` but rather in the `configure()
37-
await plugin.configureDataStore(modelProvider: plugin.modelProvider!);
37+
await plugin.configureDataStore(
38+
modelProvider: plugin.modelProvider!,
39+
errorHandler: plugin.errorHandler);
3840
plugins.add(plugin);
3941
} on AmplifyAlreadyConfiguredException {
4042
plugins.add(plugin);

0 commit comments

Comments
 (0)