-
Notifications
You must be signed in to change notification settings - Fork 165
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PageBookamrks – New Synced DB (#683)
* Create GRDBPAgeBookmarkPersistenceTests * Fix typo in GRDBPAgeBookmarkPersistence's name * Copy test logic from CoreData to GRDB for bookamrks * Add a readPublisher function to DatabaseConnection * Implement GRDBPageBookmarkPersistence * Add some documentation to DatabaseConnection * Test readPublisher of DatabaseConnection * Remove GRDBPageBookmarkPersistence's conformance to PAgeBookPErsistence * Move GRDBSyncedPageBookmarkPersistence into a separate package * Create SyncedPageBookmarkPersistence as the return type of SyncedPageBookmarkPersistence package * Wrap GRDBSyncedPageBookmarkPersistence behind a SyncedPageBookmarkPersistence protocol * Test SyncedPageBookmarkPersistence * Linting * Linting SQLitePersistence * Guard against removing with an empty remote ID * Clear unneeded modifications in Package.swift * Edit SyncedPageBookmarkPersistence's interface for consistency * Rename another part of the SyncedPageBookmarkPersistence's name * Add a unique constraint on page column for the synced table * Make SyncedPageBookmarkPersistenceModel's types public * Add a function to fetch bookmark for a given page * Increase timeouts for publisher tests in DatabaseConnectionTests * Linting
- Loading branch information
1 parent
313e3a1
commit 852b9cc
Showing
7 changed files
with
334 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
Data/SyncedPageBookmarkPersistence/Sources/GRDBSyncedPageBookmarkPersistence.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
// | ||
// GRDBSyncedPageBookmarkPersistence.swift | ||
// QuranEngine | ||
// | ||
// Created by Mohannad Hassan on 31/01/2025. | ||
// | ||
|
||
import Combine | ||
import Foundation | ||
import GRDB | ||
import SQLitePersistence | ||
import VLogging | ||
|
||
public struct GRDBSyncedPageBookmarkPersistence: SyncedPageBookmarkPersistence { | ||
// MARK: Lifecycle | ||
|
||
init(db: DatabaseConnection) { | ||
self.db = db | ||
do { | ||
try migrator.migrate(db) | ||
} catch { | ||
logger.error("Failed to to do Page Bookmarks migration: \(error)") | ||
} | ||
} | ||
|
||
public init(directory: URL) { | ||
let fileURL = directory.appendingPathComponent("pagebookmarks.db", isDirectory: false) | ||
self.init(db: DatabaseConnection(url: fileURL, readonly: false)) | ||
} | ||
|
||
// MARK: Public | ||
|
||
public func pageBookmarksPublisher() throws -> AnyPublisher<[SyncedPageBookmarkPersistenceModel], Never> { | ||
do { | ||
return try db.readPublisher { db in | ||
try GRDBSyncedPageBookmark.fetchAll(db).map { $0.toPersistenceModel() } | ||
} | ||
.catch { error in | ||
logger.error("Error in page bookmarks publisher: \(error)") | ||
return Empty<[SyncedPageBookmarkPersistenceModel], Never>() | ||
} | ||
.eraseToAnyPublisher() | ||
} catch { | ||
logger.error("Failed to create a publisher for page bookmarks: \(error)") | ||
return Empty<[SyncedPageBookmarkPersistenceModel], Never>().eraseToAnyPublisher() | ||
} | ||
} | ||
|
||
public func bookmark(page: Int) async throws -> SyncedPageBookmarkPersistenceModel? { | ||
try await db.read { db in | ||
try GRDBSyncedPageBookmark.fetchOne( | ||
db.makeStatement(sql: "SELECT * FROM \(GRDBSyncedPageBookmark.databaseTableName) WHERE page = ?"), | ||
arguments: [page] | ||
) | ||
.map { $0.toPersistenceModel() } | ||
} | ||
} | ||
|
||
public func insertBookmark(_ bookmark: SyncedPageBookmarkPersistenceModel) async throws { | ||
try await db.write { db in | ||
var bookmark = GRDBSyncedPageBookmark(bookmark) | ||
try bookmark.insert(db) | ||
} | ||
} | ||
|
||
public func removeBookmark(withRemoteID remoteID: String) async throws { | ||
guard !remoteID.isEmpty else { | ||
logger.critical("[SyncedPageBookmarkPersistence] Attempted to remove a bookmark with an empty remote ID.") | ||
fatalError() | ||
} | ||
try await db.write { db in | ||
try db.execute(sql: "DELETE FROM \(GRDBSyncedPageBookmark.databaseTableName) WHERE remote_id = ?", arguments: [remoteID]) | ||
} | ||
} | ||
|
||
// MARK: Private | ||
|
||
private let db: DatabaseConnection | ||
|
||
private var migrator: DatabaseMigrator { | ||
var migrator = DatabaseMigrator() | ||
migrator.registerMigration("createPageBookmarks") { db in | ||
try db.create(table: GRDBSyncedPageBookmark.databaseTableName, options: .ifNotExists) { table in | ||
table.column("page", .integer).notNull().unique() | ||
table.column("remote_id", .text).primaryKey() | ||
table.column("creation_date", .datetime).notNull() | ||
} | ||
} | ||
return migrator | ||
} | ||
} | ||
|
||
private struct GRDBSyncedPageBookmark: Identifiable, Codable, FetchableRecord, MutablePersistableRecord { | ||
enum CodingKeys: String, CodingKey { | ||
case page | ||
case creationDate = "creation_date" | ||
case remoteID = "remote_id" | ||
} | ||
|
||
static var databaseTableName: String { | ||
"synced_page_bookmarks" | ||
} | ||
|
||
var page: Int | ||
var creationDate: Date | ||
var remoteID: String | ||
|
||
var id: Int { | ||
page | ||
} | ||
} | ||
|
||
extension GRDBSyncedPageBookmark { | ||
init(_ bookmark: SyncedPageBookmarkPersistenceModel) { | ||
page = bookmark.page | ||
creationDate = bookmark.creationDate | ||
remoteID = bookmark.remoteID | ||
} | ||
|
||
func toPersistenceModel() -> SyncedPageBookmarkPersistenceModel { | ||
.init(page: page, remoteID: remoteID, creationDate: creationDate) | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
Data/SyncedPageBookmarkPersistence/Sources/SyncedPageBookmarkPersistence.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// | ||
// SyncedPageBookmarkPersistence.swift | ||
// QuranEngine | ||
// | ||
// Created by Mohannad Hassan on 09/02/2025. | ||
// | ||
|
||
import Combine | ||
import Foundation | ||
|
||
public protocol SyncedPageBookmarkPersistence { | ||
func pageBookmarksPublisher() throws -> AnyPublisher<[SyncedPageBookmarkPersistenceModel], Never> | ||
func bookmark(page: Int) async throws -> SyncedPageBookmarkPersistenceModel? | ||
func insertBookmark(_ bookmark: SyncedPageBookmarkPersistenceModel) async throws | ||
func removeBookmark(withRemoteID remoteID: String) async throws | ||
} |
20 changes: 20 additions & 0 deletions
20
Data/SyncedPageBookmarkPersistence/Sources/SyncedPageBookmarkPersistenceModel.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// | ||
// SyncedPageBookmarkPersistenceModel.swift | ||
// QuranEngine | ||
// | ||
// Created by Mohannad Hassan on 09/02/2025. | ||
// | ||
|
||
import Foundation | ||
|
||
public struct SyncedPageBookmarkPersistenceModel { | ||
public let page: Int | ||
public let remoteID: String | ||
public let creationDate: Date | ||
|
||
public init(page: Int, remoteID: String, creationDate: Date) { | ||
self.page = page | ||
self.remoteID = remoteID | ||
self.creationDate = creationDate | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
Data/SyncedPageBookmarkPersistence/Tests/GRDBSyncedPageBookmarkPersistenceTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
// | ||
// GRDBSyncedPageBookmarkPersistenceTests.swift | ||
// QuranEngine | ||
// | ||
// Created by Mohannad Hassan on 01/02/2025. | ||
// | ||
|
||
import AsyncUtilitiesForTesting | ||
import SQLitePersistence | ||
import XCTest | ||
@testable import SyncedPageBookmarkPersistence | ||
|
||
final class GRDBSyncedPageBookmarkPersistenceTests: XCTestCase { | ||
private var testURL: URL! | ||
private var db: DatabaseConnection! | ||
private var persistence: GRDBSyncedPageBookmarkPersistence! | ||
|
||
override func setUp() { | ||
super.setUp() | ||
|
||
testURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) | ||
db = DatabaseConnection(url: testURL, readonly: false) | ||
persistence = GRDBSyncedPageBookmarkPersistence(db: db) | ||
} | ||
|
||
override func tearDown() { | ||
try? FileManager.default.removeItem(at: testURL) | ||
super.tearDown() | ||
} | ||
|
||
func testInsertion() async throws { | ||
let pages = [1, 2, 300] | ||
|
||
let expectedBookmarkedPages = [1, 2, 300] | ||
|
||
let exp = expectation(description: "Expected to send the expected bookmarks") | ||
let cancellable = try persistence.pageBookmarksPublisher() | ||
.sink { bookmarks in | ||
let pages = bookmarks.map(\.page) | ||
guard Set(pages) == Set(expectedBookmarkedPages) else { | ||
return | ||
} | ||
exp.fulfill() | ||
} | ||
|
||
for page in pages { | ||
try await persistence.insertBookmark(SyncedPageBookmarkPersistenceModel(page: page)) | ||
} | ||
await fulfillment(of: [exp], timeout: 1) | ||
cancellable.cancel() | ||
} | ||
|
||
func testDeletion() async throws { | ||
let pageNos = [1, 2, 300] | ||
let pages = pageNos.map(SyncedPageBookmarkPersistenceModel.init(page:)) | ||
for page in pages { | ||
try await persistence.insertBookmark(page) | ||
} | ||
|
||
let expectedPageNumbers = [1, 300] | ||
let exp = expectation(description: "Expected to send the expected bookmarks without the deleted one") | ||
let cancellable = try persistence.pageBookmarksPublisher() | ||
.sink { bookmarks in | ||
let pages = bookmarks.map(\.page) | ||
guard Set(pages) == Set(expectedPageNumbers) else { | ||
return | ||
} | ||
exp.fulfill() | ||
} | ||
|
||
try await persistence.removeBookmark(withRemoteID: pages[1].remoteID) | ||
|
||
await fulfillment(of: [exp], timeout: 1) | ||
cancellable.cancel() | ||
} | ||
} | ||
|
||
private extension SyncedPageBookmarkPersistenceModel { | ||
init(page: Int) { | ||
self.init(page: page, remoteID: UUID().uuidString, creationDate: .distantPast) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters