keyv-cloudflare-kv 0.1.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # keyv-cloudflare-kv
2
+
3
+ Cloudflare KV adapter for Keyv.
4
+
5
+ This adapter allows Keyv to use Cloudflare Workers KV as a storage backend.
6
+
7
+ It supports two environments:
8
+
9
+ - Cloudflare Workers → uses native KV binding
10
+ - Node.js / Bun / servers → uses the Cloudflare API
11
+
12
+ This allows the same backend code to run anywhere.
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ This package is published on GitHub Packages.
19
+
20
+ First configure your `.npmrc`.
21
+
22
+ ### .npmrc
23
+
24
+ ```bash
25
+ @Sivothajan:registry=https://npm.pkg.github.com
26
+ //npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
27
+ ```
28
+
29
+ Replace:
30
+
31
+ - `YOUR_GITHUB_TOKEN`
32
+
33
+ ---
34
+
35
+ ### Install
36
+
37
+ ```bash
38
+ # npm
39
+ npm install @Sivothajan/keyv-cloudflare-kv
40
+
41
+ # bun
42
+ bun add @Sivothajan/keyv-cloudflare-kv
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Usage
48
+
49
+ ### Node / Bun (Cloudflare API mode)
50
+
51
+ ```javascript
52
+ import Keyv from 'keyv';
53
+ import Cloudflare from 'cloudflare';
54
+ import KeyvCloudflare from '@Sivothajan/keyv-cloudflare-kv';
55
+
56
+ const client = new Cloudflare({
57
+ apiToken: process.env.CF_API_TOKEN,
58
+ });
59
+
60
+ const store = new KeyvCloudflare({
61
+ mode: 'api',
62
+ client,
63
+ accountId: process.env.CF_ACCOUNT_ID,
64
+ namespaceId: process.env.CF_NAMESPACE_ID,
65
+ });
66
+
67
+ const keyv = new Keyv({ store });
68
+
69
+ await keyv.set('hello', 'world');
70
+
71
+ const value = await keyv.get('hello');
72
+
73
+ console.log(value);
74
+ ```
75
+
76
+ ---
77
+
78
+ ### Cloudflare Workers
79
+
80
+ ```javascript
81
+ import Keyv from 'keyv';
82
+ import KeyvCloudflare from '@Sivothajan/keyv-cloudflare-kv';
83
+
84
+ export default {
85
+ async fetch(request, env) {
86
+ const keyv = new Keyv({
87
+ store: new KeyvCloudflare({
88
+ mode: 'workers',
89
+ kv: env.MY_KV,
90
+ }),
91
+ });
92
+
93
+ await keyv.set('hello', 'world');
94
+
95
+ const value = await keyv.get('hello');
96
+
97
+ return new Response(value);
98
+ },
99
+ };
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Options
105
+
106
+ ### API Mode
107
+
108
+ | Option | Type | Required | Description |
109
+ | ----------- | ---------- | -------- | --------------------- |
110
+ | mode | "api" | yes | Use Cloudflare API |
111
+ | client | Cloudflare | yes | Cloudflare SDK client |
112
+ | accountId | string | yes | Cloudflare account ID |
113
+ | namespaceId | string | yes | KV namespace ID |
114
+ | namespace | string | no | Optional key prefix |
115
+
116
+ ---
117
+
118
+ ### Workers Mode
119
+
120
+ | Option | Type | Required | Description |
121
+ | --------- | ----------- | -------- | ------------------- |
122
+ | mode | "workers" | yes | Worker runtime |
123
+ | kv | KVNamespace | yes | Worker KV binding |
124
+ | namespace | string | no | Optional key prefix |
125
+
126
+ ---
127
+
128
+ ## Namespace Prefixing
129
+
130
+ You can optionally prefix keys:
131
+
132
+ ```text
133
+ namespace: "permissions"
134
+ ```
135
+
136
+ Example stored keys:
137
+
138
+ ```text
139
+ permissions:user:1
140
+ permissions:user:2
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Development
146
+
147
+ Install dependencies
148
+
149
+ ```bash
150
+ bun install
151
+ ```
152
+
153
+ Run lint
154
+
155
+ ```.bash
156
+ bun run lint
157
+ ```
158
+
159
+ Build
160
+
161
+ ```bash
162
+ bun run build
163
+ ```
@@ -0,0 +1,12 @@
1
+ import type { KeyvCloudflareOptions } from './types';
2
+ export default class KeyvCloudflare {
3
+ private client;
4
+ private namespace?;
5
+ constructor(options: KeyvCloudflareOptions);
6
+ get(key: string): Promise<any>;
7
+ set(key: string, value: any, ttl?: number): Promise<boolean>;
8
+ delete(key: string): Promise<boolean>;
9
+ getMany(keys: string[]): Promise<any[]>;
10
+ setMany(entries: [string, any][], ttl?: number): Promise<boolean[]>;
11
+ deleteMany(keys: string[]): Promise<boolean[]>;
12
+ }
@@ -0,0 +1,42 @@
1
+ import { ApiKVClient } from './api-client';
2
+ import { parse, prefix } from './utils';
3
+ import { WorkersKVClient } from './workers-client';
4
+ export default class KeyvCloudflare {
5
+ client;
6
+ namespace;
7
+ constructor(options) {
8
+ this.namespace = options.namespace;
9
+ if (options.mode === 'workers') {
10
+ this.client = new WorkersKVClient(options.kv);
11
+ return;
12
+ }
13
+ if (options.mode === 'api') {
14
+ this.client = new ApiKVClient(options.client, options.accountId, options.namespaceId);
15
+ return;
16
+ }
17
+ }
18
+ async get(key) {
19
+ const k = prefix(this.namespace, key);
20
+ const value = await this.client.get(k);
21
+ return parse(value);
22
+ }
23
+ async set(key, value, ttl) {
24
+ const k = prefix(this.namespace, key);
25
+ await this.client.set(k, JSON.stringify(value), ttl);
26
+ return true;
27
+ }
28
+ async delete(key) {
29
+ const k = prefix(this.namespace, key);
30
+ await this.client.delete(k);
31
+ return true;
32
+ }
33
+ async getMany(keys) {
34
+ return Promise.all(keys.map((k) => this.get(k)));
35
+ }
36
+ async setMany(entries, ttl) {
37
+ return Promise.all(entries.map(([k, v]) => this.set(k, v, ttl)));
38
+ }
39
+ async deleteMany(keys) {
40
+ return Promise.all(keys.map((k) => this.delete(k)));
41
+ }
42
+ }
@@ -0,0 +1,10 @@
1
+ import type Cloudflare from 'cloudflare';
2
+ export declare class ApiKVClient {
3
+ private client;
4
+ private accountId;
5
+ private namespaceId;
6
+ constructor(client: Cloudflare, accountId: string, namespaceId: string);
7
+ get(key: string): Promise<Response>;
8
+ set(key: string, value: string, ttl?: number): Promise<void>;
9
+ delete(key: string): Promise<void>;
10
+ }
@@ -0,0 +1,27 @@
1
+ export class ApiKVClient {
2
+ client;
3
+ accountId;
4
+ namespaceId;
5
+ constructor(client, accountId, namespaceId) {
6
+ this.client = client;
7
+ this.accountId = accountId;
8
+ this.namespaceId = namespaceId;
9
+ }
10
+ async get(key) {
11
+ return this.client.kv.namespaces.values.get(this.namespaceId, key, {
12
+ account_id: this.accountId,
13
+ });
14
+ }
15
+ async set(key, value, ttl) {
16
+ await this.client.kv.namespaces.values.update(this.namespaceId, key, {
17
+ account_id: this.accountId,
18
+ value,
19
+ expiration_ttl: ttl ? Math.floor(ttl / 1000) : undefined,
20
+ });
21
+ }
22
+ async delete(key) {
23
+ await this.client.kv.namespaces.values.delete(this.namespaceId, key, {
24
+ account_id: this.accountId,
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,3 @@
1
+ import KeyvCloudflare from './adapter';
2
+ export default KeyvCloudflare;
3
+ export { KeyvCloudflare };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import KeyvCloudflare from './adapter';
2
+ export default KeyvCloudflare;
3
+ export { KeyvCloudflare };
@@ -0,0 +1,25 @@
1
+ import type { KVNamespace } from '@cloudflare/workers-types';
2
+ import type Cloudflare from 'cloudflare';
3
+ export interface CloudflareApiOptions {
4
+ client: Cloudflare;
5
+ accountId: string;
6
+ namespaceId: string;
7
+ }
8
+ export interface WorkersKVOptions {
9
+ kv: KVNamespace;
10
+ }
11
+ interface BaseOptions {
12
+ namespace?: string;
13
+ }
14
+ export type KeyvCloudflareOptions = (BaseOptions & {
15
+ mode: 'api';
16
+ } & CloudflareApiOptions) | (BaseOptions & {
17
+ mode: 'workers';
18
+ } & WorkersKVOptions) | (BaseOptions & {
19
+ mode?: 'auto';
20
+ kv?: KVNamespace;
21
+ client?: Cloudflare;
22
+ accountId?: string;
23
+ namespaceId?: string;
24
+ });
25
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare function prefix(namespace: string | undefined, key: string): string;
2
+ export declare function parse(value: string | null): any;
package/dist/utils.js ADDED
@@ -0,0 +1,13 @@
1
+ export function prefix(namespace, key) {
2
+ return namespace ? `${namespace}:${key}` : key;
3
+ }
4
+ export function parse(value) {
5
+ if (!value)
6
+ return undefined;
7
+ try {
8
+ return JSON.parse(value);
9
+ }
10
+ catch {
11
+ return value;
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ import type { KVNamespace } from '@cloudflare/workers-types';
2
+ export declare class WorkersKVClient {
3
+ private kv;
4
+ constructor(kv: KVNamespace);
5
+ get(key: string): Promise<string | null>;
6
+ set(key: string, value: string, ttl?: number): Promise<void>;
7
+ delete(key: string): Promise<void>;
8
+ }
@@ -0,0 +1,17 @@
1
+ export class WorkersKVClient {
2
+ kv;
3
+ constructor(kv) {
4
+ this.kv = kv;
5
+ }
6
+ async get(key) {
7
+ return this.kv.get(key);
8
+ }
9
+ async set(key, value, ttl) {
10
+ await this.kv.put(key, value, {
11
+ expirationTtl: ttl ? Math.floor(ttl / 1000) : undefined,
12
+ });
13
+ }
14
+ async delete(key) {
15
+ await this.kv.delete(key);
16
+ }
17
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "keyv-cloudflare-kv",
3
+ "version": "0.1.0-alpha",
4
+ "description": "Cloudflare KV storage adapter for Keyv",
5
+ "author": {
6
+ "name": "Sivothayan",
7
+ "email": "hi@sivothayan.com",
8
+ "url": "https://sivothayan.com"
9
+ },
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "type": "module",
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "dev": "tsc -w",
26
+ "format": "prettier --write .",
27
+ "lint": "eslint .",
28
+ "lint:fix": "eslint . --fix"
29
+ },
30
+ "dependencies": {
31
+ "cloudflare": "^4.0.0"
32
+ },
33
+ "peerDependencies": {
34
+ "keyv": "^4 || ^5 || ^6"
35
+ },
36
+ "devDependencies": {
37
+ "@cloudflare/workers-types": "^4.20260312.1",
38
+ "@types/node": "^25.5.0",
39
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
40
+ "@typescript-eslint/parser": "^8.57.0",
41
+ "eslint": "^9.39.2",
42
+ "eslint-plugin-import": "^2.32.0",
43
+ "jiti": "^2.6.1",
44
+ "prettier": "^3.8.1",
45
+ "typescript": "^5.9.3"
46
+ },
47
+ "prettier": {
48
+ "printWidth": 80,
49
+ "tabWidth": 2,
50
+ "useTabs": false,
51
+ "semi": true,
52
+ "singleQuote": true,
53
+ "trailingComma": "es5",
54
+ "bracketSpacing": true,
55
+ "arrowParens": "always",
56
+ "endOfLine": "auto"
57
+ }
58
+ }