payloadcms-cloudflare-kv-plugin 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Isaiah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,484 @@
1
+ # PayloadCMS Cloudflare KV Plugin
2
+
3
+ [![npm version](https://img.shields.io/npm/v/payloadcms-cloudflare-kv-plugin.svg)](https://www.npmjs.com/package/payloadcms-cloudflare-kv-plugin)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A transparent Cloudflare KV caching layer plugin for Payload CMS v3 that automatically caches database queries to improve performance using Cloudflare's globally distributed key-value store.
7
+
8
+ ## Features
9
+
10
+ - **Automatic Query Caching** - Transparently caches all read operations (find, findOne, count, etc.)
11
+ - **Smart Invalidation** - Automatically invalidates cache on write operations (create, update, delete)
12
+ - **Flexible Configuration** - Enable caching per collection or globally with custom TTL
13
+ - **Per-Request Override** - Control cache behavior on individual requests
14
+ - **Custom Cache Keys** - Generate custom cache keys based on your needs
15
+ - **Pattern-Based Invalidation** - Invalidate related cache entries using KV prefix matching
16
+ - **Debug Mode** - Optional logging for cache hits, misses, and invalidations
17
+ - **Zero Breaking Changes** - Works seamlessly with existing Payload applications
18
+ - **Global Distribution** - Leverages Cloudflare's edge network for low-latency reads
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install payloadcms-cloudflare-kv-plugin
24
+ # or
25
+ yarn add payloadcms-cloudflare-kv-plugin
26
+ # or
27
+ pnpm add payloadcms-cloudflare-kv-plugin
28
+ ```
29
+
30
+ ## Requirements
31
+
32
+ - Payload CMS v3.37.0 or higher
33
+ - Node.js 18.20.2+ or 20.9.0+
34
+ - Cloudflare Workers KV namespace
35
+ - Cloudflare Workers environment (for production) or local development setup
36
+
37
+ ## Quick Start
38
+
39
+ ### Basic Setup
40
+
41
+ First, create a KV namespace in your Cloudflare dashboard or using Wrangler:
42
+
43
+ ```bash
44
+ wrangler kv:namespace create "CACHE"
45
+ ```
46
+
47
+ This will output a namespace ID. Add it to your `wrangler.toml`:
48
+
49
+ ```toml
50
+ [[kv_namespaces]]
51
+ binding = "CACHE"
52
+ id = "your-namespace-id"
53
+ ```
54
+
55
+ Then configure the plugin in your Payload config:
56
+
57
+ ```typescript
58
+ import { buildConfig } from 'payload'
59
+ import { cloudflareKVCache } from 'payloadcms-cloudflare-kv-plugin'
60
+
61
+ export default buildConfig({
62
+ plugins: [
63
+ cloudflareKVCache({
64
+ // Pass the KV namespace from your Cloudflare Worker environment
65
+ kv: env.CACHE, // or your KV namespace binding
66
+ // Enable caching for specific collections
67
+ collections: {
68
+ posts: true,
69
+ articles: true,
70
+ },
71
+ }),
72
+ ],
73
+ // ... rest of your config
74
+ })
75
+ ```
76
+
77
+ ### Using in Cloudflare Workers
78
+
79
+ When using in a Cloudflare Worker, pass the KV namespace from the environment:
80
+
81
+ ```typescript
82
+ import { cloudflareKVCache } from 'payloadcms-cloudflare-kv-plugin'
83
+
84
+ export default {
85
+ async fetch(request: Request, env: Env): Promise<Response> {
86
+ const config = buildConfig({
87
+ plugins: [
88
+ cloudflareKVCache({
89
+ kv: env.CACHE, // KV namespace from Worker environment
90
+ collections: {
91
+ posts: true,
92
+ },
93
+ }),
94
+ ],
95
+ // ... rest of config
96
+ })
97
+
98
+ // ... your handler
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## Configuration
104
+
105
+ ### Plugin Options
106
+
107
+ ```typescript
108
+ type CloudflareKVPluginConfig = {
109
+ // Cloudflare KV Namespace binding (required)
110
+ kv: KVNamespace
111
+
112
+ // Collections to cache
113
+ collections?: Partial<Record<CollectionSlug, CacheOptions | true>>
114
+
115
+ // Globals to cache
116
+ globals?: Partial<Record<GlobalSlug, CacheOptions | true>>
117
+
118
+ // Enable debug logging
119
+ debug?: boolean
120
+
121
+ // Default cache behavior
122
+ defaultCacheOptions?: {
123
+ generateKey?: (operation: string, args: DBOperationArgs) => string
124
+ keyPrefix?: string
125
+ ttl?: number // in seconds, default: 300 (5 minutes)
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Cache Options
131
+
132
+ ```typescript
133
+ type CacheOptions = {
134
+ key?: string // Custom cache key override
135
+ skip?: boolean // Skip cache for this collection/query
136
+ tags?: string[] // Tags for grouped invalidation (future feature)
137
+ ttl?: number // Time-to-live in seconds
138
+ }
139
+ ```
140
+
141
+ ### Advanced Configuration
142
+
143
+ ```typescript
144
+ cloudflareKVCache({
145
+ kv: env.CACHE,
146
+
147
+ // Configure collections with custom TTL
148
+ collections: {
149
+ posts: {
150
+ ttl: 600, // Cache posts for 10 minutes
151
+ skip: false,
152
+ },
153
+ articles: {
154
+ ttl: 1800, // Cache articles for 30 minutes
155
+ },
156
+ users: true, // Use default TTL (5 minutes)
157
+ },
158
+
159
+ // Cache global configurations
160
+ globals: {
161
+ settings: true,
162
+ },
163
+
164
+ // Custom default options
165
+ defaultCacheOptions: {
166
+ keyPrefix: 'myapp',
167
+ ttl: 300,
168
+ generateKey: (operation, args) => {
169
+ // Custom key generation logic
170
+ const { slug, where, locale } = args
171
+ return `${slug}:${operation}:${locale || 'default'}:${JSON.stringify(where)}`
172
+ },
173
+ },
174
+
175
+ // Enable debug logging
176
+ debug: true,
177
+ })
178
+ ```
179
+
180
+ ## Usage
181
+
182
+ ### Per-Request Cache Control
183
+
184
+ Override cache behavior for individual requests:
185
+
186
+ ```typescript
187
+ // Skip cache for a specific query
188
+ const freshPosts = await payload.find({
189
+ collection: 'posts',
190
+ req: {
191
+ context: {
192
+ cache: {
193
+ skip: true, // Bypass cache, always hit database
194
+ },
195
+ },
196
+ },
197
+ })
198
+
199
+ // Custom TTL for a specific query
200
+ const shortLivedPosts = await payload.find({
201
+ collection: 'posts',
202
+ req: {
203
+ context: {
204
+ cache: {
205
+ ttl: 60, // Cache for 1 minute only
206
+ },
207
+ },
208
+ },
209
+ })
210
+
211
+ // Custom cache key
212
+ const customCachedPosts = await payload.find({
213
+ collection: 'posts',
214
+ req: {
215
+ context: {
216
+ cache: {
217
+ key: 'posts:featured',
218
+ },
219
+ },
220
+ },
221
+ })
222
+ ```
223
+
224
+ ### Cached Operations
225
+
226
+ The following database operations are automatically cached:
227
+
228
+ **Read Operations** (cached before hitting database):
229
+
230
+ - `find` - Query collections with pagination
231
+ - `findOne` - Query single document by ID
232
+ - `findGlobal` - Query global configurations
233
+ - `findGlobalVersions` - Query global version history
234
+ - `count` - Count documents
235
+ - `countVersions` - Count document versions
236
+ - `countGlobalVersions` - Count global versions
237
+ - `queryDrafts` - Query draft documents
238
+
239
+ **Write Operations** (invalidate cache after database update):
240
+
241
+ - `create` - Create new document
242
+ - `createMany` - Batch create
243
+ - `updateOne` - Update single document
244
+ - `updateMany` - Batch update
245
+ - `deleteOne` - Delete single document
246
+ - `deleteMany` - Batch delete
247
+ - `upsert` - Create or update
248
+ - `updateGlobal` - Update global config
249
+ - `updateGlobalVersion` - Update global version
250
+ - `deleteVersions` - Delete document versions
251
+
252
+ ## How It Works
253
+
254
+ ### Cache Key Generation
255
+
256
+ By default, cache keys are generated using MD5 hashing:
257
+
258
+ ```
259
+ [prefix]:[slug]:[operation]:[md5-hash]
260
+ ```
261
+
262
+ The hash includes: `{ slug, locale, operation, where }`
263
+
264
+ Example keys:
265
+
266
+ ```
267
+ posts:find:a1b2c3d4e5f6g7h8
268
+ myapp:articles:count:x9y8z7w6v5u4t3s2
269
+ ```
270
+
271
+ ### Cache Flow
272
+
273
+ **Read Operations:**
274
+
275
+ ```
276
+ Request → Check cache config → Check skip flag
277
+ ↓ (cache enabled)
278
+ Check KV → HIT: Return cached → MISS: Hit DB → Store in KV → Return
279
+ ↓ (cache disabled/skipped)
280
+ Hit DB directly
281
+ ```
282
+
283
+ **Write Operations:**
284
+
285
+ ```
286
+ Request → Execute on DB → Get cache config → Check skip flag
287
+ ↓ (cache enabled)
288
+ Invalidate pattern → Return result
289
+ ↓ (cache disabled/skipped)
290
+ Return result directly
291
+ ```
292
+
293
+ ### Automatic Invalidation
294
+
295
+ When data changes, the plugin automatically invalidates related cache entries using prefix matching:
296
+
297
+ ```typescript
298
+ // Creating a post invalidates all post queries
299
+ await payload.create({
300
+ collection: 'posts',
301
+ data: { title: 'New Post' },
302
+ })
303
+ // Invalidates: posts:*, myapp:*:posts:*, etc.
304
+
305
+ // Updating an article invalidates all article queries
306
+ await payload.update({
307
+ collection: 'articles',
308
+ id: '123',
309
+ data: { title: 'Updated' },
310
+ })
311
+ // Invalidates: articles:*, myapp:*:articles:*, etc.
312
+ ```
313
+
314
+ **Note:** Cloudflare KV uses prefix-based listing instead of pattern matching. The plugin converts patterns like `posts:*` to prefix queries and filters matching keys.
315
+
316
+ ## Debug Mode
317
+
318
+ Enable debug logging to monitor cache behavior:
319
+
320
+ ```typescript
321
+ cloudflareKVCache({
322
+ kv: env.CACHE,
323
+ collections: { posts: true },
324
+ debug: true,
325
+ })
326
+ ```
327
+
328
+ Console output:
329
+
330
+ ```
331
+ [CloudflareKVPlugin] [find] [posts] Cache HIT
332
+ [CloudflareKVPlugin] [find] [articles] Cache MISS
333
+ [CloudflareKVPlugin] [create] [posts] Invalidating pattern: posts:*
334
+ [CloudflareKVPlugin] [update] [posts] Cache SKIP (per-request)
335
+ ```
336
+
337
+ ## TypeScript Support
338
+
339
+ The plugin includes full TypeScript definitions and extends Payload's `RequestContext` type:
340
+
341
+ ```typescript
342
+ declare module 'payload' {
343
+ export interface RequestContext {
344
+ cache?: {
345
+ key?: string
346
+ skip?: boolean
347
+ tags?: string[]
348
+ ttl?: number
349
+ }
350
+ }
351
+ }
352
+ ```
353
+
354
+ ## Performance Considerations
355
+
356
+ - **Default TTL**: 5 minutes (300 seconds)
357
+ - **Prefix Matching**: Uses KV `list()` with prefix for invalidation (may be slower with large keyspaces)
358
+ - **Silent Failures**: Cache errors don't break database queries
359
+ - **Memory**: KV has a 25 MB value size limit per key
360
+ - **Expiration**: KV automatically removes expired keys
361
+ - **Eventual Consistency**: KV is eventually consistent - writes may take a few seconds to propagate globally
362
+ - **Read Performance**: KV is optimized for high-read, low-write workloads
363
+
364
+ ## Cloudflare KV Limitations
365
+
366
+ - **Eventual Consistency**: KV is eventually consistent. Writes may take a few seconds to be visible globally
367
+ - **No Transactions**: KV doesn't support transactions or atomic operations
368
+ - **Value Size Limit**: Maximum 25 MB per value
369
+ - **List Performance**: Listing keys with prefixes can be slower with very large keyspaces
370
+ - **No Pattern Matching**: Uses prefix-based listing instead of Redis-style pattern matching
371
+
372
+ ## Development
373
+
374
+ ```bash
375
+ # Install dependencies
376
+ pnpm install
377
+
378
+ # Run development server
379
+ pnpm dev
380
+
381
+ # Run tests
382
+ pnpm test
383
+
384
+ # Build plugin
385
+ pnpm build
386
+
387
+ # Lint code
388
+ pnpm lint
389
+ ```
390
+
391
+ ## Examples
392
+
393
+ ### E-commerce Site
394
+
395
+ ```typescript
396
+ cloudflareKVCache({
397
+ kv: env.CACHE,
398
+ collections: {
399
+ products: { ttl: 3600 }, // Cache products for 1 hour
400
+ categories: { ttl: 7200 }, // Cache categories for 2 hours
401
+ orders: { skip: true }, // Never cache orders
402
+ customers: { ttl: 600 }, // Cache customers for 10 minutes
403
+ },
404
+ globals: {
405
+ siteSettings: { ttl: 86400 }, // Cache site settings for 24 hours
406
+ },
407
+ })
408
+ ```
409
+
410
+ ### Blog Platform
411
+
412
+ ```typescript
413
+ cloudflareKVCache({
414
+ kv: env.CACHE,
415
+ collections: {
416
+ posts: { ttl: 1800 }, // Cache posts for 30 minutes
417
+ authors: { ttl: 3600 }, // Cache authors for 1 hour
418
+ comments: { ttl: 300 }, // Cache comments for 5 minutes
419
+ },
420
+ defaultCacheOptions: {
421
+ keyPrefix: 'blog',
422
+ ttl: 600,
423
+ },
424
+ debug: process.env.NODE_ENV === 'development',
425
+ })
426
+ ```
427
+
428
+ ## Troubleshooting
429
+
430
+ ### KV Namespace Not Accessible
431
+
432
+ ```typescript
433
+ // Verify KV namespace is properly bound
434
+ // In wrangler.toml:
435
+ [[kv_namespaces]]
436
+ binding = "CACHE"
437
+ id = "your-namespace-id"
438
+
439
+ // In your code:
440
+ cloudflareKVCache({
441
+ kv: env.CACHE, // Make sure this matches the binding name
442
+ // ...
443
+ })
444
+ ```
445
+
446
+ ### Cache Not Working
447
+
448
+ 1. Enable debug mode to see cache behavior
449
+ 2. Verify collection/global is configured for caching
450
+ 3. Check if `skip: true` is set
451
+ 4. Ensure KV namespace is properly bound and accessible
452
+ 5. Check Cloudflare Workers logs for errors
453
+
454
+ ### High Memory Usage
455
+
456
+ 1. Reduce TTL values
457
+ 2. Be selective about which collections to cache
458
+ 3. Monitor KV usage in Cloudflare dashboard
459
+ 4. Consider using KV max keys limits
460
+
461
+ ### Eventual Consistency Issues
462
+
463
+ If you need immediate consistency:
464
+ - Use `skip: true` for critical queries
465
+ - Implement cache warming strategies
466
+ - Consider using Cloudflare Durable Objects for strongly consistent data
467
+
468
+ ## Contributing
469
+
470
+ Contributions are welcome! Please see the [GitHub repository](https://github.com/thejemish/payloadcms-cloudflare-kv-plugin) for issues and pull requests.
471
+
472
+ > **Note:** This repository was originally created for a Redis plugin but has been converted to use Cloudflare KV. The repository name may be updated in the future.
473
+
474
+ ## License
475
+
476
+ MIT
477
+
478
+ ## Links
479
+
480
+ - [GitHub Repository](https://github.com/thejemish/payloadcms-cloudflare-kv-plugin)
481
+ - [NPM Package](https://www.npmjs.com/package/payloadcms-cloudflare-kv-plugin)
482
+ - [Payload CMS Documentation](https://payloadcms.com/docs)
483
+ - [Cloudflare KV Documentation](https://developers.cloudflare.com/kv/)
484
+ - [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/)
@@ -0,0 +1,8 @@
1
+ import type { DatabaseAdapter } from 'payload';
2
+ import type { CloudflareKVPluginConfig, KVNamespace } from './types.js';
3
+ export declare function dbAdapterWithCache({ baseAdapter, kv, config, }: {
4
+ baseAdapter: DatabaseAdapter;
5
+ config: CloudflareKVPluginConfig;
6
+ kv: KVNamespace;
7
+ }): DatabaseAdapter;
8
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EAShB,MAAM,SAAS,CAAA;AAEhB,OAAO,KAAK,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAYvE,wBAAgB,kBAAkB,CAAC,EACjC,WAAW,EACX,EAAE,EACF,MAA6E,GAC9E,EAAE;IACD,WAAW,EAAE,eAAe,CAAA;IAC5B,MAAM,EAAE,wBAAwB,CAAA;IAChC,EAAE,EAAE,WAAW,CAAA;CAChB,GAAG,eAAe,CA8TlB"}