Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support cloudflare options with setItem #255

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 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,19 @@ 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", {
ttl: 300, // key expiration shorthand (in seconds)
cloudflareKvBinding: {
expiration: 1578435000,
expirationTtl: 300,
metadata: { someMetadataKey: "someMetadataValue" },
},
});
```
17 changes: 17 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,20 @@ 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", {
ttl: 300, // key expiration shorthand (in seconds)
cloudflareKvHttp: {
expiration: 1578435000,
expiration_ttl: 300,
base64: false,
metadata: { someMetadataKey: "someMetadataValue" },
},
});
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"@vue/compiler-sfc": "^3.3.4",
"azurite": "^3.25.1",
"changelogen": "^0.5.4",
"defu": "^6.1.2",
"eslint": "^8.47.0",
"eslint-config-unjs": "^0.2.1",
"fake-indexeddb": "^4.0.2",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions src/drivers/cloudflare-kv-binding.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
/// <reference types="@cloudflare/workers-types" />
import { createError, defineDriver, joinKeys } from "./utils";
import defu from "defu";

export interface KVOptions {
binding?: string | KVNamespace;

/** Adds prefix to all stored keys */
base?: string;
/**
* Default TTL for all items in seconds.
*/
ttl?: number;
}

//https://developers.cloudflare.com/workers/runtime-apis/kv#writing-key-value-pairs
export type CloudflareKvBindingSetItemOptions = KVNamespacePutOptions;

// https://developers.cloudflare.com/workers/runtime-apis/kv

const DRIVER_NAME = "cloudflare-kv-binding";
Expand Down Expand Up @@ -34,10 +43,18 @@
const binding = getBinding(opts.binding);
return binding.get(key);
},
setItem(key, value) {
setItem(key, value, options) {
key = r(key);
const binding = getBinding(opts.binding);
return binding.put(key, value);
const o = defu(
options?.cloudflareKvBinding,
options?.ttl
? { expirationTtl: options.ttl }

Check warning on line 52 in src/drivers/cloudflare-kv-binding.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/cloudflare-kv-binding.ts#L52

Added line #L52 was not covered by tests
: opts.ttl
? { expirationTtl: opts.ttl }

Check warning on line 54 in src/drivers/cloudflare-kv-binding.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/cloudflare-kv-binding.ts#L54

Added line #L54 was not covered by tests
: {}
);
return binding.put(key, value, o);
},
removeItem(key) {
key = r(key);
Expand Down
110 changes: 75 additions & 35 deletions src/drivers/cloudflare-kv-http.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { $fetch } from "ofetch";
import { $fetch, type FetchOptions } from "ofetch";
import {
createError,
createRequiredError,
defineDriver,
joinKeys,
} from "./utils";
import { defu } from "defu";
import { TransactionOptions } from "../types";

interface KVAuthAPIToken {
/**
Expand Down Expand Up @@ -57,6 +59,10 @@
* Adds prefix to all stored keys
*/
base?: string;
/**
* Default TTL for all items in seconds.
*/
ttl?: number;
} & (KVAuthServiceKey | KVAuthAPIToken | KVAuthEmailKey);

type CloudflareAuthorizationHeaders =
Expand All @@ -79,6 +85,36 @@
Authorization: `Bearer ${string}`;
};

type GetKeysResponse = {
result: { name: string }[];
result_info: { cursor?: string };
};

// https://developers.cloudflare.com/api/operations/workers-kv-namespace-write-multiple-key-value-pairs
export type CloudflareKvHttpSetItemOptions = {
/**
* Whether or not the server should base64 decode the value before storing it.
* Useful for writing values that wouldn't otherwise be valid JSON strings, such as images.
* @default false
*/
base64?: boolean;
/**
* The time, measured in number of seconds since the UNIX epoch, at which the key should expire.
* @example 1578435000
*/
expiration?: number;
/**
* The number of seconds for which the key should be visible before it expires. At least 60.
* @example 300
*/
expiration_ttl?: number;
/**
* Arbitrary JSON that is associated with a key.
* @example {"someMetadataKey":"someMetadataValue"}
*/
metadata?: Record<string, unknown>;
};

const DRIVER_NAME = "cloudflare-kv-http";

export default defineDriver<KVHTTPOptions>((opts) => {
Expand Down Expand Up @@ -106,46 +142,50 @@

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/${r(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/${r(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: TransactionOptions
) => {
const cloudflareOptions = defu(
options?.cloudflareKvHttp,
options?.ttl
? { expiration_ttl: options.ttl }

Check warning on line 176 in src/drivers/cloudflare-kv-http.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/cloudflare-kv-http.ts#L176

Added line #L176 was not covered by tests
: opts.ttl
? { expiration_ttl: opts.ttl }

Check warning on line 178 in src/drivers/cloudflare-kv-http.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/cloudflare-kv-http.ts#L178

Added line #L178 was not covered by tests
: {}
);
await kvFetch(`/bulk`, {
method: "PUT",
body: JSON.stringify([{ ...cloudflareOptions, key: r(key), value }]),
});
};

const removeItem = async (key: string) => {
return await kvFetch(`/values/${r(key)}`, { method: "DELETE" });
await kvFetch(`/values/${r(key)}`, { method: "DELETE" });
};

const getKeys = async (base?: string) => {
Expand All @@ -157,19 +197,19 @@
}

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;

Check warning on line 212 in src/drivers/cloudflare-kv-http.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/cloudflare-kv-http.ts#L210-L212

Added lines #L210 - L212 were not covered by tests
if (pageCursor) {
params.cursor = pageCursor;
} else {
Expand Down
8 changes: 4 additions & 4 deletions src/drivers/cloudflare-r2-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,22 @@ export default defineDriver((opts: CloudflareR2Options) => {
getItem(key, topts) {
key = r(key);
const binding = getBinding(opts.binding);
return binding.get(key, topts).then((r) => r?.text());
return binding.get(key, topts as any).then((r: any) => r?.text());
},
getItemRaw(key, topts) {
key = r(key);
const binding = getBinding(opts.binding);
return binding.get(key, topts).then((r) => r?.arrayBuffer());
return binding.get(key, topts as any).then((r: any) => r?.arrayBuffer());
},
async setItem(key, value, topts) {
key = r(key);
const binding = getBinding(opts.binding);
await binding.put(key, value, topts);
await binding.put(key, value, topts as any);
},
async setItemRaw(key, value, topts) {
key = r(key);
const binding = getBinding(opts.binding);
await binding.put(key, value, topts);
await binding.put(key, value, topts as any);
},
async removeItem(key) {
key = r(key);
Expand Down
14 changes: 7 additions & 7 deletions src/drivers/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
hasItem(key, topts) {
return _fetch(r(key), {
method: "HEAD",
headers: { ...opts.headers, ...topts.headers },
headers: { ...opts.headers, ...topts?.headers },
})
.then(() => true)
.catch(() => false);
Expand All @@ -37,15 +37,15 @@
headers: {
accept: "application/octet-stream",
...opts.headers,
...topts.headers,
...topts?.headers,
},
});
return value;
},
async getMeta(key, topts) {
const res = await _fetch.raw(r(key), {
method: "HEAD",
headers: { ...opts.headers, ...topts.headers },
headers: { ...opts.headers, ...topts?.headers },

Check warning on line 48 in src/drivers/http.ts

View check run for this annotation

Codecov / codecov/patch

src/drivers/http.ts#L48

Added line #L48 was not covered by tests
});
let mtime = undefined;
const _lastModified = res.headers.get("last-modified");
Expand All @@ -71,26 +71,26 @@
headers: {
"content-type": "application/octet-stream",
...opts.headers,
...topts.headers,
...topts?.headers,
},
});
},
async removeItem(key, topts) {
await _fetch(r(key), {
method: "DELETE",
headers: { ...opts.headers, ...topts.headers },
headers: { ...opts.headers, ...topts?.headers },
});
},
async getKeys(base, topts) {
const value = await _fetch(rBase(base), {
headers: { ...opts.headers, ...topts.headers },
headers: { ...opts.headers, ...topts?.headers },
});
return Array.isArray(value) ? value : [];
},
async clear(base, topts) {
await _fetch(rBase(base), {
method: "DELETE",
headers: { ...opts.headers, ...topts.headers },
headers: { ...opts.headers, ...topts?.headers },
});
},
};
Expand Down
11 changes: 10 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { CloudflareKvBindingSetItemOptions } from "./drivers/cloudflare-kv-binding";
import { CloudflareKvHttpSetItemOptions } from "./drivers/cloudflare-kv-http";

export type StorageValue = null | string | number | boolean | object;
export type WatchEvent = "update" | "remove";
export type WatchCallback = (event: WatchEvent, key: string) => any;
Expand All @@ -14,7 +17,13 @@ export interface StorageMeta {
[key: string]: StorageValue | Date | undefined;
}

export type TransactionOptions = Record<string, any>;
export type TransactionOptions =
| ({
cloudflareKvBinding?: CloudflareKvBindingSetItemOptions;
cloudflareKvHttp?: CloudflareKvHttpSetItemOptions;
ttl?: number;
} & Record<string, any>)
| undefined;

export interface Driver {
name?: string;
Expand Down
Loading