From 2b4bd93a055252df14a1d15ef16d30e7c73f5403 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Fri, 30 Jun 2023 20:16:11 +0700 Subject: [PATCH] feat: supports cloudflare options --- .../6.drivers/cloudflare-kv-binding.md | 13 ++++ docs/content/6.drivers/cloudflare-kv-http.md | 14 ++++ src/drivers/cloudflare-kv-binding.ts | 5 +- src/drivers/cloudflare-kv-http.ts | 71 ++++++++++--------- test/drivers/cloudflare-kv-http.test.ts | 14 ++-- 5 files changed, 74 insertions(+), 43 deletions(-) diff --git a/docs/content/6.drivers/cloudflare-kv-binding.md b/docs/content/6.drivers/cloudflare-kv-binding.md index 1a83b4eb5..1a45102ed 100644 --- a/docs/content/6.drivers/cloudflare-kv-binding.md +++ b/docs/content/6.drivers/cloudflare-kv-binding.md @@ -33,3 +33,16 @@ const storage = createStorage({ - `binding`: KV binding or name of namespace. Default is `STORAGE`. - `base`: Adds prefix to all stored keys + +## Using Cloudlflare KV Options + +You can pass options to cloudflare such as metadata and expiration as the 3rd argument of the `setItem` function. +Refer to the [cloudflare KV docs](https://developers.cloudflare.com/workers/runtime-apis/kv/#writing-key-value-pairs) for the list of supported options. + +```ts +await storage.setItem("key", "value", { + expiration: 1578435000, + expirationTtl: 300, + metadata: { someMetadataKey: "someMetadataValue" }, +}); +``` diff --git a/docs/content/6.drivers/cloudflare-kv-http.md b/docs/content/6.drivers/cloudflare-kv-http.md index b4c0a0338..11f4a8b8e 100644 --- a/docs/content/6.drivers/cloudflare-kv-http.md +++ b/docs/content/6.drivers/cloudflare-kv-http.md @@ -58,3 +58,17 @@ const storage = createStorage({ - `removeItem`: Maps to [Delete key-value pair](https://api.cloudflare.com/#workers-kv-namespace-delete-key-value-pair) `DELETE accounts/:account_identifier/storage/kv/namespaces/:namespace_identifier/values/:key_name` - `getKeys`: Maps to [List a Namespace's Keys](https://api.cloudflare.com/#workers-kv-namespace-list-a-namespace-s-keys) `GET accounts/:account_identifier/storage/kv/namespaces/:namespace_identifier/keys` - `clear`: Maps to [Delete key-value pair](https://api.cloudflare.com/#workers-kv-namespace-delete-multiple-key-value-pairs) `DELETE accounts/:account_identifier/storage/kv/namespaces/:namespace_identifier/bulk` + +## Using Cloudlflare KV Options + +You can pass cloudflare options such as metadata and expiration as the 3rd argument of the `setItem` function. +Refer to the [cloudflare KV API docs](https://developers.cloudflare.com/api/operations/workers-kv-namespace-write-multiple-key-value-pairs) for the list of supported options. + +```ts +await storage.setItem("key", "value", { + expiration: 1578435000, + expiration_ttl: 300, + base64: false, + metadata: { someMetadataKey: "someMetadataValue" }, +}); +``` diff --git a/src/drivers/cloudflare-kv-binding.ts b/src/drivers/cloudflare-kv-binding.ts index de3b83c2d..cf0899179 100644 --- a/src/drivers/cloudflare-kv-binding.ts +++ b/src/drivers/cloudflare-kv-binding.ts @@ -34,10 +34,9 @@ export default defineDriver((opts: KVOptions = {}) => { const binding = getBinding(opts.binding); return binding.get(key); }, - setItem(key, value) { - key = r(key); + setItem(key, value, options) { const binding = getBinding(opts.binding); - return binding.put(key, value); + return binding.put(key, value, options); }, removeItem(key) { key = r(key); diff --git a/src/drivers/cloudflare-kv-http.ts b/src/drivers/cloudflare-kv-http.ts index 026bceb8a..9916b9861 100644 --- a/src/drivers/cloudflare-kv-http.ts +++ b/src/drivers/cloudflare-kv-http.ts @@ -1,4 +1,4 @@ -import { $fetch } from "ofetch"; +import { $fetch, type FetchOptions } from "ofetch"; import { createError, createRequiredError, @@ -79,6 +79,11 @@ type CloudflareAuthorizationHeaders = Authorization: `Bearer ${string}`; }; +type GetKeysResponse = { + result: { name: string }[]; + result_info: { cursor?: string }; +}; + const DRIVER_NAME = "cloudflare-kv-http"; export default defineDriver((opts) => { @@ -106,46 +111,42 @@ export default defineDriver((opts) => { const apiURL = opts.apiURL || "https://api.cloudflare.com"; const baseURL = `${apiURL}/client/v4/accounts/${opts.accountId}/storage/kv/namespaces/${opts.namespaceId}`; - const kvFetch = $fetch.create({ baseURL, headers }); + + const kvFetch = async (url: string, fetchOptions?: FetchOptions) => + $fetch.native(`${baseURL}${url}`, { + headers, + ...(fetchOptions as any), + }); const r = (key: string = "") => (opts.base ? joinKeys(opts.base, key) : key); const hasItem = async (key: string) => { - try { - const res = await kvFetch(`/metadata/${r(key)}`); - return res?.success === true; - } catch (err: any) { - if (!err?.response) { - throw err; - } - if (err?.response?.status === 404) { - return false; - } - throw err; - } + const response = await kvFetch(`/metadata/${key}`); + if (response.status === 404) return false; + const data = await response.json<{ success: boolean }>(); + return data?.success === true; }; const getItem = async (key: string) => { - try { - // Cloudflare API returns with `content-type: application/octet-stream` - return await kvFetch(`/values/${r(key)}`).then((r) => r.text()); - } catch (err: any) { - if (!err?.response) { - throw err; - } - if (err?.response?.status === 404) { - return null; - } - throw err; - } + // Cloudflare API returns with `content-type: application/json`: https://developers.cloudflare.com/api/operations/workers-kv-namespace-read-key-value-pair + const response = await kvFetch(`/values/${key}`); + if (response.status === 404) return null; + return response.json(); }; - const setItem = async (key: string, value: any) => { - return await kvFetch(`/values/${r(key)}`, { method: "PUT", body: value }); + const setItem = async ( + key: string, + value: unknown, + options: Record + ) => { + await kvFetch(`/bulk`, { + method: "PUT", + body: JSON.stringify([{ key, value, ...options }]), + }); }; const removeItem = async (key: string) => { - return await kvFetch(`/values/${r(key)}`, { method: "DELETE" }); + await kvFetch(`/values/${key}`, { method: "DELETE" }); }; const getKeys = async (base?: string) => { @@ -157,19 +158,19 @@ export default defineDriver((opts) => { } const firstPage = await kvFetch("/keys", { params }); - firstPage.result.forEach(({ name }: { name: string }) => keys.push(name)); + const data = await firstPage.json(); + data.result.forEach(({ name }) => keys.push(name)); - const cursor = firstPage.result_info.cursor; + const cursor = data.result_info.cursor; if (cursor) { params.cursor = cursor; } while (params.cursor) { const pageResult = await kvFetch("/keys", { params }); - pageResult.result.forEach(({ name }: { name: string }) => - keys.push(name) - ); - const pageCursor = pageResult.result_info.cursor; + const dataPageResult = await pageResult.json(); + dataPageResult.result.forEach(({ name }) => keys.push(name)); + const pageCursor = dataPageResult.result_info.cursor; if (pageCursor) { params.cursor = pageCursor; } else { diff --git a/test/drivers/cloudflare-kv-http.test.ts b/test/drivers/cloudflare-kv-http.test.ts index 0f5fe9d91..894ed5227 100644 --- a/test/drivers/cloudflare-kv-http.test.ts +++ b/test/drivers/cloudflare-kv-http.test.ts @@ -18,8 +18,8 @@ const server = setupServer( } return res( ctx.status(200), - ctx.set("content-type", "application/octet-stream"), - ctx.body(store[key]) + ctx.set("content-type", "application/json"), + ctx.json(store[key]) ); }), @@ -31,9 +31,13 @@ const server = setupServer( return res(ctx.status(200), ctx.json({ success: true })); }), - rest.put(`${baseURL}/values/:key`, async (req, res, ctx) => { - const key = req.params.key as string; - store[key] = await req.text(); + rest.put(`${baseURL}/bulk`, async (req, res, ctx) => { + const items = (await req.json()) as Record[]; + if (!items) return res(ctx.status(404), ctx.json(null)); + for (const item of items) { + const { key, value } = item; + store[key] = value; + } return res(ctx.status(204), ctx.json(null)); }),