Skip to content

Commit deb58af

Browse files
authored
feat: Separate DataStore List logic out to list provider (#1000)
* 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
1 parent ae75da8 commit deb58af

35 files changed

+1830
-186
lines changed

Amplify.xcodeproj/project.pbxproj

Lines changed: 62 additions & 10 deletions
Large diffs are not rendered by default.

Amplify/Categories/API/Internal/APICategory+Resettable.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ extension AmplifyAPICategory: Resettable {
1818
}
1919

2020
ModelRegistry.reset()
21+
ModelListDecoderRegistry.reset()
2122

2223
group.wait()
2324

Amplify/Categories/DataStore/Internal/DataStoreCategory+Resettable.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ extension DataStoreCategory: Resettable {
1818
}
1919

2020
ModelRegistry.reset()
21+
ModelListDecoderRegistry.reset()
2122

2223
group.wait()
2324

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
import Combine
10+
11+
public struct ArrayLiteralListProvider<Element: Model>: ModelListProvider {
12+
let elements: [Element]
13+
public init(elements: [Element]) {
14+
self.elements = elements
15+
}
16+
17+
public func load() -> Result<[Element], CoreError> {
18+
.success(elements)
19+
}
20+
21+
public func load(completion: @escaping (Result<[Element], CoreError>) -> Void) {
22+
completion(.success(elements))
23+
}
24+
25+
public func hasNextPage() -> Bool {
26+
false
27+
}
28+
29+
public func getNextPage(completion: @escaping (Result<List<Element>, CoreError>) -> Void) {
30+
completion(.failure(CoreError.clientValidation("No pagination on an array literal",
31+
"Don't call this method",
32+
nil)))
33+
}
34+
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ import Combine
1010
@available(iOS 13.0, *)
1111
extension List {
1212

13-
public typealias LazyListPublisher = AnyPublisher<Elements, DataStoreError>
13+
public typealias LazyListPublisher = AnyPublisher<[Element], DataStoreError>
1414

15-
/// Lazy load the collection and expose the loaded `Elements` as a Combine `Publisher`.
16-
/// This is useful for integrating the `List<ModelType>` with existing Combine code
17-
/// and/or SwiftUI.
15+
/// This method has been deprecated, Use load(completion:) instead.
16+
///
17+
/// Lazy load the collection and expose the loaded `Elements` as a Combine `Publisher`. The List will retrieve the
18+
/// data from the underlying data source before the publisher is returned. This is useful for integrating the
19+
/// `List<ModelType>` with existing Combine code and/or SwiftUI. This method has been deprecated
20+
/// and is currently not a supported API.
1821
///
1922
/// - Returns: a type-erased Combine publisher
23+
@available(*, deprecated, message: "Use `load(completion:)` instead.")
2024
public func loadAsPublisher() -> LazyListPublisher {
2125
return Future { promise in
2226
self.load {

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

Lines changed: 37 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,83 +12,63 @@ import Foundation
1212
/// loaded when it's needed.
1313
extension List {
1414

15-
/// Represents the data state of the `List`.
16-
internal enum LoadState {
17-
case pending
18-
case loaded
19-
}
20-
2115
// MARK: - Asynchronous API
2216

23-
/// Trigger `DataStore` query to initialize the collection. This function always
24-
/// fetches data from the `DataStore.query`.
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 `load` 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.
2526
///
26-
/// - seealso: `load()`
27-
public func load(_ completion: DataStoreCallback<Elements>) {
28-
lazyLoad(completion)
29-
}
30-
31-
internal func lazyLoad(_ completion: DataStoreCallback<Elements>) {
32-
33-
// if the collection has no associated field, return the current elements
34-
guard let associatedId = associatedId,
35-
let associatedField = associatedField else {
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 load(_ completion: DataStoreCallback<[Element]>) {
30+
if case .loaded(let elements) = loadedState {
3631
completion(.success(elements))
3732
return
3833
}
39-
40-
let predicate: QueryPredicate = field(associatedField.name) == associatedId
41-
Amplify.DataStore.query(Element.self, where: predicate) {
42-
switch $0 {
43-
case .success(let elements):
44-
self.elements = elements
45-
self.state = .loaded
46-
completion(.success(elements))
47-
case .failure(let error):
48-
completion(.failure(causedBy: error))
34+
let result = listProvider.load()
35+
switch result {
36+
case .success(let elements):
37+
self.elements = elements
38+
completion(.success(elements))
39+
case .failure(let coreError):
40+
switch coreError {
41+
case .listOperation(_, _, let error),
42+
.clientValidation(_, _, let error):
43+
completion(.failure(causedBy: error ?? coreError))
4944
}
5045
}
5146
}
5247

5348
// MARK: - Synchronous API
5449

55-
/// Trigger `DataStore` query to initialize the collection. This function always
56-
/// fetches data from the `DataStore.query`. However, consumers must be aware of
50+
/// This method has been deprecated, Use load(completion:) instead.
51+
///
52+
/// Load data into the collection from the data source. Consumers must be aware of
5753
/// the internal behavior which relies on `DispatchSemaphore` and will block the
5854
/// current `DispatchQueue` until data is ready. When operating on large result
5955
/// sets, prefer using the asynchronous `load(completion:)` instead.
6056
///
6157
/// - Returns: the current instance after data was loaded.
6258
/// - seealso: `load(completion:)`
59+
@available(*, deprecated, message: "Use load(completion:) instead.")
6360
public func load() -> Self {
64-
lazyLoad()
65-
return self
66-
}
67-
68-
/// Internal function that only calls `lazyLoad()` if the `state` is not `.loaded`.
69-
/// - seealso: `lazyLoad()`
70-
internal func loadIfNeeded() {
71-
if state != .loaded {
72-
lazyLoad()
61+
guard case .notLoaded = loadedState else {
62+
return self
7363
}
74-
}
75-
76-
/// The synchronized version of `lazyLoad(completion:)`. This function is useful so
77-
/// instances of `List<ModelType>` behave like any other `Collection`.
78-
internal func lazyLoad() {
79-
let semaphore = DispatchSemaphore(value: 0)
80-
lazyLoad {
81-
switch $0 {
82-
case .success(let elements):
83-
self.elements = elements
84-
semaphore.signal()
85-
case .failure(let error):
86-
semaphore.signal()
87-
// TODO how to handle this failure? should it crash? just log the error?
88-
fatalError(error.errorDescription)
89-
}
64+
let result = listProvider.load()
65+
switch result {
66+
case .success(let elements):
67+
self.elements = elements
68+
case .failure(let error):
69+
Amplify.log.error(error: error)
70+
assert(false, error.errorDescription)
9071
}
91-
semaphore.wait()
72+
return self
9273
}
93-
9474
}

0 commit comments

Comments
 (0)