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.
335 lines
12 KiB
C#
335 lines
12 KiB
C#
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Web;
|
|
|
|
namespace Synor.Sdk.Database;
|
|
|
|
/// <summary>
|
|
/// Synor Database SDK client for C#.
|
|
///
|
|
/// Provides multi-model database with Key-Value, Document, Vector, and Time Series stores.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <code>
|
|
/// var db = new SynorDatabase(new DatabaseConfig { ApiKey = "your-api-key" });
|
|
///
|
|
/// // Key-Value operations
|
|
/// await db.Kv.SetAsync("mykey", "myvalue");
|
|
/// var value = await db.Kv.GetAsync("mykey");
|
|
///
|
|
/// // Document operations
|
|
/// var id = await db.Documents.CreateAsync("users", new { name = "Alice", age = 30 });
|
|
/// var doc = await db.Documents.GetAsync("users", id);
|
|
///
|
|
/// db.Dispose();
|
|
/// </code>
|
|
/// </example>
|
|
public class SynorDatabase : IDisposable
|
|
{
|
|
private readonly DatabaseConfig _config;
|
|
private readonly HttpClient _httpClient;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
private bool _disposed;
|
|
|
|
public KeyValueStore Kv { get; }
|
|
public DocumentStore Documents { get; }
|
|
public VectorStore Vectors { get; }
|
|
public TimeSeriesStore TimeSeries { get; }
|
|
|
|
public SynorDatabase(DatabaseConfig config)
|
|
{
|
|
_config = config;
|
|
_httpClient = new HttpClient
|
|
{
|
|
BaseAddress = new Uri(config.Endpoint),
|
|
Timeout = config.Timeout
|
|
};
|
|
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
|
|
_httpClient.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0");
|
|
|
|
_jsonOptions = new JsonSerializerOptions
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
|
|
Kv = new KeyValueStore(this);
|
|
Documents = new DocumentStore(this);
|
|
Vectors = new VectorStore(this);
|
|
TimeSeries = new TimeSeriesStore(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Key-Value store operations.
|
|
/// </summary>
|
|
public class KeyValueStore
|
|
{
|
|
private readonly SynorDatabase _db;
|
|
|
|
internal KeyValueStore(SynorDatabase db) => _db = db;
|
|
|
|
public async Task<object?> GetAsync(string key, CancellationToken ct = default)
|
|
{
|
|
var response = await _db.GetAsync<KvGetResponse>($"/kv/{Uri.EscapeDataString(key)}", ct);
|
|
return response.Value;
|
|
}
|
|
|
|
public async Task SetAsync(string key, object value, int? ttl = null, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["key"] = key, ["value"] = value };
|
|
if (ttl.HasValue) body["ttl"] = ttl.Value;
|
|
await _db.PutAsync<object>($"/kv/{Uri.EscapeDataString(key)}", body, ct);
|
|
}
|
|
|
|
public async Task DeleteAsync(string key, CancellationToken ct = default)
|
|
{
|
|
await _db.DeleteAsync($"/kv/{Uri.EscapeDataString(key)}", ct);
|
|
}
|
|
|
|
public async Task<List<KeyValue>> ListAsync(string prefix, CancellationToken ct = default)
|
|
{
|
|
var response = await _db.GetAsync<KvListResponse>($"/kv?prefix={Uri.EscapeDataString(prefix)}", ct);
|
|
return response.Items ?? new List<KeyValue>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Document store operations.
|
|
/// </summary>
|
|
public class DocumentStore
|
|
{
|
|
private readonly SynorDatabase _db;
|
|
|
|
internal DocumentStore(SynorDatabase db) => _db = db;
|
|
|
|
public async Task<string> CreateAsync(string collection, object document, CancellationToken ct = default)
|
|
{
|
|
var response = await _db.PostAsync<CreateDocumentResponse>(
|
|
$"/collections/{Uri.EscapeDataString(collection)}/documents", document, ct);
|
|
return response.Id;
|
|
}
|
|
|
|
public async Task<Document> GetAsync(string collection, string id, CancellationToken ct = default)
|
|
{
|
|
return await _db.GetAsync<Document>(
|
|
$"/collections/{Uri.EscapeDataString(collection)}/documents/{Uri.EscapeDataString(id)}", ct);
|
|
}
|
|
|
|
public async Task UpdateAsync(string collection, string id, object update, CancellationToken ct = default)
|
|
{
|
|
await _db.PatchAsync<object>(
|
|
$"/collections/{Uri.EscapeDataString(collection)}/documents/{Uri.EscapeDataString(id)}", update, ct);
|
|
}
|
|
|
|
public async Task DeleteAsync(string collection, string id, CancellationToken ct = default)
|
|
{
|
|
await _db.DeleteAsync(
|
|
$"/collections/{Uri.EscapeDataString(collection)}/documents/{Uri.EscapeDataString(id)}", ct);
|
|
}
|
|
|
|
public async Task<List<Document>> QueryAsync(string collection, DocumentQuery query, CancellationToken ct = default)
|
|
{
|
|
var response = await _db.PostAsync<DocumentListResponse>(
|
|
$"/collections/{Uri.EscapeDataString(collection)}/query", query, ct);
|
|
return response.Documents ?? new List<Document>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Vector store operations.
|
|
/// </summary>
|
|
public class VectorStore
|
|
{
|
|
private readonly SynorDatabase _db;
|
|
|
|
internal VectorStore(SynorDatabase db) => _db = db;
|
|
|
|
public async Task UpsertAsync(string collection, IEnumerable<VectorEntry> vectors, CancellationToken ct = default)
|
|
{
|
|
await _db.PostAsync<object>(
|
|
$"/vectors/{Uri.EscapeDataString(collection)}/upsert",
|
|
new { vectors = vectors }, ct);
|
|
}
|
|
|
|
public async Task<List<SearchResult>> SearchAsync(string collection, double[] vector, int k, CancellationToken ct = default)
|
|
{
|
|
var response = await _db.PostAsync<SearchListResponse>(
|
|
$"/vectors/{Uri.EscapeDataString(collection)}/search",
|
|
new { vector, k }, ct);
|
|
return response.Results ?? new List<SearchResult>();
|
|
}
|
|
|
|
public async Task DeleteAsync(string collection, IEnumerable<string> ids, CancellationToken ct = default)
|
|
{
|
|
await _db.DeleteAsync($"/vectors/{Uri.EscapeDataString(collection)}", new { ids = ids }, ct);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Time series store operations.
|
|
/// </summary>
|
|
public class TimeSeriesStore
|
|
{
|
|
private readonly SynorDatabase _db;
|
|
|
|
internal TimeSeriesStore(SynorDatabase db) => _db = db;
|
|
|
|
public async Task WriteAsync(string series, IEnumerable<DataPoint> points, CancellationToken ct = default)
|
|
{
|
|
await _db.PostAsync<object>(
|
|
$"/timeseries/{Uri.EscapeDataString(series)}/write",
|
|
new { points = points }, ct);
|
|
}
|
|
|
|
public async Task<List<DataPoint>> QueryAsync(
|
|
string series, TimeRange range, Aggregation? aggregation = null, CancellationToken ct = default)
|
|
{
|
|
var body = new Dictionary<string, object> { ["range"] = range };
|
|
if (aggregation != null) body["aggregation"] = aggregation;
|
|
|
|
var response = await _db.PostAsync<DataPointListResponse>(
|
|
$"/timeseries/{Uri.EscapeDataString(series)}/query", body, ct);
|
|
return response.Points ?? new List<DataPoint>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform a health check.
|
|
/// </summary>
|
|
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
var response = await GetAsync<HealthResponse>("/health", ct);
|
|
return response.Status == "healthy";
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool IsClosed => _disposed;
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
_httpClient.Dispose();
|
|
_disposed = true;
|
|
}
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
// Private HTTP methods
|
|
internal async Task<T> GetAsync<T>(string path, CancellationToken ct)
|
|
{
|
|
return await ExecuteWithRetryAsync(async () =>
|
|
{
|
|
var response = await _httpClient.GetAsync(path, ct);
|
|
await EnsureSuccessAsync(response);
|
|
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)
|
|
?? throw new DatabaseException("Failed to deserialize response");
|
|
});
|
|
}
|
|
|
|
internal async Task<T> PostAsync<T>(string path, object body, CancellationToken ct)
|
|
{
|
|
return await ExecuteWithRetryAsync(async () =>
|
|
{
|
|
var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct);
|
|
await EnsureSuccessAsync(response);
|
|
var content = await response.Content.ReadAsStringAsync(ct);
|
|
if (string.IsNullOrEmpty(content)) return default!;
|
|
return JsonSerializer.Deserialize<T>(content, _jsonOptions)!;
|
|
});
|
|
}
|
|
|
|
internal async Task<T> PutAsync<T>(string path, object body, CancellationToken ct)
|
|
{
|
|
return await ExecuteWithRetryAsync(async () =>
|
|
{
|
|
var response = await _httpClient.PutAsJsonAsync(path, body, _jsonOptions, ct);
|
|
await EnsureSuccessAsync(response);
|
|
var content = await response.Content.ReadAsStringAsync(ct);
|
|
if (string.IsNullOrEmpty(content)) return default!;
|
|
return JsonSerializer.Deserialize<T>(content, _jsonOptions)!;
|
|
});
|
|
}
|
|
|
|
internal async Task<T> PatchAsync<T>(string path, object body, CancellationToken ct)
|
|
{
|
|
return await ExecuteWithRetryAsync(async () =>
|
|
{
|
|
var request = new HttpRequestMessage(HttpMethod.Patch, path)
|
|
{
|
|
Content = JsonContent.Create(body, options: _jsonOptions)
|
|
};
|
|
var response = await _httpClient.SendAsync(request, ct);
|
|
await EnsureSuccessAsync(response);
|
|
var content = await response.Content.ReadAsStringAsync(ct);
|
|
if (string.IsNullOrEmpty(content)) return default!;
|
|
return JsonSerializer.Deserialize<T>(content, _jsonOptions)!;
|
|
});
|
|
}
|
|
|
|
internal async Task DeleteAsync(string path, CancellationToken ct)
|
|
{
|
|
await ExecuteWithRetryAsync(async () =>
|
|
{
|
|
var response = await _httpClient.DeleteAsync(path, ct);
|
|
await EnsureSuccessAsync(response);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
internal async Task DeleteAsync(string path, object body, CancellationToken ct)
|
|
{
|
|
await ExecuteWithRetryAsync(async () =>
|
|
{
|
|
var request = new HttpRequestMessage(HttpMethod.Delete, path)
|
|
{
|
|
Content = JsonContent.Create(body, options: _jsonOptions)
|
|
};
|
|
var response = await _httpClient.SendAsync(request, ct);
|
|
await EnsureSuccessAsync(response);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation)
|
|
{
|
|
Exception? lastError = null;
|
|
|
|
for (int attempt = 0; attempt < _config.Retries; attempt++)
|
|
{
|
|
try
|
|
{
|
|
return await operation();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
lastError = ex;
|
|
if (_config.Debug) Console.WriteLine($"Attempt {attempt + 1} failed: {ex.Message}");
|
|
if (attempt < _config.Retries - 1)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(1 << attempt));
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError ?? new DatabaseException("Unknown error");
|
|
}
|
|
|
|
private async Task EnsureSuccessAsync(HttpResponseMessage response)
|
|
{
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var error = JsonSerializer.Deserialize<Dictionary<string, object>>(content, _jsonOptions);
|
|
var message = error?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)response.StatusCode}";
|
|
var code = error?.GetValueOrDefault("code")?.ToString();
|
|
throw new DatabaseException(message, code, (int)response.StatusCode);
|
|
}
|
|
}
|
|
}
|