263 lines
7.4 KiB
TypeScript
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 }
|