# frozen_string_literal: true require "faraday" require "json" require "cgi" module SynorDatabase # Synor Database SDK Client # # @example # client = SynorDatabase::Client.new(api_key: 'your-api-key') # # # Key-Value operations # client.kv.set("key", "value") # value = client.kv.get("key") # # # Document operations # id = client.documents.create("users", { name: "Alice" }) # doc = client.documents.get("users", id) # class Client attr_reader :config, :kv, :documents, :vectors, :timeseries def initialize(api_key: nil, **options) @config = Config.new(api_key: api_key, **options) raise ArgumentError, "API key is required" unless @config.api_key @conn = Faraday.new(url: @config.base_url) do |f| f.request :json f.response :json f.options.timeout = @config.timeout f.headers["Authorization"] = "Bearer #{@config.api_key}" f.headers["X-SDK-Version"] = "ruby/#{VERSION}" end @closed = false @kv = KeyValueStore.new(self) @documents = DocumentStore.new(self) @vectors = VectorStore.new(self) @timeseries = TimeSeriesStore.new(self) end def closed? @closed end def close @closed = true end def health_check response = request(:get, "/health") response["status"] == "healthy" rescue StandardError false end # Key-Value Store class KeyValueStore def initialize(client) @client = client end def get(key) response = @client.request(:get, "/kv/#{CGI.escape(key)}") response["value"] end def set(key, value, ttl: nil) body = { key: key, value: value } body[:ttl] = ttl if ttl @client.request(:put, "/kv/#{CGI.escape(key)}", body) end def delete(key) @client.request(:delete, "/kv/#{CGI.escape(key)}") end def list(prefix) response = @client.request(:get, "/kv?prefix=#{CGI.escape(prefix)}") (response["items"] || []).map { |h| KeyValue.from_hash(h) } end end # Document Store class DocumentStore def initialize(client) @client = client end def create(collection, document) response = @client.request(:post, "/collections/#{CGI.escape(collection)}/documents", document) response["id"] end def get(collection, id) response = @client.request(:get, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}") Document.from_hash(response) end def update(collection, id, update) @client.request(:patch, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}", update) end def delete(collection, id) @client.request(:delete, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}") end def query(collection, query) body = query.to_h.compact response = @client.request(:post, "/collections/#{CGI.escape(collection)}/query", body) (response["documents"] || []).map { |h| Document.from_hash(h) } end end # Vector Store class VectorStore def initialize(client) @client = client end def upsert(collection, vectors) entries = vectors.map { |v| v.is_a?(Hash) ? v : v.to_h } @client.request(:post, "/vectors/#{CGI.escape(collection)}/upsert", { vectors: entries }) end def search(collection, vector, k) response = @client.request(:post, "/vectors/#{CGI.escape(collection)}/search", { vector: vector, k: k }) (response["results"] || []).map { |h| SearchResult.from_hash(h) } end def delete(collection, ids) @client.request(:delete, "/vectors/#{CGI.escape(collection)}", { ids: ids }) end end # Time Series Store class TimeSeriesStore def initialize(client) @client = client end def write(series, points) entries = points.map { |p| p.is_a?(Hash) ? p : p.to_h } @client.request(:post, "/timeseries/#{CGI.escape(series)}/write", { points: entries }) end def query(series, range, aggregation: nil) body = { range: range.to_h } body[:aggregation] = aggregation.to_h if aggregation response = @client.request(:post, "/timeseries/#{CGI.escape(series)}/query", body) (response["points"] || []).map { |h| DataPoint.from_hash(h) } end end # Internal request method def request(method, path, body = nil) check_closed! with_retry do response = case method when :get then @conn.get(path) when :post then @conn.post(path, body) when :put then @conn.put(path, body) when :patch then @conn.patch(path, body) when :delete if body @conn.delete(path) { |req| req.body = body.to_json } else @conn.delete(path) end end raise_on_error(response) unless response.success? response.body || {} end end private def check_closed! raise ClientClosedError, "Client has been closed" if @closed end def with_retry last_error = nil @config.retries.times do |attempt| begin return yield rescue StandardError => e last_error = e puts "Attempt #{attempt + 1} failed: #{e.message}" if @config.debug sleep(2**attempt) if attempt < @config.retries - 1 end end raise last_error end def raise_on_error(response) body = response.body || {} message = body["message"] || "HTTP #{response.status}" code = body["code"] raise HttpError.new(message, status_code: response.status, code: code) end end end