Skip to content

Commit 771e35a

Browse files
authored
feat: Support lazy load and pagination for API (#1009)
* chore: Separate DataStore List logic to ListProvider * address PR comments * Update CoreError - remove pluginError case * enable some datastore integ tests * fix Collection count to add implicit load * update shouldDecode with ModelType * - fixed List.load check for not loaded state - fixed DataStoreListProvider association logic - improved unit tests * update doc comment * - removed .unknown case from CoreError - added more unit tests for DataStoreListProvider * feat: address pr comments * feat: support API fetch and pagination * feat: address PR comments - added AWSAPIPlugin readme file - added back in the datastore list provider implementation of hasNextPage and getNextPage * feat: address AWSAPIPlugin.md comments, update list decoder method name * feat: update APIPlugin.md
1 parent deb58af commit 771e35a

File tree

41 files changed

+3673
-288
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3673
-288
lines changed

Amplify.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@
142142
21A4ED77259CDF6800E1047D /* List+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A4ED76259CDF6800E1047D /* List+Combine.swift */; };
143143
21A7C8AA25ACB6C8004355D6 /* ArrayLiteralListProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A7C8A925ACB6C8004355D6 /* ArrayLiteralListProviderTests.swift */; };
144144
21A7C90225ACC4D1004355D6 /* MockDataStoreResponders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A7C90125ACC4D1004355D6 /* MockDataStoreResponders.swift */; };
145+
21A4F26325A3CD3D00E1047D /* List+Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A4F26225A3CD3D00E1047D /* List+Pagination.swift */; };
146+
21A4F8F325A77D9100E1047D /* ListPaginationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A4F8F225A77D9100E1047D /* ListPaginationTests.swift */; };
145147
21AD424B249BF0DA0016FE95 /* AnyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACBAD522386160100E29E56 /* AnyModel.swift */; };
146148
21AD424C249BF0DE0016FE95 /* AnyModel+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8EE776238626D60097E4F1 /* AnyModel+Codable.swift */; };
147149
21AD424D249BF0E50016FE95 /* AnyModel+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA8EE78223862DDB0097E4F1 /* AnyModel+Schema.swift */; };
@@ -924,6 +926,8 @@
924926
217D5EAF2577F9DF009F0639 /* Project2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Project2.swift; sourceTree = "<group>"; };
925927
217D5ECD2577FA1B009F0639 /* connection-schema.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = "connection-schema.graphql"; sourceTree = "<group>"; };
926928
217D5F63257830C2009F0639 /* ModelField+GraphQL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ModelField+GraphQL.swift"; sourceTree = "<group>"; };
929+
218DC969258A73AF00D59FC6 /* CoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreError.swift; sourceTree = "<group>"; };
930+
218DC979258A73CA00D59FC6 /* Paginatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paginatable.swift; sourceTree = "<group>"; };
927931
219A887E23EB627100BBC5F2 /* ModelBasedGraphQLDocumentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelBasedGraphQLDocumentBuilder.swift; sourceTree = "<group>"; };
928932
219A888023EB629800BBC5F2 /* ModelBasedGraphQLDocumentDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelBasedGraphQLDocumentDecorator.swift; sourceTree = "<group>"; };
929933
219A888423EB897700BBC5F2 /* GraphQLRequest+AnyModelWithSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GraphQLRequest+AnyModelWithSync.swift"; sourceTree = "<group>"; };
@@ -944,6 +948,8 @@
944948
21A4ED76259CDF6800E1047D /* List+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "List+Combine.swift"; sourceTree = "<group>"; };
945949
21A7C8A925ACB6C8004355D6 /* ArrayLiteralListProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayLiteralListProviderTests.swift; sourceTree = "<group>"; };
946950
21A7C90125ACC4D1004355D6 /* MockDataStoreResponders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataStoreResponders.swift; sourceTree = "<group>"; };
951+
21A4F26225A3CD3D00E1047D /* List+Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "List+Pagination.swift"; sourceTree = "<group>"; };
952+
21A4F8F225A77D9100E1047D /* ListPaginationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListPaginationTests.swift; sourceTree = "<group>"; };
947953
21AD4255249BFFDF0016FE95 /* DeprecatedTodo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DeprecatedTodo.swift; path = Deprecated/DeprecatedTodo.swift; sourceTree = "<group>"; };
948954
21C395B2245729EC00597EA2 /* AppSyncErrorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSyncErrorType.swift; sourceTree = "<group>"; };
949955
21D79FD9237617C60057D00D /* SubscriptionEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionEvent.swift; sourceTree = "<group>"; };
@@ -1945,6 +1951,7 @@
19451951
children = (
19461952
21A7C8A825ACB68C004355D6 /* Collection */,
19471953
21665EF4259A947100841696 /* ListTests.swift */,
1954+
21A4F8F225A77D9100E1047D /* ListPaginationTests.swift */,
19481955
);
19491956
path = Model;
19501957
sourceTree = "<group>";
@@ -2892,6 +2899,8 @@
28922899
217856BD2383322700A30D19 /* GraphQLMutationType.swift */,
28932900
21409C4C23847E41000A53C9 /* LabelType.swift */,
28942901
217856BA2383320900A30D19 /* GraphQLQueryType.swift */,
2902+
218DC969258A73AF00D59FC6 /* CoreError.swift */,
2903+
218DC979258A73CA00D59FC6 /* Paginatable.swift */,
28952904
);
28962905
name = "Recovered References";
28972906
sourceTree = "<group>";
@@ -2969,6 +2978,7 @@
29692978
21A4ED76259CDF6800E1047D /* List+Combine.swift */,
29702979
21A4ED6D259CDA4600E1047D /* List+LazyLoad.swift */,
29712980
B9FAA1242388BE48009414B4 /* List+Model.swift */,
2981+
21A4F26225A3CD3D00E1047D /* List+Pagination.swift */,
29722982
);
29732983
path = Collection;
29742984
sourceTree = "<group>";
@@ -5065,6 +5075,7 @@
50655075
B9A329CC243559BF00C5B80C /* TimeInterval+Helper.swift in Sources */,
50665076
950A26DC23D15D7E00D92B19 /* PredictionsTextToSpeechOperation.swift in Sources */,
50675077
21A4ED6E259CDA4600E1047D /* List+LazyLoad.swift in Sources */,
5078+
21A4F26325A3CD3D00E1047D /* List+Pagination.swift in Sources */,
50685079
FAC235A3227A5ED000424678 /* HubChannel.swift in Sources */,
50695080
FAA2E8CE23A02A8100E420EA /* PredictionsCategory+Resettable.swift in Sources */,
50705081
FAC23516227A053D00424678 /* StorageCategoryBehavior.swift in Sources */,
@@ -5166,6 +5177,7 @@
51665177
FA5D76AF23947E9C00489864 /* Model+CodableTests.swift in Sources */,
51675178
FA58456B24DA31370028D65A /* AmplifyInProcessReportingOperationChainedTests.swift in Sources */,
51685179
FA1B964E24BF5FA70002B90A /* AmplifyOperationCombineTests.swift in Sources */,
5180+
21A4F8F325A77D9100E1047D /* ListPaginationTests.swift in Sources */,
51695181
FAAFAF3323904BA4002CF932 /* AtomicValue+NumericTests.swift in Sources */,
51705182
FA176EDD2385943000C5C5F9 /* NotificationListeningAnalyticsPlugin.swift in Sources */,
51715183
FA1C80BF258686CC006160E9 /* AWSHubPluginAmplifyVersionableTests.swift in Sources */,

Amplify/Categories/DataStore/Model/Collection/List+LazyLoad.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,35 @@ extension List {
1414

1515
// MARK: - Asynchronous API
1616

17+
/// Call this to initialize the collection if you have retrieved the list by traversing from your model objects
18+
/// to its associated children objects. For example, a Post model may contain a list of Comments. By retrieving the
19+
/// post object and traversing to the comments, the comments are not retrieved from the data source until this
20+
/// method is called. Data will be retrieved based on the plugin's data source and may have different failure
21+
/// conditions--for example, a data source that requires network connectivity may fail if the network is
22+
/// unavailable. Alternately, you can trigger an implicit `fetch` by invoking the Collection methods (such as using
23+
/// `map`, or iterating in a `for/in` loop) on the List, which will retrieve data if it hasn't already been
24+
/// retrieved. In such cases, the time to perform that operation will include the time required to request data
25+
/// from the underlying data source.
26+
///
27+
/// If you have directly created this list object (for example, by calling `List(elements:)`) then the collection
28+
/// has already been initialized and calling this method will have no effect.
29+
public func fetch(_ completion: @escaping (Result<Void, CoreError>) -> Void) {
30+
guard case .notLoaded = loadedState else {
31+
completion(.successfulVoid)
32+
return
33+
}
34+
35+
listProvider.load { result in
36+
switch result {
37+
case .success(let elements):
38+
self.elements = elements
39+
completion(.success(()))
40+
case .failure(let coreError):
41+
completion(.failure(coreError))
42+
}
43+
}
44+
}
45+
1746
/// Call this to initialize the collection if you have retrieved the list by traversing from your model objects
1847
/// to its associated children objects. For example, a Post model may contain a list of Comments. By retrieving the
1948
/// post object and traversing to the comments, the comments are not retrieved from the data source until this
@@ -26,6 +55,7 @@ extension List {
2655
///
2756
/// If you have directly created this list object (for example, by calling `List(elements:)`) then the collection
2857
/// has already been initialized and calling this method will have no effect.
58+
@available(*, deprecated, message: "Use fetch(completion:) instead.")
2959
public func load(_ completion: DataStoreCallback<[Element]>) {
3060
if case .loaded(let elements) = loadedState {
3161
completion(.success(elements))
@@ -56,7 +86,7 @@ extension List {
5686
///
5787
/// - Returns: the current instance after data was loaded.
5888
/// - seealso: `load(completion:)`
59-
@available(*, deprecated, message: "Use load(completion:) instead.")
89+
@available(*, deprecated, message: "Use fetch(completion:) instead.")
6090
public func load() -> Self {
6191
guard case .notLoaded = loadedState else {
6292
return self

Amplify/Categories/DataStore/Model/Collection/List+Model.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Combine
1414
/// before returning the data to you. Consumers must be aware that multiple calls to the data source and then stored
1515
/// into this object will happen simultaneously if the object is used from different threads, thus not thread safe.
1616
/// Lazy loading is idempotent and will return the stored results on subsequent access.
17-
public class List<ModelType: Model>: Collection, Codable, ExpressibleByArrayLiteral {
17+
public class List<ModelType: Model>: Collection, Codable, ExpressibleByArrayLiteral, ModelListMarker {
1818
public typealias Index = Int
1919
public typealias Element = ModelType
2020

@@ -36,7 +36,7 @@ public class List<ModelType: Model>: Collection, Codable, ExpressibleByArrayLite
3636
/// provider's data source. This is not thread safe as it can be performed from multiple threads, however the
3737
/// provider's call to `load` should be idempotent and should result in the final loaded state. An attempt to set
3838
/// this again will result in no-op and will not overwrite the existing loaded data.
39-
var elements: [Element] {
39+
public internal(set) var elements: [Element] {
4040
get {
4141
switch loadedState {
4242
case .loaded(let elements):
@@ -144,7 +144,7 @@ public class List<ModelType: Model>: Collection, Codable, ExpressibleByArrayLite
144144
required convenience public init(from decoder: Decoder) throws {
145145
for listDecoder in ModelListDecoderRegistry.listDecoders.get() {
146146
if listDecoder.shouldDecode(modelType: ModelType.self, decoder: decoder) {
147-
let listProvider = try listDecoder.getListProvider(modelType: ModelType.self, decoder: decoder)
147+
let listProvider = try listDecoder.makeListProvider(modelType: ModelType.self, decoder: decoder)
148148
self.init(listProvider: listProvider)
149149
return
150150
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
extension List {
11+
12+
/// Check if there is subsequent data to retrieve. If true, the next page can be retrieved using
13+
/// `getNextPage(completion:)`. Calling `hasNextPage()` will load the underlying elements from the data source if not yet
14+
/// loaded before.
15+
public func hasNextPage() -> Bool {
16+
switch loadedState {
17+
case .loaded:
18+
return listProvider.hasNextPage()
19+
case .notLoaded:
20+
let result = listProvider.load()
21+
switch result {
22+
case .success(let elements):
23+
self.elements = elements
24+
return listProvider.hasNextPage()
25+
case .failure(let coreError):
26+
Amplify.log.error(error: coreError)
27+
return false
28+
}
29+
}
30+
}
31+
32+
/// Retrieve the next page as a new in-memory List object. Calling `getNextPage(completion:)` will load the
33+
/// underlying elements of the receiver from the data source if not yet loaded before
34+
public func getNextPage(completion: @escaping (Result<List<Element>, CoreError>) -> Void) {
35+
switch loadedState {
36+
case .loaded:
37+
listProvider.getNextPage(completion: completion)
38+
case .notLoaded:
39+
let result = listProvider.load()
40+
switch result {
41+
case .success(let elements):
42+
self.elements = elements
43+
listProvider.getNextPage(completion: completion)
44+
case .failure(let coreError):
45+
Amplify.log.error(error: coreError)
46+
completion(.failure(coreError))
47+
}
48+
}
49+
}
50+
}

Amplify/Categories/DataStore/Model/Internal/ModelListDecoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ extension ModelListDecoderRegistry {
3737
/// change.
3838
public protocol ModelListDecoder {
3939
static func shouldDecode<ModelType: Model>(modelType: ModelType.Type, decoder: Decoder) -> Bool
40-
static func getListProvider<ModelType: Model>(
40+
static func makeListProvider<ModelType: Model>(
4141
modelType: ModelType.Type, decoder: Decoder) throws -> AnyModelListProvider<ModelType>
4242
}

Amplify/Categories/DataStore/Model/Internal/ModelListProvider.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
import Foundation
99
import Combine
1010

11+
/// Empty protocol used as a marker to detect when the type is a `List`
12+
///
13+
/// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used
14+
/// directly by host applications. The behavior of this may change without warning. Though it is not used by host
15+
/// application making any change to these `public` types should be backward compatible, otherwise it will be a breaking
16+
/// change.
17+
public protocol ModelListMarker { }
18+
1119
/// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used
1220
/// directly by host applications. The behavior of this may change without warning. Though it is not used by host
1321
/// application making any change to these `public` types should be backward compatible, otherwise it will be a breaking
@@ -21,11 +29,13 @@ public protocol ModelListProvider {
2129
/// Retrieve the array of `Element` from the data source asychronously.
2230
func load(completion: @escaping (Result<[Element], CoreError>) -> Void)
2331

24-
/// Checks if there is subsequent data to retrieve. If true, the next page can be retrieved using
25-
/// `getNextPage(completion:)`
32+
/// Check if there is subsequent data to retrieve. This method always returns false if the underlying provider is
33+
/// not loaded. Make sure the underlying data is loaded by calling `load(completion)` before calling this method.
34+
/// If true, the next page can be retrieved using `getNextPage(completion:)`.
2635
func hasNextPage() -> Bool
2736

28-
/// Retrieves the next page as a new in-memory List object asynchronously.
37+
/// Asynchronously retrieve the next page as a new in-memory List object. Returns a failure if there
38+
/// is no next page of results. You can validate whether the list has another page with `hasNextPage()`.
2939
func getNextPage(completion: @escaping (Result<List<Element>, CoreError>) -> Void)
3040
}
3141

0 commit comments

Comments
 (0)