Skip to content

Commit cd45b08

Browse files
authored
refactor(storage): inject KeychainHelper into storage classes via init (#1153)
Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
1 parent 719f005 commit cd45b08

5 files changed

Lines changed: 43 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333
- Internal: drop four 1-to-1 PassthroughSubject buses (`saveAsFavoriteRequested`, `focusConnectionFormWindowRequested`, `openSampleDatabaseRequested`, `resetSampleDatabaseRequested`) for direct ownership. The favorite-edit dialog query is now observable state on `MainContentCoordinator`, the connection-form focus lookup is inlined in `WelcomeViewModel`, and the sample-database menu items call `SampleDatabaseLauncher` directly. `AppDelegate` no longer needs `commandCancellables`.
3434
- Internal: tighten dependency injection in the four files that already accept `services: AppServices`. `ConnectionFormCoordinator`, `MainContentCoordinator`, `WelcomeViewModel`, and `ERDiagramViewModel` now read `services.appEvents.*` instead of mixing in raw `AppEvents.shared.*` for events they were already supposed to thread through the container.
3535
- Internal: extend `AppServices` with `appSettingsStorage`, `schemaProviderRegistry`, `aiKeyStorage`, `groupStorage`, `favoritesExpansionState`, and `linkedFolderWatcher`. Convert the raw `.shared` reads of those types in `AIChatViewModel`, `FavoritesSidebarViewModel`, `WelcomeViewModel`, and `MainContentCoordinator` to `services.*`.
36+
- Internal: thread `KeychainHelper` through `ConnectionStorage`, `SSHProfileStorage`, `AIKeyStorage`, and `LicenseStorage` via init (default `.shared`). The 24 raw `KeychainHelper.shared` reads inside those classes are gone, matching the existing injected-dependency pattern (`syncTracker`, `appSettings`).
3637
- Internal: extend `AppServices` with `queryHistoryManager`, `dateFormattingService`, `copilotService`, and `mcpServerManager`. `AppSettingsManager` now takes its eight cross-singleton dependencies (`AppSettingsStorage`, `ThemeEngine`, `SyncChangeTracker`, `AppEvents`, `DateFormattingService`, `QueryHistoryManager`, `MCPServerManager`, `CopilotService`) via init with `.shared` defaults. The 32 raw `.shared` reads inside `AppSettingsManager` are gone, including the four `Task { ... }` capture closures that previously dialed `CopilotService.shared` / `MCPServerManager.shared` mid-`didSet`. The 49 caller-side `AppSettingsManager.shared` reads are unchanged in this PR; they will be threaded as their owners adopt `services` in subsequent waves.
3738
- Internal: extend `AppServices` with `tagStorage`, `sshProfileStorage`, `licenseManager`, `conflictResolver`, and `syncMetadataStorage`. `SyncCoordinator` now takes `services: AppServices` in init (default `.live`); 34 raw `.shared` reads of those types inside `SyncCoordinator` are routed through `services.*`.
3839
- Internal: Redis sidebar key tree uses SwiftUI `OutlineGroup` instead of recursive `DisclosureGroup` + `ForEach` wrapped in `AnyView`. Expansion state is now managed natively per branch identifier; the explicit `expandedPrefixes` set is gone.

TablePro/Core/Storage/AIKeyStorage.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,21 @@ final class AIKeyStorage {
1414

1515
private static let logger = Logger(subsystem: "com.TablePro", category: "AIKeyStorage")
1616

17-
private init() {}
17+
private let keychain: KeychainHelper
18+
19+
init(keychain: KeychainHelper = .shared) {
20+
self.keychain = keychain
21+
}
1822

1923
func saveAPIKey(_ apiKey: String, for providerID: UUID) {
2024
let key = "com.TablePro.aikey.\(providerID.uuidString)"
21-
KeychainHelper.shared.writeString(apiKey, forKey: key)
25+
keychain.writeString(apiKey, forKey: key)
2226
}
2327

2428
func loadAPIKey(for providerID: UUID) -> String? {
2529
let key = "com.TablePro.aikey.\(providerID.uuidString)"
2630
let pid = providerID.uuidString
27-
switch KeychainHelper.shared.readStringResult(forKey: key) {
31+
switch keychain.readStringResult(forKey: key) {
2832
case .found(let value):
2933
return value
3034
case .notFound:
@@ -46,6 +50,6 @@ final class AIKeyStorage {
4650

4751
func deleteAPIKey(for providerID: UUID) {
4852
let key = "com.TablePro.aikey.\(providerID.uuidString)"
49-
KeychainHelper.shared.delete(forKey: key)
53+
keychain.delete(forKey: key)
5054
}
5155
}

TablePro/Core/Storage/ConnectionStorage.swift

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,20 @@ final class ConnectionStorage {
2828

2929
private let fileURL: URL
3030

31+
private let keychain: KeychainHelper
32+
3133
init(
3234
fileURL: URL = ConnectionStorage.defaultFileURL(),
3335
userDefaults: UserDefaults = .standard,
3436
syncTracker: SyncChangeTracker = .shared,
35-
appSettings: @escaping @autoclosure () -> AppSettingsStorage = .shared
37+
appSettings: @escaping @autoclosure () -> AppSettingsStorage = .shared,
38+
keychain: KeychainHelper = .shared
3639
) {
3740
self.fileURL = fileURL
3841
self.defaults = userDefaults
3942
self.syncTracker = syncTracker
4043
self.appSettingsProvider = appSettings
44+
self.keychain = keychain
4145

4246
migrateFromUserDefaultsIfNeeded()
4347
}
@@ -289,7 +293,7 @@ final class ConnectionStorage {
289293

290294
func savePassword(_ password: String, for connectionId: UUID) {
291295
let key = "com.TablePro.password.\(connectionId.uuidString)"
292-
KeychainHelper.shared.writeString(password, forKey: key)
296+
keychain.writeString(password, forKey: key)
293297
}
294298

295299
func loadPassword(for connectionId: UUID) -> String? {
@@ -299,14 +303,14 @@ final class ConnectionStorage {
299303

300304
func deletePassword(for connectionId: UUID) {
301305
let key = "com.TablePro.password.\(connectionId.uuidString)"
302-
KeychainHelper.shared.delete(forKey: key)
306+
keychain.delete(forKey: key)
303307
}
304308

305309
// MARK: - SSH Password Storage
306310

307311
func saveSSHPassword(_ password: String, for connectionId: UUID) {
308312
let key = "com.TablePro.sshpassword.\(connectionId.uuidString)"
309-
KeychainHelper.shared.writeString(password, forKey: key)
313+
keychain.writeString(password, forKey: key)
310314
}
311315

312316
func loadSSHPassword(for connectionId: UUID) -> String? {
@@ -316,14 +320,14 @@ final class ConnectionStorage {
316320

317321
func deleteSSHPassword(for connectionId: UUID) {
318322
let key = "com.TablePro.sshpassword.\(connectionId.uuidString)"
319-
KeychainHelper.shared.delete(forKey: key)
323+
keychain.delete(forKey: key)
320324
}
321325

322326
// MARK: - Key Passphrase Storage
323327

324328
func saveKeyPassphrase(_ passphrase: String, for connectionId: UUID) {
325329
let key = "com.TablePro.keypassphrase.\(connectionId.uuidString)"
326-
KeychainHelper.shared.writeString(passphrase, forKey: key)
330+
keychain.writeString(passphrase, forKey: key)
327331
}
328332

329333
func loadKeyPassphrase(for connectionId: UUID) -> String? {
@@ -333,14 +337,14 @@ final class ConnectionStorage {
333337

334338
func deleteKeyPassphrase(for connectionId: UUID) {
335339
let key = "com.TablePro.keypassphrase.\(connectionId.uuidString)"
336-
KeychainHelper.shared.delete(forKey: key)
340+
keychain.delete(forKey: key)
337341
}
338342

339343
// MARK: - Plugin Secure Field Storage
340344

341345
func savePluginSecureField(_ value: String, fieldId: String, for connectionId: UUID) {
342346
let key = "com.TablePro.plugin.\(fieldId).\(connectionId.uuidString)"
343-
KeychainHelper.shared.writeString(value, forKey: key)
347+
keychain.writeString(value, forKey: key)
344348
}
345349

346350
func loadPluginSecureField(fieldId: String, for connectionId: UUID) -> String? {
@@ -350,7 +354,7 @@ final class ConnectionStorage {
350354

351355
func deletePluginSecureField(fieldId: String, for connectionId: UUID) {
352356
let key = "com.TablePro.plugin.\(fieldId).\(connectionId.uuidString)"
353-
KeychainHelper.shared.delete(forKey: key)
357+
keychain.delete(forKey: key)
354358
}
355359

356360
func deleteAllPluginSecureFields(for connectionId: UUID, fieldIds: [String]) {
@@ -363,7 +367,7 @@ final class ConnectionStorage {
363367

364368
func saveTOTPSecret(_ secret: String, for connectionId: UUID) {
365369
let key = "com.TablePro.totpsecret.\(connectionId.uuidString)"
366-
KeychainHelper.shared.writeString(secret, forKey: key)
370+
keychain.writeString(secret, forKey: key)
367371
}
368372

369373
func loadTOTPSecret(for connectionId: UUID) -> String? {
@@ -373,7 +377,7 @@ final class ConnectionStorage {
373377

374378
func deleteTOTPSecret(for connectionId: UUID) {
375379
let key = "com.TablePro.totpsecret.\(connectionId.uuidString)"
376-
KeychainHelper.shared.delete(forKey: key)
380+
keychain.delete(forKey: key)
377381
}
378382

379383
private struct SecretContext {
@@ -384,7 +388,7 @@ final class ConnectionStorage {
384388
private func resolveString(_ context: SecretContext, forKey key: String) -> String? {
385389
let label = context.label
386390
let connId = context.connectionId.uuidString
387-
switch KeychainHelper.shared.readStringResult(forKey: key) {
391+
switch keychain.readStringResult(forKey: key) {
388392
case .found(let value):
389393
return value
390394
case .notFound:

TablePro/Core/Storage/LicenseStorage.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,25 @@ final class LicenseStorage {
1616
private static let logger = Logger(subsystem: "com.TablePro", category: "LicenseStorage")
1717

1818
private let defaults = UserDefaults.standard
19+
private let keychain: KeychainHelper
1920

2021
private enum Keys {
2122
static let keychainLicenseKey = "com.TablePro.license.key"
2223
static let licensePayload = "com.TablePro.license.payload"
2324
}
2425

25-
private init() {}
26+
init(keychain: KeychainHelper = .shared) {
27+
self.keychain = keychain
28+
}
2629

2730
// MARK: - License Key (Keychain)
2831

2932
func saveLicenseKey(_ key: String) {
30-
KeychainHelper.shared.writeString(key, forKey: Keys.keychainLicenseKey)
33+
keychain.writeString(key, forKey: Keys.keychainLicenseKey)
3134
}
3235

3336
func loadLicenseKey() -> String? {
34-
switch KeychainHelper.shared.readStringResult(forKey: Keys.keychainLicenseKey) {
37+
switch keychain.readStringResult(forKey: Keys.keychainLicenseKey) {
3538
case .found(let value):
3639
return value
3740
case .notFound:
@@ -52,7 +55,7 @@ final class LicenseStorage {
5255
}
5356

5457
func deleteLicenseKey() {
55-
KeychainHelper.shared.delete(forKey: Keys.keychainLicenseKey)
58+
keychain.delete(forKey: Keys.keychainLicenseKey)
5659
}
5760

5861
// MARK: - Signed Payload (UserDefaults)

TablePro/Core/Storage/SSHProfileStorage.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ final class SSHProfileStorage {
1414
private let defaults = UserDefaults.standard
1515
private let encoder = JSONEncoder()
1616
private let decoder = JSONDecoder()
17+
private let keychain: KeychainHelper
1718
private(set) var lastLoadFailed = false
1819

19-
private init() {}
20+
init(keychain: KeychainHelper = .shared) {
21+
self.keychain = keychain
22+
}
2023

2124
// MARK: - Profile CRUD
2225

@@ -97,7 +100,7 @@ final class SSHProfileStorage {
97100

98101
func saveSSHPassword(_ password: String, for profileId: UUID) {
99102
let key = "com.TablePro.sshprofile.password.\(profileId.uuidString)"
100-
KeychainHelper.shared.writeString(password, forKey: key)
103+
keychain.writeString(password, forKey: key)
101104
}
102105

103106
func loadSSHPassword(for profileId: UUID) -> String? {
@@ -107,14 +110,14 @@ final class SSHProfileStorage {
107110

108111
func deleteSSHPassword(for profileId: UUID) {
109112
let key = "com.TablePro.sshprofile.password.\(profileId.uuidString)"
110-
KeychainHelper.shared.delete(forKey: key)
113+
keychain.delete(forKey: key)
111114
}
112115

113116
// MARK: - Key Passphrase Storage
114117

115118
func saveKeyPassphrase(_ passphrase: String, for profileId: UUID) {
116119
let key = "com.TablePro.sshprofile.keypassphrase.\(profileId.uuidString)"
117-
KeychainHelper.shared.writeString(passphrase, forKey: key)
120+
keychain.writeString(passphrase, forKey: key)
118121
}
119122

120123
func loadKeyPassphrase(for profileId: UUID) -> String? {
@@ -124,14 +127,14 @@ final class SSHProfileStorage {
124127

125128
func deleteKeyPassphrase(for profileId: UUID) {
126129
let key = "com.TablePro.sshprofile.keypassphrase.\(profileId.uuidString)"
127-
KeychainHelper.shared.delete(forKey: key)
130+
keychain.delete(forKey: key)
128131
}
129132

130133
// MARK: - TOTP Secret Storage
131134

132135
func saveTOTPSecret(_ secret: String, for profileId: UUID) {
133136
let key = "com.TablePro.sshprofile.totpsecret.\(profileId.uuidString)"
134-
KeychainHelper.shared.writeString(secret, forKey: key)
137+
keychain.writeString(secret, forKey: key)
135138
}
136139

137140
func loadTOTPSecret(for profileId: UUID) -> String? {
@@ -141,12 +144,12 @@ final class SSHProfileStorage {
141144

142145
func deleteTOTPSecret(for profileId: UUID) {
143146
let key = "com.TablePro.sshprofile.totpsecret.\(profileId.uuidString)"
144-
KeychainHelper.shared.delete(forKey: key)
147+
keychain.delete(forKey: key)
145148
}
146149

147150
private func resolveString(label: String, profileId: UUID, forKey key: String) -> String? {
148151
let pid = profileId.uuidString
149-
switch KeychainHelper.shared.readStringResult(forKey: key) {
152+
switch keychain.readStringResult(forKey: key) {
150153
case .found(let value):
151154
return value
152155
case .notFound:

0 commit comments

Comments
 (0)