Files
mxivideo/src/utils/cloudflareKV.ts
2025-07-10 22:22:22 +08:00

263 lines
7.4 KiB
TypeScript

/**
* Cloudflare KV Storage Utility Class
*
* This utility class provides methods to interact with Cloudflare KV storage
* for storing and retrieving key-value pairs.
*/
interface CloudflareKVConfig {
apiKey: string
baseUrl: string
accountId: string
kvNamespaceId: string
}
interface CloudflareKVResponse<T = any> {
success: boolean
errors: Array<{ code: number; message: string }>
messages: string[]
result: T
}
export class CloudflareKVClient {
private config: CloudflareKVConfig
constructor(config?: Partial<CloudflareKVConfig>) {
this.config = {
apiKey: config?.apiKey || "dlGquMNiAX-S7SV9pXne7YGfdH_fEgq3TfIGgNcQ",
baseUrl: config?.baseUrl || "https://api.cloudflare.com/client/v4",
accountId: config?.accountId || "67720b647ff2b55cf37ba3ef9e677083",
kvNamespaceId: config?.kvNamespaceId || "995d7eff088547d7a7d8fea53e56115b"
}
}
/**
* Get the base URL for KV operations
*/
private getKVBaseUrl(): string {
return `${this.config.baseUrl}/accounts/${this.config.accountId}/storage/kv/namespaces/${this.config.kvNamespaceId}`
}
/**
* Get common headers for API requests
*/
private getHeaders(): HeadersInit {
return {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json'
}
}
/**
* Store a value in Cloudflare KV
*
* @param key - The key to store the value under
* @param value - The value to store (will be JSON stringified if not a string)
* @param metadata - Optional metadata to associate with the key-value pair
* @returns Promise resolving to the API response
*/
async put(key: string, value: any, metadata?: Record<string, any>): Promise<CloudflareKVResponse> {
try {
const url = `${this.getKVBaseUrl()}/values/${encodeURIComponent(key)}`
console.log(`[CloudflareKV] PUT request to: ${url}`)
const body: any = {
value: typeof value === 'string' ? value : JSON.stringify(value)
}
if (metadata) {
body.metadata = JSON.stringify(metadata)
}
console.log(`[CloudflareKV] PUT body:`, body)
const response = await fetch(url, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
console.log(`[CloudflareKV] PUT response status: ${response.status}`)
const result = await response.json()
console.log(`[CloudflareKV] PUT response:`, result)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${result.errors?.[0]?.message || 'Unknown error'}`)
}
return result
} catch (error) {
console.error('Error putting value to Cloudflare KV:', error)
throw error
}
}
/**
* Retrieve a value from Cloudflare KV
*
* @param key - The key to retrieve
* @param parseJSON - Whether to attempt JSON parsing of the retrieved value
* @returns Promise resolving to the stored value
*/
async get<T = any>(key: string, parseJSON: boolean = true): Promise<T | string | null> {
try {
const url = `${this.getKVBaseUrl()}/values/${encodeURIComponent(key)}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.config.apiKey}`
}
})
if (response.status === 404) {
return null
}
if (!response.ok) {
const errorData = await response.json()
throw new Error(`HTTP ${response.status}: ${errorData.errors?.[0]?.message || 'Unknown error'}`)
}
const textValue = await response.text()
if (parseJSON) {
try {
return JSON.parse(textValue) as T
} catch {
// If JSON parsing fails, return as string
return textValue as any
}
}
return textValue as any
} catch (error) {
console.error('Error getting value from Cloudflare KV:', error)
throw error
}
}
/**
* Delete a key-value pair from Cloudflare KV
*
* @param key - The key to delete
* @returns Promise resolving to the API response
*/
async delete(key: string): Promise<CloudflareKVResponse> {
try {
const url = `${this.getKVBaseUrl()}/values/${encodeURIComponent(key)}`
const response = await fetch(url, {
method: 'DELETE',
headers: this.getHeaders()
})
const result = await response.json()
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${result.errors?.[0]?.message || 'Unknown error'}`)
}
return result
} catch (error) {
console.error('Error deleting value from Cloudflare KV:', error)
throw error
}
}
/**
* List keys in the KV namespace
*
* @param prefix - Optional prefix to filter keys
* @param limit - Maximum number of keys to return (default: 1000)
* @param cursor - Cursor for pagination
* @returns Promise resolving to the list of keys
*/
async listKeys(prefix?: string, limit: number = 1000, cursor?: string): Promise<CloudflareKVResponse<{
keys: Array<{
name: string
expiration?: number
metadata?: Record<string, any>
}>
list_complete: boolean
cursor?: string
}>> {
try {
const params = new URLSearchParams()
if (prefix) params.append('prefix', prefix)
if (limit) params.append('limit', limit.toString())
if (cursor) params.append('cursor', cursor)
const url = `${this.getKVBaseUrl()}/keys?${params.toString()}`
const response = await fetch(url, {
method: 'GET',
headers: this.getHeaders()
})
const result = await response.json()
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${result.errors?.[0]?.message || 'Unknown error'}`)
}
return result
} catch (error) {
console.error('Error listing keys from Cloudflare KV:', error)
throw error
}
}
/**
* Check if a key exists in the KV store
*
* @param key - The key to check
* @returns Promise resolving to boolean indicating if key exists
*/
async exists(key: string): Promise<boolean> {
try {
const value = await this.get(key, false)
return value !== null
} catch (error) {
console.error('Error checking key existence in Cloudflare KV:', error)
return false
}
}
/**
* Store multiple key-value pairs in batch
*
* @param entries - Array of key-value pairs to store
* @returns Promise resolving to array of results
*/
async putBatch(entries: Array<{ key: string; value: any; metadata?: Record<string, any> }>): Promise<CloudflareKVResponse[]> {
const results = await Promise.allSettled(
entries.map(entry => this.put(entry.key, entry.value, entry.metadata))
)
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value
} else {
console.error(`Failed to put key ${entries[index].key}:`, result.reason)
return {
success: false,
errors: [{ code: -1, message: result.reason.message || 'Unknown error' }],
messages: [],
result: null
}
}
})
}
}
// Export a default instance with the default configuration
export const cloudflareKV = new CloudflareKVClient()
// Export types for external use
export type { CloudflareKVConfig, CloudflareKVResponse }