Skip to content

Commit a6e450a

Browse files
authored
feat(datastore): Add QueryPredicate to Save/Delete (#1336)
* feat(datastore): Add QueryPredicate to Save/Delete
1 parent a04c1c1 commit a6e450a

File tree

11 files changed

+156
-30
lines changed

11 files changed

+156
-30
lines changed

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import com.amplifyframework.core.model.ModelSchema
4141
import com.amplifyframework.core.model.SerializedCustomType
4242
import com.amplifyframework.core.model.SerializedModel
4343
import com.amplifyframework.core.model.query.QueryOptions
44+
import com.amplifyframework.core.model.query.predicate.QueryPredicate
4445
import com.amplifyframework.core.model.query.predicate.QueryPredicates
4546
import com.amplifyframework.datastore.AWSDataStorePlugin
4647
import com.amplifyframework.datastore.DataStoreConfiguration
@@ -278,6 +279,7 @@ class AmplifyDataStorePlugin : FlutterPlugin, MethodCallHandler {
278279
@VisibleForTesting
279280
fun onDelete(flutterResult: Result, request: Map<String, Any>) {
280281
val modelName: String
282+
val queryPredicate: QueryPredicate
281283
val serializedModelData: Map<String, Any?>
282284
val schema: ModelSchema
283285

@@ -286,6 +288,9 @@ class AmplifyDataStorePlugin : FlutterPlugin, MethodCallHandler {
286288
schema = modelProvider.modelSchemas()[modelName]!!
287289
serializedModelData =
288290
deserializeNestedModel(request["serializedModel"].safeCastToMap()!!, schema)
291+
292+
queryPredicate = QueryPredicateBuilder.fromSerializedMap(
293+
request["queryPredicate"].safeCastToMap()) ?: QueryPredicates.all()
289294
} catch (e: Exception) {
290295
uiThreadHandler.post {
291296
postExceptionToFlutterChannel(
@@ -305,6 +310,7 @@ class AmplifyDataStorePlugin : FlutterPlugin, MethodCallHandler {
305310

306311
plugin.delete(
307312
instance,
313+
queryPredicate,
308314
{
309315
LOG.info("Deleted item: " + it.item().toString())
310316
uiThreadHandler.post { flutterResult.success(null) }
@@ -328,6 +334,7 @@ class AmplifyDataStorePlugin : FlutterPlugin, MethodCallHandler {
328334
@VisibleForTesting
329335
fun onSave(flutterResult: Result, request: Map<String, Any>) {
330336
val modelName: String
337+
val queryPredicate: QueryPredicate
331338
val serializedModelData: Map<String, Any?>
332339
val schema: ModelSchema
333340

@@ -336,6 +343,9 @@ class AmplifyDataStorePlugin : FlutterPlugin, MethodCallHandler {
336343
schema = modelProvider.modelSchemas()[modelName]!!
337344
serializedModelData =
338345
deserializeNestedModel(request["serializedModel"].safeCastToMap()!!, schema)
346+
347+
queryPredicate = QueryPredicateBuilder.fromSerializedMap(
348+
request["queryPredicate"].safeCastToMap()) ?: QueryPredicates.all()
339349
} catch (e: Exception) {
340350
uiThreadHandler.post {
341351
postExceptionToFlutterChannel(
@@ -353,11 +363,9 @@ class AmplifyDataStorePlugin : FlutterPlugin, MethodCallHandler {
353363
.modelSchema(schema)
354364
.build()
355365

356-
val predicate = QueryPredicates.all()
357-
358366
plugin.save(
359367
serializedModel,
360-
predicate,
368+
queryPredicate,
361369
{
362370
LOG.info("Saved item: " + it.item().toString())
363371
uiThreadHandler.post { flutterResult.success(null) }

packages/amplify_datastore/android/src/test/kotlin/com/amazonaws/amplify/amplify_datastore/AmplifyDataStorePluginTest.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -399,14 +399,14 @@ class AmplifyDataStorePluginTest {
399399

400400
doAnswer { invocation: InvocationOnMock ->
401401
assertEquals(serializedModel, invocation.arguments[0])
402-
(invocation.arguments[1] as Consumer<DataStoreItemChange<SerializedModel>>).accept(
402+
(invocation.arguments[2] as Consumer<DataStoreItemChange<SerializedModel>>).accept(
403403
dataStoreItemChange
404404
)
405405
null as Void?
406406
}.`when`(mockAmplifyDataStorePlugin).delete(
407407
any<SerializedModel>(),
408-
any<
409-
Consumer<DataStoreItemChange<SerializedModel>>>(),
408+
any<QueryPredicate>(),
409+
any<Consumer<DataStoreItemChange<SerializedModel>>>(),
410410
any<Consumer<DataStoreException>>()
411411
)
412412

@@ -434,14 +434,14 @@ class AmplifyDataStorePluginTest {
434434

435435
doAnswer { invocation: InvocationOnMock ->
436436
assertEquals(serializedModel, invocation.arguments[0])
437-
(invocation.arguments[2] as Consumer<DataStoreException>).accept(
437+
(invocation.arguments[3] as Consumer<DataStoreException>).accept(
438438
dataStoreException
439439
)
440440
null as Void?
441441
}.`when`(mockAmplifyDataStorePlugin).delete(
442442
any<SerializedModel>(),
443-
any<
444-
Consumer<DataStoreItemChange<SerializedModel>>>(),
443+
any<QueryPredicate>(),
444+
any<Consumer<DataStoreItemChange<SerializedModel>>>(),
445445
any<Consumer<DataStoreException>>()
446446
)
447447

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2022 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_datastore_example/models/ModelProvider.dart';
17+
import 'package:integration_test/integration_test.dart';
18+
import 'package:flutter_test/flutter_test.dart';
19+
import 'package:amplify_flutter/amplify_flutter.dart';
20+
21+
import 'utils/setup_utils.dart';
22+
23+
void main() {
24+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
25+
26+
group('delete', () {
27+
setUp(() async {
28+
await configureDataStore();
29+
// clear data before each test
30+
await clearDataStore();
31+
});
32+
33+
testWidgets('predicate should prevent delete to non matching model',
34+
(WidgetTester tester) async {
35+
const originalBlogName = 'non matching blog';
36+
Blog testBlog = Blog(name: originalBlogName);
37+
await Amplify.DataStore.save(testBlog);
38+
39+
await Amplify.DataStore.delete(testBlog,
40+
where: Blog.NAME.contains("Predicate"));
41+
var blogs = await Amplify.DataStore.query(Blog.classType);
42+
expect(blogs.length, 1);
43+
expect(blogs[0].name, originalBlogName);
44+
});
45+
46+
testWidgets('predicate should not prevent delete for matching model',
47+
(WidgetTester tester) async {
48+
const originalBlogName = 'matching blog';
49+
Blog testBlog = Blog(name: originalBlogName);
50+
await Amplify.DataStore.save(testBlog);
51+
52+
await Amplify.DataStore.delete(testBlog,
53+
where: Blog.NAME.contains("matching"));
54+
var blogs = await Amplify.DataStore.query(Blog.classType);
55+
expect(blogs.length, 0);
56+
});
57+
});
58+
}

packages/amplify_datastore/example/integration_test/save_test.dart

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License").
55
* You may not use this file except in compliance with the License.
@@ -62,5 +62,38 @@ void main() {
6262
expect(updatedBlogs[0].name, updatedBlogName);
6363
},
6464
);
65+
66+
testWidgets('predicate should prevent save to non matching model',
67+
(WidgetTester tester) async {
68+
// Note that predicate for save can only be applied to updates (not initial save)
69+
const originalBlogName = 'non matching blog';
70+
Blog testBlog = Blog(name: originalBlogName);
71+
await Amplify.DataStore.save(testBlog);
72+
73+
var updatedBlog = testBlog.copyWith(name: 'changed name');
74+
await Amplify.DataStore.save(updatedBlog,
75+
where: Blog.NAME.contains("Predicate"));
76+
77+
var blogs = await Amplify.DataStore.query(Blog.classType);
78+
expect(blogs.length, 1);
79+
expect(blogs[0].name, originalBlogName);
80+
});
81+
82+
testWidgets('predicate should not prevent save for matching model',
83+
(WidgetTester tester) async {
84+
// Note that predicate for save can only be applied to updates (not initial save)
85+
const originalBlogName = 'original blog';
86+
Blog testBlog = Blog(name: originalBlogName);
87+
await Amplify.DataStore.save(testBlog);
88+
89+
const matchingBlogName = 'matching blog name';
90+
var updatedBlog = testBlog.copyWith(name: matchingBlogName);
91+
await Amplify.DataStore.save(updatedBlog,
92+
where: Blog.NAME.contains("matching"));
93+
94+
var blogs = await Amplify.DataStore.query(Blog.classType);
95+
expect(blogs.length, 1);
96+
expect(blogs[0].name, matchingBlogName);
97+
});
6598
});
6699
}

packages/amplify_datastore/example/ios/unit_tests/DataStorePluginUnitTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ class DataStorePluginUnitTests: XCTestCase {
146146
override func onDelete(
147147
serializedModel: FlutterSerializedModel,
148148
modelSchema: ModelSchema,
149+
where: QueryPredicate? = nil,
149150
completion: @escaping DataStoreCallback<Void>
150151
) throws {
151152
// Validations that we called the native library correctly
@@ -173,6 +174,7 @@ class DataStorePluginUnitTests: XCTestCase {
173174
override func onDelete(
174175
serializedModel: FlutterSerializedModel,
175176
modelSchema: ModelSchema,
177+
where: QueryPredicate? = nil,
176178
completion: @escaping DataStoreCallback<Void>) throws {
177179
// Validations that we called the native library correctly
178180
XCTAssertEqual(testSchema.name, modelSchema.name)
@@ -453,7 +455,7 @@ class DataStorePluginUnitTests: XCTestCase {
453455
override func onSave<M: Model>(
454456
serializedModel: M,
455457
modelSchema: ModelSchema,
456-
when predicate: QueryPredicate? = nil,
458+
where predicate: QueryPredicate? = nil,
457459
completion: @escaping DataStoreCallback<M>) throws {
458460
// Validations that we called the native library correctly
459461
XCTAssertEqual("9fc5fab4-37ff-4566-97e5-19c5d58a4c22", serializedModel.id)
@@ -481,7 +483,7 @@ class DataStorePluginUnitTests: XCTestCase {
481483
override func onSave<M: Model>(
482484
serializedModel: M,
483485
modelSchema: ModelSchema,
484-
when predicate: QueryPredicate? = nil,
486+
where predicate: QueryPredicate? = nil,
485487
completion: @escaping DataStoreCallback<M>) throws {
486488
// Validations that we called the native library correctly
487489
XCTAssertEqual("9fc5fab4-37ff-4566-97e5-19c5d58a4c22", serializedModel.id)

packages/amplify_datastore/ios/Classes/DataStoreBridge.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public class DataStoreBridge {
4141

4242
func onSave<M: Model>(serializedModel: M,
4343
modelSchema: ModelSchema,
44-
when predicate: QueryPredicate? = nil,
44+
where predicate: QueryPredicate? = nil,
4545
completion: @escaping DataStoreCallback<M>) throws {
4646
try getPlugin().save(serializedModel,
4747
modelSchema: modelSchema,
@@ -52,10 +52,12 @@ public class DataStoreBridge {
5252

5353
func onDelete(serializedModel: FlutterSerializedModel,
5454
modelSchema: ModelSchema,
55+
where predicate: QueryPredicate? = nil,
5556
completion: @escaping DataStoreCallback<Void>) throws {
5657

5758
try getPlugin().delete(serializedModel,
5859
modelSchema: modelSchema,
60+
where: predicate,
5961
completion: completion)
6062
}
6163

packages/amplify_datastore/ios/Classes/SwiftAmplifyDataStorePlugin.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,20 @@ public class SwiftAmplifyDataStorePlugin: NSObject, FlutterPlugin {
259259
}
260260
}
261261

262+
// Initial Save fails for QueryPredicate.all, so we must pass nil instead
263+
// TODO: Amplify iOS should change .all initial save behavior to work.
264+
// Afterwards, we can remove this function and safely pass .all as our default queryPredicates
265+
// Relevant Amplify iOS issue: https://github.com/aws-amplify/amplify-ios/issues/1636
266+
func filterQueryPredicateAll(queryPredicates: QueryPredicate) -> QueryPredicate? {
267+
if let queryPredicateConstant = queryPredicates as? QueryPredicateConstant {
268+
switch queryPredicateConstant {
269+
case .all:
270+
return nil
271+
}
272+
}
273+
return queryPredicates
274+
}
275+
262276
func onSave(args: [String: Any], flutterResult: @escaping FlutterResult) {
263277

264278
do {
@@ -267,14 +281,17 @@ public class SwiftAmplifyDataStorePlugin: NSObject, FlutterPlugin {
267281
modelSchemaRegistry: modelSchemaRegistry,
268282
modelName: modelName
269283
)
284+
let queryPredicates = filterQueryPredicateAll(queryPredicates: try QueryPredicateBuilder.fromSerializedMap(args["queryPredicate"] as? [String : Any]))
285+
270286
let serializedModelData = try FlutterDataStoreRequestUtils.getSerializedModelData(methodChannelArguments: args)
271287
let modelID = try FlutterDataStoreRequestUtils.getModelID(serializedModelData: serializedModelData)
272288

273289
let serializedModel = FlutterSerializedModel(id: modelID, map: try FlutterDataStoreRequestUtils.getJSONValue(serializedModelData))
274290

275291
try bridge.onSave(
276292
serializedModel: serializedModel,
277-
modelSchema: modelSchema
293+
modelSchema: modelSchema,
294+
where: queryPredicates
278295
) { (result) in
279296
switch result {
280297
case .failure(let error):
@@ -295,7 +312,7 @@ public class SwiftAmplifyDataStorePlugin: NSObject, FlutterPlugin {
295312
flutterResult: flutterResult)
296313
}
297314
catch {
298-
print("An unexpected error occured when parsing save arguments: \(error)")
315+
print("An unexpected error occurred when parsing save arguments: \(error)")
299316
FlutterDataStoreErrorHandler.handleDataStoreError(error: DataStoreError(error: error),
300317
flutterResult: flutterResult)
301318
}
@@ -308,14 +325,17 @@ public class SwiftAmplifyDataStorePlugin: NSObject, FlutterPlugin {
308325
modelSchemaRegistry: modelSchemaRegistry,
309326
modelName: modelName
310327
)
328+
let queryPredicates = try QueryPredicateBuilder.fromSerializedMap(args["queryPredicate"] as? [String : Any])
329+
311330
let serializedModelData = try FlutterDataStoreRequestUtils.getSerializedModelData(methodChannelArguments: args)
312331
let modelID = try FlutterDataStoreRequestUtils.getModelID(serializedModelData: serializedModelData)
313332

314333
let serializedModel = FlutterSerializedModel(id: modelID, map: try FlutterDataStoreRequestUtils.getJSONValue(serializedModelData))
315334

316335
try bridge.onDelete(
317336
serializedModel: serializedModel,
318-
modelSchema: modelSchema) { (result) in
337+
modelSchema: modelSchema,
338+
where: queryPredicates) { (result) in
319339
switch result {
320340
case .failure(let error):
321341
print("Delete API failed. Error = \(error)")

packages/amplify_datastore/lib/amplify_datastore.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,13 @@ class AmplifyDataStore extends DataStorePluginInterface {
112112
}
113113

114114
@override
115-
Future<void> delete<T extends Model>(T model) async {
116-
return _instance.delete(model);
115+
Future<void> delete<T extends Model>(T model, {QueryPredicate? where}) async {
116+
return _instance.delete(model, where: where);
117117
}
118118

119119
@override
120-
Future<void> save<T extends Model>(T model) {
121-
return _instance.save(model);
120+
Future<void> save<T extends Model>(T model, {QueryPredicate? where}) {
121+
return _instance.save(model, where: where);
122122
}
123123

124124
@override

packages/amplify_datastore/lib/method_channel_datastore.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,24 +151,27 @@ class AmplifyDataStoreMethodChannel extends AmplifyDataStore {
151151
}
152152

153153
@override
154-
Future<void> delete<T extends Model>(T model) async {
154+
Future<void> delete<T extends Model>(T model, {QueryPredicate? where}) async {
155155
try {
156156
await _setUpObserveIfNeeded();
157-
await _channel.invokeMethod('delete', <String, dynamic>{
157+
var methodChannelDeleteInput = <String, dynamic>{
158158
'modelName': model.getInstanceType().modelName(),
159+
if (where != null) 'queryPredicate': where.serializeAsMap(),
159160
'serializedModel': model.toJson(),
160-
});
161+
};
162+
await _channel.invokeMethod('delete', methodChannelDeleteInput);
161163
} on PlatformException catch (e) {
162164
throw _deserializeException(e);
163165
}
164166
}
165167

166168
@override
167-
Future<void> save<T extends Model>(T model) async {
169+
Future<void> save<T extends Model>(T model, {QueryPredicate? where}) async {
168170
try {
169171
await _setUpObserveIfNeeded();
170172
var methodChannelSaveInput = <String, dynamic>{
171173
'modelName': model.getInstanceType().modelName(),
174+
if (where != null) 'queryPredicate': where.serializeAsMap(),
172175
'serializedModel': model.toJson(),
173176
};
174177
await _channel.invokeMethod('save', methodChannelSaveInput);

packages/amplify_datastore_plugin_interface/lib/amplify_datastore_plugin_interface.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ abstract class DataStorePluginInterface extends AmplifyPluginInterface {
9797
throw UnimplementedError('query() has not been implemented.');
9898
}
9999

100-
Future<void> delete<T extends Model>(T model) {
100+
Future<void> delete<T extends Model>(T model, {QueryPredicate? where}) {
101101
throw UnimplementedError('delete() has not been implemented.');
102102
}
103103

104-
Future<void> save<T extends Model>(T model) {
104+
Future<void> save<T extends Model>(T model, {QueryPredicate? where}) {
105105
throw UnimplementedError('save() has not been implemented');
106106
}
107107

0 commit comments

Comments
 (0)