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(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(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( 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(_ operation: () async throws -> T) async throws -> T { var lastError: Error? for attempt in 0..