Implements Database, Hosting, and Bridge SDKs for remaining languages: Swift SDKs: - SynorDatabase with KV, Document, Vector, TimeSeries stores - SynorHosting with domain, DNS, deployment, SSL operations - SynorBridge with lock-mint and burn-unlock cross-chain flows C SDKs: - database.h/c - multi-model database client - hosting.h/c - hosting and domain management - bridge.h/c - cross-chain asset transfers C++ SDKs: - database.hpp - modern C++17 with std::future async - hosting.hpp - domain and deployment operations - bridge.hpp - cross-chain bridge with wait operations C# SDKs: - SynorDatabase.cs - async/await with inner store classes - SynorHosting.cs - domain management and analytics - SynorBridge.cs - cross-chain with BridgeException handling Ruby SDKs: - synor_database - Struct-based types with Faraday HTTP - synor_hosting - domain, DNS, SSL, analytics - synor_bridge - lock-mint/burn-unlock with retry logic Phase 3 complete: Database/Hosting/Bridge now available in all 12 languages.
369 lines
12 KiB
Swift
369 lines
12 KiB
Swift
import Foundation
|
|
|
|
/// Synor Database SDK client for Swift.
|
|
///
|
|
/// Provides multi-model database with Key-Value, Document, Vector, and Time Series stores.
|
|
///
|
|
/// Example:
|
|
/// ```swift
|
|
/// let db = SynorDatabase(config: DatabaseConfig(apiKey: "your-api-key"))
|
|
///
|
|
/// // Key-Value operations
|
|
/// try await db.kv.set(key: "mykey", value: "myvalue", ttl: 3600)
|
|
/// let value = try await db.kv.get(key: "mykey")
|
|
///
|
|
/// // Document operations
|
|
/// let docId = try await db.documents.create(collection: "users", document: ["name": "Alice", "age": 30])
|
|
/// let doc = try await db.documents.get(collection: "users", id: docId)
|
|
///
|
|
/// // Vector operations
|
|
/// try await db.vectors.upsert(collection: "embeddings", vectors: [
|
|
/// VectorEntry(id: "vec1", vector: [0.1, 0.2, 0.3], metadata: ["label": "test"])
|
|
/// ])
|
|
///
|
|
/// // Time series operations
|
|
/// try await db.timeseries.write(series: "metrics", points: [
|
|
/// DataPoint(timestamp: Date().timeIntervalSince1970, value: 42.0)
|
|
/// ])
|
|
/// ```
|
|
public class SynorDatabase {
|
|
private let config: DatabaseConfig
|
|
private let session: URLSession
|
|
private let decoder: JSONDecoder
|
|
private let encoder: JSONEncoder
|
|
private var closed = false
|
|
|
|
/// Key-Value store operations.
|
|
public private(set) lazy var kv = KeyValueStore(db: self)
|
|
|
|
/// Document store operations.
|
|
public private(set) lazy var documents = DocumentStore(db: self)
|
|
|
|
/// Vector store operations.
|
|
public private(set) lazy var vectors = VectorStore(db: self)
|
|
|
|
/// Time series store operations.
|
|
public private(set) lazy var timeseries = TimeSeriesStore(db: self)
|
|
|
|
public init(config: DatabaseConfig) {
|
|
self.config = config
|
|
|
|
let sessionConfig = URLSessionConfiguration.default
|
|
sessionConfig.timeoutIntervalForRequest = config.timeout
|
|
sessionConfig.timeoutIntervalForResource = config.timeout * 2
|
|
self.session = URLSession(configuration: sessionConfig)
|
|
|
|
self.decoder = JSONDecoder()
|
|
self.encoder = JSONEncoder()
|
|
}
|
|
|
|
// MARK: - Key-Value Store
|
|
|
|
/// Key-Value store for simple key-value operations.
|
|
public class KeyValueStore {
|
|
private let db: SynorDatabase
|
|
|
|
internal init(db: SynorDatabase) {
|
|
self.db = db
|
|
}
|
|
|
|
/// Get a value by key.
|
|
public func get(key: String) async throws -> Any? {
|
|
let response: KVGetResponse = try await db.request(
|
|
method: "GET",
|
|
path: "/kv/\(key.urlEncoded)"
|
|
)
|
|
return response.value?.value
|
|
}
|
|
|
|
/// Set a value for a key with optional TTL.
|
|
public func set(key: String, value: Any, ttl: Int? = nil) async throws {
|
|
var body: [String: AnyCodable] = [
|
|
"key": AnyCodable(key),
|
|
"value": AnyCodable(value)
|
|
]
|
|
if let ttl = ttl {
|
|
body["ttl"] = AnyCodable(ttl)
|
|
}
|
|
let _: EmptyResponse = try await db.request(
|
|
method: "PUT",
|
|
path: "/kv/\(key.urlEncoded)",
|
|
body: body
|
|
)
|
|
}
|
|
|
|
/// Delete a key.
|
|
public func delete(key: String) async throws {
|
|
let _: EmptyResponse = try await db.request(
|
|
method: "DELETE",
|
|
path: "/kv/\(key.urlEncoded)"
|
|
)
|
|
}
|
|
|
|
/// List keys by prefix.
|
|
public func list(prefix: String) async throws -> [KeyValue] {
|
|
let response: KVListResponse = try await db.request(
|
|
method: "GET",
|
|
path: "/kv?prefix=\(prefix.urlEncoded)"
|
|
)
|
|
return response.items ?? []
|
|
}
|
|
}
|
|
|
|
// MARK: - Document Store
|
|
|
|
/// Document store for document-oriented operations.
|
|
public class DocumentStore {
|
|
private let db: SynorDatabase
|
|
|
|
internal init(db: SynorDatabase) {
|
|
self.db = db
|
|
}
|
|
|
|
/// Create a new document.
|
|
public func create(collection: String, document: [String: Any]) async throws -> String {
|
|
let body = document.mapValues { AnyCodable($0) }
|
|
let response: CreateDocumentResponse = try await db.request(
|
|
method: "POST",
|
|
path: "/collections/\(collection.urlEncoded)/documents",
|
|
body: body
|
|
)
|
|
return response.id
|
|
}
|
|
|
|
/// Get a document by ID.
|
|
public func get(collection: String, id: String) async throws -> Document {
|
|
return try await db.request(
|
|
method: "GET",
|
|
path: "/collections/\(collection.urlEncoded)/documents/\(id.urlEncoded)"
|
|
)
|
|
}
|
|
|
|
/// Update a document.
|
|
public func update(collection: String, id: String, update: [String: Any]) async throws {
|
|
let body = update.mapValues { AnyCodable($0) }
|
|
let _: EmptyResponse = try await db.request(
|
|
method: "PATCH",
|
|
path: "/collections/\(collection.urlEncoded)/documents/\(id.urlEncoded)",
|
|
body: body
|
|
)
|
|
}
|
|
|
|
/// Delete a document.
|
|
public func delete(collection: String, id: String) async throws {
|
|
let _: EmptyResponse = try await db.request(
|
|
method: "DELETE",
|
|
path: "/collections/\(collection.urlEncoded)/documents/\(id.urlEncoded)"
|
|
)
|
|
}
|
|
|
|
/// Query documents.
|
|
public func query(collection: String, query: DocumentQuery) async throws -> [Document] {
|
|
let response: DocumentListResponse = try await db.request(
|
|
method: "POST",
|
|
path: "/collections/\(collection.urlEncoded)/query",
|
|
body: query
|
|
)
|
|
return response.documents ?? []
|
|
}
|
|
}
|
|
|
|
// MARK: - Vector Store
|
|
|
|
/// Vector store for similarity search operations.
|
|
public class VectorStore {
|
|
private let db: SynorDatabase
|
|
|
|
internal init(db: SynorDatabase) {
|
|
self.db = db
|
|
}
|
|
|
|
/// Upsert vectors into a collection.
|
|
public func upsert(collection: String, vectors: [VectorEntry]) async throws {
|
|
let body: [String: [VectorEntry]] = ["vectors": vectors]
|
|
let _: EmptyResponse = try await db.request(
|
|
method: "POST",
|
|
path: "/vectors/\(collection.urlEncoded)/upsert",
|
|
body: body
|
|
)
|
|
}
|
|
|
|
/// Search for similar vectors.
|
|
public func search(collection: String, vector: [Double], k: Int) async throws -> [SearchResult] {
|
|
let body: [String: AnyCodable] = [
|
|
"vector": AnyCodable(vector),
|
|
"k": AnyCodable(k)
|
|
]
|
|
let response: SearchListResponse = try await db.request(
|
|
method: "POST",
|
|
path: "/vectors/\(collection.urlEncoded)/search",
|
|
body: body
|
|
)
|
|
return response.results ?? []
|
|
}
|
|
|
|
/// Delete vectors by IDs.
|
|
public func delete(collection: String, ids: [String]) async throws {
|
|
let body: [String: [String]] = ["ids": ids]
|
|
let _: EmptyResponse = try await db.request(
|
|
method: "DELETE",
|
|
path: "/vectors/\(collection.urlEncoded)",
|
|
body: body
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Time Series Store
|
|
|
|
/// Time series store for time-based data operations.
|
|
public class TimeSeriesStore {
|
|
private let db: SynorDatabase
|
|
|
|
internal init(db: SynorDatabase) {
|
|
self.db = db
|
|
}
|
|
|
|
/// Write data points to a series.
|
|
public func write(series: String, points: [DataPoint]) async throws {
|
|
let body: [String: [DataPoint]] = ["points": points]
|
|
let _: EmptyResponse = try await db.request(
|
|
method: "POST",
|
|
path: "/timeseries/\(series.urlEncoded)/write",
|
|
body: body
|
|
)
|
|
}
|
|
|
|
/// Query data points from a series.
|
|
public func query(series: String, range: TimeRange, aggregation: Aggregation? = nil) async throws -> [DataPoint] {
|
|
var body: [String: AnyCodable] = ["range": AnyCodable(["start": range.start, "end": range.end])]
|
|
if let aggregation = aggregation {
|
|
body["aggregation"] = AnyCodable(["function": aggregation.function, "interval": aggregation.interval])
|
|
}
|
|
let response: DataPointListResponse = try await db.request(
|
|
method: "POST",
|
|
path: "/timeseries/\(series.urlEncoded)/query",
|
|
body: body
|
|
)
|
|
return response.points ?? []
|
|
}
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
/// Close the client.
|
|
public func close() {
|
|
closed = true
|
|
session.invalidateAndCancel()
|
|
}
|
|
|
|
/// Check if the client is closed.
|
|
public var isClosed: Bool { closed }
|
|
|
|
/// Perform a health check.
|
|
public func healthCheck() async -> Bool {
|
|
do {
|
|
let response: HealthResponse = try await request(method: "GET", path: "/health")
|
|
return response.status == "healthy"
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Internal HTTP Methods
|
|
|
|
internal func request<T: Decodable>(method: String, path: String) async throws -> T {
|
|
return try await executeWithRetry {
|
|
try await self.performRequest(method: method, path: path, body: nil as EmptyBody?)
|
|
}
|
|
}
|
|
|
|
internal func request<T: Decodable, R: Encodable>(method: String, path: String, body: R) async throws -> T {
|
|
return try await executeWithRetry {
|
|
try await self.performRequest(method: method, path: path, body: body)
|
|
}
|
|
}
|
|
|
|
private func performRequest<T: Decodable, R: Encodable>(
|
|
method: String,
|
|
path: String,
|
|
body: R?
|
|
) async throws -> T {
|
|
if closed { throw DatabaseError.clientClosed }
|
|
|
|
guard let url = URL(string: "\(config.endpoint)\(path)") else {
|
|
throw DatabaseError.invalidResponse
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = method
|
|
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
|
|
|
|
if let body = body {
|
|
do {
|
|
request.httpBody = try encoder.encode(body)
|
|
} catch {
|
|
throw DatabaseError.encodingError(error)
|
|
}
|
|
}
|
|
|
|
let (data, response): (Data, URLResponse)
|
|
do {
|
|
(data, response) = try await session.data(for: request)
|
|
} catch {
|
|
throw DatabaseError.networkError(error)
|
|
}
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw DatabaseError.invalidResponse
|
|
}
|
|
|
|
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
|
|
if data.isEmpty && T.self == EmptyResponse.self {
|
|
return EmptyResponse() as! T
|
|
}
|
|
do {
|
|
return try decoder.decode(T.self, from: data)
|
|
} catch {
|
|
throw DatabaseError.decodingError(error)
|
|
}
|
|
}
|
|
|
|
let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
let message = errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)"
|
|
let code = errorInfo?["code"] as? String
|
|
throw DatabaseError.httpError(statusCode: httpResponse.statusCode, message: message, code: code)
|
|
}
|
|
|
|
private func executeWithRetry<T>(_ operation: () async throws -> T) async throws -> T {
|
|
var lastError: Error?
|
|
|
|
for attempt in 0..<config.retries {
|
|
do {
|
|
return try await operation()
|
|
} catch {
|
|
lastError = error
|
|
if config.debug {
|
|
print("Attempt \(attempt + 1) failed: \(error)")
|
|
}
|
|
if attempt < config.retries - 1 {
|
|
try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * (1 << attempt)))
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError ?? DatabaseError.invalidResponse
|
|
}
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
private struct EmptyBody: Encodable {}
|
|
private struct EmptyResponse: Decodable {}
|
|
|
|
private extension String {
|
|
var urlEncoded: String {
|
|
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
|
|
}
|
|
}
|