Initial commit
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
struct ConnectionProfile: Identifiable, Codable, Hashable {
|
||||
var id = UUID()
|
||||
var name = "Local MySQL"
|
||||
var host = "127.0.0.1"
|
||||
var port = 3306
|
||||
var username = "root"
|
||||
var database = ""
|
||||
}
|
||||
struct QueryResult {
|
||||
var columns: [String] = []
|
||||
var rows: [[String]] = []
|
||||
var message = ""
|
||||
}
|
||||
|
||||
enum PasswordStore {
|
||||
static func read(_ id: UUID) -> String {
|
||||
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: "WorkbenchReplacement.MySQL",
|
||||
kSecAttrAccount as String: id.uuidString,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne]
|
||||
var value: CFTypeRef?
|
||||
guard SecItemCopyMatching(query as CFDictionary, &value) == errSecSuccess,
|
||||
let data = value as? Data else { return "" }
|
||||
return String(data: data, encoding: .utf8) ?? ""
|
||||
}
|
||||
static func save(_ password: String, id: UUID) {
|
||||
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: "WorkbenchReplacement.MySQL",
|
||||
kSecAttrAccount as String: id.uuidString]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
guard !password.isEmpty else { return }
|
||||
var attributes = query
|
||||
attributes[kSecValueData as String] = Data(password.utf8)
|
||||
SecItemAdd(attributes as CFDictionary, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class DatabaseModel: ObservableObject {
|
||||
@Published var profiles: [ConnectionProfile] = []
|
||||
@Published var selectedID: UUID?
|
||||
@Published var sql = "SELECT VERSION() AS server_version;"
|
||||
@Published var result = QueryResult()
|
||||
@Published var databases: [String] = []
|
||||
@Published var selectedDatabase = ""
|
||||
@Published var isRunning = false
|
||||
@Published var status = "Not connected"
|
||||
private let storageKey = "WorkbenchReplacement.connectionProfiles.native.v1"
|
||||
|
||||
init() {
|
||||
if let data = UserDefaults.standard.data(forKey: storageKey),
|
||||
let decoded = try? JSONDecoder().decode([ConnectionProfile].self, from: data) { profiles = decoded }
|
||||
if profiles.isEmpty { profiles = [ConnectionProfile()] }
|
||||
selectedID = profiles.first?.id
|
||||
}
|
||||
var selected: ConnectionProfile? { profiles.first { $0.id == selectedID } }
|
||||
func save(_ profile: ConnectionProfile, password: String) {
|
||||
if let i = profiles.firstIndex(where: { $0.id == profile.id }) { profiles[i] = profile }
|
||||
else { profiles.append(profile) }
|
||||
PasswordStore.save(password, id: profile.id)
|
||||
selectedID = profile.id
|
||||
persist()
|
||||
}
|
||||
private func persist() {
|
||||
if let data = try? JSONEncoder().encode(profiles) { UserDefaults.standard.set(data, forKey: storageKey) }
|
||||
}
|
||||
func loadDatabases() { execute("SHOW DATABASES;", listingDatabases: true) }
|
||||
func runSQL() { execute(sql, listingDatabases: false) }
|
||||
private func execute(_ statement: String, listingDatabases: Bool) {
|
||||
guard let profile = selected else { status = "Select a connection"; return }
|
||||
guard !statement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||
isRunning = true
|
||||
status = "Connecting…"
|
||||
let activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
|
||||
let password = PasswordStore.read(profile.id)
|
||||
Task {
|
||||
do {
|
||||
let parsed = try await NativeMySQLRunner.run(profile: profile, database: activeDB, password: password, sql: statement)
|
||||
if listingDatabases {
|
||||
databases = parsed.rows.compactMap { $0.first }
|
||||
status = "Connected to \(profile.name)"
|
||||
} else {
|
||||
result = parsed
|
||||
status = "Query completed (\(parsed.rows.count) rows)"
|
||||
}
|
||||
} catch {
|
||||
status = "Error"
|
||||
result = QueryResult(message: error.localizedDescription)
|
||||
}
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user