feat(sdk/kotlin): add Database, Hosting, and Bridge SDKs
- SynorDatabase: Multi-model database with KV, Document, Vector, TimeSeries stores - SynorHosting: Domain management, DNS, deployments, SSL, analytics - SynorBridge: Cross-chain transfers with lock-mint and burn-unlock patterns
This commit is contained in:
parent
dd01a06116
commit
14cd439552
3 changed files with 1124 additions and 0 deletions
469
sdk/kotlin/src/main/kotlin/io/synor/bridge/SynorBridge.kt
Normal file
469
sdk/kotlin/src/main/kotlin/io/synor/bridge/SynorBridge.kt
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
package io.synor.bridge
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* Synor Bridge SDK for Kotlin.
|
||||
* Cross-chain asset transfers with lock-mint and burn-unlock patterns.
|
||||
*/
|
||||
class SynorBridge(private val config: BridgeConfig) : AutoCloseable {
|
||||
|
||||
companion object {
|
||||
private val gson: Gson = GsonBuilder().create()
|
||||
private val FINAL_STATUSES = setOf(TransferStatus.completed, TransferStatus.failed, TransferStatus.refunded)
|
||||
}
|
||||
|
||||
private val httpClient = HttpClient(CIO) {
|
||||
engine { requestTimeout = config.timeoutSecs * 1000 }
|
||||
}
|
||||
private val closed = AtomicBoolean(false)
|
||||
|
||||
constructor(apiKey: String) : this(BridgeConfig(apiKey))
|
||||
|
||||
// ==================== Chain Operations ====================
|
||||
|
||||
suspend fun getSupportedChains(): List<Chain> {
|
||||
val resp = request("GET", "/chains")
|
||||
val result = gson.fromJson<ChainsResponse>(resp, ChainsResponse::class.java)
|
||||
return result.chains ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun getChain(chainId: ChainId): Chain {
|
||||
val resp = request("GET", "/chains/${chainId.name.lowercase()}")
|
||||
return gson.fromJson(resp, Chain::class.java)
|
||||
}
|
||||
|
||||
suspend fun isChainSupported(chainId: ChainId): Boolean = try {
|
||||
getChain(chainId).supported
|
||||
} catch (e: Exception) { false }
|
||||
|
||||
// ==================== Asset Operations ====================
|
||||
|
||||
suspend fun getSupportedAssets(chainId: ChainId): List<Asset> {
|
||||
val resp = request("GET", "/chains/${chainId.name.lowercase()}/assets")
|
||||
val result = gson.fromJson<AssetsResponse>(resp, AssetsResponse::class.java)
|
||||
return result.assets ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun getAsset(assetId: String): Asset {
|
||||
val resp = request("GET", "/assets/${assetId.urlEncode()}")
|
||||
return gson.fromJson(resp, Asset::class.java)
|
||||
}
|
||||
|
||||
suspend fun getWrappedAsset(originalAssetId: String, targetChain: ChainId): WrappedAsset {
|
||||
val path = "/assets/${originalAssetId.urlEncode()}/wrapped/${targetChain.name.lowercase()}"
|
||||
val resp = request("GET", path)
|
||||
return gson.fromJson(resp, WrappedAsset::class.java)
|
||||
}
|
||||
|
||||
// ==================== Fee & Rate Operations ====================
|
||||
|
||||
suspend fun estimateFee(asset: String, amount: String, sourceChain: ChainId, targetChain: ChainId): FeeEstimate {
|
||||
val body = mapOf(
|
||||
"asset" to asset,
|
||||
"amount" to amount,
|
||||
"sourceChain" to sourceChain.name.lowercase(),
|
||||
"targetChain" to targetChain.name.lowercase()
|
||||
)
|
||||
val resp = request("POST", "/fees/estimate", body)
|
||||
return gson.fromJson(resp, FeeEstimate::class.java)
|
||||
}
|
||||
|
||||
suspend fun getExchangeRate(fromAsset: String, toAsset: String): ExchangeRate {
|
||||
val resp = request("GET", "/rates/${fromAsset.urlEncode()}/${toAsset.urlEncode()}")
|
||||
return gson.fromJson(resp, ExchangeRate::class.java)
|
||||
}
|
||||
|
||||
// ==================== Lock-Mint Flow ====================
|
||||
|
||||
suspend fun lock(asset: String, amount: String, targetChain: ChainId, options: LockOptions? = null): LockReceipt {
|
||||
val body = mutableMapOf<String, Any>(
|
||||
"asset" to asset,
|
||||
"amount" to amount,
|
||||
"targetChain" to targetChain.name.lowercase()
|
||||
)
|
||||
options?.recipient?.let { body["recipient"] = it }
|
||||
options?.deadline?.let { body["deadline"] = it }
|
||||
options?.slippage?.let { body["slippage"] = it }
|
||||
val resp = request("POST", "/transfers/lock", body)
|
||||
return gson.fromJson(resp, LockReceipt::class.java)
|
||||
}
|
||||
|
||||
suspend fun getLockProof(lockReceiptId: String): LockProof {
|
||||
val resp = request("GET", "/transfers/lock/${lockReceiptId.urlEncode()}/proof")
|
||||
return gson.fromJson(resp, LockProof::class.java)
|
||||
}
|
||||
|
||||
suspend fun waitForLockProof(lockReceiptId: String, pollIntervalMs: Long = 5000, maxWaitMs: Long = 600000): LockProof {
|
||||
val deadline = System.currentTimeMillis() + maxWaitMs
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
try {
|
||||
return getLockProof(lockReceiptId)
|
||||
} catch (e: BridgeException) {
|
||||
if (e.code == "CONFIRMATIONS_PENDING") {
|
||||
delay(pollIntervalMs)
|
||||
continue
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
throw BridgeException("Timeout waiting for lock proof", "CONFIRMATIONS_PENDING")
|
||||
}
|
||||
|
||||
suspend fun mint(proof: LockProof, targetAddress: String, options: MintOptions? = null): SignedTransaction {
|
||||
val body = mutableMapOf<String, Any>("proof" to proof, "targetAddress" to targetAddress)
|
||||
options?.gasLimit?.let { body["gasLimit"] = it }
|
||||
options?.maxFeePerGas?.let { body["maxFeePerGas"] = it }
|
||||
options?.maxPriorityFeePerGas?.let { body["maxPriorityFeePerGas"] = it }
|
||||
val resp = request("POST", "/transfers/mint", body)
|
||||
return gson.fromJson(resp, SignedTransaction::class.java)
|
||||
}
|
||||
|
||||
// ==================== Burn-Unlock Flow ====================
|
||||
|
||||
suspend fun burn(wrappedAsset: String, amount: String, options: BurnOptions? = null): BurnReceipt {
|
||||
val body = mutableMapOf<String, Any>("wrappedAsset" to wrappedAsset, "amount" to amount)
|
||||
options?.recipient?.let { body["recipient"] = it }
|
||||
options?.deadline?.let { body["deadline"] = it }
|
||||
val resp = request("POST", "/transfers/burn", body)
|
||||
return gson.fromJson(resp, BurnReceipt::class.java)
|
||||
}
|
||||
|
||||
suspend fun getBurnProof(burnReceiptId: String): BurnProof {
|
||||
val resp = request("GET", "/transfers/burn/${burnReceiptId.urlEncode()}/proof")
|
||||
return gson.fromJson(resp, BurnProof::class.java)
|
||||
}
|
||||
|
||||
suspend fun waitForBurnProof(burnReceiptId: String, pollIntervalMs: Long = 5000, maxWaitMs: Long = 600000): BurnProof {
|
||||
val deadline = System.currentTimeMillis() + maxWaitMs
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
try {
|
||||
return getBurnProof(burnReceiptId)
|
||||
} catch (e: BridgeException) {
|
||||
if (e.code == "CONFIRMATIONS_PENDING") {
|
||||
delay(pollIntervalMs)
|
||||
continue
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
throw BridgeException("Timeout waiting for burn proof", "CONFIRMATIONS_PENDING")
|
||||
}
|
||||
|
||||
suspend fun unlock(proof: BurnProof, options: UnlockOptions? = null): SignedTransaction {
|
||||
val body = mutableMapOf<String, Any>("proof" to proof)
|
||||
options?.gasLimit?.let { body["gasLimit"] = it }
|
||||
options?.gasPrice?.let { body["gasPrice"] = it }
|
||||
val resp = request("POST", "/transfers/unlock", body)
|
||||
return gson.fromJson(resp, SignedTransaction::class.java)
|
||||
}
|
||||
|
||||
// ==================== Transfer Management ====================
|
||||
|
||||
suspend fun getTransfer(transferId: String): Transfer {
|
||||
val resp = request("GET", "/transfers/${transferId.urlEncode()}")
|
||||
return gson.fromJson(resp, Transfer::class.java)
|
||||
}
|
||||
|
||||
suspend fun getTransferStatus(transferId: String): TransferStatus = getTransfer(transferId).status
|
||||
|
||||
suspend fun listTransfers(filter: TransferFilter? = null): List<Transfer> {
|
||||
val params = mutableListOf<String>()
|
||||
filter?.status?.let { params.add("status=${it.name.lowercase()}") }
|
||||
filter?.sourceChain?.let { params.add("sourceChain=${it.name.lowercase()}") }
|
||||
filter?.targetChain?.let { params.add("targetChain=${it.name.lowercase()}") }
|
||||
filter?.limit?.let { params.add("limit=$it") }
|
||||
filter?.offset?.let { params.add("offset=$it") }
|
||||
|
||||
val path = if (params.isNotEmpty()) "/transfers?${params.joinToString("&")}" else "/transfers"
|
||||
val resp = request("GET", path)
|
||||
val result = gson.fromJson<TransfersResponse>(resp, TransfersResponse::class.java)
|
||||
return result.transfers ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun waitForTransfer(transferId: String, pollIntervalMs: Long = 10000, maxWaitMs: Long = 1800000): Transfer {
|
||||
val deadline = System.currentTimeMillis() + maxWaitMs
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
val transfer = getTransfer(transferId)
|
||||
if (transfer.status in FINAL_STATUSES) return transfer
|
||||
delay(pollIntervalMs)
|
||||
}
|
||||
throw BridgeException("Timeout waiting for transfer completion")
|
||||
}
|
||||
|
||||
// ==================== Convenience Methods ====================
|
||||
|
||||
suspend fun bridgeTo(
|
||||
asset: String,
|
||||
amount: String,
|
||||
targetChain: ChainId,
|
||||
targetAddress: String,
|
||||
lockOptions: LockOptions? = null,
|
||||
mintOptions: MintOptions? = null
|
||||
): Transfer {
|
||||
val lockReceipt = lock(asset, amount, targetChain, lockOptions)
|
||||
if (config.debug) println("Locked: ${lockReceipt.id}, waiting for confirmations...")
|
||||
|
||||
val proof = waitForLockProof(lockReceipt.id)
|
||||
if (config.debug) println("Proof ready, minting on ${targetChain.name}...")
|
||||
|
||||
mint(proof, targetAddress, mintOptions)
|
||||
return waitForTransfer(lockReceipt.id)
|
||||
}
|
||||
|
||||
suspend fun bridgeBack(
|
||||
wrappedAsset: String,
|
||||
amount: String,
|
||||
burnOptions: BurnOptions? = null,
|
||||
unlockOptions: UnlockOptions? = null
|
||||
): Transfer {
|
||||
val burnReceipt = burn(wrappedAsset, amount, burnOptions)
|
||||
if (config.debug) println("Burned: ${burnReceipt.id}, waiting for confirmations...")
|
||||
|
||||
val proof = waitForBurnProof(burnReceipt.id)
|
||||
if (config.debug) println("Proof ready, unlocking on ${burnReceipt.targetChain}...")
|
||||
|
||||
unlock(proof, unlockOptions)
|
||||
return waitForTransfer(burnReceipt.id)
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
override fun close() {
|
||||
closed.set(true)
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
fun isClosed(): Boolean = closed.get()
|
||||
|
||||
suspend fun healthCheck(): Boolean = try {
|
||||
val resp = request("GET", "/health")
|
||||
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
|
||||
map["status"] == "healthy"
|
||||
} catch (e: Exception) { false }
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
private suspend fun request(method: String, path: String, body: Any? = null): String {
|
||||
if (closed.get()) throw BridgeException("Client has been closed")
|
||||
|
||||
var lastError: Exception? = null
|
||||
repeat(config.retries) { attempt ->
|
||||
try {
|
||||
return doRequest(method, path, body)
|
||||
} catch (e: Exception) {
|
||||
lastError = e
|
||||
if (attempt < config.retries - 1) delay((1L shl attempt) * 1000)
|
||||
}
|
||||
}
|
||||
throw lastError ?: BridgeException("Unknown error")
|
||||
}
|
||||
|
||||
private suspend fun doRequest(method: String, path: String, body: Any?): String {
|
||||
val url = "${config.endpoint}$path"
|
||||
val response: HttpResponse = httpClient.request(url) {
|
||||
this.method = HttpMethod.parse(method)
|
||||
header("Authorization", "Bearer ${config.apiKey}")
|
||||
header("Content-Type", "application/json")
|
||||
header("X-SDK-Version", "kotlin/0.1.0")
|
||||
body?.let { setBody(gson.toJson(it)) }
|
||||
}
|
||||
|
||||
val responseBody = response.bodyAsText()
|
||||
if (response.status.value >= 400) {
|
||||
val errorMap = try {
|
||||
gson.fromJson<Map<String, Any>>(responseBody, object : TypeToken<Map<String, Any>>() {}.type)
|
||||
} catch (e: Exception) { emptyMap() }
|
||||
throw BridgeException(
|
||||
errorMap["message"] as? String ?: "HTTP ${response.status.value}",
|
||||
errorMap["code"] as? String,
|
||||
response.status.value
|
||||
)
|
||||
}
|
||||
return responseBody
|
||||
}
|
||||
|
||||
private fun String.urlEncode(): String = java.net.URLEncoder.encode(this, "UTF-8")
|
||||
}
|
||||
|
||||
// ==================== Types ====================
|
||||
|
||||
data class BridgeConfig(
|
||||
val apiKey: String,
|
||||
val endpoint: String = "https://bridge.synor.io/v1",
|
||||
val timeoutSecs: Long = 60,
|
||||
val retries: Int = 3,
|
||||
val debug: Boolean = false
|
||||
)
|
||||
|
||||
enum class ChainId { synor, ethereum, polygon, arbitrum, optimism, bsc, avalanche, solana, cosmos }
|
||||
enum class AssetType { native, erc20, erc721, erc1155 }
|
||||
enum class TransferStatus { pending, locked, confirming, minting, completed, failed, refunded }
|
||||
enum class TransferDirection { lock_mint, burn_unlock }
|
||||
|
||||
data class NativeCurrency(val name: String, val symbol: String, val decimals: Int)
|
||||
|
||||
data class Chain(
|
||||
val id: ChainId,
|
||||
val name: String,
|
||||
val chainId: Long,
|
||||
val rpcUrl: String,
|
||||
val explorerUrl: String,
|
||||
val nativeCurrency: NativeCurrency,
|
||||
val confirmations: Int,
|
||||
val estimatedBlockTime: Int,
|
||||
val supported: Boolean
|
||||
)
|
||||
|
||||
data class Asset(
|
||||
val id: String,
|
||||
val symbol: String,
|
||||
val name: String,
|
||||
val type: AssetType,
|
||||
val chain: ChainId,
|
||||
val contractAddress: String? = null,
|
||||
val decimals: Int,
|
||||
val logoUrl: String? = null,
|
||||
val verified: Boolean = false
|
||||
)
|
||||
|
||||
data class WrappedAsset(
|
||||
val originalAsset: Asset,
|
||||
val wrappedAsset: Asset,
|
||||
val chain: ChainId,
|
||||
val bridgeContract: String
|
||||
)
|
||||
|
||||
data class ValidatorSignature(val validator: String, val signature: String, val timestamp: Long)
|
||||
|
||||
data class LockReceipt(
|
||||
val id: String,
|
||||
val txHash: String,
|
||||
val sourceChain: ChainId,
|
||||
val targetChain: ChainId,
|
||||
val asset: Asset,
|
||||
val amount: String,
|
||||
val sender: String,
|
||||
val recipient: String,
|
||||
val lockTimestamp: Long,
|
||||
val confirmations: Int,
|
||||
val requiredConfirmations: Int
|
||||
)
|
||||
|
||||
data class LockProof(
|
||||
val lockReceipt: LockReceipt,
|
||||
val merkleProof: List<String>,
|
||||
val blockHeader: String,
|
||||
val signatures: List<ValidatorSignature>
|
||||
)
|
||||
|
||||
data class BurnReceipt(
|
||||
val id: String,
|
||||
val txHash: String,
|
||||
val sourceChain: ChainId,
|
||||
val targetChain: ChainId,
|
||||
val wrappedAsset: Asset,
|
||||
val originalAsset: Asset,
|
||||
val amount: String,
|
||||
val sender: String,
|
||||
val recipient: String,
|
||||
val burnTimestamp: Long,
|
||||
val confirmations: Int,
|
||||
val requiredConfirmations: Int
|
||||
)
|
||||
|
||||
data class BurnProof(
|
||||
val burnReceipt: BurnReceipt,
|
||||
val merkleProof: List<String>,
|
||||
val blockHeader: String,
|
||||
val signatures: List<ValidatorSignature>
|
||||
)
|
||||
|
||||
data class Transfer(
|
||||
val id: String,
|
||||
val direction: TransferDirection,
|
||||
val status: TransferStatus,
|
||||
val sourceChain: ChainId,
|
||||
val targetChain: ChainId,
|
||||
val asset: Asset,
|
||||
val amount: String,
|
||||
val sender: String,
|
||||
val recipient: String,
|
||||
val sourceTxHash: String? = null,
|
||||
val targetTxHash: String? = null,
|
||||
val fee: String,
|
||||
val feeAsset: Asset,
|
||||
val createdAt: Long,
|
||||
val updatedAt: Long,
|
||||
val completedAt: Long? = null,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
data class FeeEstimate(
|
||||
val bridgeFee: String,
|
||||
val gasFeeSource: String,
|
||||
val gasFeeTarget: String,
|
||||
val totalFee: String,
|
||||
val feeAsset: Asset,
|
||||
val estimatedTime: Int,
|
||||
val exchangeRate: String? = null
|
||||
)
|
||||
|
||||
data class ExchangeRate(
|
||||
val fromAsset: Asset,
|
||||
val toAsset: Asset,
|
||||
val rate: String,
|
||||
val inverseRate: String,
|
||||
val lastUpdated: Long,
|
||||
val source: String
|
||||
)
|
||||
|
||||
data class SignedTransaction(
|
||||
val txHash: String,
|
||||
val chain: ChainId,
|
||||
val from: String,
|
||||
val to: String,
|
||||
val value: String,
|
||||
val data: String,
|
||||
val gasLimit: String,
|
||||
val gasPrice: String? = null,
|
||||
val maxFeePerGas: String? = null,
|
||||
val maxPriorityFeePerGas: String? = null,
|
||||
val nonce: Int,
|
||||
val signature: String
|
||||
)
|
||||
|
||||
data class LockOptions(val recipient: String? = null, val deadline: Long? = null, val slippage: Double? = null)
|
||||
data class MintOptions(val gasLimit: String? = null, val maxFeePerGas: String? = null, val maxPriorityFeePerGas: String? = null)
|
||||
data class BurnOptions(val recipient: String? = null, val deadline: Long? = null)
|
||||
data class UnlockOptions(val gasLimit: String? = null, val gasPrice: String? = null)
|
||||
|
||||
data class TransferFilter(
|
||||
val status: TransferStatus? = null,
|
||||
val sourceChain: ChainId? = null,
|
||||
val targetChain: ChainId? = null,
|
||||
val asset: String? = null,
|
||||
val sender: String? = null,
|
||||
val recipient: String? = null,
|
||||
val limit: Int? = null,
|
||||
val offset: Int? = null
|
||||
)
|
||||
|
||||
internal data class ChainsResponse(val chains: List<Chain>?)
|
||||
internal data class AssetsResponse(val assets: List<Asset>?)
|
||||
internal data class TransfersResponse(val transfers: List<Transfer>?)
|
||||
|
||||
class BridgeException(
|
||||
message: String,
|
||||
val code: String? = null,
|
||||
val statusCode: Int = 0
|
||||
) : RuntimeException(message)
|
||||
271
sdk/kotlin/src/main/kotlin/io/synor/database/SynorDatabase.kt
Normal file
271
sdk/kotlin/src/main/kotlin/io/synor/database/SynorDatabase.kt
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
package io.synor.database
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* Synor Database SDK for Kotlin.
|
||||
* Multi-model database with Key-Value, Document, Vector, and Time Series stores.
|
||||
*/
|
||||
class SynorDatabase(private val config: DatabaseConfig) : AutoCloseable {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_ENDPOINT = "https://db.synor.io/v1"
|
||||
private val gson: Gson = GsonBuilder().create()
|
||||
}
|
||||
|
||||
private val httpClient = HttpClient(CIO) {
|
||||
engine {
|
||||
requestTimeout = config.timeoutSecs * 1000
|
||||
}
|
||||
}
|
||||
private val closed = AtomicBoolean(false)
|
||||
|
||||
val kv = KeyValueStore(this)
|
||||
val documents = DocumentStore(this)
|
||||
val vectors = VectorStore(this)
|
||||
val timeseries = TimeSeriesStore(this)
|
||||
|
||||
constructor(apiKey: String) : this(DatabaseConfig(apiKey))
|
||||
|
||||
// ==================== Key-Value Store ====================
|
||||
|
||||
class KeyValueStore internal constructor(private val db: SynorDatabase) {
|
||||
|
||||
suspend fun get(key: String): Any? {
|
||||
val resp = db.request("GET", "/kv/${key.urlEncode()}")
|
||||
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
|
||||
return map["value"]
|
||||
}
|
||||
|
||||
suspend fun set(key: String, value: Any, ttl: Int? = null) {
|
||||
val body = mutableMapOf<String, Any>("key" to key, "value" to value)
|
||||
ttl?.let { body["ttl"] = it }
|
||||
db.request("PUT", "/kv/${key.urlEncode()}", body)
|
||||
}
|
||||
|
||||
suspend fun delete(key: String) {
|
||||
db.request("DELETE", "/kv/${key.urlEncode()}")
|
||||
}
|
||||
|
||||
suspend fun list(prefix: String): List<KeyValue> {
|
||||
val resp = db.request("GET", "/kv?prefix=${prefix.urlEncode()}")
|
||||
val result = gson.fromJson<ListResponse<KeyValue>>(resp, object : TypeToken<ListResponse<KeyValue>>() {}.type)
|
||||
return result.items ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Document Store ====================
|
||||
|
||||
class DocumentStore internal constructor(private val db: SynorDatabase) {
|
||||
|
||||
suspend fun create(collection: String, document: Map<String, Any>): String {
|
||||
val resp = db.request("POST", "/collections/${collection.urlEncode()}/documents", document)
|
||||
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
|
||||
return map["id"] as String
|
||||
}
|
||||
|
||||
suspend fun get(collection: String, id: String): Document {
|
||||
val resp = db.request("GET", "/collections/${collection.urlEncode()}/documents/${id.urlEncode()}")
|
||||
return gson.fromJson(resp, Document::class.java)
|
||||
}
|
||||
|
||||
suspend fun update(collection: String, id: String, update: Map<String, Any>) {
|
||||
db.request("PATCH", "/collections/${collection.urlEncode()}/documents/${id.urlEncode()}", update)
|
||||
}
|
||||
|
||||
suspend fun delete(collection: String, id: String) {
|
||||
db.request("DELETE", "/collections/${collection.urlEncode()}/documents/${id.urlEncode()}")
|
||||
}
|
||||
|
||||
suspend fun query(collection: String, query: Query): List<Document> {
|
||||
val resp = db.request("POST", "/collections/${collection.urlEncode()}/query", query)
|
||||
val result = gson.fromJson<DocumentListResponse>(resp, DocumentListResponse::class.java)
|
||||
return result.documents ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Vector Store ====================
|
||||
|
||||
class VectorStore internal constructor(private val db: SynorDatabase) {
|
||||
|
||||
suspend fun upsert(collection: String, vectors: List<VectorEntry>) {
|
||||
db.request("POST", "/vectors/${collection.urlEncode()}/upsert", mapOf("vectors" to vectors))
|
||||
}
|
||||
|
||||
suspend fun search(collection: String, vector: DoubleArray, k: Int): List<SearchResult> {
|
||||
val resp = db.request("POST", "/vectors/${collection.urlEncode()}/search", mapOf("vector" to vector, "k" to k))
|
||||
val result = gson.fromJson<SearchListResponse>(resp, SearchListResponse::class.java)
|
||||
return result.results ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun delete(collection: String, ids: List<String>) {
|
||||
db.request("DELETE", "/vectors/${collection.urlEncode()}", mapOf("ids" to ids))
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Time Series Store ====================
|
||||
|
||||
class TimeSeriesStore internal constructor(private val db: SynorDatabase) {
|
||||
|
||||
suspend fun write(series: String, points: List<DataPoint>) {
|
||||
db.request("POST", "/timeseries/${series.urlEncode()}/write", mapOf("points" to points))
|
||||
}
|
||||
|
||||
suspend fun query(series: String, range: TimeRange, aggregation: Aggregation? = null): List<DataPoint> {
|
||||
val body = mutableMapOf<String, Any>("range" to range)
|
||||
aggregation?.let { body["aggregation"] = it }
|
||||
val resp = db.request("POST", "/timeseries/${series.urlEncode()}/query", body)
|
||||
val result = gson.fromJson<DataPointListResponse>(resp, DataPointListResponse::class.java)
|
||||
return result.points ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
override fun close() {
|
||||
closed.set(true)
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
fun isClosed(): Boolean = closed.get()
|
||||
|
||||
suspend fun healthCheck(): Boolean {
|
||||
return try {
|
||||
val resp = request("GET", "/health")
|
||||
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
|
||||
map["status"] == "healthy"
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
internal suspend fun request(method: String, path: String, body: Any? = null): String {
|
||||
if (closed.get()) throw DatabaseException("Client has been closed")
|
||||
|
||||
var lastError: Exception? = null
|
||||
repeat(config.retries) { attempt ->
|
||||
try {
|
||||
return doRequest(method, path, body)
|
||||
} catch (e: Exception) {
|
||||
lastError = e
|
||||
if (attempt < config.retries - 1) {
|
||||
delay((1L shl attempt) * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError ?: DatabaseException("Unknown error")
|
||||
}
|
||||
|
||||
private suspend fun doRequest(method: String, path: String, body: Any?): String {
|
||||
val url = "${config.endpoint}$path"
|
||||
|
||||
val response: HttpResponse = httpClient.request(url) {
|
||||
this.method = HttpMethod.parse(method)
|
||||
header("Authorization", "Bearer ${config.apiKey}")
|
||||
header("Content-Type", "application/json")
|
||||
header("X-SDK-Version", "kotlin/0.1.0")
|
||||
body?.let { setBody(gson.toJson(it)) }
|
||||
}
|
||||
|
||||
val responseBody = response.bodyAsText()
|
||||
|
||||
if (response.status.value >= 400) {
|
||||
val errorMap = try {
|
||||
gson.fromJson<Map<String, Any>>(responseBody, object : TypeToken<Map<String, Any>>() {}.type)
|
||||
} catch (e: Exception) { emptyMap() }
|
||||
|
||||
val message = errorMap["message"] as? String ?: "HTTP ${response.status.value}"
|
||||
throw DatabaseException(message, errorMap["code"] as? String, response.status.value)
|
||||
}
|
||||
|
||||
return responseBody
|
||||
}
|
||||
|
||||
private fun String.urlEncode(): String = java.net.URLEncoder.encode(this, "UTF-8")
|
||||
}
|
||||
|
||||
// ==================== Types ====================
|
||||
|
||||
data class DatabaseConfig(
|
||||
val apiKey: String,
|
||||
val endpoint: String = "https://db.synor.io/v1",
|
||||
val timeoutSecs: Long = 60,
|
||||
val retries: Int = 3,
|
||||
val debug: Boolean = false
|
||||
)
|
||||
|
||||
data class KeyValue(
|
||||
val key: String,
|
||||
val value: Any?,
|
||||
val ttl: Int? = null,
|
||||
val createdAt: Long? = null,
|
||||
val updatedAt: Long? = null
|
||||
)
|
||||
|
||||
data class Document(
|
||||
val id: String,
|
||||
val collection: String,
|
||||
val data: Map<String, Any>,
|
||||
val createdAt: Long,
|
||||
val updatedAt: Long
|
||||
)
|
||||
|
||||
data class Query(
|
||||
val filter: Map<String, Any>? = null,
|
||||
val sort: Map<String, Int>? = null,
|
||||
val limit: Int? = null,
|
||||
val offset: Int? = null,
|
||||
val projection: List<String>? = null
|
||||
)
|
||||
|
||||
data class VectorEntry(
|
||||
val id: String,
|
||||
val vector: DoubleArray,
|
||||
val metadata: Map<String, Any>? = null
|
||||
)
|
||||
|
||||
data class SearchResult(
|
||||
val id: String,
|
||||
val score: Double,
|
||||
val vector: DoubleArray? = null,
|
||||
val metadata: Map<String, Any>? = null
|
||||
)
|
||||
|
||||
data class DataPoint(
|
||||
val timestamp: Long,
|
||||
val value: Double,
|
||||
val tags: Map<String, String>? = null
|
||||
)
|
||||
|
||||
data class TimeRange(
|
||||
val start: Long,
|
||||
val end: Long
|
||||
)
|
||||
|
||||
data class Aggregation(
|
||||
val function: String,
|
||||
val interval: String
|
||||
)
|
||||
|
||||
// Response types
|
||||
internal data class ListResponse<T>(val items: List<T>?)
|
||||
internal data class DocumentListResponse(val documents: List<Document>?)
|
||||
internal data class SearchListResponse(val results: List<SearchResult>?)
|
||||
internal data class DataPointListResponse(val points: List<DataPoint>?)
|
||||
|
||||
class DatabaseException(
|
||||
message: String,
|
||||
val code: String? = null,
|
||||
val statusCode: Int = 0
|
||||
) : RuntimeException(message)
|
||||
384
sdk/kotlin/src/main/kotlin/io/synor/hosting/SynorHosting.kt
Normal file
384
sdk/kotlin/src/main/kotlin/io/synor/hosting/SynorHosting.kt
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
package io.synor.hosting
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* Synor Hosting SDK for Kotlin.
|
||||
* Decentralized web hosting with domain management, DNS, and deployments.
|
||||
*/
|
||||
class SynorHosting(private val config: HostingConfig) : AutoCloseable {
|
||||
|
||||
companion object {
|
||||
private val gson: Gson = GsonBuilder().create()
|
||||
}
|
||||
|
||||
private val httpClient = HttpClient(CIO) {
|
||||
engine { requestTimeout = config.timeoutSecs * 1000 }
|
||||
}
|
||||
private val closed = AtomicBoolean(false)
|
||||
|
||||
constructor(apiKey: String) : this(HostingConfig(apiKey))
|
||||
|
||||
// ==================== Domain Operations ====================
|
||||
|
||||
suspend fun checkAvailability(name: String): DomainAvailability {
|
||||
val resp = request("GET", "/domains/check/${name.urlEncode()}")
|
||||
return gson.fromJson(resp, DomainAvailability::class.java)
|
||||
}
|
||||
|
||||
suspend fun registerDomain(name: String, options: RegisterDomainOptions? = null): Domain {
|
||||
val body = mutableMapOf<String, Any>("name" to name)
|
||||
options?.years?.let { body["years"] = it }
|
||||
options?.autoRenew?.let { body["autoRenew"] = it }
|
||||
val resp = request("POST", "/domains", body)
|
||||
return gson.fromJson(resp, Domain::class.java)
|
||||
}
|
||||
|
||||
suspend fun getDomain(name: String): Domain {
|
||||
val resp = request("GET", "/domains/${name.urlEncode()}")
|
||||
return gson.fromJson(resp, Domain::class.java)
|
||||
}
|
||||
|
||||
suspend fun listDomains(): List<Domain> {
|
||||
val resp = request("GET", "/domains")
|
||||
val result = gson.fromJson<DomainsResponse>(resp, DomainsResponse::class.java)
|
||||
return result.domains ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun updateDomainRecord(name: String, record: DomainRecord): Domain {
|
||||
val resp = request("PUT", "/domains/${name.urlEncode()}/record", record)
|
||||
return gson.fromJson(resp, Domain::class.java)
|
||||
}
|
||||
|
||||
suspend fun resolveDomain(name: String): DomainRecord {
|
||||
val resp = request("GET", "/domains/${name.urlEncode()}/resolve")
|
||||
return gson.fromJson(resp, DomainRecord::class.java)
|
||||
}
|
||||
|
||||
suspend fun renewDomain(name: String, years: Int): Domain {
|
||||
val resp = request("POST", "/domains/${name.urlEncode()}/renew", mapOf("years" to years))
|
||||
return gson.fromJson(resp, Domain::class.java)
|
||||
}
|
||||
|
||||
// ==================== DNS Operations ====================
|
||||
|
||||
suspend fun getDnsZone(domain: String): DnsZone {
|
||||
val resp = request("GET", "/dns/${domain.urlEncode()}")
|
||||
return gson.fromJson(resp, DnsZone::class.java)
|
||||
}
|
||||
|
||||
suspend fun setDnsRecords(domain: String, records: List<DnsRecord>): DnsZone {
|
||||
val resp = request("PUT", "/dns/${domain.urlEncode()}", mapOf("records" to records))
|
||||
return gson.fromJson(resp, DnsZone::class.java)
|
||||
}
|
||||
|
||||
suspend fun addDnsRecord(domain: String, record: DnsRecord): DnsZone {
|
||||
val resp = request("POST", "/dns/${domain.urlEncode()}/records", record)
|
||||
return gson.fromJson(resp, DnsZone::class.java)
|
||||
}
|
||||
|
||||
suspend fun deleteDnsRecord(domain: String, recordType: String, name: String): DnsZone {
|
||||
val path = "/dns/${domain.urlEncode()}/records/$recordType/${name.urlEncode()}"
|
||||
val resp = request("DELETE", path)
|
||||
return gson.fromJson(resp, DnsZone::class.java)
|
||||
}
|
||||
|
||||
// ==================== Deployment Operations ====================
|
||||
|
||||
suspend fun deploy(cid: String, options: DeployOptions? = null): Deployment {
|
||||
val body = mutableMapOf<String, Any>("cid" to cid)
|
||||
options?.domain?.let { body["domain"] = it }
|
||||
options?.subdomain?.let { body["subdomain"] = it }
|
||||
options?.spa?.let { body["spa"] = it }
|
||||
options?.cleanUrls?.let { body["cleanUrls"] = it }
|
||||
val resp = request("POST", "/deployments", body)
|
||||
return gson.fromJson(resp, Deployment::class.java)
|
||||
}
|
||||
|
||||
suspend fun getDeployment(id: String): Deployment {
|
||||
val resp = request("GET", "/deployments/${id.urlEncode()}")
|
||||
return gson.fromJson(resp, Deployment::class.java)
|
||||
}
|
||||
|
||||
suspend fun listDeployments(domain: String? = null): List<Deployment> {
|
||||
val path = domain?.let { "/deployments?domain=${it.urlEncode()}" } ?: "/deployments"
|
||||
val resp = request("GET", path)
|
||||
val result = gson.fromJson<DeploymentsResponse>(resp, DeploymentsResponse::class.java)
|
||||
return result.deployments ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun rollback(domain: String, deploymentId: String): Deployment {
|
||||
val resp = request("POST", "/deployments/${deploymentId.urlEncode()}/rollback", mapOf("domain" to domain))
|
||||
return gson.fromJson(resp, Deployment::class.java)
|
||||
}
|
||||
|
||||
suspend fun deleteDeployment(id: String) {
|
||||
request("DELETE", "/deployments/${id.urlEncode()}")
|
||||
}
|
||||
|
||||
// ==================== SSL Operations ====================
|
||||
|
||||
suspend fun provisionSsl(domain: String, options: ProvisionSslOptions? = null): Certificate {
|
||||
val resp = request("POST", "/ssl/${domain.urlEncode()}", options)
|
||||
return gson.fromJson(resp, Certificate::class.java)
|
||||
}
|
||||
|
||||
suspend fun getCertificate(domain: String): Certificate {
|
||||
val resp = request("GET", "/ssl/${domain.urlEncode()}")
|
||||
return gson.fromJson(resp, Certificate::class.java)
|
||||
}
|
||||
|
||||
suspend fun renewCertificate(domain: String): Certificate {
|
||||
val resp = request("POST", "/ssl/${domain.urlEncode()}/renew")
|
||||
return gson.fromJson(resp, Certificate::class.java)
|
||||
}
|
||||
|
||||
suspend fun deleteCertificate(domain: String) {
|
||||
request("DELETE", "/ssl/${domain.urlEncode()}")
|
||||
}
|
||||
|
||||
// ==================== Site Configuration ====================
|
||||
|
||||
suspend fun getSiteConfig(domain: String): SiteConfig {
|
||||
val resp = request("GET", "/sites/${domain.urlEncode()}/config")
|
||||
return gson.fromJson(resp, SiteConfig::class.java)
|
||||
}
|
||||
|
||||
suspend fun updateSiteConfig(domain: String, config: Map<String, Any>): SiteConfig {
|
||||
val resp = request("PATCH", "/sites/${domain.urlEncode()}/config", config)
|
||||
return gson.fromJson(resp, SiteConfig::class.java)
|
||||
}
|
||||
|
||||
suspend fun purgeCache(domain: String, paths: List<String>? = null): Long {
|
||||
val body = paths?.let { mapOf("paths" to it) }
|
||||
val resp = request("DELETE", "/sites/${domain.urlEncode()}/cache", body)
|
||||
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
|
||||
return (map["purged"] as Number).toLong()
|
||||
}
|
||||
|
||||
// ==================== Analytics ====================
|
||||
|
||||
suspend fun getAnalytics(domain: String, options: AnalyticsOptions? = null): AnalyticsData {
|
||||
val params = mutableListOf<String>()
|
||||
options?.period?.let { params.add("period=${it.urlEncode()}") }
|
||||
options?.start?.let { params.add("start=${it.urlEncode()}") }
|
||||
options?.end?.let { params.add("end=${it.urlEncode()}") }
|
||||
val path = if (params.isNotEmpty()) {
|
||||
"/sites/${domain.urlEncode()}/analytics?${params.joinToString("&")}"
|
||||
} else {
|
||||
"/sites/${domain.urlEncode()}/analytics"
|
||||
}
|
||||
val resp = request("GET", path)
|
||||
return gson.fromJson(resp, AnalyticsData::class.java)
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
override fun close() {
|
||||
closed.set(true)
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
fun isClosed(): Boolean = closed.get()
|
||||
|
||||
suspend fun healthCheck(): Boolean = try {
|
||||
val resp = request("GET", "/health")
|
||||
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
|
||||
map["status"] == "healthy"
|
||||
} catch (e: Exception) { false }
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
private suspend fun request(method: String, path: String, body: Any? = null): String {
|
||||
if (closed.get()) throw HostingException("Client has been closed")
|
||||
|
||||
var lastError: Exception? = null
|
||||
repeat(config.retries) { attempt ->
|
||||
try {
|
||||
return doRequest(method, path, body)
|
||||
} catch (e: Exception) {
|
||||
lastError = e
|
||||
if (attempt < config.retries - 1) delay((1L shl attempt) * 1000)
|
||||
}
|
||||
}
|
||||
throw lastError ?: HostingException("Unknown error")
|
||||
}
|
||||
|
||||
private suspend fun doRequest(method: String, path: String, body: Any?): String {
|
||||
val url = "${config.endpoint}$path"
|
||||
val response: HttpResponse = httpClient.request(url) {
|
||||
this.method = HttpMethod.parse(method)
|
||||
header("Authorization", "Bearer ${config.apiKey}")
|
||||
header("Content-Type", "application/json")
|
||||
header("X-SDK-Version", "kotlin/0.1.0")
|
||||
body?.let { setBody(gson.toJson(it)) }
|
||||
}
|
||||
|
||||
val responseBody = response.bodyAsText()
|
||||
if (response.status.value >= 400) {
|
||||
val errorMap = try {
|
||||
gson.fromJson<Map<String, Any>>(responseBody, object : TypeToken<Map<String, Any>>() {}.type)
|
||||
} catch (e: Exception) { emptyMap() }
|
||||
throw HostingException(
|
||||
errorMap["message"] as? String ?: "HTTP ${response.status.value}",
|
||||
errorMap["code"] as? String,
|
||||
response.status.value
|
||||
)
|
||||
}
|
||||
return responseBody
|
||||
}
|
||||
|
||||
private fun String.urlEncode(): String = java.net.URLEncoder.encode(this, "UTF-8")
|
||||
}
|
||||
|
||||
// ==================== Types ====================
|
||||
|
||||
data class HostingConfig(
|
||||
val apiKey: String,
|
||||
val endpoint: String = "https://hosting.synor.io/v1",
|
||||
val timeoutSecs: Long = 60,
|
||||
val retries: Int = 3,
|
||||
val debug: Boolean = false
|
||||
)
|
||||
|
||||
enum class DomainStatus { pending, active, expired, suspended }
|
||||
enum class DeploymentStatus { pending, building, deploying, active, failed, inactive }
|
||||
enum class CertificateStatus { pending, issued, expired, revoked }
|
||||
enum class DnsRecordType { A, AAAA, CNAME, TXT, MX, NS, SRV, CAA }
|
||||
|
||||
data class Domain(
|
||||
val name: String,
|
||||
val status: DomainStatus,
|
||||
val owner: String,
|
||||
val registeredAt: Long,
|
||||
val expiresAt: Long,
|
||||
val autoRenew: Boolean,
|
||||
val records: DomainRecord? = null
|
||||
)
|
||||
|
||||
data class DomainRecord(
|
||||
val cid: String? = null,
|
||||
val ipv4: List<String>? = null,
|
||||
val ipv6: List<String>? = null,
|
||||
val cname: String? = null,
|
||||
val txt: List<String>? = null,
|
||||
val metadata: Map<String, String>? = null
|
||||
)
|
||||
|
||||
data class DomainAvailability(
|
||||
val name: String,
|
||||
val available: Boolean,
|
||||
val price: Double? = null,
|
||||
val premium: Boolean = false
|
||||
)
|
||||
|
||||
data class RegisterDomainOptions(
|
||||
val years: Int? = null,
|
||||
val autoRenew: Boolean? = null
|
||||
)
|
||||
|
||||
data class DnsRecord(
|
||||
val type: DnsRecordType,
|
||||
val name: String,
|
||||
val value: String,
|
||||
val ttl: Int = 3600,
|
||||
val priority: Int? = null
|
||||
)
|
||||
|
||||
data class DnsZone(
|
||||
val domain: String,
|
||||
val records: List<DnsRecord>,
|
||||
val updatedAt: Long
|
||||
)
|
||||
|
||||
data class Deployment(
|
||||
val id: String,
|
||||
val domain: String,
|
||||
val cid: String,
|
||||
val status: DeploymentStatus,
|
||||
val url: String,
|
||||
val createdAt: Long,
|
||||
val updatedAt: Long,
|
||||
val buildLogs: String? = null,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
data class DeployOptions(
|
||||
val domain: String? = null,
|
||||
val subdomain: String? = null,
|
||||
val spa: Boolean? = null,
|
||||
val cleanUrls: Boolean? = null,
|
||||
val trailingSlash: Boolean? = null
|
||||
)
|
||||
|
||||
data class Certificate(
|
||||
val domain: String,
|
||||
val status: CertificateStatus,
|
||||
val autoRenew: Boolean,
|
||||
val issuer: String,
|
||||
val issuedAt: Long? = null,
|
||||
val expiresAt: Long? = null,
|
||||
val fingerprint: String? = null
|
||||
)
|
||||
|
||||
data class ProvisionSslOptions(
|
||||
val includeWww: Boolean? = null,
|
||||
val autoRenew: Boolean? = null
|
||||
)
|
||||
|
||||
data class SiteConfig(
|
||||
val domain: String,
|
||||
val cid: String? = null,
|
||||
val headers: Map<String, String>? = null,
|
||||
val redirects: List<RedirectRule>? = null,
|
||||
val errorPages: Map<String, String>? = null,
|
||||
val spa: Boolean = false,
|
||||
val cleanUrls: Boolean = true,
|
||||
val trailingSlash: Boolean = false
|
||||
)
|
||||
|
||||
data class RedirectRule(
|
||||
val source: String,
|
||||
val destination: String,
|
||||
val statusCode: Int = 301,
|
||||
val permanent: Boolean = true
|
||||
)
|
||||
|
||||
data class AnalyticsData(
|
||||
val domain: String,
|
||||
val period: String,
|
||||
val pageViews: Long,
|
||||
val uniqueVisitors: Long,
|
||||
val bandwidth: Long,
|
||||
val topPages: List<PageView>,
|
||||
val topReferrers: List<Referrer>,
|
||||
val topCountries: List<Country>
|
||||
)
|
||||
|
||||
data class PageView(val path: String, val views: Long)
|
||||
data class Referrer(val referrer: String, val count: Long)
|
||||
data class Country(val country: String, val count: Long)
|
||||
|
||||
data class AnalyticsOptions(
|
||||
val period: String? = null,
|
||||
val start: String? = null,
|
||||
val end: String? = null
|
||||
)
|
||||
|
||||
internal data class DomainsResponse(val domains: List<Domain>?)
|
||||
internal data class DeploymentsResponse(val deployments: List<Deployment>?)
|
||||
|
||||
class HostingException(
|
||||
message: String,
|
||||
val code: String? = null,
|
||||
val statusCode: Int = 0
|
||||
) : RuntimeException(message)
|
||||
Loading…
Add table
Reference in a new issue