Skip to content
Merged
4 changes: 3 additions & 1 deletion TablePro/Core/Services/AppServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct AppServices {
let aiChatStorage: AIChatStorage
let syncTracker: SyncChangeTracker
let themeEngine: ThemeEngine
let feedbackAPIClient: FeedbackAPIClient

static let live = AppServices(
appEvents: .shared,
Expand All @@ -30,7 +31,8 @@ struct AppServices {
sqlFavoriteManager: .shared,
aiChatStorage: .shared,
syncTracker: .shared,
themeEngine: .shared
themeEngine: .shared,
feedbackAPIClient: .shared
)
}

Expand Down
20 changes: 10 additions & 10 deletions TablePro/ViewModels/AIChatViewModel+SchemaContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ extension AIChatViewModel {
return
}
guard let connection,
let driver = DatabaseManager.shared.driver(for: connection.id) else { return }
let driver = services.databaseManager.driver(for: connection.id) else { return }
let task: Task<Void, Never> = Task { [weak self] in
let columns: [ColumnInfo]
do {
Expand Down Expand Up @@ -73,7 +73,7 @@ extension AIChatViewModel {

func ensureSavedQueryLoaded(id: UUID) async {
if cachedSavedQueries[id] != nil { return }
if let favorite = await SQLFavoriteManager.shared.fetchFavorite(id: id) {
if let favorite = await services.sqlFavoriteManager.fetchFavorite(id: id) {
cachedSavedQueries[id] = favorite
}
}
Expand All @@ -93,8 +93,8 @@ extension AIChatViewModel {

private func runSchemaLoad() async {
guard let connection,
let driver = DatabaseManager.shared.driver(for: connection.id) else { return }
let settings = AppSettingsManager.shared.ai
let driver = services.databaseManager.driver(for: connection.id) else { return }
let settings = services.appSettings.ai
let tablesToFetch = Array(tables.prefix(settings.maxSchemaTables))
guard !tablesToFetch.isEmpty else { return }

Expand Down Expand Up @@ -134,16 +134,16 @@ extension AIChatViewModel {
guard let connection else { return nil }
return PromptContext(
databaseType: connection.type,
databaseName: DatabaseManager.shared.activeDatabaseName(for: connection),
databaseName: services.databaseManager.activeDatabaseName(for: connection),
tables: tables,
columnsByTable: columnsByTable,
foreignKeys: foreignKeysByTable,
currentQuery: settings.includeCurrentQuery ? currentQuery : nil,
queryResults: settings.includeQueryResults ? queryResults : nil,
settings: settings,
identifierQuote: PluginManager.shared.sqlDialect(for: connection.type)?.identifierQuote ?? "\"",
editorLanguage: PluginManager.shared.editorLanguage(for: connection.type),
queryLanguageName: PluginManager.shared.queryLanguageName(for: connection.type),
identifierQuote: services.pluginManager.sqlDialect(for: connection.type)?.identifierQuote ?? "\"",
editorLanguage: services.pluginManager.editorLanguage(for: connection.type),
queryLanguageName: services.pluginManager.queryLanguageName(for: connection.type),
connectionRules: connection.aiRules
)
}
Expand All @@ -163,9 +163,9 @@ extension AIChatViewModel {

func renderedSchemaSection() -> String? {
guard !tables.isEmpty else { return nil }
let settings = AppSettingsManager.shared.ai
let settings = services.appSettings.ai
let identifierQuote = connection.flatMap {
PluginManager.shared.sqlDialect(for: $0.type)?.identifierQuote
services.pluginManager.sqlDialect(for: $0.type)?.identifierQuote
} ?? "\""
let section = AISchemaContext.buildSchemaSection(
tables: tables,
Expand Down
2 changes: 1 addition & 1 deletion TablePro/ViewModels/AIChatViewModel+SlashCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ extension AIChatViewModel {
let renderingContext = CustomSlashCommandRenderer.Context(
query: currentQuery,
schema: needsSchema ? renderedSchemaSection() : nil,
database: connection.flatMap { DatabaseManager.shared.activeDatabaseName(for: $0) },
database: connection.flatMap { services.databaseManager.activeDatabaseName(for: $0) },
body: body
)
let prompt = CustomSlashCommandRenderer.render(command, context: renderingContext)
Expand Down
2 changes: 1 addition & 1 deletion TablePro/ViewModels/AIChatViewModel+Streaming.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ extension AIChatViewModel {
func startStreaming() {
guard case .idle = streamingState else { return }

let settings = AppSettingsManager.shared.ai
let settings = services.appSettings.ai

let resolved = AIProviderFactory.resolve(
settings: settings,
Expand Down
2 changes: 1 addition & 1 deletion TablePro/ViewModels/AIChatViewModel+ToolApproval.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ extension AIChatViewModel {
guard !current.aiAlwaysAllowedTools.contains(toolName) else { return }
current.aiAlwaysAllowedTools.insert(toolName)
connection = current
ConnectionStorage.shared.updateConnection(current)
services.connectionStorage.updateConnection(current)
}

func dispatchCopilotInvocation(
Expand Down
12 changes: 7 additions & 5 deletions TablePro/ViewModels/AIChatViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ final class AIChatViewModel {

var tables: [TableInfo] {
guard let id = connection?.id else { return [] }
return SchemaService.shared.tables(for: id)
return services.schemaService.tables(for: id)
}

var columnsByTable: [String: [ColumnInfo]] = [:]
Expand Down Expand Up @@ -74,13 +74,15 @@ final class AIChatViewModel {
@ObservationIgnored nonisolated(unsafe) var streamingTask: Task<Void, Never>?
@ObservationIgnored var prepTask: Task<Void, Never>?

let chatStorage = AIChatStorage.shared
@ObservationIgnored let services: AppServices
var chatStorage: AIChatStorage { services.aiChatStorage }
var sessionApprovedConnections: Set<UUID> = []
@ObservationIgnored var cachedSavedQueries: [UUID: SQLFavorite] = [:]

static let maxMessageCount = 200

init() {
init(services: AppServices = .live) {
self.services = services
loadConversations()
}

Expand Down Expand Up @@ -224,7 +226,7 @@ final class AIChatViewModel {
}

func loadAvailableModels() async {
let settings = AppSettingsManager.shared.ai
let settings = services.appSettings.ai
let pending = settings.providers.filter { availableModels[$0.id] == nil }
guard !pending.isEmpty else { return }

Expand Down Expand Up @@ -275,7 +277,7 @@ final class AIChatViewModel {
savedQueries = []
return
}
let favorites = await SQLFavoriteManager.shared.fetchFavorites(connectionId: connectionId)
let favorites = await services.sqlFavoriteManager.fetchFavorites(connectionId: connectionId)
savedQueries = favorites
for favorite in favorites {
cachedSavedQueries[favorite.id] = favorite
Expand Down
18 changes: 10 additions & 8 deletions TablePro/ViewModels/DatabaseSwitcherViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ final class DatabaseSwitcherViewModel {
private let currentDatabase: String?
private let currentSchema: String?
private let databaseType: DatabaseType
@ObservationIgnored private let services: AppServices

// MARK: - Computed Properties

Expand All @@ -59,13 +60,14 @@ final class DatabaseSwitcherViewModel {

init(
connectionId: UUID, currentDatabase: String?, currentSchema: String?,
databaseType: DatabaseType
databaseType: DatabaseType, services: AppServices = .live
) {
self.connectionId = connectionId
self.currentDatabase = currentDatabase
self.currentSchema = currentSchema
self.databaseType = databaseType
self.mode = PluginManager.shared.supportsSchemaSwitching(for: databaseType) ? .schema : .database
self.services = services
self.mode = services.pluginManager.supportsSchemaSwitching(for: databaseType) ? .schema : .database
}

// MARK: - Public Methods
Expand All @@ -76,7 +78,7 @@ final class DatabaseSwitcherViewModel {
errorMessage = nil

do {
guard let driver = DatabaseManager.shared.driver(for: connectionId) else {
guard let driver = services.databaseManager.driver(for: connectionId) else {
errorMessage = String(localized: "No active connection")
isLoading = false
return
Expand Down Expand Up @@ -132,14 +134,14 @@ final class DatabaseSwitcherViewModel {
}

func loadCreateDatabaseForm() async throws -> CreateDatabaseFormSpec? {
guard let driver = DatabaseManager.shared.driver(for: connectionId) else {
guard let driver = services.databaseManager.driver(for: connectionId) else {
throw DatabaseError.notConnected
}
return try await driver.createDatabaseFormSpec()
}

func createDatabase(name: String, values: [String: String]) async throws {
guard let driver = DatabaseManager.shared.driver(for: connectionId) else {
guard let driver = services.databaseManager.driver(for: connectionId) else {
throw DatabaseError.notConnected
}
let request = CreateDatabaseRequest(name: name, values: values)
Expand All @@ -148,7 +150,7 @@ final class DatabaseSwitcherViewModel {

/// Drop a database
func dropDatabase(name: String) async throws {
guard let driver = DatabaseManager.shared.driver(for: connectionId) else {
guard let driver = services.databaseManager.driver(for: connectionId) else {
throw DatabaseError.notConnected
}

Expand Down Expand Up @@ -192,10 +194,10 @@ final class DatabaseSwitcherViewModel {

private func isSystemItem(_ name: String) -> Bool {
if isSchemaMode {
let schemaNames = PluginManager.shared.systemSchemaNames(for: databaseType)
let schemaNames = services.pluginManager.systemSchemaNames(for: databaseType)
return schemaNames.contains(name)
}
let dbNames = PluginManager.shared.systemDatabaseNames(for: databaseType)
let dbNames = services.pluginManager.systemDatabaseNames(for: databaseType)
return dbNames.contains(name)
}
}
9 changes: 6 additions & 3 deletions TablePro/ViewModels/ERDiagramViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,14 @@ final class ERDiagramViewModel {
@ObservationIgnored private var columnCountByNodeId: [UUID: Int] = [:]
@ObservationIgnored private var nodeIdToName: [UUID: String] = [:]

@ObservationIgnored private let services: AppServices

// MARK: - Initialization

init(connectionId: UUID, schemaKey: String) {
init(connectionId: UUID, schemaKey: String, services: AppServices = .live) {
self.connectionId = connectionId
self.schemaKey = schemaKey
self.services = services
}

deinit {
Expand All @@ -87,11 +90,11 @@ final class ERDiagramViewModel {
guard loadState != .loaded else { return }
loadState = .loading

if DatabaseManager.shared.driver(for: connectionId) == nil {
if services.databaseManager.driver(for: connectionId) == nil {
await waitForConnection()
}

guard let driver = DatabaseManager.shared.driver(for: connectionId) else {
guard let driver = services.databaseManager.driver(for: connectionId) else {
loadState = .failed(String(localized: "No database connection"))
return
}
Expand Down
6 changes: 4 additions & 2 deletions TablePro/ViewModels/FavoritesSidebarViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ internal final class FavoritesSidebarViewModel {

@ObservationIgnored private let connectionId: UUID
@ObservationIgnored private let cache: ConnectionDataCache
@ObservationIgnored private let manager = SQLFavoriteManager.shared
@ObservationIgnored private let services: AppServices
@ObservationIgnored private var manager: SQLFavoriteManager { services.sqlFavoriteManager }

var isInitialLoadComplete: Bool { cache.isInitialLoadComplete }

Expand All @@ -137,8 +138,9 @@ internal final class FavoritesSidebarViewModel {
return roots
}

init(connectionId: UUID) {
init(connectionId: UUID, services: AppServices = .live) {
self.connectionId = connectionId
self.services = services
self.cache = ConnectionDataCache.shared(for: connectionId)
cache.ensureLoaded()
}
Expand Down
6 changes: 4 additions & 2 deletions TablePro/ViewModels/FeedbackViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ final class FeedbackViewModel {
@ObservationIgnored private var draftSaveTask: Task<Void, Never>?
@ObservationIgnored private var isLoadingDraft = false
@ObservationIgnored var captureTargetWindow: NSWindow?
@ObservationIgnored private let services: AppServices

// MARK: - Init

init() {
init(services: AppServices = .live) {
self.services = services
self.diagnostics = FeedbackDiagnosticsCollector.collect()
loadDraft()
}
Expand Down Expand Up @@ -172,7 +174,7 @@ final class FeedbackViewModel {
)

do {
let response = try await FeedbackAPIClient.shared.submitFeedback(request: request)
let response = try await services.feedbackAPIClient.submitFeedback(request: request)
if let url = URL(string: response.issueUrl) {
submissionResult = .success(issueUrl: url, issueNumber: response.issueNumber)
clearDraft()
Expand Down
15 changes: 8 additions & 7 deletions TablePro/ViewModels/ServerDashboardViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ final class ServerDashboardViewModel {
// MARK: - Private

@ObservationIgnored nonisolated(unsafe) private var refreshTask: Task<Void, Never>?
@ObservationIgnored private let services: AppServices

// MARK: - Computed Properties

Expand All @@ -72,10 +73,11 @@ final class ServerDashboardViewModel {

// MARK: - Initialization

init(connectionId: UUID, databaseType: DatabaseType) {
init(connectionId: UUID, databaseType: DatabaseType, services: AppServices = .live) {
self.connectionId = connectionId
self.databaseType = databaseType
self.provider = ServerDashboardQueryProviderFactory.provider(for: databaseType)
self.services = services
}

deinit {
Expand Down Expand Up @@ -120,14 +122,13 @@ final class ServerDashboardViewModel {
return
}

// Skip silently if connection is not ready yet — the refresh loop will retry
guard DatabaseManager.shared.driver(for: connectionId) != nil else { return }
guard services.databaseManager.driver(for: connectionId) != nil else { return }

isRefreshing = true
defer { isRefreshing = false }

let execute: (String) async throws -> QueryResult = { [connectionId] query in
guard let driver = DatabaseManager.shared.driver(for: connectionId) else {
let execute: (String) async throws -> QueryResult = { [connectionId, services] query in
guard let driver = services.databaseManager.driver(for: connectionId) else {
throw DatabaseError.connectionFailed(
String(localized: "No active connection")
)
Expand Down Expand Up @@ -184,7 +185,7 @@ final class ServerDashboardViewModel {
guard let sql = provider?.killSessionSQL(processId: processId) else { return }

do {
guard let driver = DatabaseManager.shared.driver(for: connectionId) else {
guard let driver = services.databaseManager.driver(for: connectionId) else {
throw DatabaseError.connectionFailed(
String(localized: "No active connection")
)
Expand Down Expand Up @@ -213,7 +214,7 @@ final class ServerDashboardViewModel {
guard let sql = provider?.cancelQuerySQL(processId: processId) else { return }

do {
guard let driver = DatabaseManager.shared.driver(for: connectionId) else {
guard let driver = services.databaseManager.driver(for: connectionId) else {
throw DatabaseError.connectionFailed(
String(localized: "No active connection")
)
Expand Down
13 changes: 10 additions & 3 deletions TablePro/ViewModels/WelcomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ enum WelcomeActiveSheet: Identifiable {
final class WelcomeViewModel {
private static let logger = Logger(subsystem: "com.TablePro", category: "WelcomeViewModel")

private let storage = ConnectionStorage.shared
@ObservationIgnored let services: AppServices
private var storage: ConnectionStorage { services.connectionStorage }
private let groupStorage = GroupStorage.shared

// MARK: - State
Expand Down Expand Up @@ -140,6 +141,12 @@ final class WelcomeViewModel {
return groups.first { $0.id == groupId }?.name
}

// MARK: - Initialization

init(services: AppServices = .live) {
self.services = services
}

// MARK: - Setup & Teardown

func setUp() {
Expand Down Expand Up @@ -536,7 +543,7 @@ final class WelcomeViewModel {

storage.saveConnections(connections)
if !dirtyIds.isEmpty {
SyncChangeTracker.shared.markDirty(.connection, ids: dirtyIds)
services.syncTracker.markDirty(.connection, ids: dirtyIds)
}
rebuildTree()
}
Expand Down Expand Up @@ -571,7 +578,7 @@ final class WelcomeViewModel {

storage.saveConnections(connections)
if !dirtyIds.isEmpty {
SyncChangeTracker.shared.markDirty(.connection, ids: dirtyIds)
services.syncTracker.markDirty(.connection, ids: dirtyIds)
}
rebuildTree()
}
Expand Down
Loading
Loading