From a0c6388b067fbf7850d4cbc254007f134df4109e Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 18 Oct 2023 13:07:56 +1300 Subject: [PATCH 1/6] Implement fetching posts/pages list in PostRepository --- .../Classes/Services/PostRepository.swift | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/WordPress/Classes/Services/PostRepository.swift b/WordPress/Classes/Services/PostRepository.swift index c09d99d12b20..49bc45a890a9 100644 --- a/WordPress/Classes/Services/PostRepository.swift +++ b/WordPress/Classes/Services/PostRepository.swift @@ -228,3 +228,165 @@ final class PostRepository { } } + +// MARK: - Posts/Pages List + +private final class PostRepositoryPostsSerivceRemoteOptions: NSObject, PostServiceRemoteOptions { + struct Options { + var statuses: [String]? + var number: Int = 100 + var offset: Int = 0 + var order: PostServiceResultsOrder = .descending + var orderBy: PostServiceResultsOrdering = .byDate + var authorID: NSNumber? + var search: String? + var meta: String? = "autosave" + } + + var options: Options + + init(options: Options) { + self.options = options + } + + func statuses() -> [String]? { + options.statuses + } + + func number() -> NSNumber { + NSNumber(value: options.number) + } + + func offset() -> NSNumber { + NSNumber(value: options.offset) + } + + func order() -> PostServiceResultsOrder { + options.order + } + + func orderBy() -> PostServiceResultsOrdering { + options.orderBy + } + + func authorID() -> NSNumber? { + options.authorID + } + + func search() -> String? { + options.search + } + + func meta() -> String? { + options.meta + } +} + +private extension PostServiceRemote { + + func getPosts(ofType type: String, options: PostRepositoryPostsSerivceRemoteOptions) async throws -> [RemotePost] { + try await withCheckedThrowingContinuation { continuation in + self.getPostsOfType(type, options: self.dictionary(with: options), success: { + continuation.resume(returning: $0 ?? []) + }, failure: { + continuation.resume(throwing: $0!) + }) + } + } +} + +extension PostRepository { + + func paginate(type: P.Type = P.self, statuses: [BasePost.Status], authorUserID: NSNumber? = nil, offset: Int, number: Int, in blogID: TaggedManagedObjectID) async throws -> [TaggedManagedObjectID

] { + try await fetch( + type: type, + statuses: statuses, + authorUserID: authorUserID, + range: offset...(offset + max(number - 1, 0)), + orderBy: .byDate, + descending: true, + // Only delete other local posts if the current call is the first pagination request. + deleteOtherLocalPosts: offset == 0, + in: blogID + ) + } + + func search(type: P.Type = P.self, input: String? = nil, statuses: [BasePost.Status], authorUserID: NSNumber? = nil, limit: Int, orderBy: PostServiceResultsOrdering, descending: Bool, in blogID: TaggedManagedObjectID) async throws -> [TaggedManagedObjectID

] { + try await fetch( + type: type, + searchInput: input, + statuses: statuses, + authorUserID: authorUserID, + range: 0...max(limit - 1, 0), + orderBy: orderBy, + descending: descending, + deleteOtherLocalPosts: false, + in: blogID + ) + } + + private func fetch( + type: P.Type, + searchInput: String? = nil, + statuses: [BasePost.Status]?, + authorUserID: NSNumber?, + range: ClosedRange, + orderBy: PostServiceResultsOrdering = .byDate, + descending: Bool = true, + deleteOtherLocalPosts: Bool, + in blogID: TaggedManagedObjectID + ) async throws -> [TaggedManagedObjectID

] { + assert(type == Post.self || type == Page.self, "Only support fetching Post or Page") + assert(range.lowerBound >= 0) + + let postType: String + if type == Post.self { + postType = "post" + } else if type == Page.self { + postType = "page" + } else { + // There is an assertion above to ensure the app doesn't fall into this case. + return [] + } + + let remote = try await coreDataStack.performQuery { [remoteFactory] context in + let blog = try context.existingObject(with: blogID) + return remoteFactory.forBlog(blog) + } + guard let remote else { + throw PostRepository.Error.remoteAPIUnavailable + } + + let options = PostRepositoryPostsSerivceRemoteOptions(options: .init( + statuses: statuses?.strings, + number: range.count, + offset: range.lowerBound, + order: descending ? .descending : .ascending, + orderBy: orderBy, + authorID: authorUserID, + search: searchInput + )) + let remotePosts = try await remote.getPosts(ofType: postType, options: options) + + let updatedPosts = try await coreDataStack.performAndSave { context in + let updatedPosts = PostHelper.merge( + remotePosts, + ofType: postType, + withStatuses: statuses?.strings, + byAuthor: authorUserID, + for: try context.existingObject(with: blogID), + purgeExisting: deleteOtherLocalPosts, + in: context + ) + return updatedPosts.compactMap { + guard let post = $0 as? P else { + fatalError("Expecting a \(postType) as \(type), but got \($0)") + } + return TaggedManagedObjectID(post) + } + } + + return updatedPosts + } + +} From 4f8178a96aeaeaabc276eb01cafe0f0357219b1b Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 19 Oct 2023 10:57:31 +1300 Subject: [PATCH 2/6] Add API docs --- .../Classes/Services/PostRepository.swift | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/Services/PostRepository.swift b/WordPress/Classes/Services/PostRepository.swift index 49bc45a890a9..ada392e60d46 100644 --- a/WordPress/Classes/Services/PostRepository.swift +++ b/WordPress/Classes/Services/PostRepository.swift @@ -297,7 +297,24 @@ private extension PostServiceRemote { extension PostRepository { - func paginate(type: P.Type = P.self, statuses: [BasePost.Status], authorUserID: NSNumber? = nil, offset: Int, number: Int, in blogID: TaggedManagedObjectID) async throws -> [TaggedManagedObjectID

] { + /// Fetch posts or pages from the given site page by page. All fetched posts are saved to the local database. + /// + /// - Parameters: + /// - type: `Post.self` and `Page.self` are the only acceptable types. + /// - statuses: Filter posts or pages with given status. + /// - authorUserID: Filter posts or pages that are authored by given user. + /// - offset: The position of the paginated request. Pass 0 for the first page and count of already fetched results for following pages. + /// - number: Number of posts or pages should be fetched. Pass 0 to fetch all posts or pages in one request. + /// - blogID: The blog from which to fetch posts or pages + /// - Returns: Object identifiers of the fetched posts. + func paginate( + type: P.Type = P.self, + statuses: [BasePost.Status], + authorUserID: NSNumber? = nil, + offset: Int, + number: Int, + in blogID: TaggedManagedObjectID + ) async throws -> [TaggedManagedObjectID

] { try await fetch( type: type, statuses: statuses, @@ -311,7 +328,28 @@ extension PostRepository { ) } - func search(type: P.Type = P.self, input: String? = nil, statuses: [BasePost.Status], authorUserID: NSNumber? = nil, limit: Int, orderBy: PostServiceResultsOrdering, descending: Bool, in blogID: TaggedManagedObjectID) async throws -> [TaggedManagedObjectID

] { + /// Search posts or pages in the given site. All fetched posts are saved to the local database. + /// + /// - Parameters: + /// - type: `Post.self` and `Page.self` are the only acceptable types. + /// - input: The text input from user. Or `nil` for searching all posts or pages. + /// - statuses: Filter posts or pages with given status. + /// - authorUserID: Filter posts or pages that are authored by given user. + /// - limit: Number of posts or pages should be fetched. Pass 0 to fetch all posts or pages in one request. + /// - orderBy: The property by which to sort posts or pages. + /// - descending: Whether to sort the results in descending order. + /// - blogID: The blog from which to search posts or pages + /// - Returns: Object identifiers of the search result. + func search( + type: P.Type = P.self, + input: String?, + statuses: [BasePost.Status], + authorUserID: NSNumber? = nil, + limit: Int, + orderBy: PostServiceResultsOrdering, + descending: Bool, + in blogID: TaggedManagedObjectID + ) async throws -> [TaggedManagedObjectID

] { try await fetch( type: type, searchInput: input, From fa641ec2b2f9279177c33f88655faf6f483f78ec Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 19 Oct 2023 11:05:40 +1300 Subject: [PATCH 3/6] Allow fetching post in empty range which means returning unlimited result --- WordPress/Classes/Services/PostRepository.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/Services/PostRepository.swift b/WordPress/Classes/Services/PostRepository.swift index ada392e60d46..51fb8f7562d0 100644 --- a/WordPress/Classes/Services/PostRepository.swift +++ b/WordPress/Classes/Services/PostRepository.swift @@ -319,7 +319,7 @@ extension PostRepository { type: type, statuses: statuses, authorUserID: authorUserID, - range: offset...(offset + max(number - 1, 0)), + range: offset..<(offset + max(number, 0)), orderBy: .byDate, descending: true, // Only delete other local posts if the current call is the first pagination request. @@ -355,7 +355,7 @@ extension PostRepository { searchInput: input, statuses: statuses, authorUserID: authorUserID, - range: 0...max(limit - 1, 0), + range: 0.., + range: Range, orderBy: PostServiceResultsOrdering = .byDate, descending: Bool = true, deleteOtherLocalPosts: Bool, From ed47e2bf5ddf2f118e0be4d337c8488d357e9fb6 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 19 Oct 2023 12:30:26 +1300 Subject: [PATCH 4/6] Update the doc to fix incorrect REST API behaviour --- WordPress/Classes/Services/PostRepository.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/Services/PostRepository.swift b/WordPress/Classes/Services/PostRepository.swift index 51fb8f7562d0..d716a2beb6fb 100644 --- a/WordPress/Classes/Services/PostRepository.swift +++ b/WordPress/Classes/Services/PostRepository.swift @@ -304,9 +304,10 @@ extension PostRepository { /// - statuses: Filter posts or pages with given status. /// - authorUserID: Filter posts or pages that are authored by given user. /// - offset: The position of the paginated request. Pass 0 for the first page and count of already fetched results for following pages. - /// - number: Number of posts or pages should be fetched. Pass 0 to fetch all posts or pages in one request. + /// - number: Number of posts or pages should be fetched. /// - blogID: The blog from which to fetch posts or pages /// - Returns: Object identifiers of the fetched posts. + /// - SeeAlso: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/ func paginate( type: P.Type = P.self, statuses: [BasePost.Status], @@ -335,11 +336,12 @@ extension PostRepository { /// - input: The text input from user. Or `nil` for searching all posts or pages. /// - statuses: Filter posts or pages with given status. /// - authorUserID: Filter posts or pages that are authored by given user. - /// - limit: Number of posts or pages should be fetched. Pass 0 to fetch all posts or pages in one request. + /// - limit: Number of posts or pages should be fetched. /// - orderBy: The property by which to sort posts or pages. /// - descending: Whether to sort the results in descending order. /// - blogID: The blog from which to search posts or pages /// - Returns: Object identifiers of the search result. + /// - SeeAlso: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/ func search( type: P.Type = P.self, input: String?, From c980a3079ba8533003d5a79c4bf832096d3149e3 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 19 Oct 2023 12:31:51 +1300 Subject: [PATCH 5/6] Add unit tests for fetching posts list --- WordPress/WordPress.xcodeproj/project.pbxproj | 4 + .../PostRepositoryPostsListTests.swift | 191 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 WordPress/WordPressTest/PostRepositoryPostsListTests.swift diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 1a43ec89c33b..046c3c38ae71 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -1267,6 +1267,7 @@ 4AA33F022999D11A005B6E23 /* ReaderSiteTopic+Lookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA33F002999D11A005B6E23 /* ReaderSiteTopic+Lookup.swift */; }; 4AA33F04299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA33F03299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift */; }; 4AA33F05299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA33F03299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift */; }; + 4AA7EE0F2ADF7367007D261D /* PostRepositoryPostsListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA7EE0E2ADF7367007D261D /* PostRepositoryPostsListTests.swift */; }; 4AAD69082A6F68A5007FE77E /* MediaRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AAD69072A6F68A5007FE77E /* MediaRepositoryTests.swift */; }; 4AD5656C28E3D0670054C676 /* ReaderPost+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5656B28E3D0670054C676 /* ReaderPost+Helper.swift */; }; 4AD5656D28E3D0670054C676 /* ReaderPost+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5656B28E3D0670054C676 /* ReaderPost+Helper.swift */; }; @@ -6869,6 +6870,7 @@ 4AA33EFA2999AE3B005B6E23 /* ReaderListTopic+Creation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderListTopic+Creation.swift"; sourceTree = ""; }; 4AA33F002999D11A005B6E23 /* ReaderSiteTopic+Lookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderSiteTopic+Lookup.swift"; sourceTree = ""; }; 4AA33F03299A1F93005B6E23 /* ReaderTagTopic+Lookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderTagTopic+Lookup.swift"; sourceTree = ""; }; + 4AA7EE0E2ADF7367007D261D /* PostRepositoryPostsListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostRepositoryPostsListTests.swift; sourceTree = ""; }; 4AAD69072A6F68A5007FE77E /* MediaRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaRepositoryTests.swift; sourceTree = ""; }; 4AD5656B28E3D0670054C676 /* ReaderPost+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderPost+Helper.swift"; sourceTree = ""; }; 4AD5656E28E413160054C676 /* Blog+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blog+History.swift"; sourceTree = ""; }; @@ -15478,6 +15480,7 @@ 570B037622F1FFF6009D8411 /* PostCoordinatorFailedPostsFetcherTests.swift */, 57569CF1230485680052EE14 /* PostAutoUploadInteractorTests.swift */, 4A2C73F62A9585B000ACE79E /* PostRepositoryTests.swift */, + 4AA7EE0E2ADF7367007D261D /* PostRepositoryPostsListTests.swift */, 57D66B9C234BB78B005A2D74 /* PostServiceWPComTests.swift */, 57240223234E5BE200227067 /* PostServiceSelfHostedTests.swift */, 575802122357C41200E4C63C /* MediaCoordinatorTests.swift */, @@ -23563,6 +23566,7 @@ 3FFE3C0828FE00D10021BB96 /* StatsSegmentedControlDataTests.swift in Sources */, 015BA4EB29A788A300920F4B /* StatsTotalInsightsCellTests.swift in Sources */, D81C2F5820F86CEA002AE1F1 /* NetworkStatus.swift in Sources */, + 4AA7EE0F2ADF7367007D261D /* PostRepositoryPostsListTests.swift in Sources */, E1C545801C6C79BB001CEB0E /* MediaSettingsTests.swift in Sources */, 806BA11C2A492B0F00052422 /* BlazeCampaignDetailsWebViewModelTests.swift in Sources */, 7E987F5A2108122A00CAFB88 /* NotificationUtility.swift in Sources */, diff --git a/WordPress/WordPressTest/PostRepositoryPostsListTests.swift b/WordPress/WordPressTest/PostRepositoryPostsListTests.swift new file mode 100644 index 000000000000..504a5c6197c5 --- /dev/null +++ b/WordPress/WordPressTest/PostRepositoryPostsListTests.swift @@ -0,0 +1,191 @@ +import Foundation +import XCTest +import OHHTTPStubs + +@testable import WordPress + +class PostRepositoryPostsListTests: CoreDataTestCase { + + var repository: PostRepository! + var blogID: TaggedManagedObjectID! + + override func setUp() async throws { + repository = PostRepository(coreDataStack: contextManager) + + let loggedIn = try await signIn() + blogID = try await contextManager.performAndSave { + let blog = try BlogBuilder($0) + .with(dotComID: 42) + .withAccount(id: loggedIn) + .build() + return TaggedManagedObjectID(blog) + } + } + + override func tearDown() async throws { + HTTPStubs.removeAllStubs() + } + + func testPagination() async throws { + // Given there are 15 published posts on the site + try await preparePostsList(type: "post", total: 15) + + // When fetching all of the posts + let firstPage = try await repository.paginate(type: Post.self, statuses: [.publish], offset: 0, number: 10, in: blogID) + let secondPage = try await repository.paginate(type: Post.self, statuses: [.publish], offset: 10, number: 10, in: blogID) + + XCTAssertEqual(firstPage.count, 10) + XCTAssertEqual(secondPage.count, 5) + + // All of the posts are saved + let total = await contextManager.performQuery { $0.countObjects(ofType: Post.self) } + XCTAssertEqual(total, 15) + } + + func testSearching() async throws { + // Given there are 15 published posts on the site + try await preparePostsList(type: "post", total: 15) + + // When fetching all of the posts + let _ = try await repository.paginate(type: Post.self, statuses: [.publish], offset: 0, number: 15, in: blogID) + + // There should 15 posts saved locally before performing search + var total = await contextManager.performQuery { $0.countObjects(ofType: Post.self) } + XCTAssertEqual(total, 15) + + // Perform search + let postIDs: [TaggedManagedObjectID] = try await repository.search(input: "1", statuses: [.publish], limit: 1, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(postIDs.count, 1) + + // There should still be 15 posts after the search: no local posts should be deleted + total = await contextManager.performQuery { $0.countObjects(ofType: Post.self) } + XCTAssertEqual(total, 15) + } + +} + +// MARK: - Tests that ensure the `preparePostsList` works as expected + +extension PostRepositoryPostsListTests { + + func testPostsListStubReturnPostsAsRequested() async throws { + try await preparePostsList(type: "post", total: 20) + + var result = try await repository.search(type: Post.self, input: nil, statuses: [], limit: 10, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result.count, 10) + + result = try await repository.search(type: Post.self, input: nil, statuses: [], limit: 20, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result.count, 20) + + result = try await repository.search(type: Post.self, input: nil, statuses: [], limit: 30, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result.count, 20) + } + + func testPostsListStubReturnPostsAtCorrectPosition() async throws { + try await preparePostsList(type: "post", total: 20) + + let all = try await repository.search(type: Post.self, input: nil, statuses: [], limit: 30, orderBy: .byDate, descending: true, in: blogID) + + var result = try await repository.paginate(type: Post.self, statuses: [], offset: 0, number: 5, in: blogID) + XCTAssertEqual(result, Array(all[0..<5])) + + result = try await repository.paginate(type: Post.self, statuses: [], offset: 3, number: 2, in: blogID) + XCTAssertEqual(result, [all[3], all[4]]) + } + + func testPostsListStubReturnPostsSearch() async throws { + try await preparePostsList(type: "post", total: 10) + + let all = try await repository.search(type: Post.self, input: nil, statuses: [], limit: 30, orderBy: .byDate, descending: true, in: blogID) + + var result = try await repository.search(type: Post.self, input: "1", statuses: [], limit: 1, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result, [all[0]]) + + result = try await repository.search(type: Post.self, input: "2", statuses: [], limit: 1, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result, [all[1]]) + } + + func testPostsListStubReturnDefaultNumberOfPosts() async throws { + try await preparePostsList(type: "post", total: 100) + + let result = try await repository.search(type: Post.self, input: nil, statuses: [], limit: 0, orderBy: .byDate, descending: true, in: blogID) + XCTAssertEqual(result.count, 20) + } + +} + +private extension PostRepositoryPostsListTests { + /// This is a helper function to create HTTP stubs for fetching posts(GET /sites/%s/posts) requests. + /// + /// The returned fake posts have only basic properties. The stubs ensure post id is unique and starts from 1. + /// But it does not promise the returned posts match the request filters (like status). + /// + /// You can use the `update` closure to update the returned posts if needed. + /// + /// Here are the supported features: + /// - Pagination. The stubs simulates `total` number of posts in the site, to handle paginated request accordingly. + /// - Search, but limited. Search is based on title. All fake posts have a title like "Random Post - [post-id]", where post id starts from 1. So, search "1" returns the posts whose id has "1" in it (1, 1x, x1, and so on). + /// + /// Here are unsupported features: + /// - Order. The sorting related arguments are ignored. + /// - Filter by status. The status argument is ignored. + func preparePostsList(type: String, total: Int, update: ((inout [String: Any]) -> Void)? = nil) async throws { + let allPosts = (1...total).map { id -> [String: Any] in + [ + "ID": id, + "title": "Random Post - \(id)", + "content": "This is a test.", + "status": BasePost.Status.publish.rawValue, + "type": type + ] + } + + let siteID = try await contextManager.performQuery { try XCTUnwrap($0.existingObject(with: self.blogID).dotComID) } + stub(condition: isMethodGET() && pathMatches("/sites/\(siteID)/posts", options: [])) { request in + let queryItems = URLComponents(url: request.url!, resolvingAgainstBaseURL: true)?.queryItems ?? [] + + var result = allPosts + + if let search = queryItems.first(where: { $0.name == "search" })?.value { + result = result.filter { + let title = $0["title"] as? String + return title?.contains(search) == true + } + } + + var number = (queryItems.first { $0.name == "number" }?.value.flatMap { Int($0) }) ?? 0 + number = number == 0 ? 20 : number // The REST API uses the default value 20 when number is 0. + let offset = (queryItems.first { $0.name == "offset" }?.value.flatMap { Int($0) }) ?? 0 + let upperBound = number == 0 ? result.endIndex : max(offset, offset + number - 1) + let allowed = 0.. NSManagedObjectID { + let loggedIn = await contextManager.performQuery { + try? WPAccount.lookupDefaultWordPressComAccount(in: $0)?.objectID + } + if let loggedIn { + return loggedIn + } + + let service = AccountService(coreDataStack: contextManager) + return service.createOrUpdateAccount(withUsername: "test-user", authToken: "test-token") + } + +} From 4dac5b7e2ddffe8320a0b095f4b2435e95236385 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 19 Oct 2023 16:11:18 +1300 Subject: [PATCH 6/6] Use map instead of compactMap --- WordPress/Classes/Services/PostRepository.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/Services/PostRepository.swift b/WordPress/Classes/Services/PostRepository.swift index d716a2beb6fb..05f5cde3c7fc 100644 --- a/WordPress/Classes/Services/PostRepository.swift +++ b/WordPress/Classes/Services/PostRepository.swift @@ -418,7 +418,7 @@ extension PostRepository { purgeExisting: deleteOtherLocalPosts, in: context ) - return updatedPosts.compactMap { + return updatedPosts.map { guard let post = $0 as? P else { fatalError("Expecting a \(postType) as \(type), but got \($0)") }