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.
206 lines
5.8 KiB
Ruby
206 lines
5.8 KiB
Ruby
# 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
|