Skip to content

Commit 78bf700

Browse files
authored
feat(DataStore): ObserveQuery API (#1422)
* feat(DataStore): observe query API * fix: ObserveQuery API with QuerySortDescriptors for Flutter * address PR comments * address PR comments * address PR comments * updated observeQuery implementation * address PR comments * address comments * fix delete scenario * address comments * add class doc
1 parent 7cbbf22 commit 78bf700

24 files changed

+1906
-38
lines changed

Amplify.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
210DBC142332B3C6009B9E51 /* StorageGetURLOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210DBC132332B3C6009B9E51 /* StorageGetURLOperation.swift */; };
1717
210DBC162332B3CB009B9E51 /* StorageDownloadDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210DBC152332B3CB009B9E51 /* StorageDownloadDataOperation.swift */; };
1818
210DBC472332F0C5009B9E51 /* StorageError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210DBC462332F0C5009B9E51 /* StorageError.swift */; };
19+
211FFEE326CD650500F0DB75 /* DataStoreQuerySnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 211FFEE226CD650500F0DB75 /* DataStoreQuerySnapshot.swift */; };
1920
2125E2542319EC3100B3DEB5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2125E2532319EC3100B3DEB5 /* AppDelegate.swift */; };
2021
2125E2562319EC3100B3DEB5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2125E2552319EC3100B3DEB5 /* ViewController.swift */; };
2122
2125E2592319EC3100B3DEB5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2125E2572319EC3100B3DEB5 /* Main.storyboard */; };
@@ -159,6 +160,8 @@
159160
21A905372616446F00EC141D /* EnumTestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A9052A2616446F00EC141D /* EnumTestModel.swift */; };
160161
21A905382616446F00EC141D /* TestEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A9052B2616446F00EC141D /* TestEnum.swift */; };
161162
21A905602616484A00EC141D /* Scalar+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A9055F2616484A00EC141D /* Scalar+Equatable.swift */; };
163+
21AAE24026DFE94B007BA909 /* ModelSyncMetadata+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21AAE23E26DFE94B007BA909 /* ModelSyncMetadata+Schema.swift */; };
164+
21AAE24126DFE94B007BA909 /* ModelSyncMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21AAE23F26DFE94B007BA909 /* ModelSyncMetadata.swift */; };
162165
21AD424B249BF0DA0016FE95 /* AnyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACBAD522386160100E29E56 /* AnyModel.swift */; };
163166
21AD424C249BF0DE0016FE95 /* AnyModel+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8EE776238626D60097E4F1 /* AnyModel+Codable.swift */; };
164167
21AD424D249BF0E50016FE95 /* AnyModel+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8EE78223862DDB0097E4F1 /* AnyModel+Schema.swift */; };
@@ -845,6 +848,7 @@
845848
210DBC132332B3C6009B9E51 /* StorageGetURLOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageGetURLOperation.swift; sourceTree = "<group>"; };
846849
210DBC152332B3CB009B9E51 /* StorageDownloadDataOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageDownloadDataOperation.swift; sourceTree = "<group>"; };
847850
210DBC462332F0C5009B9E51 /* StorageError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageError.swift; sourceTree = "<group>"; };
851+
211FFEE226CD650500F0DB75 /* DataStoreQuerySnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreQuerySnapshot.swift; sourceTree = "<group>"; };
848852
2125E2102318D73B00B3DEB5 /* awsconfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = awsconfiguration.json; sourceTree = "<group>"; };
849853
2125E2512319EC3000B3DEB5 /* AmplifyTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AmplifyTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
850854
2125E2532319EC3100B3DEB5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -999,6 +1003,8 @@
9991003
21A9052A2616446F00EC141D /* EnumTestModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnumTestModel.swift; sourceTree = "<group>"; };
10001004
21A9052B2616446F00EC141D /* TestEnum.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestEnum.swift; sourceTree = "<group>"; };
10011005
21A9055F2616484A00EC141D /* Scalar+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Scalar+Equatable.swift"; sourceTree = "<group>"; };
1006+
21AAE23E26DFE94B007BA909 /* ModelSyncMetadata+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ModelSyncMetadata+Schema.swift"; sourceTree = "<group>"; };
1007+
21AAE23F26DFE94B007BA909 /* ModelSyncMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelSyncMetadata.swift; sourceTree = "<group>"; };
10021008
21AD4255249BFFDF0016FE95 /* DeprecatedTodo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DeprecatedTodo.swift; path = Deprecated/DeprecatedTodo.swift; sourceTree = "<group>"; };
10031009
21C395B2245729EC00597EA2 /* AppSyncErrorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSyncErrorType.swift; sourceTree = "<group>"; };
10041010
21D79FD9237617C60057D00D /* SubscriptionEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEvent.swift; sourceTree = "<group>"; };
@@ -1931,6 +1937,7 @@
19311937
2129BE3F23948909006363A1 /* Sync */ = {
19321938
isa = PBXGroup;
19331939
children = (
1940+
21AAE23D26DFE94B007BA909 /* ModelSync */,
19341941
2129BE4023948912006363A1 /* MutationSync */,
19351942
2129BE542395CAEF006363A1 /* PaginatedList.swift */,
19361943
);
@@ -2176,6 +2183,15 @@
21762183
path = Scalar;
21772184
sourceTree = "<group>";
21782185
};
2186+
21AAE23D26DFE94B007BA909 /* ModelSync */ = {
2187+
isa = PBXGroup;
2188+
children = (
2189+
21AAE23E26DFE94B007BA909 /* ModelSyncMetadata+Schema.swift */,
2190+
21AAE23F26DFE94B007BA909 /* ModelSyncMetadata.swift */,
2191+
);
2192+
path = ModelSync;
2193+
sourceTree = "<group>";
2194+
};
21792195
21AD424A249BEC440016FE95 /* Internal */ = {
21802196
isa = PBXGroup;
21812197
children = (
@@ -3852,6 +3868,7 @@
38523868
isa = PBXGroup;
38533869
children = (
38543870
FAD393702381FD3C00463F5E /* DataStoreCategory+Subscribe.swift */,
3871+
211FFEE226CD650500F0DB75 /* DataStoreQuerySnapshot.swift */,
38553872
FA9FD2332381CD0000A7CAF5 /* MutationEvent.swift */,
38563873
FA8F4D232395B1B600861D91 /* MutationEvent+Model.swift */,
38573874
FACBAD4F2386101100E29E56 /* MutationEvent+MutationType.swift */,
@@ -4754,6 +4771,7 @@
47544771
files = (
47554772
2129BE4423948951006363A1 /* MutationSyncMetadata.swift in Sources */,
47564773
B9675A2E24752621002FC843 /* GraphQLRequest+Model.swift in Sources */,
4774+
21AAE24026DFE94B007BA909 /* ModelSyncMetadata+Schema.swift in Sources */,
47574775
2129BE1E2394806B006363A1 /* QueryPredicate+GraphQL.swift in Sources */,
47584776
21420A8F237222A900FA140C /* AWSIAMConfiguration.swift in Sources */,
47594777
219A888523EB897700BBC5F2 /* GraphQLRequest+AnyModelWithSync.swift in Sources */,
@@ -4787,6 +4805,7 @@
47874805
7608EE14268142F300FEC9CD /* AWSPluginOptions.swift in Sources */,
47884806
6BBECD7123ADA7E100C8DFBE /* AmplifyAWSServiceConfiguration.swift in Sources */,
47894807
21AD424F249BF0EC0016FE95 /* Model+AnyModel.swift in Sources */,
4808+
21AAE24126DFE94B007BA909 /* ModelSyncMetadata.swift in Sources */,
47904809
B4EBEB682462050B00D06375 /* AuthCognitoIdentityProvider.swift in Sources */,
47914810
21AD424C249BF0DE0016FE95 /* AnyModel+Codable.swift in Sources */,
47924811
212CE6FE23E9E5A2007D8E71 /* GraphQLDocumentnputValue.swift in Sources */,
@@ -5237,6 +5256,7 @@
52375256
B4251A0124250369007F59EF /* AuthConfirmResetPasswordRequest.swift in Sources */,
52385257
FAAFAF2F23904B14002CF932 /* AtomicValue+Bool.swift in Sources */,
52395258
B9B50DC823DA15890086F1E1 /* DataStoreError+Temporal.swift in Sources */,
5259+
211FFEE326CD650500F0DB75 /* DataStoreQuerySnapshot.swift in Sources */,
52405260
FA249EEB24C5FE66009B3CE8 /* AmplifyAPICategory+GraphQLBehavior+Combine.swift in Sources */,
52415261
FACD26502386E9410068FBE6 /* JSONValue+Subscript.swift in Sources */,
52425262
B450741324115C260098F02D /* AuthSignInRequest.swift in Sources */,

Amplify/Categories/DataStore/DataStoreCategoryBehavior.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,16 @@ public protocol DataStoreSubscribeBehavior {
6565
/// - Parameter modelType: The model type to observe
6666
@available(iOS 13.0, *)
6767
func publisher<M: Model>(for modelType: M.Type) -> AnyPublisher<MutationEvent, DataStoreError>
68+
69+
/// Returns a Publisher for query snapshots.
70+
///
71+
/// - Parameters:
72+
/// - modelType: The model type to observe
73+
/// - predicate: The predicate to match for filtered results
74+
/// - sortInput: The field and order of data to be returned
75+
@available(iOS 13.0, *)
76+
func observeQuery<M: Model>(for modelType: M.Type,
77+
where predicate: QueryPredicate?,
78+
sort sortInput: QuerySortInput?)
79+
-> AnyPublisher<DataStoreQuerySnapshot<M>, DataStoreError>
6880
}

Amplify/Categories/DataStore/Subscribe/DataStoreCategory+Subscribe.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,12 @@ extension DataStoreCategory: DataStoreSubscribeBehavior {
1212
public func publisher<M: Model>(for modelType: M.Type) -> AnyPublisher<MutationEvent, DataStoreError> {
1313
return plugin.publisher(for: modelType)
1414
}
15+
16+
@available(iOS 13.0, *)
17+
public func observeQuery<M: Model>(for modelType: M.Type,
18+
where predicate: QueryPredicate? = nil,
19+
sort sortInput: QuerySortInput? = nil)
20+
-> AnyPublisher<DataStoreQuerySnapshot<M>, DataStoreError> {
21+
return plugin.observeQuery(for: modelType, where: predicate, sort: sortInput)
22+
}
1523
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
/// A snapshot of the items from DataStore, the changes since last snapshot, and whether this model has
11+
/// finished syncing and subscriptions are active
12+
public struct DataStoreQuerySnapshot<M: Model> {
13+
14+
/// All model instances from the local store
15+
public let items: [M]
16+
17+
/// Indicates whether all sync queries for this model are complete, and subscriptions are active
18+
public let isSynced: Bool
19+
20+
public init(items: [M], isSynced: Bool) {
21+
self.items = items
22+
self.isSynced = isSynced
23+
}
24+
}

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/AWSDataStorePlugin+DataStoreBaseBehavior.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,17 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior {
174174
}
175175

176176
public func start(completion: @escaping DataStoreCallback<Void>) {
177-
reinitStorageEngineIfNeeded(completion: completion)
177+
reinitStorageEngineIfNeeded { result in
178+
completion(result)
179+
}
178180
}
179181

180182
public func stop(completion: @escaping DataStoreCallback<Void>) {
183+
operationQueue.operations.forEach { operation in
184+
if let operation = operation as? DataStoreObserveQueryOperation {
185+
operation.resetState()
186+
}
187+
}
181188
storageEngineInitSemaphore.wait()
182189
if storageEngine == nil {
183190
storageEngineInitSemaphore.signal()
@@ -192,6 +199,11 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior {
192199
}
193200

194201
public func clear(completion: @escaping DataStoreCallback<Void>) {
202+
operationQueue.operations.forEach { operation in
203+
if let operation = operation as? DataStoreObserveQueryOperation {
204+
operation.resetState()
205+
}
206+
}
195207
storageEngineInitSemaphore.wait()
196208
if storageEngine == nil {
197209
storageEngineInitSemaphore.signal()

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/AWSDataStorePlugin+DataStoreSubscribeBehavior.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,39 @@ extension AWSDataStorePlugin: DataStoreSubscribeBehavior {
2727
public func publisher(for modelName: ModelName) -> AnyPublisher<MutationEvent, DataStoreError> {
2828
return publisher.filter { $0.modelName == modelName }.eraseToAnyPublisher()
2929
}
30+
31+
@available(iOS 13.0, *)
32+
public func observeQuery<M: Model>(for modelType: M.Type,
33+
where predicate: QueryPredicate? = nil,
34+
sort sortInput: QuerySortInput? = nil)
35+
-> AnyPublisher<DataStoreQuerySnapshot<M>, DataStoreError> {
36+
return observeQuery(for: modelType,
37+
modelSchema: modelType.schema,
38+
where: predicate,
39+
sort: sortInput?.asSortDescriptors())
40+
}
41+
42+
@available(iOS 13.0, *)
43+
public func observeQuery<M: Model>(for modelType: M.Type,
44+
modelSchema: ModelSchema,
45+
where predicate: QueryPredicate? = nil,
46+
sort sortInput: [QuerySortDescriptor]? = nil)
47+
-> AnyPublisher<DataStoreQuerySnapshot<M>, DataStoreError> {
48+
reinitStorageEngineIfNeeded()
49+
50+
guard let dataStorePublisher = dataStorePublisher else {
51+
return Fail(error: DataStoreError.unknown(
52+
"`dataStorePublisher` is expected to exist for deployment targets >=iOS13.0",
53+
"", nil)).eraseToAnyPublisher()
54+
}
55+
let operation = AWSDataStoreObserveQueryOperation(modelType: modelType,
56+
modelSchema: modelSchema,
57+
predicate: predicate,
58+
sortInput: sortInput,
59+
storageEngine: storageEngine,
60+
dataStorePublisher: dataStorePublisher,
61+
dataStoreConfiguration: dataStoreConfiguration)
62+
operationQueue.addOperation(operation)
63+
return operation.publisher
64+
}
3065
}

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/AWSDataStorePlugin.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ final public class AWSDataStorePlugin: DataStoreCategoryPlugin {
1717
/// `true` if any models are syncable. Resolved during configuration phase
1818
var isSyncEnabled: Bool
1919

20+
/// The listener on hub events unsubscribe token
21+
var hubListener: UnsubscribeToken?
22+
2023
/// The Publisher that sends mutation events to subscribers
2124
var dataStorePublisher: ModelSubcriptionBehavior?
2225

@@ -25,6 +28,10 @@ final public class AWSDataStorePlugin: DataStoreCategoryPlugin {
2528
/// The DataStore configuration
2629
let dataStoreConfiguration: DataStoreConfiguration
2730

31+
/// A queue that regulates the execution of operations. This will be instantiated during initalization phase,
32+
/// and is clearable by `reset()`. This is implicitly unwrapped to be destroyed when resetting.
33+
var operationQueue: OperationQueue!
34+
2835
let validAPIPluginKey: String
2936

3037
let validAuthPluginKey: String
@@ -53,6 +60,7 @@ final public class AWSDataStorePlugin: DataStoreCategoryPlugin {
5360
self.modelRegistration = modelRegistration
5461
self.dataStoreConfiguration = dataStoreConfiguration
5562
self.isSyncEnabled = false
63+
self.operationQueue = OperationQueue()
5664
self.validAPIPluginKey = "awsAPIPlugin"
5765
self.validAuthPluginKey = "awsCognitoAuthPlugin"
5866
self.storageEngineBehaviorFactory =
@@ -70,10 +78,12 @@ final public class AWSDataStorePlugin: DataStoreCategoryPlugin {
7078
configuration dataStoreConfiguration: DataStoreConfiguration = .default,
7179
storageEngineBehaviorFactory: StorageEngineBehaviorFactory? = nil,
7280
dataStorePublisher: ModelSubcriptionBehavior,
81+
operationQueue: OperationQueue = OperationQueue(),
7382
validAPIPluginKey: String,
7483
validAuthPluginKey: String) {
7584
self.modelRegistration = modelRegistration
7685
self.dataStoreConfiguration = dataStoreConfiguration
86+
self.operationQueue = operationQueue
7787
self.isSyncEnabled = false
7888
self.storageEngineBehaviorFactory = storageEngineBehaviorFactory ??
7989
StorageEngine.init(isSyncEnabled:dataStoreConfiguration:validAPIPluginKey:validAuthPluginKey:modelRegistryVersion:userDefault:)
@@ -108,7 +118,15 @@ final public class AWSDataStorePlugin: DataStoreCategoryPlugin {
108118
try resolveStorageEngine(dataStoreConfiguration: dataStoreConfiguration)
109119
try storageEngine.setUp(modelSchemas: ModelRegistry.modelSchemas)
110120
storageEngineInitSemaphore.signal()
111-
storageEngine.startSync(completion: completion)
121+
storageEngine.startSync { result in
122+
123+
self.operationQueue.operations.forEach { operation in
124+
if let operation = operation as? DataStoreObserveQueryOperation {
125+
operation.startObserveQuery(with: self.storageEngine)
126+
}
127+
}
128+
completion(result)
129+
}
112130
} catch {
113131
storageEngineInitSemaphore.signal()
114132
completion(.failure(causedBy: error))
@@ -174,6 +192,13 @@ final public class AWSDataStorePlugin: DataStoreCategoryPlugin {
174192
}
175193

176194
public func reset(onComplete: @escaping (() -> Void)) {
195+
if operationQueue != nil {
196+
operationQueue = nil
197+
}
198+
if let listener = hubListener {
199+
Amplify.Hub.removeListener(listener)
200+
hubListener = nil
201+
}
177202
let group = DispatchGroup()
178203
if let resettable = storageEngine as? Resettable {
179204
log.verbose("Resetting storageEngine")

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Storage/SQLite/QuerySort+SQLite.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ extension QuerySortBy {
2626
return QuerySortOrder.descending
2727
}
2828
}
29+
30+
var sortDescriptor: QuerySortDescriptor {
31+
switch self {
32+
case .ascending(let key):
33+
return .init(fieldName: key.stringValue, order: .ascending)
34+
case .descending(let key):
35+
return .init(fieldName: key.stringValue, order: .descending)
36+
}
37+
}
2938
}
3039

3140
extension QuerySortInput {

0 commit comments

Comments
 (0)