Skip to content

Commit 12da4cd

Browse files
authored
Merge pull request #571 from TableProApp/feat/tablepro-core-package
feat: TablePro iOS
2 parents bbc52c0 + 016e033 commit 12da4cd

133 files changed

Lines changed: 14362 additions & 89 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: Build TablePro
22

33
on:
4+
workflow_dispatch:
45
push:
56
tags: ["v*"]
67
paths-ignore:
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// swift-tools-version: 5.9
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "TableProCore",
7+
platforms: [
8+
.macOS(.v14),
9+
.iOS(.v17)
10+
],
11+
products: [
12+
.library(name: "TableProPluginKit", targets: ["TableProPluginKit"]),
13+
.library(name: "TableProModels", targets: ["TableProModels"]),
14+
.library(name: "TableProDatabase", targets: ["TableProDatabase"]),
15+
.library(name: "TableProQuery", targets: ["TableProQuery"]),
16+
.library(name: "TableProSync", targets: ["TableProSync"])
17+
],
18+
targets: [
19+
.target(
20+
name: "TableProPluginKit",
21+
dependencies: [],
22+
path: "Sources/TableProPluginKit"
23+
),
24+
.target(
25+
name: "TableProModels",
26+
dependencies: ["TableProPluginKit"],
27+
path: "Sources/TableProModels"
28+
),
29+
.target(
30+
name: "TableProDatabase",
31+
dependencies: ["TableProModels"],
32+
path: "Sources/TableProDatabase"
33+
),
34+
.target(
35+
name: "TableProQuery",
36+
dependencies: ["TableProModels", "TableProPluginKit"],
37+
path: "Sources/TableProQuery"
38+
),
39+
.target(
40+
name: "TableProSync",
41+
dependencies: ["TableProModels"],
42+
path: "Sources/TableProSync"
43+
),
44+
.testTarget(
45+
name: "TableProModelsTests",
46+
dependencies: ["TableProModels", "TableProPluginKit"],
47+
path: "Tests/TableProModelsTests"
48+
),
49+
.testTarget(
50+
name: "TableProDatabaseTests",
51+
dependencies: ["TableProDatabase", "TableProModels"],
52+
path: "Tests/TableProDatabaseTests"
53+
),
54+
.testTarget(
55+
name: "TableProQueryTests",
56+
dependencies: ["TableProQuery", "TableProModels", "TableProPluginKit"],
57+
path: "Tests/TableProQueryTests"
58+
)
59+
]
60+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Foundation
2+
3+
public enum ConnectionError: Error, LocalizedError {
4+
case driverNotFound(String)
5+
case notConnected
6+
case sshNotSupported
7+
8+
public var errorDescription: String? {
9+
switch self {
10+
case .driverNotFound(let type):
11+
return "No driver available for database type: \(type)"
12+
case .notConnected:
13+
return "Not connected to database"
14+
case .sshNotSupported:
15+
return "SSH tunneling is not available on this platform"
16+
}
17+
}
18+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import Foundation
2+
import TableProModels
3+
4+
public final class ConnectionManager: @unchecked Sendable {
5+
private let driverFactory: DriverFactory
6+
private let secureStore: SecureStore
7+
private let sshProvider: SSHProvider?
8+
9+
private let lock = NSLock()
10+
private var sessions: [UUID: ConnectionSession] = [:]
11+
12+
public init(
13+
driverFactory: DriverFactory,
14+
secureStore: SecureStore,
15+
sshProvider: SSHProvider? = nil
16+
) {
17+
self.driverFactory = driverFactory
18+
self.secureStore = secureStore
19+
self.sshProvider = sshProvider
20+
}
21+
22+
public func connect(_ connection: DatabaseConnection) async throws -> ConnectionSession {
23+
let password = try secureStore.retrieve(forKey: Self.passwordKey(for: connection.id))
24+
25+
var effectiveHost = connection.host
26+
var effectivePort = connection.port
27+
if connection.sshEnabled, let ssh = connection.sshConfiguration {
28+
guard let provider = sshProvider else {
29+
throw ConnectionError.sshNotSupported
30+
}
31+
let tunnel = try await provider.createTunnel(
32+
config: ssh,
33+
remoteHost: connection.host,
34+
remotePort: connection.port
35+
)
36+
effectiveHost = tunnel.localHost
37+
effectivePort = tunnel.localPort
38+
}
39+
40+
do {
41+
var effectiveConnection = connection
42+
effectiveConnection.host = effectiveHost
43+
effectiveConnection.port = effectivePort
44+
45+
let driver = try driverFactory.createDriver(for: effectiveConnection, password: password)
46+
try await driver.connect()
47+
48+
let session = ConnectionSession(
49+
connectionId: connection.id,
50+
driver: driver,
51+
activeDatabase: connection.database,
52+
status: .connected
53+
)
54+
storeSession(session, for: connection.id)
55+
return session
56+
} catch {
57+
if connection.sshEnabled, let provider = sshProvider {
58+
try? await provider.closeTunnel(for: connection.id)
59+
}
60+
throw error
61+
}
62+
}
63+
64+
public func storePassword(_ password: String, for connectionId: UUID) throws {
65+
try secureStore.store(password, forKey: Self.passwordKey(for: connectionId))
66+
}
67+
68+
public func deletePassword(for connectionId: UUID) throws {
69+
try secureStore.delete(forKey: Self.passwordKey(for: connectionId))
70+
}
71+
72+
private static func passwordKey(for connectionId: UUID) -> String {
73+
"com.TablePro.password.\(connectionId.uuidString)"
74+
}
75+
76+
public func disconnect(_ connectionId: UUID) async {
77+
let session = removeSession(for: connectionId)
78+
guard let session else { return }
79+
try? await session.driver.disconnect()
80+
if let sshProvider {
81+
try? await sshProvider.closeTunnel(for: connectionId)
82+
}
83+
}
84+
85+
public func disconnectAll() async {
86+
let ids = allSessionIds()
87+
for id in ids {
88+
await disconnect(id)
89+
}
90+
}
91+
92+
private func allSessionIds() -> [UUID] {
93+
lock.lock()
94+
defer { lock.unlock() }
95+
return Array(sessions.keys)
96+
}
97+
98+
public func updateSession(_ connectionId: UUID, _ mutation: (inout ConnectionSession) -> Void) {
99+
lock.lock()
100+
defer { lock.unlock() }
101+
guard var session = sessions[connectionId] else { return }
102+
mutation(&session)
103+
sessions[connectionId] = session
104+
}
105+
106+
public func switchDatabase(_ connectionId: UUID, to database: String) async throws {
107+
guard let session = session(for: connectionId) else {
108+
throw ConnectionError.notConnected
109+
}
110+
try await session.driver.switchDatabase(to: database)
111+
updateSession(connectionId) { $0.activeDatabase = database }
112+
}
113+
114+
public func session(for connectionId: UUID) -> ConnectionSession? {
115+
lock.lock()
116+
defer { lock.unlock() }
117+
return sessions[connectionId]
118+
}
119+
120+
private func storeSession(_ session: ConnectionSession, for id: UUID) {
121+
lock.lock()
122+
sessions[id] = session
123+
lock.unlock()
124+
}
125+
126+
private func removeSession(for id: UUID) -> ConnectionSession? {
127+
lock.lock()
128+
let session = sessions.removeValue(forKey: id)
129+
lock.unlock()
130+
return session
131+
}
132+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
import TableProModels
3+
4+
/// Note: Views hold a snapshot of this struct. Mutable fields (activeDatabase, status)
5+
/// are only updated through ConnectionManager.updateSession and should be re-fetched
6+
/// from the manager when needed rather than read from a held copy.
7+
public struct ConnectionSession: Sendable {
8+
public let connectionId: UUID
9+
public let driver: any DatabaseDriver
10+
public internal(set) var activeDatabase: String
11+
public internal(set) var currentSchema: String?
12+
public internal(set) var status: ConnectionStatus
13+
public internal(set) var tables: [TableInfo]
14+
15+
public init(
16+
connectionId: UUID,
17+
driver: any DatabaseDriver,
18+
activeDatabase: String,
19+
currentSchema: String? = nil,
20+
status: ConnectionStatus = .connected,
21+
tables: [TableInfo] = []
22+
) {
23+
self.connectionId = connectionId
24+
self.driver = driver
25+
self.activeDatabase = activeDatabase
26+
self.currentSchema = currentSchema
27+
self.status = status
28+
self.tables = tables
29+
}
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
import TableProModels
3+
4+
public protocol DatabaseDriver: AnyObject, Sendable {
5+
func connect() async throws
6+
func disconnect() async throws
7+
func ping() async throws -> Bool
8+
9+
func execute(query: String) async throws -> QueryResult
10+
func cancelCurrentQuery() async throws
11+
12+
func fetchTables(schema: String?) async throws -> [TableInfo]
13+
func fetchColumns(table: String, schema: String?) async throws -> [ColumnInfo]
14+
func fetchIndexes(table: String, schema: String?) async throws -> [IndexInfo]
15+
func fetchForeignKeys(table: String, schema: String?) async throws -> [ForeignKeyInfo]
16+
func fetchDatabases() async throws -> [String]
17+
18+
func switchDatabase(to name: String) async throws
19+
var supportsSchemas: Bool { get }
20+
func switchSchema(to name: String) async throws
21+
func fetchSchemas() async throws -> [String]
22+
var currentSchema: String? { get }
23+
24+
var supportsTransactions: Bool { get }
25+
func beginTransaction() async throws
26+
func commitTransaction() async throws
27+
func rollbackTransaction() async throws
28+
29+
var serverVersion: String? { get }
30+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
import TableProModels
3+
4+
/// Creates database drivers for a given connection.
5+
/// macOS: plugin-based implementation. iOS: direct driver creation.
6+
public protocol DriverFactory: Sendable {
7+
func createDriver(for connection: DatabaseConnection, password: String?) throws -> any DatabaseDriver
8+
func supportedTypes() -> [DatabaseType]
9+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Foundation
2+
import TableProModels
3+
4+
public protocol SSHProvider: Sendable {
5+
func createTunnel(
6+
config: SSHConfiguration,
7+
remoteHost: String,
8+
remotePort: Int
9+
) async throws -> SSHTunnel
10+
11+
func closeTunnel(for connectionId: UUID) async throws
12+
}
13+
14+
public struct SSHTunnel: Sendable {
15+
public let localHost: String
16+
public let localPort: Int
17+
18+
public init(localHost: String, localPort: Int) {
19+
self.localHost = localHost
20+
self.localPort = localPort
21+
}
22+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Foundation
2+
3+
public protocol SecureStore: Sendable {
4+
func store(_ value: String, forKey key: String) throws
5+
func retrieve(forKey key: String) throws -> String?
6+
func delete(forKey key: String) throws
7+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
3+
public struct ConnectionGroup: Identifiable, Codable, Sendable {
4+
public var id: UUID
5+
public var name: String
6+
public var sortOrder: Int
7+
8+
public init(
9+
id: UUID = UUID(),
10+
name: String = "",
11+
sortOrder: Int = 0
12+
) {
13+
self.id = id
14+
self.name = name
15+
self.sortOrder = sortOrder
16+
}
17+
}

0 commit comments

Comments
 (0)