diff --git a/docs/api/.manifest b/docs/api/.manifest index 1779327..6b1f601 100644 --- a/docs/api/.manifest +++ b/docs/api/.manifest @@ -24,6 +24,7 @@ "Cloudflare.NET.Accounts.AccountsApi.Buckets": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.AccountsApi.CreateAccountAsync(Cloudflare.NET.Accounts.Models.CreateAccountRequest,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.AccountsApi.CreateR2BucketAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2LocationHint},System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Nullable{Cloudflare.NET.Accounts.Models.R2StorageClass},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", + "Cloudflare.NET.Accounts.AccountsApi.D1": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.AccountsApi.DeleteAccountAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.AccountsApi.DeleteBucketCorsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.AccountsApi.DeleteBucketLifecycleAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", @@ -34,6 +35,7 @@ "Cloudflare.NET.Accounts.AccountsApi.GetBucketCorsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.AccountsApi.GetBucketLifecycleAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.AccountsApi.GetCustomDomainStatusAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", + "Cloudflare.NET.Accounts.AccountsApi.Kv": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.AccountsApi.ListAccountsAsync(Cloudflare.NET.Accounts.Models.ListAccountsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.AccountsApi.ListAllAccountsAsync(Cloudflare.NET.Accounts.Models.ListAccountsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.AccountsApi.ListAllR2BucketsAsync(Cloudflare.NET.Accounts.Models.ListR2BucketsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", @@ -44,66 +46,215 @@ "Cloudflare.NET.Accounts.AccountsApi.UpdateAccountAsync(System.String,Cloudflare.NET.Accounts.Models.UpdateAccountRequest,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.AccountsApi.yml", "Cloudflare.NET.Accounts.Buckets": "Cloudflare.NET.Accounts.Buckets.yml", "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.AttachCustomDomainAsync(System.String,System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.AttachCustomDomainAsync(System.String,System.String,System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.CreateAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2LocationHint},System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Nullable{Cloudflare.NET.Accounts.Models.R2StorageClass},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.CreateTempCredentialsAsync(Cloudflare.NET.Accounts.Models.CreateTempCredentialsRequest,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DeleteAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DeleteCorsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DeleteLifecycleAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DeleteLockAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DetachCustomDomainAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DisableManagedDomainAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DisableSippyAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.EnableManagedDomainAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.EnableSippyAsync(System.String,Cloudflare.NET.Accounts.Models.EnableSippyRequest,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DeleteAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DeleteCorsAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DeleteLifecycleAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DeleteLockAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DetachCustomDomainAsync(System.String,System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DisableManagedDomainAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.DisableSippyAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.EnableManagedDomainAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.EnableSippyAsync(System.String,Cloudflare.NET.Accounts.Models.EnableSippyRequest,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetCorsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetCustomDomainStatusAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetLifecycleAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetLockAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetManagedDomainAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetSippyAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.ListAllAsync(Cloudflare.NET.Accounts.Models.ListR2BucketsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.ListAsync(Cloudflare.NET.Accounts.Models.ListR2BucketsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.ListCustomDomainsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.SetCorsAsync(System.String,Cloudflare.NET.Accounts.Models.BucketCorsPolicy,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.SetLifecycleAsync(System.String,Cloudflare.NET.Accounts.Models.BucketLifecyclePolicy,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.SetLockAsync(System.String,Cloudflare.NET.Accounts.Models.BucketLockPolicy,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.UpdateCustomDomainAsync(System.String,System.String,Cloudflare.NET.Accounts.Models.UpdateCustomDomainRequest,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetCorsAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetCustomDomainStatusAsync(System.String,System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetLifecycleAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetLockAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetManagedDomainAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.GetSippyAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.ListAllAsync(Cloudflare.NET.Accounts.Models.ListR2BucketsFilters,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.ListAsync(Cloudflare.NET.Accounts.Models.ListR2BucketsFilters,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.ListCustomDomainsAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.SetCorsAsync(System.String,Cloudflare.NET.Accounts.Models.BucketCorsPolicy,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.SetLifecycleAsync(System.String,Cloudflare.NET.Accounts.Models.BucketLifecyclePolicy,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.SetLockAsync(System.String,Cloudflare.NET.Accounts.Models.BucketLockPolicy,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.UpdateAsync(System.String,Cloudflare.NET.Accounts.Models.R2StorageClass,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.UpdateCustomDomainAsync(System.String,System.String,Cloudflare.NET.Accounts.Models.UpdateCustomDomainRequest,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.IR2BucketsApi.yml", "Cloudflare.NET.Accounts.Buckets.R2BucketsApi": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.#ctor(System.Net.Http.HttpClient,Microsoft.Extensions.Options.IOptions{Cloudflare.NET.Core.CloudflareApiOptions},Microsoft.Extensions.Logging.ILoggerFactory)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.AttachCustomDomainAsync(System.String,System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.AttachCustomDomainAsync(System.String,System.String,System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.CreateAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2LocationHint},System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Nullable{Cloudflare.NET.Accounts.Models.R2StorageClass},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.CreateTempCredentialsAsync(Cloudflare.NET.Accounts.Models.CreateTempCredentialsRequest,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DeleteAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DeleteCorsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DeleteLifecycleAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DeleteLockAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DetachCustomDomainAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DisableManagedDomainAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DisableSippyAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.EnableManagedDomainAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.EnableSippyAsync(System.String,Cloudflare.NET.Accounts.Models.EnableSippyRequest,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DeleteAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DeleteCorsAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DeleteLifecycleAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DeleteLockAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DetachCustomDomainAsync(System.String,System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DisableManagedDomainAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.DisableSippyAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.EnableManagedDomainAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.EnableSippyAsync(System.String,Cloudflare.NET.Accounts.Models.EnableSippyRequest,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetCorsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetCustomDomainStatusAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetLifecycleAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetLockAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetManagedDomainAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetSippyAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.ListAllAsync(Cloudflare.NET.Accounts.Models.ListR2BucketsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.ListAsync(Cloudflare.NET.Accounts.Models.ListR2BucketsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.ListCustomDomainsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.SetCorsAsync(System.String,Cloudflare.NET.Accounts.Models.BucketCorsPolicy,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.SetLifecycleAsync(System.String,Cloudflare.NET.Accounts.Models.BucketLifecyclePolicy,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.SetLockAsync(System.String,Cloudflare.NET.Accounts.Models.BucketLockPolicy,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", - "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.UpdateCustomDomainAsync(System.String,System.String,Cloudflare.NET.Accounts.Models.UpdateCustomDomainRequest,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetCorsAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetCustomDomainStatusAsync(System.String,System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetLifecycleAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetLockAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetManagedDomainAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.GetSippyAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.ListAllAsync(Cloudflare.NET.Accounts.Models.ListR2BucketsFilters,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.ListAsync(Cloudflare.NET.Accounts.Models.ListR2BucketsFilters,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.ListCustomDomainsAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.SetCorsAsync(System.String,Cloudflare.NET.Accounts.Models.BucketCorsPolicy,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.SetLifecycleAsync(System.String,Cloudflare.NET.Accounts.Models.BucketLifecyclePolicy,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.SetLockAsync(System.String,Cloudflare.NET.Accounts.Models.BucketLockPolicy,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.UpdateAsync(System.String,Cloudflare.NET.Accounts.Models.R2StorageClass,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.UpdateCustomDomainAsync(System.String,System.String,Cloudflare.NET.Accounts.Models.UpdateCustomDomainRequest,System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Buckets.R2BucketsApi.yml", + "Cloudflare.NET.Accounts.D1": "Cloudflare.NET.Accounts.D1.yml", + "Cloudflare.NET.Accounts.D1.D1Api": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.#ctor(System.Net.Http.HttpClient,Microsoft.Extensions.Options.IOptions{Cloudflare.NET.Core.CloudflareApiOptions},Microsoft.Extensions.Logging.ILoggerFactory)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.CompleteImportAsync(System.String,System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.CreateAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2LocationHint},System.Nullable{Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.DeleteAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.GetAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.ListAllAsync(Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.ListAsync(Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.PollExportAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.PollImportAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.QueryAsync(System.String,System.String,System.Collections.Generic.IReadOnlyList{System.Object},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.QueryAsync``1(System.String,System.String,System.Collections.Generic.IReadOnlyList{System.Object},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.QueryRawAsync(System.String,System.String,System.Collections.Generic.IReadOnlyList{System.Object},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.StartExportAsync(System.String,Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.StartImportAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.D1Api.UpdateAsync(System.String,Cloudflare.NET.Accounts.D1.Models.UpdateD1DatabaseOptions,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.D1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.CompleteImportAsync(System.String,System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.CreateAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2LocationHint},System.Nullable{Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.DeleteAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.GetAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.ListAllAsync(Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.ListAsync(Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.PollExportAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.PollImportAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.QueryAsync(System.String,System.String,System.Collections.Generic.IReadOnlyList{System.Object},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.QueryAsync``1(System.String,System.String,System.Collections.Generic.IReadOnlyList{System.Object},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.QueryRawAsync(System.String,System.String,System.Collections.Generic.IReadOnlyList{System.Object},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.StartExportAsync(System.String,Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.StartImportAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.ID1Api.UpdateAsync(System.String,Cloudflare.NET.Accounts.D1.Models.UpdateD1DatabaseOptions,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.D1.ID1Api.yml", + "Cloudflare.NET.Accounts.D1.Models": "Cloudflare.NET.Accounts.D1.Models.yml", + "Cloudflare.NET.Accounts.D1.Models.CreateD1DatabaseOptions": "Cloudflare.NET.Accounts.D1.Models.CreateD1DatabaseOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.CreateD1DatabaseOptions.#ctor(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2LocationHint},System.Nullable{Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction})": "Cloudflare.NET.Accounts.D1.Models.CreateD1DatabaseOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.CreateD1DatabaseOptions.Jurisdiction": "Cloudflare.NET.Accounts.D1.Models.CreateD1DatabaseOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.CreateD1DatabaseOptions.Name": "Cloudflare.NET.Accounts.D1.Models.CreateD1DatabaseOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.CreateD1DatabaseOptions.PrimaryLocationHint": "Cloudflare.NET.Accounts.D1.Models.CreateD1DatabaseOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Database": "Cloudflare.NET.Accounts.D1.Models.D1Database.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Database.#ctor(System.String,System.String,System.Nullable{System.DateTimeOffset},System.Nullable{System.Int64},System.Nullable{System.Int32},System.String,Cloudflare.NET.Accounts.D1.Models.D1ReadReplication,System.String)": "Cloudflare.NET.Accounts.D1.Models.D1Database.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Database.CreatedAt": "Cloudflare.NET.Accounts.D1.Models.D1Database.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Database.FileSize": "Cloudflare.NET.Accounts.D1.Models.D1Database.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Database.Name": "Cloudflare.NET.Accounts.D1.Models.D1Database.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Database.NumTables": "Cloudflare.NET.Accounts.D1.Models.D1Database.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Database.ReadReplication": "Cloudflare.NET.Accounts.D1.Models.D1Database.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Database.RunningInRegion": "Cloudflare.NET.Accounts.D1.Models.D1Database.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Database.Uuid": "Cloudflare.NET.Accounts.D1.Models.D1Database.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Database.Version": "Cloudflare.NET.Accounts.D1.Models.D1Database.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions": "Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions.#ctor(System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Collections.Generic.IReadOnlyList{System.String})": "Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions.NoData": "Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions.NoSchema": "Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions.Tables": "Cloudflare.NET.Accounts.D1.Models.D1ExportDumpOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse": "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.#ctor(System.String,System.String,Cloudflare.NET.Accounts.D1.Models.D1ExportResultDetails,System.String,System.String,System.Boolean,System.Collections.Generic.IReadOnlyList{System.String})": "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.AtBookmark": "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.Error": "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.Messages": "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.Result": "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.Status": "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.Success": "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.Type": "Cloudflare.NET.Accounts.D1.Models.D1ExportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResultDetails": "Cloudflare.NET.Accounts.D1.Models.D1ExportResultDetails.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResultDetails.#ctor(System.String,System.String)": "Cloudflare.NET.Accounts.D1.Models.D1ExportResultDetails.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResultDetails.Filename": "Cloudflare.NET.Accounts.D1.Models.D1ExportResultDetails.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ExportResultDetails.SignedUrl": "Cloudflare.NET.Accounts.D1.Models.D1ExportResultDetails.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.#ctor(System.String,System.String,Cloudflare.NET.Accounts.D1.Models.D1ImportResultDetails,System.String,System.String,System.Boolean,System.String,System.String,System.Collections.Generic.IReadOnlyList{System.String})": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.AtBookmark": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.Error": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.Filename": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.Messages": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.Result": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.Status": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.Success": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.Type": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.UploadUrl": "Cloudflare.NET.Accounts.D1.Models.D1ImportResponse.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResultDetails": "Cloudflare.NET.Accounts.D1.Models.D1ImportResultDetails.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResultDetails.#ctor(System.Nullable{System.Int32},Cloudflare.NET.Accounts.D1.Models.D1QueryMeta)": "Cloudflare.NET.Accounts.D1.Models.D1ImportResultDetails.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResultDetails.Meta": "Cloudflare.NET.Accounts.D1.Models.D1ImportResultDetails.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ImportResultDetails.NumQueries": "Cloudflare.NET.Accounts.D1.Models.D1ImportResultDetails.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.#ctor(System.String)": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.Create(System.String)": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.Equals(Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction)": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.Equals(System.Object)": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.EuropeanUnion": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.FedRamp": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.GetHashCode": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.ToString": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.Value": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.op_Equality(Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction,Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction)": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.op_Implicit(Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction)~System.String": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.op_Implicit(System.String)~Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.op_Inequality(Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction,Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction)": "Cloudflare.NET.Accounts.D1.Models.D1Jurisdiction.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.#ctor(System.Boolean,System.Int32,System.Double,System.Int64,System.Int64,System.Int64,System.String,System.Nullable{System.Boolean},System.String,System.Nullable{System.Int64},Cloudflare.NET.Accounts.D1.Models.D1QueryTimings,System.Nullable{System.Int32})": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.ChangedDb": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.Changes": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.Duration": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.LastRowId": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.RowsRead": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.RowsWritten": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.ServedBy": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.ServedByPrimary": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.ServedByRegion": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.SizeAfter": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.Timings": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.TotalAttempts": "Cloudflare.NET.Accounts.D1.Models.D1QueryMeta.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryRequest": "Cloudflare.NET.Accounts.D1.Models.D1QueryRequest.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryRequest.#ctor(System.String,System.Collections.Generic.IReadOnlyList{System.Object})": "Cloudflare.NET.Accounts.D1.Models.D1QueryRequest.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryRequest.Params": "Cloudflare.NET.Accounts.D1.Models.D1QueryRequest.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryRequest.Sql": "Cloudflare.NET.Accounts.D1.Models.D1QueryRequest.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryResult": "Cloudflare.NET.Accounts.D1.Models.D1QueryResult.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryResult.#ctor(Cloudflare.NET.Accounts.D1.Models.D1QueryMeta,System.Collections.Generic.IReadOnlyList{System.Text.Json.JsonElement},System.Boolean)": "Cloudflare.NET.Accounts.D1.Models.D1QueryResult.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryResult.Meta": "Cloudflare.NET.Accounts.D1.Models.D1QueryResult.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryResult.Results": "Cloudflare.NET.Accounts.D1.Models.D1QueryResult.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryResult.Success": "Cloudflare.NET.Accounts.D1.Models.D1QueryResult.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryResult`1": "Cloudflare.NET.Accounts.D1.Models.D1QueryResult-1.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryResult`1.#ctor(Cloudflare.NET.Accounts.D1.Models.D1QueryMeta,System.Collections.Generic.IReadOnlyList{`0},System.Boolean)": "Cloudflare.NET.Accounts.D1.Models.D1QueryResult-1.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryResult`1.Meta": "Cloudflare.NET.Accounts.D1.Models.D1QueryResult-1.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryResult`1.Results": "Cloudflare.NET.Accounts.D1.Models.D1QueryResult-1.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryResult`1.Success": "Cloudflare.NET.Accounts.D1.Models.D1QueryResult-1.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryTimings": "Cloudflare.NET.Accounts.D1.Models.D1QueryTimings.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryTimings.#ctor(System.Nullable{System.Double})": "Cloudflare.NET.Accounts.D1.Models.D1QueryTimings.yml", + "Cloudflare.NET.Accounts.D1.Models.D1QueryTimings.SqlDurationMs": "Cloudflare.NET.Accounts.D1.Models.D1QueryTimings.yml", + "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResult": "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResult.yml", + "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResult.#ctor(Cloudflare.NET.Accounts.D1.Models.D1QueryMeta,Cloudflare.NET.Accounts.D1.Models.D1RawQueryResultSet,System.Boolean)": "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResult.yml", + "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResult.Meta": "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResult.yml", + "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResult.Results": "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResult.yml", + "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResult.Success": "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResult.yml", + "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResultSet": "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResultSet.yml", + "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResultSet.#ctor(System.Collections.Generic.IReadOnlyList{System.String},System.Collections.Generic.IReadOnlyList{System.Collections.Generic.IReadOnlyList{System.Text.Json.JsonElement}})": "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResultSet.yml", + "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResultSet.Columns": "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResultSet.yml", + "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResultSet.Rows": "Cloudflare.NET.Accounts.D1.Models.D1RawQueryResultSet.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ReadReplication": "Cloudflare.NET.Accounts.D1.Models.D1ReadReplication.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ReadReplication.#ctor(System.String)": "Cloudflare.NET.Accounts.D1.Models.D1ReadReplication.yml", + "Cloudflare.NET.Accounts.D1.Models.D1ReadReplication.Mode": "Cloudflare.NET.Accounts.D1.Models.D1ReadReplication.yml", + "Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters": "Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters.yml", + "Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters.#ctor(System.String,System.Nullable{System.Int32},System.Nullable{System.Int32})": "Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters.yml", + "Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters.Name": "Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters.yml", + "Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters.Page": "Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters.yml", + "Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters.PerPage": "Cloudflare.NET.Accounts.D1.Models.ListD1DatabasesFilters.yml", + "Cloudflare.NET.Accounts.D1.Models.UpdateD1DatabaseOptions": "Cloudflare.NET.Accounts.D1.Models.UpdateD1DatabaseOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.UpdateD1DatabaseOptions.#ctor(Cloudflare.NET.Accounts.D1.Models.D1ReadReplication)": "Cloudflare.NET.Accounts.D1.Models.UpdateD1DatabaseOptions.yml", + "Cloudflare.NET.Accounts.D1.Models.UpdateD1DatabaseOptions.ReadReplication": "Cloudflare.NET.Accounts.D1.Models.UpdateD1DatabaseOptions.yml", "Cloudflare.NET.Accounts.IAccountsApi": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.AccessRules": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.AttachCustomDomainAsync(System.String,System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.Buckets": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.CreateAccountAsync(Cloudflare.NET.Accounts.Models.CreateAccountRequest,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.CreateR2BucketAsync(System.String,System.Nullable{Cloudflare.NET.Accounts.Models.R2LocationHint},System.Nullable{Cloudflare.NET.Accounts.Models.R2Jurisdiction},System.Nullable{Cloudflare.NET.Accounts.Models.R2StorageClass},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", + "Cloudflare.NET.Accounts.IAccountsApi.D1": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.DeleteAccountAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.DeleteBucketCorsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.DeleteBucketLifecycleAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", @@ -114,6 +265,7 @@ "Cloudflare.NET.Accounts.IAccountsApi.GetBucketCorsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.GetBucketLifecycleAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.GetCustomDomainStatusAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", + "Cloudflare.NET.Accounts.IAccountsApi.Kv": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.ListAccountsAsync(Cloudflare.NET.Accounts.Models.ListAccountsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.ListAllAccountsAsync(Cloudflare.NET.Accounts.Models.ListAccountsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.ListAllR2BucketsAsync(Cloudflare.NET.Accounts.Models.ListR2BucketsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", @@ -122,6 +274,108 @@ "Cloudflare.NET.Accounts.IAccountsApi.SetBucketCorsAsync(System.String,Cloudflare.NET.Accounts.Models.BucketCorsPolicy,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.SetBucketLifecycleAsync(System.String,Cloudflare.NET.Accounts.Models.BucketLifecyclePolicy,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", "Cloudflare.NET.Accounts.IAccountsApi.UpdateAccountAsync(System.String,Cloudflare.NET.Accounts.Models.UpdateAccountRequest,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.IAccountsApi.yml", + "Cloudflare.NET.Accounts.Kv": "Cloudflare.NET.Accounts.Kv.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.BulkDeleteAsync(System.String,System.Collections.Generic.IEnumerable{System.String},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.BulkGetAsync(System.String,System.Collections.Generic.IEnumerable{System.String},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.BulkGetWithMetadataAsync(System.String,System.Collections.Generic.IEnumerable{System.String},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.BulkWriteAsync(System.String,System.Collections.Generic.IEnumerable{Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.CreateAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.DeleteAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.DeleteValueAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.GetAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.GetMetadataAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.GetValueAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.GetValueBytesAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.GetValueBytesWithExpirationAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.GetValueWithExpirationAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.ListAllAsync(Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.ListAllKeysAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.ListAsync(Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.ListKeysAsync(System.String,Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.RenameAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.WriteValueAsync(System.String,System.String,System.Byte[],Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.IKvApi.WriteValueAsync(System.String,System.String,System.String,Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.IKvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.#ctor(System.Net.Http.HttpClient,Microsoft.Extensions.Options.IOptions{Cloudflare.NET.Core.CloudflareApiOptions},Microsoft.Extensions.Logging.ILoggerFactory)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.BulkDeleteAsync(System.String,System.Collections.Generic.IEnumerable{System.String},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.BulkGetAsync(System.String,System.Collections.Generic.IEnumerable{System.String},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.BulkGetWithMetadataAsync(System.String,System.Collections.Generic.IEnumerable{System.String},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.BulkWriteAsync(System.String,System.Collections.Generic.IEnumerable{Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem},System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.CreateAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.DeleteAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.DeleteValueAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.GetAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.GetMetadataAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.GetValueAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.GetValueBytesAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.GetValueBytesWithExpirationAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.GetValueWithExpirationAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.ListAllAsync(Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.ListAllKeysAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.ListAsync(Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.ListKeysAsync(System.String,Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.RenameAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.WriteValueAsync(System.String,System.String,System.Byte[],Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.KvApi.WriteValueAsync(System.String,System.String,System.String,Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions,System.Threading.CancellationToken)": "Cloudflare.NET.Accounts.Kv.KvApi.yml", + "Cloudflare.NET.Accounts.Kv.Models": "Cloudflare.NET.Accounts.Kv.Models.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkDeleteResult": "Cloudflare.NET.Accounts.Kv.Models.KvBulkDeleteResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkDeleteResult.#ctor(System.Int32,System.Collections.Generic.IReadOnlyList{System.String})": "Cloudflare.NET.Accounts.Kv.Models.KvBulkDeleteResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkDeleteResult.SuccessfulKeyCount": "Cloudflare.NET.Accounts.Kv.Models.KvBulkDeleteResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkDeleteResult.UnsuccessfulKeys": "Cloudflare.NET.Accounts.Kv.Models.KvBulkDeleteResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkGetItemWithMetadata": "Cloudflare.NET.Accounts.Kv.Models.KvBulkGetItemWithMetadata.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkGetItemWithMetadata.#ctor(System.String,System.Nullable{System.Text.Json.JsonElement})": "Cloudflare.NET.Accounts.Kv.Models.KvBulkGetItemWithMetadata.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkGetItemWithMetadata.Metadata": "Cloudflare.NET.Accounts.Kv.Models.KvBulkGetItemWithMetadata.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkGetItemWithMetadata.Value": "Cloudflare.NET.Accounts.Kv.Models.KvBulkGetItemWithMetadata.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.#ctor(System.String,System.String,System.Nullable{System.Boolean},System.Nullable{System.Int64},System.Nullable{System.Int32},System.Nullable{System.Text.Json.JsonElement})": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.Base64": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.Expiration": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.ExpirationTtl": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.Key": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.Metadata": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.Value": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteItem.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteResult": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteResult.#ctor(System.Int32,System.Collections.Generic.IReadOnlyList{System.String})": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteResult.SuccessfulKeyCount": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteResult.UnsuccessfulKeys": "Cloudflare.NET.Accounts.Kv.Models.KvBulkWriteResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvKey": "Cloudflare.NET.Accounts.Kv.Models.KvKey.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvKey.#ctor(System.String,System.Nullable{System.Int64},System.Nullable{System.Text.Json.JsonElement})": "Cloudflare.NET.Accounts.Kv.Models.KvKey.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvKey.Expiration": "Cloudflare.NET.Accounts.Kv.Models.KvKey.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvKey.Metadata": "Cloudflare.NET.Accounts.Kv.Models.KvKey.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvKey.Name": "Cloudflare.NET.Accounts.Kv.Models.KvKey.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvNamespace": "Cloudflare.NET.Accounts.Kv.Models.KvNamespace.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvNamespace.#ctor(System.String,System.String,System.Nullable{System.Boolean})": "Cloudflare.NET.Accounts.Kv.Models.KvNamespace.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvNamespace.Id": "Cloudflare.NET.Accounts.Kv.Models.KvNamespace.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvNamespace.SupportsUrlEncoding": "Cloudflare.NET.Accounts.Kv.Models.KvNamespace.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvNamespace.Title": "Cloudflare.NET.Accounts.Kv.Models.KvNamespace.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvNamespaceOrderField": "Cloudflare.NET.Accounts.Kv.Models.KvNamespaceOrderField.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvNamespaceOrderField.Id": "Cloudflare.NET.Accounts.Kv.Models.KvNamespaceOrderField.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvNamespaceOrderField.Title": "Cloudflare.NET.Accounts.Kv.Models.KvNamespaceOrderField.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvStringValueResult": "Cloudflare.NET.Accounts.Kv.Models.KvStringValueResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvStringValueResult.#ctor(System.String,System.Nullable{System.Int64})": "Cloudflare.NET.Accounts.Kv.Models.KvStringValueResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvStringValueResult.Expiration": "Cloudflare.NET.Accounts.Kv.Models.KvStringValueResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvStringValueResult.Value": "Cloudflare.NET.Accounts.Kv.Models.KvStringValueResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvValueResult": "Cloudflare.NET.Accounts.Kv.Models.KvValueResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvValueResult.#ctor(System.Byte[],System.Nullable{System.Int64})": "Cloudflare.NET.Accounts.Kv.Models.KvValueResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvValueResult.Expiration": "Cloudflare.NET.Accounts.Kv.Models.KvValueResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvValueResult.Value": "Cloudflare.NET.Accounts.Kv.Models.KvValueResult.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions": "Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions.#ctor(System.Nullable{System.Int64},System.Nullable{System.Int32},System.Nullable{System.Text.Json.JsonElement})": "Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions.Expiration": "Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions.ExpirationTtl": "Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions.yml", + "Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions.Metadata": "Cloudflare.NET.Accounts.Kv.Models.KvWriteOptions.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters": "Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters.#ctor(System.String,System.Nullable{System.Int32},System.String)": "Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters.Cursor": "Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters.Limit": "Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters.Prefix": "Cloudflare.NET.Accounts.Kv.Models.ListKvKeysFilters.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters": "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.#ctor(System.Nullable{System.Int32},System.Nullable{System.Int32},System.Nullable{Cloudflare.NET.Accounts.Kv.Models.KvNamespaceOrderField},System.Nullable{Cloudflare.NET.Security.Firewall.Models.ListOrderDirection})": "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.Direction": "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.Order": "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.Page": "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.yml", + "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.PerPage": "Cloudflare.NET.Accounts.Kv.Models.ListKvNamespacesFilters.yml", "Cloudflare.NET.Accounts.Models": "Cloudflare.NET.Accounts.Models.yml", "Cloudflare.NET.Accounts.Models.AbortMultipartUploadsTransition": "Cloudflare.NET.Accounts.Models.AbortMultipartUploadsTransition.yml", "Cloudflare.NET.Accounts.Models.AbortMultipartUploadsTransition.#ctor(Cloudflare.NET.Accounts.Models.LifecycleCondition)": "Cloudflare.NET.Accounts.Models.AbortMultipartUploadsTransition.yml", @@ -285,9 +539,13 @@ "Cloudflare.NET.Accounts.Models.ListCustomDomainsResponse.#ctor(System.Collections.Generic.IReadOnlyList{Cloudflare.NET.Accounts.Models.CustomDomain})": "Cloudflare.NET.Accounts.Models.ListCustomDomainsResponse.yml", "Cloudflare.NET.Accounts.Models.ListCustomDomainsResponse.Domains": "Cloudflare.NET.Accounts.Models.ListCustomDomainsResponse.yml", "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters": "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.yml", - "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.#ctor(System.Nullable{System.Int32},System.String)": "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.yml", + "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.#ctor(System.Nullable{System.Int32},System.String,System.String,System.String,System.String,System.String)": "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.yml", "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.Cursor": "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.yml", + "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.Direction": "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.yml", + "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.NameContains": "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.yml", + "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.Order": "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.yml", "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.PerPage": "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.yml", + "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.StartAfter": "Cloudflare.NET.Accounts.Models.ListR2BucketsFilters.yml", "Cloudflare.NET.Accounts.Models.ListR2BucketsResponse": "Cloudflare.NET.Accounts.Models.ListR2BucketsResponse.yml", "Cloudflare.NET.Accounts.Models.ListR2BucketsResponse.#ctor(System.Collections.Generic.IReadOnlyList{Cloudflare.NET.Accounts.Models.R2Bucket})": "Cloudflare.NET.Accounts.Models.ListR2BucketsResponse.yml", "Cloudflare.NET.Accounts.Models.ListR2BucketsResponse.Buckets": "Cloudflare.NET.Accounts.Models.ListR2BucketsResponse.yml", @@ -312,6 +570,8 @@ "Cloudflare.NET.Accounts.Models.R2Jurisdiction.EuropeanUnion": "Cloudflare.NET.Accounts.Models.R2Jurisdiction.yml", "Cloudflare.NET.Accounts.Models.R2Jurisdiction.FedRamp": "Cloudflare.NET.Accounts.Models.R2Jurisdiction.yml", "Cloudflare.NET.Accounts.Models.R2Jurisdiction.GetHashCode": "Cloudflare.NET.Accounts.Models.R2Jurisdiction.yml", + "Cloudflare.NET.Accounts.Models.R2Jurisdiction.GetS3EndpointUrl(System.String)": "Cloudflare.NET.Accounts.Models.R2Jurisdiction.yml", + "Cloudflare.NET.Accounts.Models.R2Jurisdiction.GetS3Subdomain": "Cloudflare.NET.Accounts.Models.R2Jurisdiction.yml", "Cloudflare.NET.Accounts.Models.R2Jurisdiction.ToString": "Cloudflare.NET.Accounts.Models.R2Jurisdiction.yml", "Cloudflare.NET.Accounts.Models.R2Jurisdiction.Value": "Cloudflare.NET.Accounts.Models.R2Jurisdiction.yml", "Cloudflare.NET.Accounts.Models.R2Jurisdiction.op_Equality(Cloudflare.NET.Accounts.Models.R2Jurisdiction,Cloudflare.NET.Accounts.Models.R2Jurisdiction)": "Cloudflare.NET.Accounts.Models.R2Jurisdiction.yml", @@ -730,17 +990,22 @@ "Cloudflare.NET.Core": "Cloudflare.NET.Core.yml", "Cloudflare.NET.Core.ApiResource": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.#ctor(System.Net.Http.HttpClient,Microsoft.Extensions.Logging.ILogger)": "Cloudflare.NET.Core.ApiResource.yml", + "Cloudflare.NET.Core.ApiResource.DeleteAsync``1(System.String,System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,System.String}},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.DeleteAsync``1(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", + "Cloudflare.NET.Core.ApiResource.GetAsync``1(System.String,System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,System.String}},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.GetAsync``1(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.GetCursorPaginatedAsync``1(System.String,System.Nullable{System.Int32},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", + "Cloudflare.NET.Core.ApiResource.GetCursorPaginatedAsync``2(System.String,System.Nullable{System.Int32},System.Func{``0,System.Collections.Generic.IReadOnlyList{``1}},System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,System.String}},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.GetCursorPaginatedAsync``2(System.String,System.Nullable{System.Int32},System.Func{``0,System.Collections.Generic.IReadOnlyList{``1}},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.GetCursorPaginatedResultAsync``1(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", + "Cloudflare.NET.Core.ApiResource.GetCursorPaginatedResultAsync``2(System.String,System.Func{``0,System.Collections.Generic.IReadOnlyList{``1}},System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,System.String}},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.GetCursorPaginatedResultAsync``2(System.String,System.Func{``0,System.Collections.Generic.IReadOnlyList{``1}},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.GetPagePaginatedResultAsync``1(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.GetPaginatedAsync``1(System.String,System.Nullable{System.Int32},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.GetStringAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.HttpClient": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.Logger": "Cloudflare.NET.Core.ApiResource.yml", + "Cloudflare.NET.Core.ApiResource.PatchAsync``1(System.String,System.Object,System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,System.String}},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.PatchAsync``1(System.String,System.Object,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.PostAsync``1(System.String,System.Object,System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,System.String}},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.PostAsync``1(System.String,System.Object,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", @@ -748,7 +1013,9 @@ "Cloudflare.NET.Core.ApiResource.PostMultipartFileAsync``1(System.String,System.IO.Stream,System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.ProcessAndDeserializeAsync``1(System.Net.Http.HttpResponseMessage,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.ProcessResponse``1(System.Net.Http.HttpResponseMessage,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", + "Cloudflare.NET.Core.ApiResource.PutAsync``1(System.String,System.Object,System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,System.String}},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.PutAsync``1(System.String,System.Object,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", + "Cloudflare.NET.Core.ApiResource.PutJsonAsync``1(System.String,System.String,System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,System.String}},System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.ApiResource.PutJsonAsync``1(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Core.ApiResource.yml", "Cloudflare.NET.Core.Auth": "Cloudflare.NET.Core.Auth.yml", "Cloudflare.NET.Core.Auth.AuthenticationHandler": "Cloudflare.NET.Core.Auth.AuthenticationHandler.yml", @@ -833,17 +1100,20 @@ "Cloudflare.NET.Core.Models.PagePaginatedResult`1.Items": "Cloudflare.NET.Core.Models.PagePaginatedResult-1.yml", "Cloudflare.NET.Core.Models.PagePaginatedResult`1.PageInfo": "Cloudflare.NET.Core.Models.PagePaginatedResult-1.yml", "Cloudflare.NET.Core.Models.ResultInfo": "Cloudflare.NET.Core.Models.ResultInfo.yml", - "Cloudflare.NET.Core.Models.ResultInfo.#ctor(System.Int32,System.Int32,System.Int32,System.Int32,System.Int32)": "Cloudflare.NET.Core.Models.ResultInfo.yml", + "Cloudflare.NET.Core.Models.ResultInfo.#ctor(System.Int32,System.Int32,System.Int32,System.Int32,System.Int32,System.String)": "Cloudflare.NET.Core.Models.ResultInfo.yml", "Cloudflare.NET.Core.Models.ResultInfo.Count": "Cloudflare.NET.Core.Models.ResultInfo.yml", + "Cloudflare.NET.Core.Models.ResultInfo.Cursor": "Cloudflare.NET.Core.Models.ResultInfo.yml", "Cloudflare.NET.Core.Models.ResultInfo.Page": "Cloudflare.NET.Core.Models.ResultInfo.yml", "Cloudflare.NET.Core.Models.ResultInfo.PerPage": "Cloudflare.NET.Core.Models.ResultInfo.yml", "Cloudflare.NET.Core.Models.ResultInfo.TotalCount": "Cloudflare.NET.Core.Models.ResultInfo.yml", "Cloudflare.NET.Core.Models.ResultInfo.TotalPages": "Cloudflare.NET.Core.Models.ResultInfo.yml", "Cloudflare.NET.Core.RateLimitingOptions": "Cloudflare.NET.Core.RateLimitingOptions.yml", + "Cloudflare.NET.Core.RateLimitingOptions.EnableProactiveThrottling": "Cloudflare.NET.Core.RateLimitingOptions.yml", "Cloudflare.NET.Core.RateLimitingOptions.IsEnabled": "Cloudflare.NET.Core.RateLimitingOptions.yml", "Cloudflare.NET.Core.RateLimitingOptions.MaxRetries": "Cloudflare.NET.Core.RateLimitingOptions.yml", "Cloudflare.NET.Core.RateLimitingOptions.PermitLimit": "Cloudflare.NET.Core.RateLimitingOptions.yml", "Cloudflare.NET.Core.RateLimitingOptions.QueueLimit": "Cloudflare.NET.Core.RateLimitingOptions.yml", + "Cloudflare.NET.Core.RateLimitingOptions.QuotaLowThreshold": "Cloudflare.NET.Core.RateLimitingOptions.yml", "Cloudflare.NET.Core.ServiceCollectionExtensions": "Cloudflare.NET.Core.ServiceCollectionExtensions.yml", "Cloudflare.NET.Core.ServiceCollectionExtensions.AddCloudflareApiClient(Microsoft.Extensions.DependencyInjection.IServiceCollection,Microsoft.Extensions.Configuration.IConfiguration)": "Cloudflare.NET.Core.ServiceCollectionExtensions.yml", "Cloudflare.NET.Core.ServiceCollectionExtensions.AddCloudflareApiClient(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{Cloudflare.NET.Core.CloudflareApiOptions})": "Cloudflare.NET.Core.ServiceCollectionExtensions.yml", @@ -1160,9 +1430,11 @@ "Cloudflare.NET.R2.Configuration": "Cloudflare.NET.R2.Configuration.yml", "Cloudflare.NET.R2.Configuration.R2Settings": "Cloudflare.NET.R2.Configuration.R2Settings.yml", "Cloudflare.NET.R2.Configuration.R2Settings.AccessKeyId": "Cloudflare.NET.R2.Configuration.R2Settings.yml", - "Cloudflare.NET.R2.Configuration.R2Settings.DefaultEndpointUrl": "Cloudflare.NET.R2.Configuration.R2Settings.yml", "Cloudflare.NET.R2.Configuration.R2Settings.DefaultRegion": "Cloudflare.NET.R2.Configuration.R2Settings.yml", "Cloudflare.NET.R2.Configuration.R2Settings.EndpointUrl": "Cloudflare.NET.R2.Configuration.R2Settings.yml", + "Cloudflare.NET.R2.Configuration.R2Settings.GetEffectiveEndpointUrl(System.String)": "Cloudflare.NET.R2.Configuration.R2Settings.yml", + "Cloudflare.NET.R2.Configuration.R2Settings.GetEndpointUrlForJurisdiction(System.String,Cloudflare.NET.Accounts.Models.R2Jurisdiction)": "Cloudflare.NET.R2.Configuration.R2Settings.yml", + "Cloudflare.NET.R2.Configuration.R2Settings.Jurisdiction": "Cloudflare.NET.R2.Configuration.R2Settings.yml", "Cloudflare.NET.R2.Configuration.R2Settings.Region": "Cloudflare.NET.R2.Configuration.R2Settings.yml", "Cloudflare.NET.R2.Configuration.R2Settings.SecretAccessKey": "Cloudflare.NET.R2.Configuration.R2Settings.yml", "Cloudflare.NET.R2.Configuration.R2SettingsValidator": "Cloudflare.NET.R2.Configuration.R2SettingsValidator.yml", @@ -1205,7 +1477,9 @@ "Cloudflare.NET.R2.IR2Client.UploadSinglePartAsync(System.String,System.String,System.IO.Stream,System.Threading.CancellationToken)": "Cloudflare.NET.R2.IR2Client.yml", "Cloudflare.NET.R2.IR2Client.UploadSinglePartAsync(System.String,System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.R2.IR2Client.yml", "Cloudflare.NET.R2.IR2ClientFactory": "Cloudflare.NET.R2.IR2ClientFactory.yml", - "Cloudflare.NET.R2.IR2ClientFactory.CreateClient(System.String)": "Cloudflare.NET.R2.IR2ClientFactory.yml", + "Cloudflare.NET.R2.IR2ClientFactory.GetClient(Cloudflare.NET.Accounts.Models.R2Jurisdiction)": "Cloudflare.NET.R2.IR2ClientFactory.yml", + "Cloudflare.NET.R2.IR2ClientFactory.GetClient(System.String)": "Cloudflare.NET.R2.IR2ClientFactory.yml", + "Cloudflare.NET.R2.IR2ClientFactory.GetClient(System.String,Cloudflare.NET.Accounts.Models.R2Jurisdiction)": "Cloudflare.NET.R2.IR2ClientFactory.yml", "Cloudflare.NET.R2.Models": "Cloudflare.NET.R2.Models.yml", "Cloudflare.NET.R2.Models.ListedPart": "Cloudflare.NET.R2.Models.ListedPart.yml", "Cloudflare.NET.R2.Models.ListedPart.#ctor(System.Nullable{System.Int32},System.String,System.Nullable{System.Int64},System.Nullable{System.DateTime})": "Cloudflare.NET.R2.Models.ListedPart.yml", @@ -1286,8 +1560,10 @@ "Cloudflare.NET.R2.R2Client.UploadSinglePartAsync(System.String,System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.R2.R2Client.yml", "Cloudflare.NET.R2.R2ClientFactory": "Cloudflare.NET.R2.R2ClientFactory.yml", "Cloudflare.NET.R2.R2ClientFactory.#ctor(Microsoft.Extensions.Options.IOptionsMonitor{Cloudflare.NET.R2.Configuration.R2Settings},Microsoft.Extensions.Options.IOptionsMonitor{Cloudflare.NET.Core.CloudflareApiOptions},Microsoft.Extensions.Logging.ILoggerFactory)": "Cloudflare.NET.R2.R2ClientFactory.yml", - "Cloudflare.NET.R2.R2ClientFactory.CreateClient(System.String)": "Cloudflare.NET.R2.R2ClientFactory.yml", "Cloudflare.NET.R2.R2ClientFactory.Dispose": "Cloudflare.NET.R2.R2ClientFactory.yml", + "Cloudflare.NET.R2.R2ClientFactory.GetClient(Cloudflare.NET.Accounts.Models.R2Jurisdiction)": "Cloudflare.NET.R2.R2ClientFactory.yml", + "Cloudflare.NET.R2.R2ClientFactory.GetClient(System.String)": "Cloudflare.NET.R2.R2ClientFactory.yml", + "Cloudflare.NET.R2.R2ClientFactory.GetClient(System.String,Cloudflare.NET.Accounts.Models.R2Jurisdiction)": "Cloudflare.NET.R2.R2ClientFactory.yml", "Cloudflare.NET.R2.ServiceCollectionExtensions": "Cloudflare.NET.R2.ServiceCollectionExtensions.yml", "Cloudflare.NET.R2.ServiceCollectionExtensions.AddCloudflareR2Client(Microsoft.Extensions.DependencyInjection.IServiceCollection,Microsoft.Extensions.Configuration.IConfiguration)": "Cloudflare.NET.R2.ServiceCollectionExtensions.yml", "Cloudflare.NET.R2.ServiceCollectionExtensions.AddCloudflareR2Client(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{Cloudflare.NET.R2.Configuration.R2Settings})": "Cloudflare.NET.R2.ServiceCollectionExtensions.yml", @@ -2393,7 +2669,7 @@ "Cloudflare.NET.Zones.IZonesApi.FindDnsRecordByNameAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", "Cloudflare.NET.Zones.IZonesApi.GetZoneDetailsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", "Cloudflare.NET.Zones.IZonesApi.GetZoneHoldAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", - "Cloudflare.NET.Zones.IZonesApi.GetZoneSettingAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", + "Cloudflare.NET.Zones.IZonesApi.GetZoneSettingAsync(System.String,Cloudflare.NET.Zones.Models.ZoneSettingId,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", "Cloudflare.NET.Zones.IZonesApi.ImportDnsRecordsAsync(System.String,System.IO.Stream,System.Boolean,System.Boolean,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", "Cloudflare.NET.Zones.IZonesApi.ListAllDnsRecordsAsync(System.String,Cloudflare.NET.Dns.Models.ListDnsRecordsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", "Cloudflare.NET.Zones.IZonesApi.ListAllZonesAsync(Cloudflare.NET.Zones.Models.ListZonesFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", @@ -2405,7 +2681,7 @@ "Cloudflare.NET.Zones.IZonesApi.Rulesets": "Cloudflare.NET.Zones.IZonesApi.yml", "Cloudflare.NET.Zones.IZonesApi.SetVanityNameServersAsync(System.String,System.Collections.Generic.IReadOnlyList{System.String},System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", "Cloudflare.NET.Zones.IZonesApi.SetZonePausedAsync(System.String,System.Boolean,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", - "Cloudflare.NET.Zones.IZonesApi.SetZoneSettingAsync``1(System.String,System.String,``0,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", + "Cloudflare.NET.Zones.IZonesApi.SetZoneSettingAsync``1(System.String,Cloudflare.NET.Zones.Models.ZoneSettingId,``0,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", "Cloudflare.NET.Zones.IZonesApi.SetZoneTypeAsync(System.String,Cloudflare.NET.Zones.Models.ZoneType,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", "Cloudflare.NET.Zones.IZonesApi.TriggerActivationCheckAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.IZonesApi.yml", "Cloudflare.NET.Zones.IZonesApi.UaRules": "Cloudflare.NET.Zones.IZonesApi.yml", @@ -2526,11 +2802,73 @@ "Cloudflare.NET.Zones.Models.ZonePlanReference.#ctor(System.String)": "Cloudflare.NET.Zones.Models.ZonePlanReference.yml", "Cloudflare.NET.Zones.Models.ZonePlanReference.Id": "Cloudflare.NET.Zones.Models.ZonePlanReference.yml", "Cloudflare.NET.Zones.Models.ZoneSetting": "Cloudflare.NET.Zones.Models.ZoneSetting.yml", - "Cloudflare.NET.Zones.Models.ZoneSetting.#ctor(System.String,System.Text.Json.JsonElement,System.Boolean,System.Nullable{System.DateTime})": "Cloudflare.NET.Zones.Models.ZoneSetting.yml", + "Cloudflare.NET.Zones.Models.ZoneSetting.#ctor(Cloudflare.NET.Zones.Models.ZoneSettingId,System.Text.Json.JsonElement,System.Boolean,System.Nullable{System.DateTime})": "Cloudflare.NET.Zones.Models.ZoneSetting.yml", "Cloudflare.NET.Zones.Models.ZoneSetting.Editable": "Cloudflare.NET.Zones.Models.ZoneSetting.yml", "Cloudflare.NET.Zones.Models.ZoneSetting.Id": "Cloudflare.NET.Zones.Models.ZoneSetting.yml", "Cloudflare.NET.Zones.Models.ZoneSetting.ModifiedOn": "Cloudflare.NET.Zones.Models.ZoneSetting.yml", "Cloudflare.NET.Zones.Models.ZoneSetting.Value": "Cloudflare.NET.Zones.Models.ZoneSetting.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.#ctor(System.String)": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.AdvancedDdos": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Aegis": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.AlwaysOnline": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.AlwaysUseHttps": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.AutomaticHttpsRewrites": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.AutomaticPlatformOptimization": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Brotli": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.BrowserCacheTtl": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.BrowserCheck": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.CacheLevel": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.ChallengeTtl": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Ciphers": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Create(System.String)": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.DevelopmentMode": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.EarlyHints": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.EmailObfuscation": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Equals(Cloudflare.NET.Zones.Models.ZoneSettingId)": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Equals(System.Object)": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.FontSettings": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.GetHashCode": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.H2Prioritization": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.HotlinkProtection": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Http2": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Http3": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.ImageResizing": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.IpGeolocation": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Ipv6": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.MinTlsVersion": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Mirage": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Nel": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.OpportunisticEncryption": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.OpportunisticOnion": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.OrangeToOrange": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.OriginErrorPagePassThru": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.OriginMaxHttpVersion": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Polish": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.PrefetchPreload": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.ProxyReadTimeout": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.PseudoIpv4": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.ResponseBuffering": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.RocketLoader": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.SecurityHeaders": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.SecurityLevel": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.ServerSideExcludes": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.SortQueryStringForCache": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Ssl": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.SslRecommender": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Tls13": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.TlsClientAuth": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.ToString": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.TrueClientIpHeader": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Value": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Waf": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Webp": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.Websockets": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.ZeroRtt": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.op_Equality(Cloudflare.NET.Zones.Models.ZoneSettingId,Cloudflare.NET.Zones.Models.ZoneSettingId)": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.op_Implicit(Cloudflare.NET.Zones.Models.ZoneSettingId)~System.String": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.op_Implicit(System.String)~Cloudflare.NET.Zones.Models.ZoneSettingId": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", + "Cloudflare.NET.Zones.Models.ZoneSettingId.op_Inequality(Cloudflare.NET.Zones.Models.ZoneSettingId,Cloudflare.NET.Zones.Models.ZoneSettingId)": "Cloudflare.NET.Zones.Models.ZoneSettingId.yml", "Cloudflare.NET.Zones.Models.ZoneStatus": "Cloudflare.NET.Zones.Models.ZoneStatus.yml", "Cloudflare.NET.Zones.Models.ZoneStatus.#ctor(System.String)": "Cloudflare.NET.Zones.Models.ZoneStatus.yml", "Cloudflare.NET.Zones.Models.ZoneStatus.Active": "Cloudflare.NET.Zones.Models.ZoneStatus.yml", @@ -2665,7 +3003,7 @@ "Cloudflare.NET.Zones.ZonesApi.FindDnsRecordByNameAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", "Cloudflare.NET.Zones.ZonesApi.GetZoneDetailsAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", "Cloudflare.NET.Zones.ZonesApi.GetZoneHoldAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", - "Cloudflare.NET.Zones.ZonesApi.GetZoneSettingAsync(System.String,System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", + "Cloudflare.NET.Zones.ZonesApi.GetZoneSettingAsync(System.String,Cloudflare.NET.Zones.Models.ZoneSettingId,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", "Cloudflare.NET.Zones.ZonesApi.ImportDnsRecordsAsync(System.String,System.IO.Stream,System.Boolean,System.Boolean,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", "Cloudflare.NET.Zones.ZonesApi.ListAllDnsRecordsAsync(System.String,Cloudflare.NET.Dns.Models.ListDnsRecordsFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", "Cloudflare.NET.Zones.ZonesApi.ListAllZonesAsync(Cloudflare.NET.Zones.Models.ListZonesFilters,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", @@ -2677,7 +3015,7 @@ "Cloudflare.NET.Zones.ZonesApi.Rulesets": "Cloudflare.NET.Zones.ZonesApi.yml", "Cloudflare.NET.Zones.ZonesApi.SetVanityNameServersAsync(System.String,System.Collections.Generic.IReadOnlyList{System.String},System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", "Cloudflare.NET.Zones.ZonesApi.SetZonePausedAsync(System.String,System.Boolean,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", - "Cloudflare.NET.Zones.ZonesApi.SetZoneSettingAsync``1(System.String,System.String,``0,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", + "Cloudflare.NET.Zones.ZonesApi.SetZoneSettingAsync``1(System.String,Cloudflare.NET.Zones.Models.ZoneSettingId,``0,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", "Cloudflare.NET.Zones.ZonesApi.SetZoneTypeAsync(System.String,Cloudflare.NET.Zones.Models.ZoneType,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", "Cloudflare.NET.Zones.ZonesApi.TriggerActivationCheckAsync(System.String,System.Threading.CancellationToken)": "Cloudflare.NET.Zones.ZonesApi.yml", "Cloudflare.NET.Zones.ZonesApi.UaRules": "Cloudflare.NET.Zones.ZonesApi.yml", diff --git a/docs/articles/accounts/d1.md b/docs/articles/accounts/d1.md new file mode 100644 index 0000000..a5839a7 --- /dev/null +++ b/docs/articles/accounts/d1.md @@ -0,0 +1,508 @@ +# D1 Serverless Database + +D1 is Cloudflare's native serverless SQL database built on SQLite. This API allows you to manage D1 databases, execute SQL queries, and perform import/export operations programmatically. + +## Overview + +```csharp +public class D1Service(ICloudflareApiClient cf) +{ + public async Task> ExecuteQueryAsync( + string databaseId, + string sql) + { + return await cf.Accounts.D1.QueryAsync(databaseId, sql); + } +} +``` + +## API Limits + +| Limit | Value (Workers Paid) | +|-------|----------------------| +| Max databases | 50,000 per account | +| Max database size | 10 GB | +| Max account storage | 1 TB | +| Max SQL statement length | 100 KB | +| Max query duration | 30 seconds | +| Max bound parameters | 100 per query | +| Max row/string/BLOB size | 2 MB | +| Max columns per table | 100 | + +> [!WARNING] +> D1's REST API is subject to global Cloudflare API rate limits and is best suited for administrative use. For high-throughput query access outside Workers, consider creating a proxy Worker with the D1 Worker binding. + +## Database Operations + +### Creating a Database + +```csharp +// Basic creation +var db = await cf.Accounts.D1.CreateAsync("my-database"); + +Console.WriteLine($"Database ID: {db.Uuid}"); +Console.WriteLine($"Name: {db.Name}"); +Console.WriteLine($"Version: {db.Version}"); +``` + +### With Location Hint + +Suggest a geographic region for database placement: + +```csharp +// Create database in Western Europe +var db = await cf.Accounts.D1.CreateAsync( + "eu-database", + primaryLocationHint: R2LocationHint.WestEurope +); + +Console.WriteLine($"Running in region: {db.RunningInRegion}"); +``` + +### With Jurisdiction (Data Residency) + +Enforce data residency within a specific boundary: + +```csharp +// Create EU-jurisdictional database for GDPR compliance +var db = await cf.Accounts.D1.CreateAsync( + "gdpr-database", + jurisdiction: D1Jurisdiction.EuropeanUnion +); +``` + +> [!NOTE] +> When specifying a jurisdiction, the `primaryLocationHint` parameter is ignored. The jurisdiction takes precedence and guarantees data residency within the specified region. + +### Getting a Database + +```csharp +var db = await cf.Accounts.D1.GetAsync(databaseId); + +Console.WriteLine($"Name: {db.Name}"); +Console.WriteLine($"Size: {db.FileSize} bytes"); +Console.WriteLine($"Tables: {db.NumTables}"); +Console.WriteLine($"Region: {db.RunningInRegion}"); +``` + +### Listing Databases + +```csharp +// List all databases automatically +await foreach (var db in cf.Accounts.D1.ListAllAsync()) +{ + Console.WriteLine($"{db.Name}: {db.Uuid}"); +} +``` + +#### With Filters + +```csharp +// Filter by name +var filters = new ListD1DatabasesFilters(Name: "production"); + +await foreach (var db in cf.Accounts.D1.ListAllAsync(filters)) +{ + Console.WriteLine(db.Name); +} +``` + +#### Manual Pagination + +```csharp +var page = await cf.Accounts.D1.ListAsync(new ListD1DatabasesFilters( + Page: 1, + PerPage: 50 +)); + +Console.WriteLine($"Page {page.PageInfo.Page}, returned {page.Items.Count} items"); + +foreach (var db in page.Items) +{ + Console.WriteLine(db.Name); +} + +// Fetch next page if current page is full +if (page.Items.Count >= 50) +{ + var nextPage = await cf.Accounts.D1.ListAsync(new ListD1DatabasesFilters( + Page: 2, + PerPage: 50 + )); +} +``` + +> [!WARNING] +> The D1 API returns `total_count=0` and `total_pages=0` in the pagination info, which is inconsistent with other Cloudflare APIs. Use `ListAllAsync` for automatic pagination, or check if the current page is full to determine if more pages exist. + +### Updating a Database + +Update database configuration such as read replication: + +```csharp +// Enable read replication +var updated = await cf.Accounts.D1.UpdateAsync( + databaseId, + new UpdateD1DatabaseOptions( + ReadReplication: new D1ReadReplication("auto") + ) +); + +Console.WriteLine($"Replication mode: {updated.ReadReplication?.Mode}"); +``` + +### Deleting a Database + +```csharp +await cf.Accounts.D1.DeleteAsync(databaseId); +``` + +> [!WARNING] +> Deleting a database permanently removes all data. This action cannot be undone. + +## Query Operations + +### Simple Queries + +```csharp +var results = await cf.Accounts.D1.QueryAsync(databaseId, "SELECT * FROM users"); + +foreach (var result in results) +{ + if (result.Success) + { + Console.WriteLine($"Duration: {result.Meta.Duration}ms"); + Console.WriteLine($"Rows read: {result.Meta.RowsRead}"); + + foreach (var row in result.Results) + { + Console.WriteLine(row); + } + } +} +``` + +### Parameterized Queries + +Use `?` placeholders to prevent SQL injection: + +```csharp +// Safe parameterized query +var results = await cf.Accounts.D1.QueryAsync( + databaseId, + "SELECT * FROM users WHERE id = ? AND status = ?", + new object?[] { userId, "active" } +); +``` + +### Insert Operations + +```csharp +var results = await cf.Accounts.D1.QueryAsync( + databaseId, + "INSERT INTO users (name, email) VALUES (?, ?)", + new object?[] { "John Doe", "john@example.com" } +); + +var meta = results[0].Meta; +Console.WriteLine($"Rows inserted: {meta.Changes}"); +Console.WriteLine($"Last row ID: {meta.LastRowId}"); +``` + +### Multiple Statements + +Execute multiple SQL statements in a single request: + +```csharp +var results = await cf.Accounts.D1.QueryAsync( + databaseId, + """ + INSERT INTO users (name) VALUES ('Alice'); + INSERT INTO users (name) VALUES ('Bob'); + SELECT COUNT(*) as total FROM users + """ +); + +// Each statement produces a separate result +Console.WriteLine($"Statement count: {results.Count}"); +``` + +> [!NOTE] +> When executing multiple statements, parameter binding is not supported. Use literal values or execute statements separately with parameters. + +### Typed Query Results + +Deserialize results directly to strongly-typed objects: + +```csharp +public record User(long Id, string Name, string Email); + +var results = await cf.Accounts.D1.QueryAsync( + databaseId, + "SELECT id, name, email FROM users WHERE active = ?", + new object?[] { true } +); + +foreach (var user in results[0].Results) +{ + Console.WriteLine($"{user.Name}: {user.Email}"); +} +``` + +### Raw Query Format + +For better performance with large result sets, use the raw format which returns rows as arrays instead of objects: + +```csharp +var results = await cf.Accounts.D1.QueryRawAsync( + databaseId, + "SELECT id, name, email FROM users" +); + +var result = results[0]; +Console.WriteLine($"Columns: {string.Join(", ", result.Results.Columns)}"); + +foreach (var row in result.Results.Rows) +{ + // Row values are in column order + Console.WriteLine($"ID: {row[0]}, Name: {row[1]}, Email: {row[2]}"); +} +``` + +## Export Operations + +Export a database to SQL format for backup or migration: + +```csharp +// Start export +var export = await cf.Accounts.D1.StartExportAsync(databaseId); +var bookmark = export.AtBookmark; + +// Poll until complete +while (export.Status != "complete") +{ + await Task.Delay(1000); + export = await cf.Accounts.D1.PollExportAsync(databaseId, bookmark!); +} + +// Download the SQL file +Console.WriteLine($"Download URL: {export.Result?.SignedUrl}"); +``` + +### Export Options + +Control what data is included in the export: + +```csharp +// Schema only (no data) +var schemaExport = await cf.Accounts.D1.StartExportAsync( + databaseId, + new D1ExportDumpOptions(NoData: true) +); + +// Data only (no schema) +var dataExport = await cf.Accounts.D1.StartExportAsync( + databaseId, + new D1ExportDumpOptions(NoSchema: true) +); + +// Specific tables only +var tablesExport = await cf.Accounts.D1.StartExportAsync( + databaseId, + new D1ExportDumpOptions(Tables: new[] { "users", "orders" }) +); +``` + +> [!NOTE] +> The database is unavailable for queries during export operations. Exports that aren't polled will auto-cancel. + +## Import Operations + +Import SQL data into a database: + +```csharp +// Step 1: Compute MD5 hash of your SQL file +var sqlContent = await File.ReadAllBytesAsync("backup.sql"); +var md5 = Convert.ToHexString(MD5.HashData(sqlContent)).ToLower(); + +// Step 2: Request upload URL +var initResponse = await cf.Accounts.D1.StartImportAsync(databaseId, md5); +var uploadUrl = initResponse.UploadUrl; +var filename = initResponse.Filename; + +// Step 3: Upload the SQL file to the signed URL +using var httpClient = new HttpClient(); +using var content = new ByteArrayContent(sqlContent); +await httpClient.PutAsync(uploadUrl, content); + +// Step 4: Start the import +var importResponse = await cf.Accounts.D1.CompleteImportAsync(databaseId, md5, filename!); +var bookmark = importResponse.AtBookmark; + +// Step 5: Poll until complete +while (importResponse.Status != "complete") +{ + await Task.Delay(1000); + importResponse = await cf.Accounts.D1.PollImportAsync(databaseId, bookmark!); +} + +Console.WriteLine($"Imported {importResponse.Result?.NumQueries} queries"); +``` + +> [!WARNING] +> The database is blocked during import operations. Plan imports during maintenance windows. + +## Models Reference + +### D1Database + +| Property | Type | Description | +|----------|------|-------------| +| `Uuid` | `string` | Unique database identifier (UUID format) | +| `Name` | `string` | Database name | +| `CreatedAt` | `DateTimeOffset?` | When the database was created | +| `FileSize` | `long?` | Database size in bytes | +| `NumTables` | `int?` | Number of tables in the database | +| `Version` | `string?` | Database version: "production" or "alpha" | +| `ReadReplication` | `D1ReadReplication?` | Read replication configuration | +| `RunningInRegion` | `string?` | Region code where the database is running | + +### D1QueryMeta + +| Property | Type | Description | +|----------|------|-------------| +| `ChangedDb` | `bool` | Whether the database was modified | +| `Changes` | `int` | Number of rows modified | +| `Duration` | `double` | Query execution time in milliseconds | +| `LastRowId` | `long` | Row ID of last inserted row | +| `RowsRead` | `long` | Number of rows read (including indices) | +| `RowsWritten` | `long` | Number of rows written (including indices) | +| `ServedByRegion` | `string?` | Region that handled the query | +| `ServedByPrimary` | `bool?` | Whether handled by primary instance | +| `SizeAfter` | `long?` | Database size after the query | + +### D1Jurisdiction (Extensible Enum) + +Jurisdictions guarantee data residency within specific geographic or regulatory boundaries. + +| Constant | Value | Description | +|----------|-------|-------------| +| `EuropeanUnion` | `eu` | EU data residency (GDPR compliance) | +| `FedRamp` | `fedramp` | US federal compliance (Enterprise only) | + +```csharp +// EU jurisdiction for GDPR compliance +var jurisdiction = D1Jurisdiction.EuropeanUnion; + +// Using custom values for new jurisdictions +D1Jurisdiction customJurisdiction = "new-jurisdiction"; +``` + +### ListD1DatabasesFilters + +| Property | Type | Description | +|----------|------|-------------| +| `Name` | `string?` | Filter by database name (partial match) | +| `Page` | `int?` | Page number (1-based) | +| `PerPage` | `int?` | Number of results per page | + +### D1ExportDumpOptions + +| Property | Type | Description | +|----------|------|-------------| +| `NoData` | `bool?` | Export only schema (no data) | +| `NoSchema` | `bool?` | Export only data (no schema) | +| `Tables` | `IReadOnlyList?` | Filter to specific tables | + +## Common Patterns + +### Schema Management + +```csharp +public class SchemaManager(ICloudflareApiClient cf) +{ + public async Task CreateTablesAsync(string databaseId) + { + await cf.Accounts.D1.QueryAsync(databaseId, """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')) + ); + + CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + """); + } +} +``` + +### Connection Pooling Pattern + +```csharp +public class D1Repository(ICloudflareApiClient cf, string databaseId) +{ + public async Task GetUserByEmailAsync(string email) + { + var results = await cf.Accounts.D1.QueryAsync( + databaseId, + "SELECT * FROM users WHERE email = ?", + new object?[] { email } + ); + + return results[0].Results.FirstOrDefault(); + } + + public async Task CreateUserAsync(string name, string email) + { + var results = await cf.Accounts.D1.QueryAsync( + databaseId, + """ + INSERT INTO users (name, email) VALUES (?, ?) + RETURNING * + """, + new object?[] { name, email } + ); + + return results[0].Results[0]; + } +} +``` + +### Backup and Restore + +```csharp +public class BackupService(ICloudflareApiClient cf) +{ + public async Task BackupDatabaseAsync(string databaseId) + { + // Start export + var export = await cf.Accounts.D1.StartExportAsync(databaseId); + + // Poll until complete + while (export.Status != "complete") + { + await Task.Delay(2000); + export = await cf.Accounts.D1.PollExportAsync(databaseId, export.AtBookmark!); + + if (export.Status == "error") + throw new Exception($"Export failed: {export.Error}"); + } + + return export.Result!.SignedUrl!; + } +} +``` + +## Required Permissions + +| Permission | Scope | Level | +|------------|-------|-------| +| D1 | Account | Read (for listing and querying) | +| D1 | Account | Write (for create, update, delete, import/export) | + +## Related + +- [SDK Conventions](../conventions.md) - Pagination patterns and common usage +- [API Coverage](../api-coverage.md) - Full list of supported endpoints +- [Workers KV](workers/kv.md) - Key-value storage alternative diff --git a/docs/articles/accounts/index.md b/docs/articles/accounts/index.md index 7be6194..7c1a3c4 100644 --- a/docs/articles/accounts/index.md +++ b/docs/articles/accounts/index.md @@ -29,6 +29,7 @@ public class AccountService(ICloudflareApiClient cf) | [Turnstile](turnstile.md) | `cf.Turnstile` | Manage Turnstile widgets | | [R2 Buckets](r2/buckets.md) | `cf.Accounts` | Create and manage R2 buckets | | [Workers KV](workers/kv.md) | `cf.Accounts.Kv` | Global key-value storage | +| [D1 Databases](d1.md) | `cf.Accounts.D1` | Serverless SQL databases | | [Access Rules](security/access-rules.md) | `cf.Accounts.AccessRules` | Account-level IP access control | | [Rulesets](security/rulesets.md) | `cf.Accounts.Rulesets` | Account-level WAF rules | @@ -72,6 +73,9 @@ Console.WriteLine($"2FA Required: {account.Settings?.EnforceTwofactor}"); ### Workers - [Workers KV](workers/kv.md) - Global low-latency key-value storage +### D1 Databases +- [D1 Serverless Database](d1.md) - Serverless SQL databases with SQLite + ### Security - [Access Rules](security/access-rules.md) - Account-level IP firewall rules - [WAF Rulesets](security/rulesets.md) - Account-level WAF custom rules @@ -88,6 +92,7 @@ Console.WriteLine($"2FA Required: {account.Settings?.EnforceTwofactor}"); | Turnstile | Turnstile | Read/Write | | R2 Buckets | Workers R2 Storage | Read/Write | | Workers KV | Workers KV Storage | Read/Write | +| D1 Databases | D1 | Read/Write | | Access Rules | Account Firewall Access Rules | Read/Write | | Rulesets | Account Rulesets | Read/Write | diff --git a/docs/articles/api-coverage.md b/docs/articles/api-coverage.md index b00f59f..d1858bd 100644 --- a/docs/articles/api-coverage.md +++ b/docs/articles/api-coverage.md @@ -54,6 +54,7 @@ This document provides a comprehensive overview of the Cloudflare API endpoints DNS Records12Full DNS lifecycle including batch and BIND import/export R2 Buckets22Bucket management, CORS, lifecycle, domains, and Sippy Workers KV19Namespace and key-value CRUD, metadata, bulk operations +D1 Databases11Database CRUD, SQL queries, export/import operations Subscriptions6Account, zone, and user subscription details Turnstile5CAPTCHA widget configuration and secret rotation User11Profile, invitations, and membership management @@ -537,6 +538,66 @@ This document provides a comprehensive overview of the Cloudflare API endpoints --- +## D1 Databases + +### Database Operations + + ++++++ + + + + + + + + + +
OperationMethodEndpointStatus
List DatabasesD1.ListAsyncGET /accounts/{id}/d1/database✅
List All DatabasesD1.ListAllAsyncGET /accounts/{id}/d1/database (auto)✅
Create DatabaseD1.CreateAsyncPOST /accounts/{id}/d1/database✅
Get DatabaseD1.GetAsyncGET /accounts/{id}/d1/database/{uuid}✅
Update DatabaseD1.UpdateAsyncPATCH /accounts/{id}/d1/database/{uuid}✅
Delete DatabaseD1.DeleteAsyncDELETE /accounts/{id}/d1/database/{uuid}✅
+ +### Query Operations + + ++++++ + + + + + + +
OperationMethodEndpointStatus
Execute QueryD1.QueryAsyncPOST /accounts/{id}/d1/database/{uuid}/query✅
Execute Query (typed)D1.QueryAsync<T>POST /accounts/{id}/d1/database/{uuid}/query✅
Execute Query (raw)D1.QueryRawAsyncPOST /accounts/{id}/d1/database/{uuid}/raw✅
+ +### Export/Import Operations + + ++++++ + + + + + + + + +
OperationMethodEndpointStatus
Start ExportD1.StartExportAsyncPOST /accounts/{id}/d1/database/{uuid}/export✅
Poll ExportD1.PollExportAsyncPOST /accounts/{id}/d1/database/{uuid}/export✅
Start ImportD1.StartImportAsyncPOST /accounts/{id}/d1/database/{uuid}/import✅
Complete ImportD1.CompleteImportAsyncPOST /accounts/{id}/d1/database/{uuid}/import✅
Poll ImportD1.PollImportAsyncPOST /accounts/{id}/d1/database/{uuid}/import✅
+ +--- + ## Workers Route diff --git a/docs/articles/conventions.md b/docs/articles/conventions.md index 84ea8b4..79fbbcb 100644 --- a/docs/articles/conventions.md +++ b/docs/articles/conventions.md @@ -18,6 +18,7 @@ Page-based pagination uses `page` and `per_page` parameters. This pattern is use - Zone Lockdown Rules - User-Agent Rules - Workers KV Namespaces +- D1 Databases #### Automatic Pagination @@ -285,6 +286,12 @@ if (bucket.StorageClass is { } sc && sc == R2StorageClass.InfrequentAccess) | `ZoneStatus` | Zone activation status | `active`, `pending`, `initializing`, `moved`, `deleted`, `deactivated` | | `ZoneSettingId` | Zone setting identifier | `ssl`, `min_tls_version`, `always_use_https`, `brotli`, `http2`, `http3`, `development_mode`, `security_level`, etc. | +#### D1 Databases + +| Type | Purpose | Known Values | +|------|---------|--------------| +| `D1Jurisdiction` | D1 data residency | `eu`, `fedramp` | + #### Security & Firewall | Type | Purpose | Known Values | diff --git a/docs/articles/permissions.md b/docs/articles/permissions.md index d1c319e..606987b 100644 --- a/docs/articles/permissions.md +++ b/docs/articles/permissions.md @@ -93,6 +93,18 @@ Cloudflare uses a permission-based system for API tokens. To adhere to the princ
+### D1 Databases + + + + + + + + + +
FeaturePermissionLevel
D1 Databases (read)D1Account: Read
D1 Databases (write)D1Account: Write
D1 QueriesD1Account: Write
D1 Export/ImportD1Account: Write
+ ### Account Security diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index a3145a4..e9de773 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -11,39 +11,6 @@ href: conventions.md - name: REST API -- name: Zones API - href: zones/index.md - items: - - name: Overview - href: zones/index.md - - name: Zone Management - href: zones/zone-management.md - - name: Zone Holds - href: zones/zone-holds.md - - name: Zone Settings - href: zones/zone-settings.md - - name: DNS Records - href: zones/dns-records.md - - name: DNS Scanning - href: zones/dns-scanning.md - - name: Worker Routes - href: zones/worker-routes.md - - name: Cache Purge - href: zones/cache-purge.md - - name: Custom Hostnames (SaaS) - href: zones/custom-hostnames.md - - name: Security - items: - - name: Access Rules - href: zones/security/access-rules.md - - name: WAF Rulesets - href: zones/security/rulesets.md - - name: Zone Lockdown - href: zones/security/lockdown.md - - name: User-Agent Rules - href: zones/security/ua-rules.md - - name: Subscriptions - href: subscriptions.md - name: Accounts API href: accounts/index.md items: @@ -51,38 +18,34 @@ href: accounts/index.md - name: Account Management href: accounts/account-management.md - - name: Members - href: accounts/members.md - - name: Roles - href: accounts/roles.md - name: API Tokens href: accounts/api-tokens.md - name: Audit Logs href: accounts/audit-logs.md - - name: Turnstile - href: accounts/turnstile.md + - name: D1 Databases + href: accounts/d1.md + - name: Members + href: accounts/members.md - name: R2 Buckets items: + - name: Bucket Locks + href: accounts/r2/bucket-locks.md - name: Bucket Management href: accounts/r2/buckets.md - - name: Custom Domains - href: accounts/r2/custom-domains.md - - name: Managed Domains (r2.dev) - href: accounts/r2/managed-domains.md - name: CORS Configuration href: accounts/r2/cors.md + - name: Custom Domains + href: accounts/r2/custom-domains.md - name: Lifecycle Policies href: accounts/r2/lifecycle.md - - name: Bucket Locks - href: accounts/r2/bucket-locks.md + - name: Managed Domains (r2.dev) + href: accounts/r2/managed-domains.md - name: Sippy Migration href: accounts/r2/sippy.md - name: Temporary Credentials href: accounts/r2/temp-credentials.md - - name: Workers - items: - - name: Workers KV - href: accounts/workers/kv.md + - name: Roles + href: accounts/roles.md - name: Security items: - name: Access Rules @@ -91,49 +54,88 @@ href: accounts/security/rulesets.md - name: Subscriptions href: subscriptions.md + - name: Turnstile + href: accounts/turnstile.md + - name: Workers + items: + - name: Workers KV + href: accounts/workers/kv.md - name: User API href: user/index.md items: - name: Overview href: user/index.md - - name: Profile - href: user/profile.md - - name: Memberships - href: user/memberships.md - - name: Invitations - href: user/invitations.md - name: API Tokens href: user/api-tokens.md - name: Audit Logs href: user/audit-logs.md + - name: Invitations + href: user/invitations.md + - name: Memberships + href: user/memberships.md + - name: Profile + href: user/profile.md + - name: Subscriptions + href: subscriptions.md +- name: Zones API + href: zones/index.md + items: + - name: Overview + href: zones/index.md + - name: Cache Purge + href: zones/cache-purge.md + - name: Custom Hostnames (SaaS) + href: zones/custom-hostnames.md + - name: DNS Records + href: zones/dns-records.md + - name: DNS Scanning + href: zones/dns-scanning.md + - name: Security + items: + - name: Access Rules + href: zones/security/access-rules.md + - name: User-Agent Rules + href: zones/security/ua-rules.md + - name: WAF Rulesets + href: zones/security/rulesets.md + - name: Zone Lockdown + href: zones/security/lockdown.md - name: Subscriptions href: subscriptions.md + - name: Worker Routes + href: zones/worker-routes.md + - name: Zone Holds + href: zones/zone-holds.md + - name: Zone Management + href: zones/zone-management.md + - name: Zone Settings + href: zones/zone-settings.md - name: Extension Packages +- name: Analytics API + href: analytics/index.md + items: + - name: Overview + href: analytics/index.md + - name: GraphQL Queries + href: analytics/graphql.md - name: R2 Object Storage href: r2/index.md items: - name: Overview href: r2/index.md - - name: Uploading Objects - href: r2/uploads.md - - name: Downloading Objects - href: r2/downloads.md - name: Deleting Objects href: r2/deletes.md + - name: Downloading Objects + href: r2/downloads.md - name: Listing Objects href: r2/listing.md - name: Multipart Uploads href: r2/multipart.md - name: Presigned URLs href: r2/presigned-urls.md -- name: Analytics API - href: analytics/index.md - items: - - name: Overview - href: analytics/index.md - - name: GraphQL Queries - href: analytics/graphql.md + - name: Uploading Objects + href: r2/uploads.md - name: Reference - name: API Coverage diff --git a/docs/template/public/main.css b/docs/template/public/main.css index ae6abf3..591b63f 100644 --- a/docs/template/public/main.css +++ b/docs/template/public/main.css @@ -104,7 +104,15 @@ article .alert { margin-bottom: revert; } -/* Navigation expand indicator spacing */ -.flex-fill .expand-stub { - margin-right: 0.5rem; +/* Navigation expand indicator spacing - align expandable and non-expandable items */ +.sidetoc .nav li { + position: relative; + padding-left: 1.25rem; +} + +.sidetoc .nav .expand-stub { + position: absolute; + left: 0; + width: 1.25rem; + text-align: center; } diff --git a/src/Cloudflare.NET/Accounts/AccountsApi.cs b/src/Cloudflare.NET/Accounts/AccountsApi.cs index 973753c..858a20b 100644 --- a/src/Cloudflare.NET/Accounts/AccountsApi.cs +++ b/src/Cloudflare.NET/Accounts/AccountsApi.cs @@ -5,6 +5,7 @@ using Core; using Core.Internal; using Core.Models; +using D1; using Kv; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -28,6 +29,9 @@ public class AccountsApi : ApiResource, IAccountsApi /// The lazy-initialized Workers KV API resource. private readonly Lazy _kv; + /// The lazy-initialized D1 Database API resource. + private readonly Lazy _d1; + #endregion @@ -44,6 +48,7 @@ public AccountsApi(HttpClient httpClient, IOptions options _accessRules = new Lazy(() => new AccountAccessRulesApi(httpClient, options, loggerFactory)); _rulesets = new Lazy(() => new AccountRulesetsApi(httpClient, options, loggerFactory)); _kv = new Lazy(() => new KvApi(httpClient, options, loggerFactory)); + _d1 = new Lazy(() => new D1Api(httpClient, options, loggerFactory)); } #endregion @@ -63,6 +68,9 @@ public AccountsApi(HttpClient httpClient, IOptions options /// public IKvApi Kv => _kv.Value; + /// + public ID1Api D1 => _d1.Value; + #endregion #region Methods Impl - Legacy Bucket Operations (Delegating to Buckets API) diff --git a/src/Cloudflare.NET/Accounts/D1/D1Api.cs b/src/Cloudflare.NET/Accounts/D1/D1Api.cs new file mode 100644 index 0000000..b0fb83e --- /dev/null +++ b/src/Cloudflare.NET/Accounts/D1/D1Api.cs @@ -0,0 +1,318 @@ +namespace Cloudflare.NET.Accounts.D1; + +using System.Runtime.CompilerServices; +using Core; +using Core.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Models; +using Cloudflare.NET.Accounts.Models; + +/// Implementation of Cloudflare D1 Database API operations. +/// +/// +/// D1 is Cloudflare's native serverless SQL database built on SQLite. +/// All operations are account-scoped, using the account ID from configuration. +/// +/// +/// The REST API is subject to global Cloudflare API rate limits and is best suited +/// for administrative operations. For high-throughput query access outside Workers, +/// consider creating a proxy Worker with D1 Worker bindings. +/// +/// +public class D1Api : ApiResource, ID1Api +{ + #region Properties & Fields - Non-Public + + /// The Cloudflare Account ID. + private readonly string _accountId; + + #endregion + + + #region Constructors + + /// Initializes a new instance of the class. + /// The HttpClient for making requests. + /// The Cloudflare API options containing the account ID. + /// The factory to create loggers. + public D1Api(HttpClient httpClient, IOptions options, ILoggerFactory loggerFactory) + : base(httpClient, loggerFactory.CreateLogger()) + { + _accountId = options.Value.AccountId; + } + + #endregion + + + #region Database Operations + + /// + public async Task> ListAsync( + ListD1DatabasesFilters? filters = null, + CancellationToken cancellationToken = default) + { + var queryParams = new List(); + + if (!string.IsNullOrEmpty(filters?.Name)) + queryParams.Add($"name={Uri.EscapeDataString(filters.Name)}"); + + if (filters?.Page.HasValue == true) + queryParams.Add($"page={filters.Page.Value}"); + + if (filters?.PerPage.HasValue == true) + queryParams.Add($"per_page={filters.PerPage.Value}"); + + var queryString = queryParams.Count > 0 ? $"?{string.Join('&', queryParams)}" : string.Empty; + var endpoint = $"accounts/{_accountId}/d1/database{queryString}"; + + return await GetPagePaginatedResultAsync(endpoint, cancellationToken); + } + + /// + /// + /// + /// The D1 API returns total_count=0 and total_pages=0 in the pagination info, which is + /// inconsistent with the standard Cloudflare API schema. To work around this limitation, this implementation + /// checks if the current page is full (item count equals PerPage) to determine if there are more pages. + /// + /// + public async IAsyncEnumerable ListAllAsync( + ListD1DatabasesFilters? filters = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var page = 1; + var perPage = filters?.PerPage ?? 100; + var hasMore = true; + + while (hasMore) + { + cancellationToken.ThrowIfCancellationRequested(); + + var pageFilters = new ListD1DatabasesFilters( + Name: filters?.Name, + Page: page, + PerPage: perPage + ); + + var result = await ListAsync(pageFilters, cancellationToken); + + foreach (var database in result.Items) + { + yield return database; + } + + // D1 API limitation: TotalPages is always 0, so we cannot rely on it. + // Instead, check if the current page is full (item count equals PerPage). + // If the page is full, there might be more items on the next page. + hasMore = result.Items.Count >= perPage; + + page++; + } + } + + /// + public async Task CreateAsync( + string name, + R2LocationHint? primaryLocationHint = null, + D1Jurisdiction? jurisdiction = null, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database"; + + var request = new CreateD1DatabaseRequest( + Name: name, + PrimaryLocationHint: primaryLocationHint?.Value, + Jurisdiction: jurisdiction?.Value + ); + + return await PostAsync(endpoint, request, cancellationToken); + } + + /// + public async Task GetAsync( + string databaseId, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}"; + + return await GetAsync(endpoint, cancellationToken); + } + + /// + public async Task UpdateAsync( + string databaseId, + UpdateD1DatabaseOptions options, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}"; + + var request = new UpdateD1DatabaseRequest( + ReadReplication: options.ReadReplication + ); + + return await PatchAsync(endpoint, request, cancellationToken); + } + + /// + public async Task DeleteAsync( + string databaseId, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}"; + + await DeleteAsync(endpoint, cancellationToken); + } + + #endregion + + + #region Query Operations + + /// + public async Task> QueryAsync( + string databaseId, + string sql, + IReadOnlyList? @params = null, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}/query"; + + var request = new D1QueryRequest( + Sql: sql, + Params: @params + ); + + return await PostAsync>(endpoint, request, cancellationToken); + } + + /// + public async Task>> QueryAsync( + string databaseId, + string sql, + IReadOnlyList? @params = null, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}/query"; + + var request = new D1QueryRequest( + Sql: sql, + Params: @params + ); + + return await PostAsync>>(endpoint, request, cancellationToken); + } + + /// + public async Task> QueryRawAsync( + string databaseId, + string sql, + IReadOnlyList? @params = null, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}/raw"; + + var request = new D1QueryRequest( + Sql: sql, + Params: @params + ); + + return await PostAsync>(endpoint, request, cancellationToken); + } + + #endregion + + + #region Export Operations + + /// + public async Task StartExportAsync( + string databaseId, + D1ExportDumpOptions? dumpOptions = null, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}/export"; + + var request = new D1ExportRequest( + OutputFormat: "polling", + CurrentBookmark: null, + DumpOptions: dumpOptions + ); + + return await PostAsync(endpoint, request, cancellationToken); + } + + /// + public async Task PollExportAsync( + string databaseId, + string bookmark, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}/export"; + + var request = new D1ExportRequest( + OutputFormat: "polling", + CurrentBookmark: bookmark, + DumpOptions: null + ); + + return await PostAsync(endpoint, request, cancellationToken); + } + + #endregion + + + #region Import Operations + + /// + public async Task StartImportAsync( + string databaseId, + string etag, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}/import"; + + var request = new D1ImportRequest( + Action: "init", + Etag: etag + ); + + return await PostAsync(endpoint, request, cancellationToken); + } + + /// + public async Task CompleteImportAsync( + string databaseId, + string etag, + string filename, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}/import"; + + var request = new D1ImportRequest( + Action: "ingest", + Etag: etag, + Filename: filename + ); + + return await PostAsync(endpoint, request, cancellationToken); + } + + /// + public async Task PollImportAsync( + string databaseId, + string bookmark, + CancellationToken cancellationToken = default) + { + var endpoint = $"accounts/{_accountId}/d1/database/{Uri.EscapeDataString(databaseId)}/import"; + + var request = new D1ImportRequest( + Action: "poll", + CurrentBookmark: bookmark + ); + + return await PostAsync(endpoint, request, cancellationToken); + } + + #endregion +} diff --git a/src/Cloudflare.NET/Accounts/D1/ID1Api.cs b/src/Cloudflare.NET/Accounts/D1/ID1Api.cs new file mode 100644 index 0000000..2e6e091 --- /dev/null +++ b/src/Cloudflare.NET/Accounts/D1/ID1Api.cs @@ -0,0 +1,250 @@ +namespace Cloudflare.NET.Accounts.D1; + +using Core.Models; +using Models; +using Cloudflare.NET.Accounts.Models; + +/// +/// Provides access to Cloudflare D1 database operations. +/// D1 is Cloudflare's native serverless SQL database built on SQLite. +/// +/// +/// +/// All operations are account-scoped. The account ID is configured +/// via . +/// +/// +/// API Limits (Workers Paid plan): +/// +/// Max databases: 50,000 per account +/// Max database size: 10 GB +/// Max account storage: 1 TB +/// Max SQL statement length: 100 KB +/// Max query duration: 30 seconds +/// Max bound parameters: 100 per query +/// Max row/string/BLOB size: 2 MB +/// Max columns per table: 100 +/// +/// +/// +/// Important: D1's REST API is best suited for administrative use +/// as the global Cloudflare API rate limit applies. For high-throughput query access +/// outside Workers, consider creating a proxy Worker with the D1 Worker binding. +/// +/// +/// +/// +public interface ID1Api +{ + #region Database Operations + + /// Lists D1 databases in the account. + /// Optional filters for pagination and name search. + /// Cancellation token. + /// A page of databases with pagination info. + /// + Task> ListAsync( + ListD1DatabasesFilters? filters = null, + CancellationToken cancellationToken = default); + + /// Lists all D1 databases, automatically handling pagination. + /// Optional filters for name search. + /// Cancellation token. + /// An async enumerable of all databases. + IAsyncEnumerable ListAllAsync( + ListD1DatabasesFilters? filters = null, + CancellationToken cancellationToken = default); + + /// Creates a new D1 database. + /// The name for the new database. + /// Optional region hint for database placement. + /// Optional jurisdictional restriction (overrides location hint). + /// Cancellation token. + /// The created database. + /// + Task CreateAsync( + string name, + R2LocationHint? primaryLocationHint = null, + D1Jurisdiction? jurisdiction = null, + CancellationToken cancellationToken = default); + + /// Gets a specific D1 database by ID. + /// The database UUID. + /// Cancellation token. + /// The database details. + /// + Task GetAsync( + string databaseId, + CancellationToken cancellationToken = default); + + /// Updates a D1 database's configuration. + /// The database UUID. + /// The update options (e.g., read replication settings). + /// Cancellation token. + /// The updated database. + /// + /// Use this to enable/disable read replication by setting + /// ReadReplication.Mode to "auto" or "disabled". + /// + /// + Task UpdateAsync( + string databaseId, + UpdateD1DatabaseOptions options, + CancellationToken cancellationToken = default); + + /// Deletes a D1 database. + /// The database UUID to delete. + /// Cancellation token. + /// + /// This permanently deletes the database and all its data. + /// The operation cannot be undone. + /// + /// + Task DeleteAsync( + string databaseId, + CancellationToken cancellationToken = default); + + #endregion + + + #region Query Operations + + /// Executes a SQL query and returns results as objects. + /// The database UUID. + /// The SQL statement to execute. + /// Optional parameter values for parameterized queries. + /// Cancellation token. + /// Query results with metadata. + /// + /// Use ? placeholders in SQL and provide values in params array. + /// Multiple statements can be separated by semicolons for batch execution. + /// + /// + Task> QueryAsync( + string databaseId, + string sql, + IReadOnlyList? @params = null, + CancellationToken cancellationToken = default); + + /// Executes a SQL query and returns strongly-typed results. + /// The type to deserialize each row into. + /// The database UUID. + /// The SQL statement to execute. + /// Optional parameter values for parameterized queries. + /// Cancellation token. + /// Typed query results with metadata. + Task>> QueryAsync( + string databaseId, + string sql, + IReadOnlyList? @params = null, + CancellationToken cancellationToken = default); + + /// Executes a SQL query and returns results as arrays (raw format). + /// The database UUID. + /// The SQL statement to execute. + /// Optional parameter values for parameterized queries. + /// Cancellation token. + /// Raw query results with rows as arrays. + /// + /// The raw endpoint returns rows as arrays instead of objects, + /// which is more efficient for large result sets. + /// Column order matches the SELECT clause order. + /// + /// + Task> QueryRawAsync( + string databaseId, + string sql, + IReadOnlyList? @params = null, + CancellationToken cancellationToken = default); + + #endregion + + + #region Export Operations + + /// Initiates an export of the database to SQL format. + /// The database UUID. + /// Optional filtering options (schema only, data only, specific tables). + /// Cancellation token. + /// Initial export response with bookmark for polling. + /// + /// + /// Export is an async operation that requires polling. + /// Call with the returned bookmark until status is "complete". + /// + /// + /// The database is unavailable for queries during export. + /// Exports that aren't polled will auto-cancel. + /// + /// + /// + Task StartExportAsync( + string databaseId, + D1ExportDumpOptions? dumpOptions = null, + CancellationToken cancellationToken = default); + + /// Polls the status of an ongoing export operation. + /// The database UUID. + /// The bookmark from the previous response. + /// Cancellation token. + /// Export status. When complete, includes signed_url for download. + Task PollExportAsync( + string databaseId, + string bookmark, + CancellationToken cancellationToken = default); + + #endregion + + + #region Import Operations + + /// Initiates an import operation by requesting an upload URL. + /// The database UUID. + /// MD5 hash of the SQL file content. + /// Cancellation token. + /// Response containing upload_url and filename for SQL file upload. + /// + /// + /// Import workflow: + /// + /// Compute MD5 hash of your SQL file + /// Call with the hash to get upload URL and filename + /// PUT your SQL file to the upload_url (verify ETag matches) + /// Call with etag and filename to start ingestion + /// Poll with until status is "complete" + /// + /// + /// + /// The database is blocked during import operations. + /// + /// + /// + Task StartImportAsync( + string databaseId, + string etag, + CancellationToken cancellationToken = default); + + /// Completes an import after uploading the SQL file. + /// The database UUID. + /// The MD5 hash/ETag of the uploaded file. + /// The filename returned from . + /// Cancellation token. + /// Import response with bookmark for polling. + Task CompleteImportAsync( + string databaseId, + string etag, + string filename, + CancellationToken cancellationToken = default); + + /// Polls the status of an ongoing import operation. + /// The database UUID. + /// The bookmark from the previous response. + /// Cancellation token. + /// Import status with progress information. + Task PollImportAsync( + string databaseId, + string bookmark, + CancellationToken cancellationToken = default); + + #endregion +} diff --git a/src/Cloudflare.NET/Accounts/D1/Models/D1ExportModels.cs b/src/Cloudflare.NET/Accounts/D1/Models/D1ExportModels.cs new file mode 100644 index 0000000..fa7aecf --- /dev/null +++ b/src/Cloudflare.NET/Accounts/D1/Models/D1ExportModels.cs @@ -0,0 +1,84 @@ +namespace Cloudflare.NET.Accounts.D1.Models; + +using System.Text.Json.Serialization; + +#region Export Options + +/// Options for filtering what data to include in a database export. +/// If true, export only table definitions (schema), not contents. +/// If true, export only table contents (data), not definitions. +/// Filter export to specific tables. Empty or null means all tables. +public record D1ExportDumpOptions( + [property: JsonPropertyName("no_data")] + bool? NoData = null, + + [property: JsonPropertyName("no_schema")] + bool? NoSchema = null, + + [property: JsonPropertyName("tables")] + IReadOnlyList? Tables = null +); + +#endregion + + +#region Export Request/Response + +/// Request for initiating or polling a database export. +/// The output format. Use "polling" for async exports. +/// Bookmark from previous poll response (for polling). +/// Optional filtering options for the export. +internal record D1ExportRequest( + [property: JsonPropertyName("output_format")] + string OutputFormat = "polling", + + [property: JsonPropertyName("current_bookmark")] + string? CurrentBookmark = null, + + [property: JsonPropertyName("dump_options")] + D1ExportDumpOptions? DumpOptions = null +); + +/// Response from a database export operation. +/// Time-travel bookmark for polling. Stable for duration of export. +/// Export status: "active", "complete", or "error". +/// Export result containing download URL (when status is "complete"). +/// Error message (only present when status is "error"). +/// Operation type: "export". +/// Whether the operation was successful. +/// Status messages from the operation. +public record D1ExportResponse( + [property: JsonPropertyName("at_bookmark")] + string? AtBookmark = null, + + [property: JsonPropertyName("status")] + string? Status = null, + + [property: JsonPropertyName("result")] + D1ExportResultDetails? Result = null, + + [property: JsonPropertyName("error")] + string? Error = null, + + [property: JsonPropertyName("type")] + string? Type = null, + + [property: JsonPropertyName("success")] + bool Success = false, + + [property: JsonPropertyName("messages")] + IReadOnlyList? Messages = null +); + +/// Export result details containing the download URL. +/// The filename of the exported SQL file. +/// Temporary signed URL to download the SQL file. +public record D1ExportResultDetails( + [property: JsonPropertyName("filename")] + string? Filename = null, + + [property: JsonPropertyName("signed_url")] + string? SignedUrl = null +); + +#endregion diff --git a/src/Cloudflare.NET/Accounts/D1/Models/D1ImportModels.cs b/src/Cloudflare.NET/Accounts/D1/Models/D1ImportModels.cs new file mode 100644 index 0000000..f20b1bc --- /dev/null +++ b/src/Cloudflare.NET/Accounts/D1/Models/D1ImportModels.cs @@ -0,0 +1,81 @@ +namespace Cloudflare.NET.Accounts.D1.Models; + +using System.Text.Json.Serialization; + +#region Import Request + +/// Request for initiating or polling a database import. +/// The import action: "init" to start, "poll" to check status, "ingest" to complete. +/// MD5 hash/ETag of the SQL file (required for "init" and "ingest" actions). +/// Filename from init response (required for "ingest" action). +/// Bookmark from previous poll response (for polling). +internal record D1ImportRequest( + [property: JsonPropertyName("action")] + string Action, + + [property: JsonPropertyName("etag")] + string? Etag = null, + + [property: JsonPropertyName("filename")] + string? Filename = null, + + [property: JsonPropertyName("current_bookmark")] + string? CurrentBookmark = null +); + +#endregion + + +#region Import Response + +/// Response from a database import operation. +/// Time-travel bookmark for polling. +/// Import status: "active", "complete", or "error". +/// Import result details. +/// Error message (only present when status is "error"). +/// Operation type: "import". +/// Whether the operation was successful. +/// Filename for upload (when action is "init"). +/// Temporary URL for uploading SQL file (when action is "init"). +/// Status messages from the operation. +public record D1ImportResponse( + [property: JsonPropertyName("at_bookmark")] + string? AtBookmark = null, + + [property: JsonPropertyName("status")] + string? Status = null, + + [property: JsonPropertyName("result")] + D1ImportResultDetails? Result = null, + + [property: JsonPropertyName("error")] + string? Error = null, + + [property: JsonPropertyName("type")] + string? Type = null, + + [property: JsonPropertyName("success")] + bool Success = false, + + [property: JsonPropertyName("filename")] + string? Filename = null, + + [property: JsonPropertyName("upload_url")] + string? UploadUrl = null, + + [property: JsonPropertyName("messages")] + IReadOnlyList? Messages = null +); + +/// Import result details. +/// Number of queries executed during import. +/// Aggregated metadata from all import queries. +public record D1ImportResultDetails( + [property: JsonPropertyName("num_queries")] + int? NumQueries = null, + + [property: JsonPropertyName("meta")] + D1QueryMeta? Meta = null +); + +#endregion diff --git a/src/Cloudflare.NET/Accounts/D1/Models/D1Jurisdiction.cs b/src/Cloudflare.NET/Accounts/D1/Models/D1Jurisdiction.cs new file mode 100644 index 0000000..ae24675 --- /dev/null +++ b/src/Cloudflare.NET/Accounts/D1/Models/D1Jurisdiction.cs @@ -0,0 +1,136 @@ +namespace Cloudflare.NET.Accounts.D1.Models; + +using System.Diagnostics; +using System.Text.Json.Serialization; +using Core.Json; + +/// +/// Represents a jurisdictional restriction for D1 database data storage. +/// +/// Jurisdictions provide a guarantee that data stored in the database will remain within a specific +/// geographic or regulatory boundary. Unlike location hints, jurisdictions are enforced constraints. +/// +/// +/// +/// +/// This is an extensible enum that provides IntelliSense for known values while allowing custom values +/// for new jurisdictions that may be added to the Cloudflare API in the future. +/// +/// +/// When creating a D1 database with a jurisdiction, the primary_location_hint parameter is ignored. +/// The jurisdiction takes precedence and guarantees data residency within the specified region. +/// +/// +/// The jurisdiction requires Enterprise customer status. Contact your Cloudflare +/// account team for access. +/// +/// +/// +[DebuggerDisplay("{Value}")] +[JsonConverter(typeof(ExtensibleEnumConverter))] +public readonly struct D1Jurisdiction : IExtensibleEnum, IEquatable +{ + #region Properties & Fields - Non-Public + + /// The underlying string value of this jurisdiction. + private readonly string? _value; + + #endregion + + + #region Properties & Fields - Public + + /// Gets the underlying string value of this jurisdiction. + public string Value => _value ?? string.Empty; + + #endregion + + + #region Known Values + + /// European Union jurisdiction. + /// + /// Data is guaranteed to be stored within the European Union, helping meet GDPR + /// and other EU data residency requirements. + /// + public static D1Jurisdiction EuropeanUnion { get; } = new("eu"); + + /// FedRAMP jurisdiction. + /// + /// + /// Data is stored in compliance with FedRAMP (Federal Risk and Authorization Management Program) + /// requirements for U.S. federal government data. + /// + /// + /// This jurisdiction requires Enterprise customer status. Contact your Cloudflare account team + /// or Cloudflare Support for access. + /// + /// + public static D1Jurisdiction FedRamp { get; } = new("fedramp"); + + #endregion + + + #region Constructors + + /// Creates a new with the specified value. + /// The string value representing the jurisdiction. + /// Thrown when is null. + public D1Jurisdiction(string value) + { + ArgumentNullException.ThrowIfNull(value); + _value = value; + } + + #endregion + + + #region Static Factory + + /// + public static D1Jurisdiction Create(string value) => new(value); + + #endregion + + + #region Operators + + /// Implicitly converts a string to a . + /// The string value to convert. + public static implicit operator D1Jurisdiction(string value) => new(value); + + /// Implicitly converts a to its string value. + /// The jurisdiction to convert. + public static implicit operator string(D1Jurisdiction jurisdiction) => jurisdiction.Value; + + /// Determines whether two values are equal. + public static bool operator ==(D1Jurisdiction left, D1Jurisdiction right) => left.Equals(right); + + /// Determines whether two values are not equal. + public static bool operator !=(D1Jurisdiction left, D1Jurisdiction right) => !left.Equals(right); + + #endregion + + + #region Equality + + /// + public bool Equals(D1Jurisdiction other) => + string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override bool Equals(object? obj) => obj is D1Jurisdiction other && Equals(other); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + #endregion + + + #region Methods + + /// + public override string ToString() => Value; + + #endregion +} diff --git a/src/Cloudflare.NET/Accounts/D1/Models/D1Models.cs b/src/Cloudflare.NET/Accounts/D1/Models/D1Models.cs new file mode 100644 index 0000000..4b5bbf0 --- /dev/null +++ b/src/Cloudflare.NET/Accounts/D1/Models/D1Models.cs @@ -0,0 +1,115 @@ +namespace Cloudflare.NET.Accounts.D1.Models; + +using System.Text.Json.Serialization; +using Cloudflare.NET.Accounts.Models; + +#region Database Types + +/// Represents a D1 database. +/// The unique identifier for the database (UUID format). +/// The name of the database. +/// Timestamp when the database was created (ISO 8601). +/// The database size in bytes. +/// Number of tables in the database. +/// Database version: "production" or "alpha". +/// Configuration for D1 read replication. +/// Region code where the database is actually running (e.g., "WEUR", "ENAM"). Undocumented field present in API responses. +public record D1Database( + [property: JsonPropertyName("uuid")] + string Uuid, + + [property: JsonPropertyName("name")] + string Name, + + [property: JsonPropertyName("created_at")] + DateTimeOffset? CreatedAt = null, + + [property: JsonPropertyName("file_size")] + long? FileSize = null, + + [property: JsonPropertyName("num_tables")] + int? NumTables = null, + + [property: JsonPropertyName("version")] + string? Version = null, + + [property: JsonPropertyName("read_replication")] + D1ReadReplication? ReadReplication = null, + + [property: JsonPropertyName("running_in_region")] + string? RunningInRegion = null +); + +/// Read replication configuration for a D1 database. +/// The replication mode: "auto" to enable, "disabled" to disable. +public record D1ReadReplication( + [property: JsonPropertyName("mode")] + string Mode +); + +/// Filters for listing D1 databases. +/// Filter by database name (partial match). +/// Page number of results to return (1-based). +/// Number of results per page. +public record ListD1DatabasesFilters( + string? Name = null, + int? Page = null, + int? PerPage = null +); + +#endregion + + +#region Create/Update Types + +/// Options for creating a D1 database. +/// The name for the new database (required). +/// +/// Optional region hint for database placement. +/// Use R2LocationHint constants (e.g., R2LocationHint.WestNorthAmerica). +/// Ignored if Jurisdiction is specified. +/// +/// +/// Optional jurisdictional restriction guaranteeing data residency. +/// Overrides PrimaryLocationHint when specified. +/// +public record CreateD1DatabaseOptions( + string Name, + R2LocationHint? PrimaryLocationHint = null, + D1Jurisdiction? Jurisdiction = null +); + +/// Options for updating a D1 database. +/// Configuration for read replication. +public record UpdateD1DatabaseOptions( + D1ReadReplication? ReadReplication = null +); + +#endregion + + +#region Internal Request Types + +/// Internal request body for creating a D1 database. +/// The name for the new database. +/// Optional region hint for database placement. +/// Optional jurisdictional restriction. +internal record CreateD1DatabaseRequest( + [property: JsonPropertyName("name")] + string Name, + + [property: JsonPropertyName("primary_location_hint")] + string? PrimaryLocationHint = null, + + [property: JsonPropertyName("jurisdiction")] + string? Jurisdiction = null +); + +/// Internal request body for updating a D1 database. +/// Configuration for read replication. +internal record UpdateD1DatabaseRequest( + [property: JsonPropertyName("read_replication")] + D1ReadReplication? ReadReplication = null +); + +#endregion diff --git a/src/Cloudflare.NET/Accounts/D1/Models/D1QueryModels.cs b/src/Cloudflare.NET/Accounts/D1/Models/D1QueryModels.cs new file mode 100644 index 0000000..d10e785 --- /dev/null +++ b/src/Cloudflare.NET/Accounts/D1/Models/D1QueryModels.cs @@ -0,0 +1,157 @@ +namespace Cloudflare.NET.Accounts.D1.Models; + +using System.Text.Json; +using System.Text.Json.Serialization; + +#region Query Request + +/// Request for executing a SQL query. +/// The SQL statement to execute. Supports multiple statements separated by semicolons. +/// Optional array of parameter values for parameterized queries. +/// +/// Use parameterized queries with ? placeholders to prevent SQL injection. +/// Example: "SELECT * FROM users WHERE id = ?" with params ["123"] +/// +public record D1QueryRequest( + [property: JsonPropertyName("sql")] + string Sql, + + [property: JsonPropertyName("params")] + IReadOnlyList? Params = null +); + +#endregion + + +#region Query Result Types + +/// Result of a D1 query execution. +/// The type of each row in the results. +/// Metadata about the query execution. +/// The query result rows. +/// Whether the query executed successfully. +public record D1QueryResult( + [property: JsonPropertyName("meta")] + D1QueryMeta Meta, + + [property: JsonPropertyName("results")] + IReadOnlyList Results, + + [property: JsonPropertyName("success")] + bool Success +); + +/// Non-generic query result for dynamic/untyped results. +/// Metadata about the query execution. +/// The query result rows as JSON elements. +/// Whether the query executed successfully. +public record D1QueryResult( + [property: JsonPropertyName("meta")] + D1QueryMeta Meta, + + [property: JsonPropertyName("results")] + IReadOnlyList Results, + + [property: JsonPropertyName("success")] + bool Success +); + +#endregion + + +#region Query Metadata + +/// Metadata about a D1 query execution. +/// Whether the database was modified by this query. +/// Approximate number of rows modified (from sqlite3_total_changes). +/// Duration of SQL execution inside the database (ms). +/// Row ID of last inserted row (for INTEGER PRIMARY KEY tables, null for WITHOUT ROWID tables). +/// Number of rows read during execution (including indices). +/// Number of rows written during execution (including indices). +/// Version of Cloudflare's backend Worker that returned the result. +/// Whether the query was handled by the primary instance. +/// Region code of the instance that handled the query. +/// Database size in bytes after the query committed. +/// Detailed timing information. +/// Number of execution attempts for read-only queries (D1 auto-retries up to 2 times). +public record D1QueryMeta( + [property: JsonPropertyName("changed_db")] + bool ChangedDb, + + [property: JsonPropertyName("changes")] + int Changes, + + [property: JsonPropertyName("duration")] + double Duration, + + [property: JsonPropertyName("last_row_id")] + long LastRowId, + + [property: JsonPropertyName("rows_read")] + long RowsRead, + + [property: JsonPropertyName("rows_written")] + long RowsWritten, + + [property: JsonPropertyName("served_by")] + string? ServedBy = null, + + [property: JsonPropertyName("served_by_primary")] + bool? ServedByPrimary = null, + + [property: JsonPropertyName("served_by_region")] + string? ServedByRegion = null, + + [property: JsonPropertyName("size_after")] + long? SizeAfter = null, + + [property: JsonPropertyName("timings")] + D1QueryTimings? Timings = null, + + [property: JsonPropertyName("total_attempts")] + int? TotalAttempts = null +); + +/// Detailed timing information for a D1 query. +/// SQL execution duration in milliseconds. +public record D1QueryTimings( + [property: JsonPropertyName("sql_duration_ms")] + double? SqlDurationMs = null +); + +#endregion + + +#region Raw Query Result Types + +/// Result of a raw query execution (array format). +/// +/// Raw queries return rows as arrays instead of objects for better performance. +/// Column names are provided in the Columns array; row values are in the Rows array. +/// +/// Metadata about the query execution. +/// The raw query result set containing columns and rows. +/// Whether the query executed successfully. +public record D1RawQueryResult( + [property: JsonPropertyName("meta")] + D1QueryMeta Meta, + + [property: JsonPropertyName("results")] + D1RawQueryResultSet Results, + + [property: JsonPropertyName("success")] + bool Success +); + +/// Result set from a raw query containing columns and rows. +/// Array of column names in SELECT order. +/// Array of row arrays, each containing values in column order. +public record D1RawQueryResultSet( + [property: JsonPropertyName("columns")] + IReadOnlyList Columns, + + [property: JsonPropertyName("rows")] + IReadOnlyList> Rows +); + +#endregion diff --git a/src/Cloudflare.NET/Accounts/IAccountsApi.cs b/src/Cloudflare.NET/Accounts/IAccountsApi.cs index c5ebc55..c101227 100644 --- a/src/Cloudflare.NET/Accounts/IAccountsApi.cs +++ b/src/Cloudflare.NET/Accounts/IAccountsApi.cs @@ -3,6 +3,7 @@ using AccessRules; using Buckets; using Core.Models; +using D1; using Kv; using Models; using Rulesets; @@ -36,6 +37,19 @@ public interface IAccountsApi /// IKvApi Kv { get; } + /// Gets the API for managing D1 serverless SQL databases. + /// + /// Corresponds to the `/accounts/{account_id}/d1/database` endpoint family. + /// + /// D1 is Cloudflare's native serverless SQL database built on SQLite. The REST API + /// is subject to global Cloudflare API rate limits and is best suited for administrative + /// operations. For high-throughput query access, consider using a Worker with D1 bindings. + /// + /// + /// + /// + ID1Api D1 { get; } + /// Creates a new R2 bucket within the configured account with optional location and storage settings. /// The desired name for the new bucket. Must be unique. /// diff --git a/src/Cloudflare.NET/Cloudflare.NET.csproj b/src/Cloudflare.NET/Cloudflare.NET.csproj index 808b69f..99d9268 100644 --- a/src/Cloudflare.NET/Cloudflare.NET.csproj +++ b/src/Cloudflare.NET/Cloudflare.NET.csproj @@ -5,7 +5,7 @@ Cloudflare.NET.Api - 3.2.0 + 3.3.0 Cloudflare.NET - A comprehensive C# client library for the Cloudflare REST API. Manage DNS records, Zones, R2 buckets, Workers, WAF rules, Turnstile, and security features with strongly-typed .NET code. cloudflare;cloudflare-api;cloudflare-sdk;cloudflare-client;dotnet;csharp;dns;r2;waf;firewall;zone;workers;turnstile;api-client;rest-client diff --git a/tests/Cloudflare.NET.Tests/IntegrationTests/D1ApiIntegrationTests.cs b/tests/Cloudflare.NET.Tests/IntegrationTests/D1ApiIntegrationTests.cs new file mode 100644 index 0000000..6cab967 --- /dev/null +++ b/tests/Cloudflare.NET.Tests/IntegrationTests/D1ApiIntegrationTests.cs @@ -0,0 +1,995 @@ +namespace Cloudflare.NET.Tests.IntegrationTests; + +using System.Net; +using System.Security.Cryptography; +using System.Text; +using Accounts; +using Accounts.D1; +using Accounts.D1.Models; +using Accounts.Models; +using Cloudflare.NET.Core.Exceptions; +using Fixtures; +using Microsoft.Extensions.DependencyInjection; +using Shared.Fixtures; +using Shared.Helpers; +using Xunit.Abstractions; + +/// +/// Contains integration tests for the D1 database operations of . These tests interact with the +/// live Cloudflare API and require credentials. +/// +/// +/// This test class covers all D1 database operations including: +/// +/// Database CRUD operations (List, Create, Get, Update, Delete) +/// Query operations (Query, QueryRaw, typed queries) +/// Read replication configuration +/// Pagination handling +/// Export operations with polling workflow +/// Import operations with file upload and polling workflow +/// Jurisdiction and location hint options +/// +/// +[Trait("Category", TestConstants.TestCategories.Integration)] +public class D1ApiIntegrationTests : IClassFixture, IAsyncLifetime +{ + #region Properties & Fields - Non-Public + + /// The subject under test, resolved from the test fixture. + private readonly ID1Api _sut; + + /// A unique name for the D1 database used in this test run, to avoid collisions. + private readonly string _databaseName = $"cfnet-test-d1-{Guid.NewGuid():N}"[..32]; + + /// The UUID of the created database, populated during InitializeAsync. + private string _databaseId = string.Empty; + + /// The xUnit test output helper for writing logs. + private readonly ITestOutputHelper _output; + + #endregion + + + #region Constructors + + /// Initializes a new instance of the class. + /// The shared test fixture that provides configured API clients. + /// The xUnit test output helper. + public D1ApiIntegrationTests(CloudflareApiTestFixture fixture, ITestOutputHelper output) + { + // The SUT is resolved via the fixture's pre-configured DI container. + _sut = fixture.AccountsApi.D1; + _output = output; + + // Wire up the logger provider to the current test's output. + var loggerProvider = fixture.ServiceProvider.GetRequiredService(); + loggerProvider.Current = output; + } + + #endregion + + + #region Methods Impl + + /// Asynchronously creates the D1 database required for the tests. This runs once before any tests in this class. + public async Task InitializeAsync() + { + // Create a new D1 database for the test run. + var db = await _sut.CreateAsync(_databaseName); + _databaseId = db.Uuid; + + _output.WriteLine($"Created test database: {_databaseId} ({_databaseName})"); + + // Create a test table for query operations. + await _sut.QueryAsync(_databaseId, """ + CREATE TABLE IF NOT EXISTS test_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT UNIQUE, + created_at TEXT DEFAULT (datetime('now')) + ) + """); + + _output.WriteLine("Created test_users table"); + } + + /// Asynchronously deletes the D1 database after all tests in this class have run. + /// + /// If cleanup fails, the exception propagates to the test framework. This ensures we are immediately + /// aware of any issues with resource cleanup rather than silently logging and continuing. + /// + public async Task DisposeAsync() + { + // Clean up the D1 database. Let any exceptions propagate - we want deterministic failure visibility. + if (!string.IsNullOrEmpty(_databaseId)) + { + await _sut.DeleteAsync(_databaseId); + _output.WriteLine($"Deleted test database: {_databaseId}"); + } + } + + #endregion + + + #region Database Operations + + /// Verifies that D1 databases can be listed successfully. + /// + /// + /// IMPORTANT API LIMITATION: The D1 API returns total_count=0 and total_pages=0 in the + /// result_info, which contradicts the standard Cloudflare API schema. This appears to be a D1-specific + /// bug or limitation. Our works around this by checking if the current + /// page is full (item count equals PerPage) rather than relying on TotalPages. + /// + /// + [IntegrationTest] + public async Task ListAsync_CanListSuccessfully() + { + // Arrange (database is created in InitializeAsync) + + // Act + var result = await _sut.ListAsync(); + + // Assert + result.Items.Should().NotBeEmpty("at least one database should exist"); + result.Items.Should().Contain(db => db.Uuid == _databaseId, "the test database should be in the list"); + + // Verify pagination info is returned + result.PageInfo.Should().NotBeNull("pagination info should be present"); + + // Log actual PageInfo values for diagnostics (D1 API returns 0 for TotalCount/TotalPages - this is a known limitation) + _output.WriteLine($"PageInfo: Page={result.PageInfo!.Page}, PerPage={result.PageInfo.PerPage}, " + + $"Count={result.PageInfo.Count}, TotalCount={result.PageInfo.TotalCount}, TotalPages={result.PageInfo.TotalPages}"); + + // Assert expected pagination invariants (Note: TotalCount and TotalPages are NOT asserted due to D1 API limitation) + result.PageInfo.Page.Should().BeGreaterThanOrEqualTo(1, "page number should be at least 1"); + result.PageInfo.PerPage.Should().BeGreaterThan(0, "per_page should be positive"); + result.PageInfo.Count.Should().Be(result.Items.Count, "count should match actual items returned"); + + // Document the API limitation: TotalCount and TotalPages are always 0 from the D1 API + // This is inconsistent with the standard Cloudflare API schema which should include proper values + _output.WriteLine("NOTE: D1 API returns TotalCount=0 and TotalPages=0 - this is a known Cloudflare D1 API limitation"); + } + + /// Verifies that ListAsync with name filter works correctly. + [IntegrationTest] + public async Task ListAsync_WithNameFilter_FiltersCorrectly() + { + // Arrange (database is created in InitializeAsync) + + // Act + var result = await _sut.ListAsync(new ListD1DatabasesFilters(Name: _databaseName)); + + // Assert + result.Items.Should().ContainSingle("only the test database should match the filter"); + result.Items[0].Uuid.Should().Be(_databaseId); + result.Items[0].Name.Should().Be(_databaseName); + } + + /// Verifies that ListAllAsync can iterate through all databases. + [IntegrationTest] + public async Task ListAllAsync_CanIterateThroughAllDatabases() + { + // Arrange - Create a second database to ensure multiple exist + var secondDatabaseName = $"cfnet-test-d1-{Guid.NewGuid():N}"[..32]; + var secondDb = await _sut.CreateAsync(secondDatabaseName); + + try + { + // Act + var allDatabases = new List(); + await foreach (var db in _sut.ListAllAsync()) + allDatabases.Add(db); + + // Assert + allDatabases.Should().NotBeEmpty(); + allDatabases.Should().Contain(db => db.Uuid == _databaseId, "the primary test database should be found"); + allDatabases.Should().Contain(db => db.Uuid == secondDb.Uuid, "the second test database should be found"); + } + finally + { + // Cleanup + await _sut.DeleteAsync(secondDb.Uuid); + } + } + + /// + /// Verifies that ListAllAsync correctly paginates through multiple pages when using a small PerPage value. + /// This test specifically validates the pagination logic works around the D1 API limitation where + /// TotalPages is always 0. + /// + [IntegrationTest] + public async Task ListAllAsync_WithSmallPerPage_CorrectlyIteratesMultiplePages() + { + // Arrange - Create additional databases to ensure we have at least 3 (including the fixture database) + var db2Name = $"cfnet-page-a-{Guid.NewGuid():N}"[..32]; + var db3Name = $"cfnet-page-b-{Guid.NewGuid():N}"[..32]; + var db2 = await _sut.CreateAsync(db2Name); + var db3 = await _sut.CreateAsync(db3Name); + + try + { + // Act - Use PerPage=1 to force multiple pages + var filters = new ListD1DatabasesFilters(PerPage: 1); + var allDatabases = new List(); + + await foreach (var db in _sut.ListAllAsync(filters)) + allDatabases.Add(db); + + // Assert - Should find all 3 test databases even though each page only has 1 item + allDatabases.Should().HaveCountGreaterThanOrEqualTo(3, "at least 3 databases should exist"); + allDatabases.Should().Contain(db => db.Uuid == _databaseId, "fixture database should be found"); + allDatabases.Should().Contain(db => db.Uuid == db2.Uuid, "second database should be found"); + allDatabases.Should().Contain(db => db.Uuid == db3.Uuid, "third database should be found"); + + _output.WriteLine($"Successfully iterated through {allDatabases.Count} databases with PerPage=1"); + } + finally + { + // Cleanup + await _sut.DeleteAsync(db2.Uuid); + await _sut.DeleteAsync(db3.Uuid); + } + } + + /// Verifies that GetAsync retrieves database properties. + [IntegrationTest] + public async Task GetAsync_ReturnsDatabaseProperties() + { + // Arrange (database is created in InitializeAsync) + + // Act + var result = await _sut.GetAsync(_databaseId); + + // Assert + result.Should().NotBeNull(); + result.Uuid.Should().Be(_databaseId); + result.Name.Should().Be(_databaseName); + result.Version.Should().Be("production", "new databases should be production version"); + result.NumTables.Should().BeGreaterThanOrEqualTo(1, "at least the test_users table should exist"); + } + + /// Verifies that a database can be created and deleted successfully. + [IntegrationTest] + public async Task CanCreateAndDeleteDatabase() + { + // Arrange + var name = $"cfnet-standalone-{Guid.NewGuid():N}"[..32]; + + // Act + var createResult = await _sut.CreateAsync(name); + + // Assert + createResult.Should().NotBeNull(); + createResult.Uuid.Should().NotBeNullOrEmpty(); + createResult.Name.Should().Be(name); + + // Cleanup & verify deletion works + var deleteAction = async () => await _sut.DeleteAsync(createResult.Uuid); + await deleteAction.Should().NotThrowAsync("deletion should succeed"); + } + + /// Verifies that a database can be created with a location hint. + [IntegrationTest] + public async Task CreateAsync_WithLocationHint_CreatesSuccessfully() + { + // Arrange + var name = $"cfnet-loc-{Guid.NewGuid():N}"[..32]; + + // Act + var createResult = await _sut.CreateAsync(name, primaryLocationHint: R2LocationHint.WestEurope); + + try + { + // Assert create response + createResult.Should().NotBeNull(); + createResult.Uuid.Should().NotBeNullOrEmpty(); + createResult.Name.Should().Be(name); + createResult.Version.Should().Be("production"); + + // The API may not return RunningInRegion immediately on create - fetch full details via GetAsync + var result = await _sut.GetAsync(createResult.Uuid); + + result.RunningInRegion.Should().NotBeNullOrEmpty("database should report its running region after Get"); + // With WestEurope hint, we expect a European region (WEUR or EEUR) + result.RunningInRegion.Should().BeOneOf("WEUR", "EEUR", "weur", "eeur", + "database with WestEurope hint should be placed in a European region"); + + _output.WriteLine($"Database created in region: {result.RunningInRegion}"); + } + finally + { + // Cleanup + await _sut.DeleteAsync(createResult.Uuid); + } + } + + /// Verifies that UpdateAsync can modify read replication settings. + [IntegrationTest] + public async Task UpdateAsync_CanEnableReadReplication() + { + // Arrange - Create a temporary database + var name = $"cfnet-update-{Guid.NewGuid():N}"[..32]; + var db = await _sut.CreateAsync(name); + + try + { + // Act - Enable read replication + var updateOptions = new UpdateD1DatabaseOptions( + ReadReplication: new D1ReadReplication("auto") + ); + var result = await _sut.UpdateAsync(db.Uuid, updateOptions); + + // Assert + result.Should().NotBeNull(); + result.ReadReplication.Should().NotBeNull(); + result.ReadReplication!.Mode.Should().Be("auto"); + } + finally + { + // Cleanup + await _sut.DeleteAsync(db.Uuid); + } + } + + /// Verifies that UpdateAsync can disable read replication. + [IntegrationTest] + public async Task UpdateAsync_CanDisableReadReplication() + { + // Arrange - Create a temporary database and enable replication first + var name = $"cfnet-disable-rep-{Guid.NewGuid():N}"[..32]; + var db = await _sut.CreateAsync(name); + + try + { + // First enable read replication + await _sut.UpdateAsync(db.Uuid, new UpdateD1DatabaseOptions( + ReadReplication: new D1ReadReplication("auto") + )); + + // Act - Disable read replication + var updateOptions = new UpdateD1DatabaseOptions( + ReadReplication: new D1ReadReplication("disabled") + ); + var result = await _sut.UpdateAsync(db.Uuid, updateOptions); + + // Assert + result.Should().NotBeNull(); + result.ReadReplication.Should().NotBeNull(); + result.ReadReplication!.Mode.Should().Be("disabled"); + } + finally + { + // Cleanup + await _sut.DeleteAsync(db.Uuid); + } + } + + /// Verifies that a database can be created with EU jurisdiction for GDPR compliance. + [IntegrationTest] + public async Task CreateAsync_WithEuJurisdiction_CreatesSuccessfully() + { + // Arrange + var name = $"cfnet-eu-{Guid.NewGuid():N}"[..32]; + + // Act + var createResult = await _sut.CreateAsync(name, jurisdiction: D1Jurisdiction.EuropeanUnion); + + try + { + // Assert create response + createResult.Should().NotBeNull(); + createResult.Uuid.Should().NotBeNullOrEmpty(); + createResult.Name.Should().Be(name); + createResult.Version.Should().Be("production"); + + // The API may not return RunningInRegion immediately on create - fetch full details via GetAsync + var result = await _sut.GetAsync(createResult.Uuid); + + // EU jurisdiction is a HARD guarantee - database MUST be in an EU region + result.RunningInRegion.Should().NotBeNullOrEmpty("database should report its running region after Get"); + result.RunningInRegion.Should().BeOneOf("WEUR", "EEUR", "weur", "eeur", + "EU jurisdiction database MUST be placed in a European region"); + + _output.WriteLine($"EU jurisdiction database created in region: {result.RunningInRegion}"); + } + finally + { + // Cleanup + await _sut.DeleteAsync(createResult.Uuid); + } + } + + /// Verifies that ListAsync with pagination filters works correctly. + /// + /// Note: The D1 API does not reliably return TotalCount in the pagination info, so this test + /// focuses on verifying that PerPage limits are respected and pages contain different items. + /// + [IntegrationTest] + public async Task ListAsync_WithPaginationFilters_ReturnsCorrectPage() + { + // Arrange - Create multiple databases to ensure we have enough for pagination + var db1Name = $"cfnet-page-1-{Guid.NewGuid():N}"[..32]; + var db2Name = $"cfnet-page-2-{Guid.NewGuid():N}"[..32]; + var db1 = await _sut.CreateAsync(db1Name); + var db2 = await _sut.CreateAsync(db2Name); + + try + { + // Act - Request first page with perPage=1 + var page1 = await _sut.ListAsync(new ListD1DatabasesFilters(Page: 1, PerPage: 1)); + + // Assert page 1 basic properties + page1.Items.Should().HaveCount(1, "perPage=1 should return exactly 1 item"); + page1.PageInfo.Should().NotBeNull("pagination info should always be present"); + page1.PageInfo!.Page.Should().Be(1, "should be on page 1"); + page1.PageInfo.PerPage.Should().Be(1, "should respect perPage limit"); + + // Act - Request second page + var page2 = await _sut.ListAsync(new ListD1DatabasesFilters(Page: 2, PerPage: 1)); + + // Assert page 2 basic properties + page2.Items.Should().HaveCount(1, "second page should also have 1 item"); + page2.PageInfo.Should().NotBeNull("pagination info should always be present"); + page2.PageInfo!.Page.Should().Be(2, "should be on page 2"); + + // The items on page 1 and page 2 should be different (key pagination invariant) + page1.Items[0].Uuid.Should().NotBe(page2.Items[0].Uuid, "different pages MUST have different items"); + + // Verify we can find our test databases across pages using ListAllAsync + var allDatabases = new List(); + await foreach (var db in _sut.ListAllAsync()) + allDatabases.Add(db); + + allDatabases.Should().Contain(d => d.Uuid == db1.Uuid, "first test database should exist"); + allDatabases.Should().Contain(d => d.Uuid == db2.Uuid, "second test database should exist"); + + _output.WriteLine($"Pagination verified: page 1 = {page1.Items[0].Uuid}, page 2 = {page2.Items[0].Uuid}"); + } + finally + { + // Cleanup + await _sut.DeleteAsync(db1.Uuid); + await _sut.DeleteAsync(db2.Uuid); + } + } + + #endregion + + + #region Query Operations + + /// Verifies that a simple SELECT query can be executed. + [IntegrationTest] + public async Task QueryAsync_CanExecuteSelectQuery() + { + // Arrange + var sql = "SELECT 1 as value"; + + // Act + var result = await _sut.QueryAsync(_databaseId, sql); + + // Assert + result.Should().ContainSingle(); + result[0].Success.Should().BeTrue(); + result[0].Meta.Duration.Should().BeGreaterThan(0, "query should have measurable duration"); + } + + /// Verifies that an INSERT query can be executed with parameters. + [IntegrationTest] + public async Task QueryAsync_CanExecuteInsertWithParams() + { + // Arrange + var uniqueEmail = $"test-{Guid.NewGuid():N}@example.com"; + var sql = "INSERT INTO test_users (name, email) VALUES (?, ?)"; + + // Act + var result = await _sut.QueryAsync(_databaseId, sql, new object?[] { "Test User", uniqueEmail }); + + // Assert + result.Should().ContainSingle(); + result[0].Success.Should().BeTrue(); + result[0].Meta.ChangedDb.Should().BeTrue("INSERT should modify the database"); + result[0].Meta.Changes.Should().BeGreaterThanOrEqualTo(1, "at least one row should be inserted"); + } + + /// Verifies that a SELECT query with parameters works correctly. + [IntegrationTest] + public async Task QueryAsync_CanExecuteSelectWithParams() + { + // Arrange - Insert a test row first + var uniqueEmail = $"param-test-{Guid.NewGuid():N}@example.com"; + await _sut.QueryAsync(_databaseId, "INSERT INTO test_users (name, email) VALUES (?, ?)", + new object?[] { "Param Test", uniqueEmail }); + + var sql = "SELECT id, name, email FROM test_users WHERE email = ?"; + + // Act + var result = await _sut.QueryAsync(_databaseId, sql, new object?[] { uniqueEmail }); + + // Assert + result.Should().ContainSingle(); + result[0].Success.Should().BeTrue(); + result[0].Results.Should().ContainSingle("one row should match"); + } + + /// Verifies that multiple statements can be executed in a single query. + /// + /// Note: D1 API does not support params with multiple statements, so we use literal values. + /// + [IntegrationTest] + public async Task QueryAsync_CanExecuteMultipleStatements() + { + // Arrange - Multiple statements separated by semicolons. + // Note: D1 API does not support params with multiple statements, so we embed unique values directly. + var uniqueId1 = Guid.NewGuid().ToString("N"); + var uniqueId2 = Guid.NewGuid().ToString("N"); + var sql = $"INSERT INTO test_users (name, email) VALUES ('User 1', 'multi-1-{uniqueId1}@example.com'); INSERT INTO test_users (name, email) VALUES ('User 2', 'multi-2-{uniqueId2}@example.com')"; + + // Act + var result = await _sut.QueryAsync(_databaseId, sql); + + // Assert + result.Should().HaveCount(2, "two statements should produce two results"); + result[0].Success.Should().BeTrue(); + result[1].Success.Should().BeTrue(); + } + + /// Verifies that QueryRawAsync returns array-format results. + [IntegrationTest] + public async Task QueryRawAsync_ReturnsArrayFormatResults() + { + // Arrange - Insert a test row first + var uniqueEmail = $"raw-test-{Guid.NewGuid():N}@example.com"; + await _sut.QueryAsync(_databaseId, "INSERT INTO test_users (name, email) VALUES (?, ?)", + new object?[] { "Raw Test", uniqueEmail }); + + var sql = "SELECT id, name, email FROM test_users WHERE email = ?"; + + // Act + var result = await _sut.QueryRawAsync(_databaseId, sql, new object?[] { uniqueEmail }); + + // Assert + result.Should().ContainSingle(); + result[0].Success.Should().BeTrue(); + result[0].Results.Columns.Should().ContainInOrder("id", "name", "email"); + result[0].Results.Rows.Should().ContainSingle("one row should match"); + } + + /// Verifies that query metadata includes region and timing information. + [IntegrationTest] + public async Task QueryAsync_ReturnsDetailedMetadata() + { + // Arrange + var sql = "SELECT COUNT(*) FROM test_users"; + + // Act + var result = await _sut.QueryAsync(_databaseId, sql); + + // Assert + result.Should().ContainSingle(); + var meta = result[0].Meta; + meta.Duration.Should().BeGreaterThan(0, "query duration should be measurable"); + meta.RowsRead.Should().BeGreaterThanOrEqualTo(0, "rows_read should be populated"); + meta.RowsWritten.Should().Be(0, "SELECT query should not write rows"); + meta.ChangedDb.Should().BeFalse("SELECT query should not change the database"); + meta.ServedByRegion.Should().NotBeNullOrEmpty("query should report serving region"); + } + + /// Verifies that SQL syntax errors are reported correctly. + [IntegrationTest] + public async Task QueryAsync_SqlSyntaxError_ThrowsException() + { + // Arrange + var sql = "SELEC * FORM users"; // Intentional typos + + // Act + var action = async () => await _sut.QueryAsync(_databaseId, sql); + + // Assert + await action.Should().ThrowAsync("invalid SQL should throw an exception"); + } + + /// Verifies that an UPDATE query can be executed with parameters. + [IntegrationTest] + public async Task QueryAsync_CanExecuteUpdateWithParams() + { + // Arrange - Insert a test row first + var uniqueEmail = $"update-test-{Guid.NewGuid():N}@example.com"; + await _sut.QueryAsync(_databaseId, "INSERT INTO test_users (name, email) VALUES (?, ?)", + new object?[] { "Original Name", uniqueEmail }); + + var sql = "UPDATE test_users SET name = ? WHERE email = ?"; + + // Act + var result = await _sut.QueryAsync(_databaseId, sql, new object?[] { "Updated Name", uniqueEmail }); + + // Assert + result.Should().ContainSingle(); + result[0].Success.Should().BeTrue(); + result[0].Meta.ChangedDb.Should().BeTrue("UPDATE should modify the database"); + result[0].Meta.Changes.Should().BeGreaterThanOrEqualTo(1, "at least one row should be updated"); + + // Verify the update took effect + var verifyResult = await _sut.QueryAsync(_databaseId, "SELECT name FROM test_users WHERE email = ?", + new object?[] { uniqueEmail }); + verifyResult[0].Results.Should().ContainSingle(); + } + + /// Verifies that a DELETE query can be executed with parameters. + [IntegrationTest] + public async Task QueryAsync_CanExecuteDeleteWithParams() + { + // Arrange - Insert a test row first + var uniqueEmail = $"delete-test-{Guid.NewGuid():N}@example.com"; + await _sut.QueryAsync(_databaseId, "INSERT INTO test_users (name, email) VALUES (?, ?)", + new object?[] { "To Be Deleted", uniqueEmail }); + + // Verify the row exists + var beforeDelete = await _sut.QueryAsync(_databaseId, "SELECT id FROM test_users WHERE email = ?", + new object?[] { uniqueEmail }); + beforeDelete[0].Results.Should().ContainSingle("row should exist before delete"); + + var sql = "DELETE FROM test_users WHERE email = ?"; + + // Act + var result = await _sut.QueryAsync(_databaseId, sql, new object?[] { uniqueEmail }); + + // Assert + result.Should().ContainSingle(); + result[0].Success.Should().BeTrue(); + result[0].Meta.ChangedDb.Should().BeTrue("DELETE should modify the database"); + result[0].Meta.Changes.Should().BeGreaterThanOrEqualTo(1, "at least one row should be deleted"); + + // Verify the row no longer exists + var afterDelete = await _sut.QueryAsync(_databaseId, "SELECT id FROM test_users WHERE email = ?", + new object?[] { uniqueEmail }); + afterDelete[0].Results.Should().BeEmpty("row should not exist after delete"); + } + + /// Verifies that a transaction with RETURNING clause works correctly. + [IntegrationTest] + public async Task QueryAsync_CanExecuteInsertWithReturning() + { + // Arrange + var uniqueEmail = $"returning-test-{Guid.NewGuid():N}@example.com"; + var sql = "INSERT INTO test_users (name, email) VALUES (?, ?) RETURNING id, name, email"; + + // Act + var result = await _sut.QueryAsync(_databaseId, sql, new object?[] { "Returning Test", uniqueEmail }); + + // Assert + result.Should().ContainSingle(); + result[0].Success.Should().BeTrue(); + result[0].Results.Should().ContainSingle("RETURNING should return the inserted row"); + result[0].Meta.Changes.Should().BeGreaterThanOrEqualTo(1); + } + + /// Verifies that typed query results can be retrieved. + [IntegrationTest] + public async Task QueryAsyncTyped_ReturnsTypedResults() + { + // Arrange - Insert a test row first + var uniqueEmail = $"typed-test-{Guid.NewGuid():N}@example.com"; + await _sut.QueryAsync(_databaseId, "INSERT INTO test_users (name, email) VALUES (?, ?)", + new object?[] { "Typed Test", uniqueEmail }); + + var sql = "SELECT id, name, email FROM test_users WHERE email = ?"; + + // Act + var result = await _sut.QueryAsync(_databaseId, sql, new object?[] { uniqueEmail }); + + // Assert + result.Should().ContainSingle(); + result[0].Success.Should().BeTrue(); + result[0].Results.Should().ContainSingle(); + var user = result[0].Results[0]; + user.Name.Should().Be("Typed Test"); + user.Email.Should().Be(uniqueEmail); + } + + #endregion + + + #region Error Handling + + /// Verifies that GetAsync for a non-existent database returns appropriate error. + [IntegrationTest] + public async Task GetAsync_NonExistentDatabase_ThrowsError() + { + // Arrange + var nonExistentId = Guid.NewGuid().ToString(); + + // Act + var action = async () => await _sut.GetAsync(nonExistentId); + + // Assert + await action.Should().ThrowAsync("accessing a non-existent database should fail"); + } + + /// Verifies that DeleteAsync for a non-existent database throws appropriate error. + [IntegrationTest] + public async Task DeleteAsync_NonExistentDatabase_ThrowsError() + { + // Arrange + var nonExistentId = Guid.NewGuid().ToString(); + + // Act + var action = async () => await _sut.DeleteAsync(nonExistentId); + + // Assert + await action.Should().ThrowAsync("deleting a non-existent database should fail"); + } + + #endregion + + + #region Export Operations + + /// Verifies that a database can be exported to SQL format using the polling workflow. + [IntegrationTest] + public async Task Export_FullWorkflow_CompletesSuccessfully() + { + // Arrange - Ensure there's data to export + var uniqueEmail = $"export-test-{Guid.NewGuid():N}@example.com"; + await _sut.QueryAsync(_databaseId, "INSERT INTO test_users (name, email) VALUES (?, ?)", + new object?[] { "Export Test User", uniqueEmail }); + + // Act - Start export + var exportStart = await _sut.StartExportAsync(_databaseId); + + // Assert initial response + exportStart.Should().NotBeNull(); + exportStart.AtBookmark.Should().NotBeNullOrEmpty("export should return a bookmark for polling"); + exportStart.Status.Should().BeOneOf("active", "complete", "pending"); + + _output.WriteLine($"Export started with bookmark: {exportStart.AtBookmark}, status: {exportStart.Status}"); + + // Poll until complete (with timeout) + var maxAttempts = 30; // 30 seconds max + var attempt = 0; + var currentExport = exportStart; + + while (currentExport.Status != "complete" && attempt < maxAttempts) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + currentExport = await _sut.PollExportAsync(_databaseId, currentExport.AtBookmark!); + attempt++; + + _output.WriteLine($"Poll attempt {attempt}: status = {currentExport.Status}"); + + // Fail fast on error status + currentExport.Status.Should().NotBe("error", $"export failed with error: {currentExport.Error}"); + } + + // Assert final state + currentExport.Status.Should().Be("complete", $"export should complete within {maxAttempts} seconds"); + currentExport.Result.Should().NotBeNull("completed export should have result"); + currentExport.Result!.SignedUrl.Should().NotBeNullOrEmpty("completed export should have download URL"); + currentExport.Result.Filename.Should().NotBeNullOrEmpty("completed export should have filename"); + + _output.WriteLine($"Export completed. Filename: {currentExport.Result.Filename}"); + _output.WriteLine($"Download URL available (not shown for security)"); + } + + /// Verifies that export with schema-only option works correctly. + [IntegrationTest] + public async Task Export_SchemaOnly_CompletesSuccessfully() + { + // Arrange + var dumpOptions = new D1ExportDumpOptions(NoData: true); + + // Act - Start export with schema-only option + var exportStart = await _sut.StartExportAsync(_databaseId, dumpOptions); + + // Assert initial response + exportStart.Should().NotBeNull(); + exportStart.AtBookmark.Should().NotBeNullOrEmpty(); + + // Poll until complete + var maxAttempts = 30; + var attempt = 0; + var currentExport = exportStart; + + while (currentExport.Status != "complete" && attempt < maxAttempts) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + currentExport = await _sut.PollExportAsync(_databaseId, currentExport.AtBookmark!); + attempt++; + + currentExport.Status.Should().NotBe("error", $"export failed with error: {currentExport.Error}"); + } + + // Assert + currentExport.Status.Should().Be("complete"); + currentExport.Result.Should().NotBeNull(); + currentExport.Result!.SignedUrl.Should().NotBeNullOrEmpty(); + + _output.WriteLine($"Schema-only export completed in {attempt} poll(s)"); + } + + /// Verifies that export with specific tables option works correctly. + [IntegrationTest] + public async Task Export_SpecificTables_CompletesSuccessfully() + { + // Arrange + var dumpOptions = new D1ExportDumpOptions(Tables: new[] { "test_users" }); + + // Act - Start export with specific tables + var exportStart = await _sut.StartExportAsync(_databaseId, dumpOptions); + + // Assert initial response + exportStart.Should().NotBeNull(); + exportStart.AtBookmark.Should().NotBeNullOrEmpty(); + + // Poll until complete + var maxAttempts = 30; + var attempt = 0; + var currentExport = exportStart; + + while (currentExport.Status != "complete" && attempt < maxAttempts) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + currentExport = await _sut.PollExportAsync(_databaseId, currentExport.AtBookmark!); + attempt++; + + currentExport.Status.Should().NotBe("error", $"export failed with error: {currentExport.Error}"); + } + + // Assert + currentExport.Status.Should().Be("complete"); + currentExport.Result.Should().NotBeNull(); + currentExport.Result!.SignedUrl.Should().NotBeNullOrEmpty(); + + _output.WriteLine($"Single-table export completed in {attempt} poll(s)"); + } + + #endregion + + + #region Import Operations + + /// Verifies that a database can be imported from SQL format using the full workflow. + [IntegrationTest] + public async Task Import_FullWorkflow_CompletesSuccessfully() + { + // Arrange - Create a temporary database for import testing (to avoid polluting the main test database) + var importDbName = $"cfnet-import-{Guid.NewGuid():N}"[..32]; + var importDb = await _sut.CreateAsync(importDbName); + + try + { + // Create SQL content to import + var sqlContent = """ + CREATE TABLE IF NOT EXISTS imported_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + status TEXT DEFAULT 'active' + ); + INSERT INTO imported_users (username, status) VALUES ('user1', 'active'); + INSERT INTO imported_users (username, status) VALUES ('user2', 'inactive'); + """; + var sqlBytes = Encoding.UTF8.GetBytes(sqlContent); + + // Compute MD5 hash + var md5Hash = Convert.ToHexString(MD5.HashData(sqlBytes)).ToLowerInvariant(); + + _output.WriteLine($"SQL content size: {sqlBytes.Length} bytes, MD5: {md5Hash}"); + + // Act - Step 1: Initialize import to get upload URL + var initResponse = await _sut.StartImportAsync(importDb.Uuid, md5Hash); + + // Assert init response + initResponse.Should().NotBeNull(); + initResponse.UploadUrl.Should().NotBeNullOrEmpty("init should return upload URL"); + initResponse.Filename.Should().NotBeNullOrEmpty("init should return filename"); + + _output.WriteLine($"Import initialized. Filename: {initResponse.Filename}"); + + // Act - Step 2: Upload SQL file to the signed URL + using var httpClient = new HttpClient(); + using var uploadContent = new ByteArrayContent(sqlBytes); + uploadContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); + + var uploadResponse = await httpClient.PutAsync(initResponse.UploadUrl, uploadContent); + uploadResponse.StatusCode.Should().Be(HttpStatusCode.OK, "upload to signed URL should succeed"); + + _output.WriteLine("SQL file uploaded successfully"); + + // Act - Step 3: Complete the import (ingest) + var ingestResponse = await _sut.CompleteImportAsync(importDb.Uuid, md5Hash, initResponse.Filename!); + + // Assert ingest response + ingestResponse.Should().NotBeNull(); + ingestResponse.AtBookmark.Should().NotBeNullOrEmpty("ingest should return bookmark for polling"); + + _output.WriteLine($"Import ingestion started. Bookmark: {ingestResponse.AtBookmark}"); + + // Act - Step 4: Poll until complete + var maxAttempts = 60; // Import can take longer than export + var attempt = 0; + var currentImport = ingestResponse; + + while (currentImport.Status != "complete" && attempt < maxAttempts) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + currentImport = await _sut.PollImportAsync(importDb.Uuid, currentImport.AtBookmark!); + attempt++; + + _output.WriteLine($"Poll attempt {attempt}: status = {currentImport.Status}"); + + // Fail fast on error status + currentImport.Status.Should().NotBe("error", $"import failed with error: {currentImport.Error}"); + } + + // Assert final state + currentImport.Status.Should().Be("complete", $"import should complete within {maxAttempts} seconds"); + currentImport.Result.Should().NotBeNull("completed import should have result"); + currentImport.Result!.NumQueries.Should().BeGreaterThan(0, "import should have executed queries"); + + _output.WriteLine($"Import completed. Queries executed: {currentImport.Result.NumQueries}"); + + // Verify the imported data exists + var verifyResult = await _sut.QueryAsync(importDb.Uuid, "SELECT COUNT(*) as count FROM imported_users"); + verifyResult[0].Success.Should().BeTrue(); + verifyResult[0].Results.Should().NotBeEmpty("should have count result"); + + _output.WriteLine("Import verification successful - imported_users table exists with data"); + } + finally + { + // Cleanup + await _sut.DeleteAsync(importDb.Uuid); + _output.WriteLine($"Cleaned up import test database: {importDb.Uuid}"); + } + } + + /// Verifies that StartImportAsync returns proper upload URL and filename. + [IntegrationTest] + public async Task StartImportAsync_ReturnsUploadUrlAndFilename() + { + // Arrange - Create a temporary database + var importDbName = $"cfnet-import-init-{Guid.NewGuid():N}"[..32]; + var importDb = await _sut.CreateAsync(importDbName); + + try + { + // Create a simple SQL content and compute MD5 + var sqlContent = "SELECT 1;"; + var sqlBytes = Encoding.UTF8.GetBytes(sqlContent); + var md5Hash = Convert.ToHexString(MD5.HashData(sqlBytes)).ToLowerInvariant(); + + // Act + var response = await _sut.StartImportAsync(importDb.Uuid, md5Hash); + + // Assert + response.Should().NotBeNull(); + response.UploadUrl.Should().NotBeNullOrEmpty("should return upload URL"); + response.UploadUrl.Should().StartWith("https://", "upload URL should be HTTPS"); + response.Filename.Should().NotBeNullOrEmpty("should return filename for ingest step"); + + _output.WriteLine($"Upload URL received (starts with): {response.UploadUrl![..50]}..."); + _output.WriteLine($"Filename: {response.Filename}"); + } + finally + { + // Cleanup + await _sut.DeleteAsync(importDb.Uuid); + } + } + + #endregion + + + #region Nested Types + + /// A test DTO for typed query deserialization. + private record TestUser + { + public long Id { get; init; } + public string Name { get; init; } = string.Empty; + public string Email { get; init; } = string.Empty; + } + + #endregion +} diff --git a/tests/Cloudflare.NET.Tests/IntegrationTests/KvApiIntegrationTests.cs b/tests/Cloudflare.NET.Tests/IntegrationTests/KvApiIntegrationTests.cs index 9cf4b48..c0c04ec 100644 --- a/tests/Cloudflare.NET.Tests/IntegrationTests/KvApiIntegrationTests.cs +++ b/tests/Cloudflare.NET.Tests/IntegrationTests/KvApiIntegrationTests.cs @@ -402,9 +402,13 @@ public async Task DeleteValueAsync_CanDeleteExistingKey() // Act await _sut.DeleteValueAsync(_namespaceId, key); - // Assert - Value should no longer exist - var afterDelete = await _sut.GetValueAsync(_namespaceId, key); - afterDelete.Should().BeNull("the key should be deleted"); + // Assert - Value should no longer exist (retry for eventual consistency) + const int maxRetries = 10; + string? afterDelete = value; + for (var attempt = 1; attempt <= maxRetries && afterDelete is not null; attempt++) + afterDelete = await _sut.GetValueAsync(_namespaceId, key); + + afterDelete.Should().BeNull($"the key should be deleted after {maxRetries} retries"); } /// Verifies that deleting a non-existent key does not throw an error. @@ -645,11 +649,15 @@ public async Task BulkDeleteAsync_CanDeleteMultipleKeys() result.SuccessfulKeyCount.Should().Be(3); result.UnsuccessfulKeys.Should().BeNullOrEmpty(); - // Verify they are deleted + // Verify they are deleted (retry for eventual consistency - no delays, just retries) + const int maxRetries = 10; foreach (var key in keys) { - var value = await _sut.GetValueAsync(_namespaceId, key); - value.Should().BeNull($"{key} should be deleted"); + string? value = "not null"; + for (var attempt = 1; attempt <= maxRetries && value is not null; attempt++) + value = await _sut.GetValueAsync(_namespaceId, key); + + value.Should().BeNull($"{key} should be deleted after {maxRetries} retries"); } } diff --git a/tests/Cloudflare.NET.Tests/UnitTests/D1ApiUnitTests.cs b/tests/Cloudflare.NET.Tests/UnitTests/D1ApiUnitTests.cs new file mode 100644 index 0000000..fe6d840 --- /dev/null +++ b/tests/Cloudflare.NET.Tests/UnitTests/D1ApiUnitTests.cs @@ -0,0 +1,1063 @@ +namespace Cloudflare.NET.Tests.UnitTests; + +using System.Net; +using System.Text.Json; +using Accounts.D1; +using Accounts.D1.Models; +using Accounts.Models; +using Cloudflare.NET.Core.Exceptions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq.Protected; +using Shared.Fixtures; +using Xunit.Abstractions; + +/// Contains unit tests for the class. +/// +/// This test class covers all D1 database operations including: +/// +/// Database operations (List, Create, Get, Update, Delete) +/// Query operations (Query, QueryRaw) +/// Export operations (StartExport, PollExport) +/// Import operations (StartImport, CompleteImport, PollImport) +/// Error handling and edge cases +/// +/// +[Trait("Category", TestConstants.TestCategories.Unit)] +public class D1ApiUnitTests +{ + #region Properties & Fields - Non-Public + + /// The logger factory for creating loggers. + private readonly ILoggerFactory _loggerFactory; + + /// JSON serializer options for snake_case property naming. + private readonly JsonSerializerOptions _serializerOptions = + new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; + + /// The test account ID used in all tests. + private const string TestAccountId = "test-account-id"; + + #endregion + + + #region Constructors + + /// Initializes a new instance of the class. + /// The xUnit test output helper. + public D1ApiUnitTests(ITestOutputHelper output) + { + var loggerProvider = new XunitTestOutputLoggerProvider { Current = output }; + _loggerFactory = new LoggerFactory([loggerProvider]); + } + + #endregion + + + #region Helper Methods + + /// Creates the system under test with a mocked HTTP handler. + /// The JSON content to return. + /// The HTTP status code to return. + /// Optional callback to capture the request. + /// A configured instance. + private D1Api CreateSut( + string responseContent, + HttpStatusCode statusCode = HttpStatusCode.OK, + Action? callback = null) + { + var mockHandler = HttpFixtures.GetMockHttpMessageHandler(responseContent, statusCode, callback); + var httpClient = new HttpClient(mockHandler.Object) { BaseAddress = new Uri("https://api.cloudflare.com/client/v4/") }; + var options = Options.Create(new CloudflareApiOptions { AccountId = TestAccountId }); + + return new D1Api(httpClient, options, _loggerFactory); + } + + /// Creates a paginated response for database listings. + /// The databases to include. + /// Current page number. + /// Items per page. + /// Total number of items. + /// JSON string representing the paginated response. + private string CreatePaginatedDatabaseResponse(D1Database[] databases, int page, int perPage, int totalCount) + { + var totalPages = totalCount == 0 ? 0 : (int)Math.Ceiling((double)totalCount / perPage); + var response = new + { + success = true, + errors = Array.Empty(), + messages = Array.Empty(), + result = databases, + result_info = new + { + page, + per_page = perPage, + count = databases.Length, + total_count = totalCount, + total_pages = totalPages + } + }; + return JsonSerializer.Serialize(response, _serializerOptions); + } + + /// Creates a sample D1Database for testing. + /// The database UUID. + /// The database name. + /// A D1Database instance. + private static D1Database CreateTestDatabase(string uuid, string name) => + new( + Uuid: uuid, + Name: name, + CreatedAt: DateTimeOffset.Parse("2024-01-01T00:00:00Z"), + FileSize: 1024, + NumTables: 5, + Version: "production", + ReadReplication: new D1ReadReplication("auto"), + RunningInRegion: "WEUR" + ); + + #endregion + + + #region Database Operations - ListAsync + + /// Verifies that ListAsync sends a correctly formatted GET request with no filters. + [Fact] + public async Task ListAsync_WithNoFilters_SendsCorrectRequest() + { + // Arrange + var databases = new[] { CreateTestDatabase("db-1", "My Database") }; + var response = CreatePaginatedDatabaseResponse(databases, 1, 20, 1); + + HttpRequestMessage? capturedRequest = null; + var sut = CreateSut(response, callback: (req, _) => capturedRequest = req); + + // Act + var result = await sut.ListAsync(); + + // Assert + result.Items.Should().HaveCount(1); + result.Items[0].Uuid.Should().Be("db-1"); + result.Items[0].Name.Should().Be("My Database"); + capturedRequest.Should().NotBeNull(); + capturedRequest!.Method.Should().Be(HttpMethod.Get); + capturedRequest.RequestUri!.ToString().Should().Be($"https://api.cloudflare.com/client/v4/accounts/{TestAccountId}/d1/database"); + } + + /// Verifies that ListAsync includes page filter in query string. + [Fact] + public async Task ListAsync_WithPageFilter_SendsCorrectRequest() + { + // Arrange + var response = CreatePaginatedDatabaseResponse(Array.Empty(), 2, 20, 40); + + HttpRequestMessage? capturedRequest = null; + var sut = CreateSut(response, callback: (req, _) => capturedRequest = req); + + // Act + await sut.ListAsync(new ListD1DatabasesFilters(Page: 2)); + + // Assert + capturedRequest.Should().NotBeNull(); + capturedRequest!.RequestUri!.ToString().Should().Contain("page=2"); + } + + /// Verifies that ListAsync includes per_page filter in query string. + [Fact] + public async Task ListAsync_WithPerPageFilter_SendsCorrectRequest() + { + // Arrange + var response = CreatePaginatedDatabaseResponse(Array.Empty(), 1, 50, 0); + + HttpRequestMessage? capturedRequest = null; + var sut = CreateSut(response, callback: (req, _) => capturedRequest = req); + + // Act + await sut.ListAsync(new ListD1DatabasesFilters(PerPage: 50)); + + // Assert + capturedRequest.Should().NotBeNull(); + capturedRequest!.RequestUri!.ToString().Should().Contain("per_page=50"); + } + + /// Verifies that ListAsync includes name filter in query string. + [Fact] + public async Task ListAsync_WithNameFilter_SendsCorrectRequest() + { + // Arrange + var response = CreatePaginatedDatabaseResponse(Array.Empty(), 1, 20, 0); + + HttpRequestMessage? capturedRequest = null; + var sut = CreateSut(response, callback: (req, _) => capturedRequest = req); + + // Act + await sut.ListAsync(new ListD1DatabasesFilters(Name: "my-database")); + + // Assert + capturedRequest.Should().NotBeNull(); + capturedRequest!.RequestUri!.ToString().Should().Contain("name=my-database"); + } + + /// Verifies that ListAsync includes all filters in query string. + [Fact] + public async Task ListAsync_WithAllFilters_SendsCorrectRequest() + { + // Arrange + var filters = new ListD1DatabasesFilters( + Name: "test-db", + Page: 2, + PerPage: 50 + ); + var response = CreatePaginatedDatabaseResponse(Array.Empty(), 2, 50, 100); + + HttpRequestMessage? capturedRequest = null; + var sut = CreateSut(response, callback: (req, _) => capturedRequest = req); + + // Act + await sut.ListAsync(filters); + + // Assert + capturedRequest.Should().NotBeNull(); + var uri = capturedRequest!.RequestUri!.ToString(); + uri.Should().Contain("name=test-db"); + uri.Should().Contain("page=2"); + uri.Should().Contain("per_page=50"); + } + + #endregion + + + #region Database Operations - ListAllAsync + + /// Verifies that ListAllAsync handles pagination correctly. + /// + /// The D1 API returns TotalPages=0, so ListAllAsync uses the "is page full?" pattern to determine + /// if there are more pages. This test verifies that pattern works correctly. + /// + [Fact] + public async Task ListAllAsync_ShouldHandlePaginationCorrectly() + { + // Arrange + var db1 = CreateTestDatabase("db-1", "Database 1"); + var db2 = CreateTestDatabase("db-2", "Database 2"); + + // First page response - page is "full" (1 item with perPage=1), so more pages exist. + var responsePage1 = CreatePaginatedDatabaseResponse(new[] { db1 }, 1, 1, 2); + + // Second page response - page is NOT full (1 item with perPage=1 but it's the last page). + // Note: The implementation checks Items.Count >= perPage, so we need page 2 to have fewer items + // to signal "no more pages". In this case, we return 1 item but the next request would return 0. + var responsePage2 = CreatePaginatedDatabaseResponse(new[] { db2 }, 2, 1, 2); + + // Empty third page to signal end of pagination. + var responsePage3 = CreatePaginatedDatabaseResponse(Array.Empty(), 3, 1, 2); + + var capturedRequests = new List(); + var mockHandler = new Mock(); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((req, _) => capturedRequests.Add(req)) + .Returns((HttpRequestMessage req, CancellationToken _) => + { + if (req.RequestUri!.ToString().Contains("page=3")) + return Task.FromResult( + new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(responsePage3) }); + + if (req.RequestUri!.ToString().Contains("page=2")) + return Task.FromResult( + new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(responsePage2) }); + + return Task.FromResult( + new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(responsePage1) }); + }); + + var httpClient = new HttpClient(mockHandler.Object) { BaseAddress = new Uri("https://api.cloudflare.com/client/v4/") }; + var options = Options.Create(new CloudflareApiOptions { AccountId = TestAccountId }); + var sut = new D1Api(httpClient, options, _loggerFactory); + + // Act - Must pass PerPage=1 to match the mock data (default is 100). + var allDatabases = new List(); + await foreach (var db in sut.ListAllAsync(new ListD1DatabasesFilters(PerPage: 1))) + allDatabases.Add(db); + + // Assert - 3 requests: page 1 (full), page 2 (full), page 3 (empty = stop). + capturedRequests.Should().HaveCount(3); + capturedRequests[0].RequestUri!.Query.Should().Contain("page=1"); + capturedRequests[1].RequestUri!.Query.Should().Contain("page=2"); + capturedRequests[2].RequestUri!.Query.Should().Contain("page=3"); + allDatabases.Should().HaveCount(2); + allDatabases.Select(d => d.Uuid).Should().ContainInOrder("db-1", "db-2"); + } + + #endregion + + + #region Database Operations - CreateAsync + + /// Verifies that CreateAsync sends a correctly formatted POST request. + [Fact] + public async Task CreateAsync_SendsCorrectRequest() + { + // Arrange + var name = "My New Database"; + var expectedResult = CreateTestDatabase("db-new-id", name); + var successResponse = HttpFixtures.CreateSuccessResponse(expectedResult); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + // Act + var result = await sut.CreateAsync(name); + + // Assert + result.Name.Should().Be(name); + capturedRequest.Should().NotBeNull(); + capturedRequest!.Method.Should().Be(HttpMethod.Post); + capturedRequest.RequestUri!.ToString().Should().Be($"https://api.cloudflare.com/client/v4/accounts/{TestAccountId}/d1/database"); + + // Verify JSON body contains the name. + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("name").GetString().Should().Be(name); + } + + /// Verifies that CreateAsync includes location hint when provided. + [Fact] + public async Task CreateAsync_WithLocationHint_IncludesLocationHint() + { + // Arrange + var name = "My Database"; + var expectedResult = CreateTestDatabase("db-id", name); + var successResponse = HttpFixtures.CreateSuccessResponse(expectedResult); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + // Act + await sut.CreateAsync(name, primaryLocationHint: R2LocationHint.WestEurope); + + // Assert + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("primary_location_hint").GetString().Should().Be("weur"); + } + + /// Verifies that CreateAsync includes jurisdiction when provided. + [Fact] + public async Task CreateAsync_WithJurisdiction_IncludesJurisdiction() + { + // Arrange + var name = "EU Database"; + var expectedResult = CreateTestDatabase("db-id", name); + var successResponse = HttpFixtures.CreateSuccessResponse(expectedResult); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + // Act + await sut.CreateAsync(name, jurisdiction: D1Jurisdiction.EuropeanUnion); + + // Assert + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("jurisdiction").GetString().Should().Be("eu"); + } + + #endregion + + + #region Database Operations - GetAsync + + /// Verifies that GetAsync sends a correctly formatted GET request. + [Fact] + public async Task GetAsync_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-123"; + var expectedResult = CreateTestDatabase(databaseId, "My Database"); + var successResponse = HttpFixtures.CreateSuccessResponse(expectedResult); + + HttpRequestMessage? capturedRequest = null; + var sut = CreateSut(successResponse, callback: (req, _) => capturedRequest = req); + + // Act + var result = await sut.GetAsync(databaseId); + + // Assert + result.Uuid.Should().Be(databaseId); + capturedRequest.Should().NotBeNull(); + capturedRequest!.Method.Should().Be(HttpMethod.Get); + capturedRequest.RequestUri!.ToString().Should().Be($"https://api.cloudflare.com/client/v4/accounts/{TestAccountId}/d1/database/{databaseId}"); + } + + /// Verifies that GetAsync URL-encodes special characters in database IDs. + [Fact] + public async Task GetAsync_UrlEncodesDatabaseId() + { + // Arrange + var databaseId = "db id+special"; + var expectedResult = CreateTestDatabase(databaseId, "My Database"); + var successResponse = HttpFixtures.CreateSuccessResponse(expectedResult); + + HttpRequestMessage? capturedRequest = null; + var sut = CreateSut(successResponse, callback: (req, _) => capturedRequest = req); + + // Act + await sut.GetAsync(databaseId); + + // Assert + capturedRequest.Should().NotBeNull(); + capturedRequest!.RequestUri!.OriginalString.Should().Contain("db%20id%2Bspecial"); + } + + #endregion + + + #region Database Operations - UpdateAsync + + /// Verifies that UpdateAsync sends a correctly formatted PATCH request. + [Fact] + public async Task UpdateAsync_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-123"; + var expectedResult = CreateTestDatabase(databaseId, "My Database"); + var successResponse = HttpFixtures.CreateSuccessResponse(expectedResult); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + var options = new UpdateD1DatabaseOptions( + ReadReplication: new D1ReadReplication("auto") + ); + + // Act + var result = await sut.UpdateAsync(databaseId, options); + + // Assert + result.Uuid.Should().Be(databaseId); + capturedRequest.Should().NotBeNull(); + capturedRequest!.Method.Should().Be(HttpMethod.Patch); + capturedRequest.RequestUri!.ToString().Should().Be($"https://api.cloudflare.com/client/v4/accounts/{TestAccountId}/d1/database/{databaseId}"); + + // Verify JSON body contains read_replication. + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("read_replication").GetProperty("mode").GetString().Should().Be("auto"); + } + + #endregion + + + #region Database Operations - DeleteAsync + + /// Verifies that DeleteAsync sends a correctly formatted DELETE request. + [Fact] + public async Task DeleteAsync_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-to-delete"; + var successResponse = HttpFixtures.CreateSuccessResponse(null); + + HttpRequestMessage? capturedRequest = null; + var sut = CreateSut(successResponse, callback: (req, _) => capturedRequest = req); + + // Act + await sut.DeleteAsync(databaseId); + + // Assert + capturedRequest.Should().NotBeNull(); + capturedRequest!.Method.Should().Be(HttpMethod.Delete); + capturedRequest.RequestUri!.ToString().Should().Be($"https://api.cloudflare.com/client/v4/accounts/{TestAccountId}/d1/database/{databaseId}"); + } + + #endregion + + + #region Query Operations - QueryAsync + + /// Verifies that QueryAsync sends a correctly formatted POST request. + [Fact] + public async Task QueryAsync_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-123"; + var sql = "SELECT * FROM users WHERE id = ?"; + var queryResult = new D1QueryResult( + Meta: new D1QueryMeta(false, 0, 1.5, 0, 10, 0), + Results: new List(), + Success: true + ); + var successResponse = HttpFixtures.CreateSuccessResponse(new[] { queryResult }); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + // Act + var result = await sut.QueryAsync(databaseId, sql, new object?[] { "user-1" }); + + // Assert + result.Should().HaveCount(1); + result[0].Success.Should().BeTrue(); + capturedRequest.Should().NotBeNull(); + capturedRequest!.Method.Should().Be(HttpMethod.Post); + capturedRequest.RequestUri!.ToString().Should().Be($"https://api.cloudflare.com/client/v4/accounts/{TestAccountId}/d1/database/{databaseId}/query"); + + // Verify JSON body contains sql and params. + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("sql").GetString().Should().Be(sql); + doc.RootElement.GetProperty("params").GetArrayLength().Should().Be(1); + } + + /// Verifies that QueryAsync works without parameters. + [Fact] + public async Task QueryAsync_WithoutParams_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-123"; + var sql = "SELECT * FROM users"; + var queryResult = new D1QueryResult( + Meta: new D1QueryMeta(false, 0, 1.0, 0, 5, 0), + Results: new List(), + Success: true + ); + var successResponse = HttpFixtures.CreateSuccessResponse(new[] { queryResult }); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + // Act + await sut.QueryAsync(databaseId, sql); + + // Assert + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("sql").GetString().Should().Be(sql); + // params should be null or not present. + doc.RootElement.TryGetProperty("params", out var paramsElement).Should().BeFalse(); + } + + #endregion + + + #region Query Operations - QueryRawAsync + + /// Verifies that QueryRawAsync sends a correctly formatted POST request to the raw endpoint. + [Fact] + public async Task QueryRawAsync_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-123"; + var sql = "SELECT id, name FROM users"; + var rawResult = new D1RawQueryResult( + Meta: new D1QueryMeta(false, 0, 0.8, 0, 3, 0), + Results: new D1RawQueryResultSet( + Columns: new[] { "id", "name" }, + Rows: new List>() + ), + Success: true + ); + var successResponse = HttpFixtures.CreateSuccessResponse(new[] { rawResult }); + + HttpRequestMessage? capturedRequest = null; + var sut = CreateSut(successResponse, callback: (req, _) => capturedRequest = req); + + // Act + var result = await sut.QueryRawAsync(databaseId, sql); + + // Assert + result.Should().HaveCount(1); + result[0].Success.Should().BeTrue(); + result[0].Results.Columns.Should().ContainInOrder("id", "name"); + capturedRequest.Should().NotBeNull(); + capturedRequest!.Method.Should().Be(HttpMethod.Post); + capturedRequest.RequestUri!.ToString().Should().Be($"https://api.cloudflare.com/client/v4/accounts/{TestAccountId}/d1/database/{databaseId}/raw"); + } + + #endregion + + + #region Export Operations - StartExportAsync + + /// Verifies that StartExportAsync sends a correctly formatted POST request. + [Fact] + public async Task StartExportAsync_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-123"; + var exportResponse = new D1ExportResponse( + AtBookmark: "bookmark-123", + Status: "active", + Result: null, + Error: null, + Type: "export", + Success: true + ); + var successResponse = HttpFixtures.CreateSuccessResponse(exportResponse); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + // Act + var result = await sut.StartExportAsync(databaseId); + + // Assert + result.AtBookmark.Should().Be("bookmark-123"); + result.Status.Should().Be("active"); + capturedRequest.Should().NotBeNull(); + capturedRequest!.Method.Should().Be(HttpMethod.Post); + capturedRequest.RequestUri!.ToString().Should().Be($"https://api.cloudflare.com/client/v4/accounts/{TestAccountId}/d1/database/{databaseId}/export"); + + // Verify JSON body contains output_format. + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("output_format").GetString().Should().Be("polling"); + } + + /// Verifies that StartExportAsync includes dump options when provided. + [Fact] + public async Task StartExportAsync_WithDumpOptions_IncludesDumpOptions() + { + // Arrange + var databaseId = "db-123"; + var exportResponse = new D1ExportResponse( + AtBookmark: "bookmark-123", + Status: "active", + Success: true + ); + var successResponse = HttpFixtures.CreateSuccessResponse(exportResponse); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + var dumpOptions = new D1ExportDumpOptions( + NoData: true, + Tables: new[] { "users", "orders" } + ); + + // Act + await sut.StartExportAsync(databaseId, dumpOptions); + + // Assert + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + var dumpOptionsElement = doc.RootElement.GetProperty("dump_options"); + dumpOptionsElement.GetProperty("no_data").GetBoolean().Should().BeTrue(); + dumpOptionsElement.GetProperty("tables").GetArrayLength().Should().Be(2); + } + + #endregion + + + #region Export Operations - PollExportAsync + + /// Verifies that PollExportAsync sends a correctly formatted POST request with bookmark. + [Fact] + public async Task PollExportAsync_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-123"; + var bookmark = "bookmark-abc"; + var exportResponse = new D1ExportResponse( + AtBookmark: bookmark, + Status: "complete", + Result: new D1ExportResultDetails( + Filename: "export.sql", + SignedUrl: "https://example.com/signed-url" + ), + Success: true + ); + var successResponse = HttpFixtures.CreateSuccessResponse(exportResponse); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + // Act + var result = await sut.PollExportAsync(databaseId, bookmark); + + // Assert + result.Status.Should().Be("complete"); + result.Result!.SignedUrl.Should().Be("https://example.com/signed-url"); + capturedRequest.Should().NotBeNull(); + capturedRequest!.Method.Should().Be(HttpMethod.Post); + + // Verify JSON body contains current_bookmark. + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("current_bookmark").GetString().Should().Be(bookmark); + } + + #endregion + + + #region Import Operations - StartImportAsync + + /// Verifies that StartImportAsync sends a correctly formatted POST request. + [Fact] + public async Task StartImportAsync_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-123"; + var etag = "abc123def456"; + var importResponse = new D1ImportResponse( + Filename: "import-file.sql", + UploadUrl: "https://example.com/upload-url", + Success: true + ); + var successResponse = HttpFixtures.CreateSuccessResponse(importResponse); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + // Act + var result = await sut.StartImportAsync(databaseId, etag); + + // Assert + result.Filename.Should().Be("import-file.sql"); + result.UploadUrl.Should().Be("https://example.com/upload-url"); + capturedRequest.Should().NotBeNull(); + capturedRequest!.Method.Should().Be(HttpMethod.Post); + capturedRequest.RequestUri!.ToString().Should().Be($"https://api.cloudflare.com/client/v4/accounts/{TestAccountId}/d1/database/{databaseId}/import"); + + // Verify JSON body contains action and etag. + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("action").GetString().Should().Be("init"); + doc.RootElement.GetProperty("etag").GetString().Should().Be(etag); + } + + #endregion + + + #region Import Operations - CompleteImportAsync + + /// Verifies that CompleteImportAsync sends a correctly formatted POST request. + [Fact] + public async Task CompleteImportAsync_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-123"; + var etag = "abc123def456"; + var filename = "import-file.sql"; + var importResponse = new D1ImportResponse( + AtBookmark: "bookmark-xyz", + Status: "active", + Success: true + ); + var successResponse = HttpFixtures.CreateSuccessResponse(importResponse); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + // Act + var result = await sut.CompleteImportAsync(databaseId, etag, filename); + + // Assert + result.AtBookmark.Should().Be("bookmark-xyz"); + result.Status.Should().Be("active"); + + // Verify JSON body contains action, etag, and filename. + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("action").GetString().Should().Be("ingest"); + doc.RootElement.GetProperty("etag").GetString().Should().Be(etag); + doc.RootElement.GetProperty("filename").GetString().Should().Be(filename); + } + + #endregion + + + #region Import Operations - PollImportAsync + + /// Verifies that PollImportAsync sends a correctly formatted POST request. + [Fact] + public async Task PollImportAsync_SendsCorrectRequest() + { + // Arrange + var databaseId = "db-123"; + var bookmark = "bookmark-xyz"; + var importResponse = new D1ImportResponse( + AtBookmark: bookmark, + Status: "complete", + Result: new D1ImportResultDetails( + NumQueries: 100, + Meta: new D1QueryMeta(true, 50, 500.0, 1, 200, 150) + ), + Success: true + ); + var successResponse = HttpFixtures.CreateSuccessResponse(importResponse); + + HttpRequestMessage? capturedRequest = null; + string? capturedJsonBody = null; + var sut = CreateSut(successResponse, callback: (req, _) => + { + capturedRequest = req; + capturedJsonBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); + }); + + // Act + var result = await sut.PollImportAsync(databaseId, bookmark); + + // Assert + result.Status.Should().Be("complete"); + result.Result!.NumQueries.Should().Be(100); + + // Verify JSON body contains action and current_bookmark. + capturedJsonBody.Should().NotBeNull(); + using var doc = JsonDocument.Parse(capturedJsonBody!); + doc.RootElement.GetProperty("action").GetString().Should().Be("poll"); + doc.RootElement.GetProperty("current_bookmark").GetString().Should().Be(bookmark); + } + + #endregion + + + #region Error Handling + + /// Verifies that API errors are properly propagated as CloudflareApiException. + [Fact] + public async Task CreateAsync_WhenApiReturnsError_ThrowsCloudflareApiException() + { + // Arrange + var errorResponse = HttpFixtures.CreateErrorResponse(7003, "Invalid database name"); + var sut = CreateSut(errorResponse); + + // Act + var action = async () => await sut.CreateAsync("Invalid Name!"); + + // Assert + var ex = await action.Should().ThrowAsync(); + ex.Which.Message.Should().Contain("7003"); + ex.Which.Errors.Should().ContainSingle().Which.Code.Should().Be(7003); + } + + /// Verifies that GetAsync throws on non-existent database. + [Fact] + public async Task GetAsync_WhenDatabaseNotFound_ThrowsCloudflareApiException() + { + // Arrange + var errorResponse = HttpFixtures.CreateErrorResponse(7000, "Database not found"); + var sut = CreateSut(errorResponse); + + // Act + var action = async () => await sut.GetAsync("non-existent-db"); + + // Assert + var ex = await action.Should().ThrowAsync(); + ex.Which.Errors.Should().ContainSingle().Which.Code.Should().Be(7000); + } + + /// Verifies that DeleteAsync propagates API errors correctly. + [Fact] + public async Task DeleteAsync_WhenApiReturnsError_ThrowsCloudflareApiException() + { + // Arrange + var errorResponse = HttpFixtures.CreateErrorResponse(7000, "Database not found"); + var sut = CreateSut(errorResponse); + + // Act + var action = async () => await sut.DeleteAsync("non-existent-db"); + + // Assert + await action.Should().ThrowAsync(); + } + + /// Verifies that QueryAsync propagates API errors correctly. + [Fact] + public async Task QueryAsync_WhenApiReturnsError_ThrowsCloudflareApiException() + { + // Arrange + var errorResponse = HttpFixtures.CreateErrorResponse(7500, "SQL syntax error"); + var sut = CreateSut(errorResponse); + + // Act + var action = async () => await sut.QueryAsync("db-123", "SELECT * FORM users"); + + // Assert + await action.Should().ThrowAsync(); + } + + #endregion + + + #region Model Serialization Tests + + /// Verifies that D1Jurisdiction serializes correctly. + [Fact] + public void D1Jurisdiction_SerializesCorrectly() + { + // Arrange + var jurisdiction = D1Jurisdiction.EuropeanUnion; + + // Act + var json = JsonSerializer.Serialize(jurisdiction); + + // Assert + json.Should().Be("\"eu\""); + } + + /// Verifies that D1Jurisdiction deserializes correctly. + [Fact] + public void D1Jurisdiction_DeserializesCorrectly() + { + // Arrange + var json = "\"fedramp\""; + + // Act + var jurisdiction = JsonSerializer.Deserialize(json); + + // Assert + jurisdiction.Should().Be(D1Jurisdiction.FedRamp); + } + + /// Verifies that D1Jurisdiction handles custom values. + [Fact] + public void D1Jurisdiction_CustomValue_WorksCorrectly() + { + // Arrange + var customJurisdiction = new D1Jurisdiction("custom-region"); + + // Act + var json = JsonSerializer.Serialize(customJurisdiction); + var deserialized = JsonSerializer.Deserialize(json); + + // Assert + json.Should().Be("\"custom-region\""); + deserialized.Value.Should().Be("custom-region"); + } + + /// Verifies that D1Database deserializes correctly from API response format. + [Fact] + public void D1Database_DeserializesFromApiResponse() + { + // Arrange + var json = """ + { + "uuid": "test-uuid-123", + "name": "my-database", + "created_at": "2024-06-15T10:30:00Z", + "file_size": 2048, + "num_tables": 3, + "version": "production", + "read_replication": { + "mode": "auto" + }, + "running_in_region": "ENAM" + } + """; + + // Act + var database = JsonSerializer.Deserialize(json, _serializerOptions); + + // Assert + database.Should().NotBeNull(); + database!.Uuid.Should().Be("test-uuid-123"); + database.Name.Should().Be("my-database"); + database.FileSize.Should().Be(2048); + database.NumTables.Should().Be(3); + database.Version.Should().Be("production"); + database.ReadReplication!.Mode.Should().Be("auto"); + database.RunningInRegion.Should().Be("ENAM"); + } + + /// Verifies that D1QueryMeta deserializes correctly from API response format. + [Fact] + public void D1QueryMeta_DeserializesFromApiResponse() + { + // Arrange + var json = """ + { + "changed_db": true, + "changes": 5, + "duration": 2.5, + "last_row_id": 42, + "rows_read": 100, + "rows_written": 5, + "served_by": "v1.2.3", + "served_by_primary": true, + "served_by_region": "WEUR", + "size_after": 4096, + "timings": { + "sql_duration_ms": 2.3 + }, + "total_attempts": 1 + } + """; + + // Act + var meta = JsonSerializer.Deserialize(json, _serializerOptions); + + // Assert + meta.Should().NotBeNull(); + meta!.ChangedDb.Should().BeTrue(); + meta.Changes.Should().Be(5); + meta.Duration.Should().Be(2.5); + meta.LastRowId.Should().Be(42); + meta.RowsRead.Should().Be(100); + meta.RowsWritten.Should().Be(5); + meta.ServedBy.Should().Be("v1.2.3"); + meta.ServedByPrimary.Should().BeTrue(); + meta.ServedByRegion.Should().Be("WEUR"); + meta.SizeAfter.Should().Be(4096); + meta.Timings!.SqlDurationMs.Should().Be(2.3); + meta.TotalAttempts.Should().Be(1); + } + + #endregion +}