Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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`.
- 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.
- 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.*`.
- 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`).
- 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.
- 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.*`.
- 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.
Expand Down
12 changes: 8 additions & 4 deletions TablePro/Core/Storage/AIKeyStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@ final class AIKeyStorage {

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

private init() {}
private let keychain: KeychainHelper

init(keychain: KeychainHelper = .shared) {
self.keychain = keychain
}

func saveAPIKey(_ apiKey: String, for providerID: UUID) {
let key = "com.TablePro.aikey.\(providerID.uuidString)"
KeychainHelper.shared.writeString(apiKey, forKey: key)
keychain.writeString(apiKey, forKey: key)
}

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

func deleteAPIKey(for providerID: UUID) {
let key = "com.TablePro.aikey.\(providerID.uuidString)"
KeychainHelper.shared.delete(forKey: key)
keychain.delete(forKey: key)
}
}
28 changes: 16 additions & 12 deletions TablePro/Core/Storage/ConnectionStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,20 @@ final class ConnectionStorage {

private let fileURL: URL

private let keychain: KeychainHelper

init(
fileURL: URL = ConnectionStorage.defaultFileURL(),
userDefaults: UserDefaults = .standard,
syncTracker: SyncChangeTracker = .shared,
appSettings: @escaping @autoclosure () -> AppSettingsStorage = .shared
appSettings: @escaping @autoclosure () -> AppSettingsStorage = .shared,
keychain: KeychainHelper = .shared
) {
self.fileURL = fileURL
self.defaults = userDefaults
self.syncTracker = syncTracker
self.appSettingsProvider = appSettings
self.keychain = keychain

migrateFromUserDefaultsIfNeeded()
}
Expand Down Expand Up @@ -289,7 +293,7 @@ final class ConnectionStorage {

func savePassword(_ password: String, for connectionId: UUID) {
let key = "com.TablePro.password.\(connectionId.uuidString)"
KeychainHelper.shared.writeString(password, forKey: key)
keychain.writeString(password, forKey: key)
}

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

func deletePassword(for connectionId: UUID) {
let key = "com.TablePro.password.\(connectionId.uuidString)"
KeychainHelper.shared.delete(forKey: key)
keychain.delete(forKey: key)
}

// MARK: - SSH Password Storage

func saveSSHPassword(_ password: String, for connectionId: UUID) {
let key = "com.TablePro.sshpassword.\(connectionId.uuidString)"
KeychainHelper.shared.writeString(password, forKey: key)
keychain.writeString(password, forKey: key)
}

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

func deleteSSHPassword(for connectionId: UUID) {
let key = "com.TablePro.sshpassword.\(connectionId.uuidString)"
KeychainHelper.shared.delete(forKey: key)
keychain.delete(forKey: key)
}

// MARK: - Key Passphrase Storage

func saveKeyPassphrase(_ passphrase: String, for connectionId: UUID) {
let key = "com.TablePro.keypassphrase.\(connectionId.uuidString)"
KeychainHelper.shared.writeString(passphrase, forKey: key)
keychain.writeString(passphrase, forKey: key)
}

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

func deleteKeyPassphrase(for connectionId: UUID) {
let key = "com.TablePro.keypassphrase.\(connectionId.uuidString)"
KeychainHelper.shared.delete(forKey: key)
keychain.delete(forKey: key)
}

// MARK: - Plugin Secure Field Storage

func savePluginSecureField(_ value: String, fieldId: String, for connectionId: UUID) {
let key = "com.TablePro.plugin.\(fieldId).\(connectionId.uuidString)"
KeychainHelper.shared.writeString(value, forKey: key)
keychain.writeString(value, forKey: key)
}

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

func deletePluginSecureField(fieldId: String, for connectionId: UUID) {
let key = "com.TablePro.plugin.\(fieldId).\(connectionId.uuidString)"
KeychainHelper.shared.delete(forKey: key)
keychain.delete(forKey: key)
}

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

func saveTOTPSecret(_ secret: String, for connectionId: UUID) {
let key = "com.TablePro.totpsecret.\(connectionId.uuidString)"
KeychainHelper.shared.writeString(secret, forKey: key)
keychain.writeString(secret, forKey: key)
}

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

func deleteTOTPSecret(for connectionId: UUID) {
let key = "com.TablePro.totpsecret.\(connectionId.uuidString)"
KeychainHelper.shared.delete(forKey: key)
keychain.delete(forKey: key)
}

private struct SecretContext {
Expand All @@ -384,7 +388,7 @@ final class ConnectionStorage {
private func resolveString(_ context: SecretContext, forKey key: String) -> String? {
let label = context.label
let connId = context.connectionId.uuidString
switch KeychainHelper.shared.readStringResult(forKey: key) {
switch keychain.readStringResult(forKey: key) {
case .found(let value):
return value
case .notFound:
Expand Down
11 changes: 7 additions & 4 deletions TablePro/Core/Storage/LicenseStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,25 @@ final class LicenseStorage {
private static let logger = Logger(subsystem: "com.TablePro", category: "LicenseStorage")

private let defaults = UserDefaults.standard
private let keychain: KeychainHelper

private enum Keys {
static let keychainLicenseKey = "com.TablePro.license.key"
static let licensePayload = "com.TablePro.license.payload"
}

private init() {}
init(keychain: KeychainHelper = .shared) {
self.keychain = keychain
}

// MARK: - License Key (Keychain)

func saveLicenseKey(_ key: String) {
KeychainHelper.shared.writeString(key, forKey: Keys.keychainLicenseKey)
keychain.writeString(key, forKey: Keys.keychainLicenseKey)
}

func loadLicenseKey() -> String? {
switch KeychainHelper.shared.readStringResult(forKey: Keys.keychainLicenseKey) {
switch keychain.readStringResult(forKey: Keys.keychainLicenseKey) {
case .found(let value):
return value
case .notFound:
Expand All @@ -52,7 +55,7 @@ final class LicenseStorage {
}

func deleteLicenseKey() {
KeychainHelper.shared.delete(forKey: Keys.keychainLicenseKey)
keychain.delete(forKey: Keys.keychainLicenseKey)
}

// MARK: - Signed Payload (UserDefaults)
Expand Down
19 changes: 11 additions & 8 deletions TablePro/Core/Storage/SSHProfileStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ final class SSHProfileStorage {
private let defaults = UserDefaults.standard
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
private let keychain: KeychainHelper
private(set) var lastLoadFailed = false

private init() {}
init(keychain: KeychainHelper = .shared) {
self.keychain = keychain
}

// MARK: - Profile CRUD

Expand Down Expand Up @@ -97,7 +100,7 @@ final class SSHProfileStorage {

func saveSSHPassword(_ password: String, for profileId: UUID) {
let key = "com.TablePro.sshprofile.password.\(profileId.uuidString)"
KeychainHelper.shared.writeString(password, forKey: key)
keychain.writeString(password, forKey: key)
}

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

func deleteSSHPassword(for profileId: UUID) {
let key = "com.TablePro.sshprofile.password.\(profileId.uuidString)"
KeychainHelper.shared.delete(forKey: key)
keychain.delete(forKey: key)
}

// MARK: - Key Passphrase Storage

func saveKeyPassphrase(_ passphrase: String, for profileId: UUID) {
let key = "com.TablePro.sshprofile.keypassphrase.\(profileId.uuidString)"
KeychainHelper.shared.writeString(passphrase, forKey: key)
keychain.writeString(passphrase, forKey: key)
}

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

func deleteKeyPassphrase(for profileId: UUID) {
let key = "com.TablePro.sshprofile.keypassphrase.\(profileId.uuidString)"
KeychainHelper.shared.delete(forKey: key)
keychain.delete(forKey: key)
}

// MARK: - TOTP Secret Storage

func saveTOTPSecret(_ secret: String, for profileId: UUID) {
let key = "com.TablePro.sshprofile.totpsecret.\(profileId.uuidString)"
KeychainHelper.shared.writeString(secret, forKey: key)
keychain.writeString(secret, forKey: key)
}

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

func deleteTOTPSecret(for profileId: UUID) {
let key = "com.TablePro.sshprofile.totpsecret.\(profileId.uuidString)"
KeychainHelper.shared.delete(forKey: key)
keychain.delete(forKey: key)
}

private func resolveString(label: String, profileId: UUID, forKey key: String) -> String? {
let pid = profileId.uuidString
switch KeychainHelper.shared.readStringResult(forKey: key) {
switch keychain.readStringResult(forKey: key) {
case .found(let value):
return value
case .notFound:
Expand Down
Loading