diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift index 19fa42483..52f8a72a0 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift @@ -8,7 +8,98 @@ struct InternalLinkedItemQueryResult { let cursor: String? } +struct InternalLinkedItemUpdatesQueryResult { + let items: [InternalLinkedItem] + let deletedItemIDs: [String] + let cursor: String? +} + +private struct SyncItemEdge { + let itemID: String + let isDeletedItem: Bool + let item: InternalLinkedItem? +} + extension DataService { + // swiftlint:disable:next function_body_length + func linkedItemUpdates( + limit: Int, + cursor: String?, + since: Date? + ) async throws -> InternalLinkedItemUpdatesQueryResult { + struct QuerySuccessResult { + let edges: [SyncItemEdge] + let cursor: String? + } + enum QueryResult { + case success(result: QuerySuccessResult) + case error(error: String) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + let selection = Selection { + try $0.on( + updatesSinceError: .init { + QueryResult.error(error: try $0.errorCodes().description) + }, + updatesSinceSuccess: .init { + QueryResult.success( + result: QuerySuccessResult( + edges: try $0.edges(selection: syncItemEdgeSelection.list), + cursor: try $0.pageInfo(selection: Selection.PageInfo { + try $0.endCursor() + }) + ) + ) + } + ) + } + + let query = Selection.Query { + try $0.updatesSince( + after: OptionalArgument(cursor), + first: OptionalArgument(limit), + since: DateTime(from: since ?? Date(timeIntervalSinceReferenceDate: 0)), + selection: selection + ) + } + + return try await withCheckedThrowingContinuation { continuation in + send(query, to: path, headers: headers) { queryResult in + guard let payload = try? queryResult.get() else { + continuation.resume(throwing: ContentFetchError.network) + return + } + + switch payload.data { + case let .success(result: result): + var items = [InternalLinkedItem]() + var deletedItemIDs = [String]() + + for edge in result.edges { + if edge.isDeletedItem { + deletedItemIDs.append(edge.itemID) + } else if let item = edge.item { + items.append(item) + } + } + + continuation.resume( + returning: InternalLinkedItemUpdatesQueryResult( + items: items, + deletedItemIDs: deletedItemIDs, + cursor: result.cursor + ) + ) + case let .error(error): + continuation.resume(throwing: ContentFetchError.unknown(description: error.description)) + } + } + } + } + /// Performs GraphQL request to fetch `InternalLinkedItem`s and a cursor value /// - Parameters: /// - limit: max number of items to return @@ -152,6 +243,14 @@ private let libraryArticleSelection = Selection.Article { ) } +private let syncItemEdgeSelection = Selection.SyncUpdatedItemEdge { + SyncItemEdge( + itemID: try $0.itemId(), + isDeletedItem: try $0.updateReason() == .deleted, + item: try $0.node(selection: searchItemSelection.nullable) + ) +} + private let searchItemSelection = Selection.SearchItem { InternalLinkedItem( id: try $0.id(),