Skip to content

Commit

Permalink
feat: supports cloudflare options
Browse files Browse the repository at this point in the history
  • Loading branch information
Hebilicious committed Jul 8, 2023
1 parent c392d45 commit 2b4bd93
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 43 deletions.
13 changes: 13 additions & 0 deletions docs/content/6.drivers/cloudflare-kv-binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
});
```
14 changes: 14 additions & 0 deletions docs/content/6.drivers/cloudflare-kv-http.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
});
```
5 changes: 2 additions & 3 deletions src/drivers/cloudflare-kv-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
71 changes: 36 additions & 35 deletions src/drivers/cloudflare-kv-http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { $fetch } from "ofetch";
import { $fetch, type FetchOptions } from "ofetch";
import {
createError,
createRequiredError,
Expand Down Expand Up @@ -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<KVHTTPOptions>((opts) => {
Expand Down Expand Up @@ -106,46 +111,42 @@ export default defineDriver<KVHTTPOptions>((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<string, unknown>
) => {
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) => {
Expand All @@ -157,19 +158,19 @@ export default defineDriver<KVHTTPOptions>((opts) => {
}

const firstPage = await kvFetch("/keys", { params });
firstPage.result.forEach(({ name }: { name: string }) => keys.push(name));
const data = await firstPage.json<GetKeysResponse>();
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<GetKeysResponse>();
dataPageResult.result.forEach(({ name }) => keys.push(name));
const pageCursor = dataPageResult.result_info.cursor;
if (pageCursor) {
params.cursor = pageCursor;
} else {
Expand Down
14 changes: 9 additions & 5 deletions test/drivers/cloudflare-kv-http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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])
);
}),

Expand All @@ -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<string, any>[];
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));
}),

Expand Down

0 comments on commit 2b4bd93

Please sign in to comment.