From c1917c4b334046d45e81939669edee154b1d22f7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 8 Feb 2026 00:10:41 +0000 Subject: [PATCH] feat(cloudflare): complete Cursor plugin for Cloudflare services - Update .cursor/plugin.json to match spec (keywords, description) - Update rules/cloudflare-workers.mdc: new globs (worker*.*, **/workers/**), add HTMLRewriter streaming transformation and Cache API edge caching sections - Add rules/cloudflare-d1.mdc: comprehensive D1 best practices covering parameterized queries, batch operations, prepared statements, error handling, migrations, edge query optimization, consistency model, SQLite data types, and pagination patterns (offset-based and cursor-based) - Rename agent to cloudflare-workers-agent.md for consistency - Update README.md with D1 rules documentation and corrected references - Update CHANGELOG.md with complete 1.0.0 release notes Co-authored-by: n2parko --- plugins/cloudflare/.cursor/plugin.json | 4 +- plugins/cloudflare/CHANGELOG.md | 7 +- plugins/cloudflare/README.md | 23 +- ...r-agent.md => cloudflare-workers-agent.md} | 0 plugins/cloudflare/rules/cloudflare-d1.mdc | 377 ++++++++++++++++++ .../cloudflare/rules/cloudflare-workers.mdc | 77 +++- 6 files changed, 478 insertions(+), 10 deletions(-) rename plugins/cloudflare/agents/{cloudflare-worker-agent.md => cloudflare-workers-agent.md} (100%) create mode 100644 plugins/cloudflare/rules/cloudflare-d1.mdc diff --git a/plugins/cloudflare/.cursor/plugin.json b/plugins/cloudflare/.cursor/plugin.json index e3a8c7b..d6b135b 100644 --- a/plugins/cloudflare/.cursor/plugin.json +++ b/plugins/cloudflare/.cursor/plugin.json @@ -1,12 +1,12 @@ { "name": "cloudflare", "version": "1.0.0", - "description": "Cursor plugin for Cloudflare — Workers, Pages, R2, KV, D1, and edge computing", + "description": "Cursor plugin for Cloudflare — Workers, Pages, R2, D1, KV, and edge computing", "author": { "name": "Cursor", "email": "plugins@cursor.com" }, "homepage": "https://github.com/cursor/service-plugin-generation", "repository": "https://github.com/cursor/service-plugin-generation", "license": "MIT", - "keywords": ["cloudflare", "workers", "edge", "cdn", "r2", "kv", "d1", "pages"], + "keywords": ["cloudflare", "workers", "pages", "edge", "r2", "d1", "kv"], "category": "infrastructure", "tags": ["edge-computing", "cdn", "serverless"], "agents": "./agents/", diff --git a/plugins/cloudflare/CHANGELOG.md b/plugins/cloudflare/CHANGELOG.md index 58309be..388b450 100644 --- a/plugins/cloudflare/CHANGELOG.md +++ b/plugins/cloudflare/CHANGELOG.md @@ -4,16 +4,17 @@ All notable changes to the Cloudflare plugin for Cursor will be documented in th The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2026-02-07 +## [1.0.0] - 2026-02-08 ### Added - **Plugin manifest** (`.cursor/plugin.json`) — plugin metadata, entry points, and configuration - **Rules** - - `cloudflare-workers.mdc` — best practices for Cloudflare Workers including Web Standard APIs, error handling, storage selection (KV, D1, R2, Durable Objects, Queues), CPU time limits, `waitUntil()`, CORS, environment bindings, and module Worker syntax + - `cloudflare-workers.mdc` — best practices for Cloudflare Workers including Web Standard APIs, error handling, storage selection (KV, D1, R2, Durable Objects, Queues), CPU time limits, `waitUntil()`, CORS, environment bindings, module Worker syntax, HTMLRewriter streaming transformations, and Cache API edge caching + - `cloudflare-d1.mdc` — comprehensive D1 database best practices covering parameterized queries, batch operations, prepared statements, error handling, migrations, edge query optimization, consistency model, SQLite data types, and pagination patterns - `cloudflare-config.mdc` — configuration best practices for `wrangler.toml` and `wrangler.jsonc`, covering compatibility date, bindings (KV, R2, D1, Durable Objects, Queues), routes, environments, cron triggers, secrets management, Smart Placement, and observability - **Agent** - - `cloudflare-worker-agent.md` — Worker development agent for creating, debugging, and optimizing Cloudflare Workers, with guidance on architecture patterns and storage selection + - `cloudflare-workers-agent.md` — Worker development agent for creating, debugging, and optimizing Cloudflare Workers, with guidance on architecture patterns and storage selection - **Skills** - `create-worker/SKILL.md` — step-by-step guide for creating a new Cloudflare Worker from project setup through deployment with bindings, local development, testing, custom domains, and CI/CD - `setup-d1-database/SKILL.md` — comprehensive guide for setting up a D1 database with migrations, schema design, parameterized queries, batch transactions, seeding, and production deployment diff --git a/plugins/cloudflare/README.md b/plugins/cloudflare/README.md index 28797fb..cce9303 100644 --- a/plugins/cloudflare/README.md +++ b/plugins/cloudflare/README.md @@ -18,9 +18,10 @@ plugins/cloudflare/ ├── .cursor/ │ └── plugin.json # Plugin manifest ├── agents/ -│ └── cloudflare-worker-agent.md # Worker development agent +│ └── cloudflare-workers-agent.md # Worker development agent ├── rules/ │ ├── cloudflare-workers.mdc # Workers best practices +│ ├── cloudflare-d1.mdc # D1 database best practices │ └── cloudflare-config.mdc # wrangler.toml configuration rules ├── skills/ │ ├── create-worker/ @@ -53,7 +54,8 @@ This plugin is included in the `service-plugin-generation` repository. To use it 1. Clone the repository or copy the `plugins/cloudflare/` directory into your project 2. The plugin's rules will automatically activate when you edit matching files: - - `src/**/*.ts`, `src/**/*.js`, `worker.*`, `functions/**/*` → Workers best practices + - `src/**/*.ts`, `src/**/*.js`, `worker*.*`, `**/workers/**` → Workers best practices + - `**/*.sql`, `**/migrations/**`, `**/*.ts` → D1 database best practices - `wrangler.toml`, `wrangler.jsonc` → configuration best practices 3. Access the Worker agent for interactive help with development and deployment @@ -84,7 +86,22 @@ Activated when editing Worker source files. Covers: - CORS configuration - Environment bindings and typed `Env` interface - Module Worker syntax (ES modules) -- Cache API and HTMLRewriter +- Cache API for edge caching +- HTMLRewriter for streaming HTML transformation + +### cloudflare-d1.mdc + +Activated when editing SQL files, migrations, and TypeScript sources. Covers: + +- Parameterized queries with `.bind()` — never concatenate strings +- Batch operations with `db.batch()` for transactions and performance +- D1 error handling (constraint violations, missing records) +- Migration workflow with Wrangler (`create`, `apply --local`, `apply --remote`) +- Query optimization for the edge (LIMIT, SELECT specific columns, indexes) +- D1 consistency model (strong consistency, single-region writes, read replication) +- SQLite-compatible data types (TEXT keys, INTEGER booleans, datetime as TEXT) +- Prepared statements (`.first()`, `.all()`, `.run()`, `.raw()`) +- Pagination patterns (offset-based and cursor-based) ### cloudflare-config.mdc diff --git a/plugins/cloudflare/agents/cloudflare-worker-agent.md b/plugins/cloudflare/agents/cloudflare-workers-agent.md similarity index 100% rename from plugins/cloudflare/agents/cloudflare-worker-agent.md rename to plugins/cloudflare/agents/cloudflare-workers-agent.md diff --git a/plugins/cloudflare/rules/cloudflare-d1.mdc b/plugins/cloudflare/rules/cloudflare-d1.mdc new file mode 100644 index 0000000..07eb062 --- /dev/null +++ b/plugins/cloudflare/rules/cloudflare-d1.mdc @@ -0,0 +1,377 @@ +--- +description: Best practices for Cloudflare D1 serverless SQL database +globs: + - "**/*.sql" + - "**/migrations/**" + - "**/*.ts" +alwaysApply: false +--- + +# Cloudflare D1 Best Practices + +D1 is Cloudflare's serverless SQL database built on SQLite, designed to run at the edge alongside Workers. + +## Parameterized Queries — Prevent SQL Injection + +- **Always** use `.bind()` with `?` placeholders for user-supplied values — never concatenate strings into SQL. +- `.bind()` parameters are positional: the first `?` maps to the first `.bind()` argument. +- This applies to all query methods: `.prepare()`, `.first()`, `.all()`, `.run()`, `.raw()`. + +```ts +// ✅ Safe — parameterized query +const user = await env.DB.prepare( + "SELECT id, email, name FROM users WHERE email = ? AND active = ?" +) + .bind(email, 1) + .first(); + +// ✅ Safe — parameterized INSERT +await env.DB.prepare( + "INSERT INTO users (id, email, name) VALUES (?, ?, ?)" +) + .bind(crypto.randomUUID(), email, name) + .run(); + +// ❌ DANGEROUS — SQL injection vulnerability +const user = await env.DB.prepare( + `SELECT * FROM users WHERE email = '${email}'` +).first(); + +// ❌ DANGEROUS — string concatenation +const query = "SELECT * FROM users WHERE name = '" + name + "'"; +``` + +## Batch Operations for Performance and Atomicity + +- Use `db.batch()` to execute multiple statements in a **single transaction** — all succeed or all fail. +- Batching reduces round-trips between the Worker and D1, significantly improving performance. +- Each statement in a batch must be a prepared statement with `.bind()` already applied. + +```ts +// Transactional: create user + default settings in one atomic operation +const userId = crypto.randomUUID(); +const results = await env.DB.batch([ + env.DB.prepare( + "INSERT INTO users (id, email, name) VALUES (?, ?, ?)" + ).bind(userId, email, name), + env.DB.prepare( + "INSERT INTO user_settings (user_id, theme, notifications) VALUES (?, ?, ?)" + ).bind(userId, "system", 1), + env.DB.prepare( + "INSERT INTO audit_log (user_id, action) VALUES (?, ?)" + ).bind(userId, "account_created"), +]); + +// All three inserts succeed or none do +``` + +- Use batching for bulk inserts instead of individual `INSERT` statements: + +```ts +// ✅ Efficient — single batch for multiple rows +const statements = items.map((item) => + env.DB.prepare( + "INSERT INTO products (id, name, price) VALUES (?, ?, ?)" + ).bind(item.id, item.name, item.price) +); +await env.DB.batch(statements); + +// ❌ Inefficient — N separate round-trips +for (const item of items) { + await env.DB.prepare( + "INSERT INTO products (id, name, price) VALUES (?, ?, ?)" + ).bind(item.id, item.name, item.price).run(); +} +``` + +## Prepared Statements + +- Always use `db.prepare()` to create prepared statements — they are parsed once and can be reused with different bindings. +- Use the correct result method for your query: + +| Method | Returns | Use When | +|---|---|---| +| `.first()` | Single row or `null` | Fetching one record (e.g., by primary key) | +| `.first()` | Typed single row or `null` | Fetching one record with TypeScript types | +| `.all()` | `{ results, success, meta }` | Fetching multiple rows | +| `.run()` | `{ success, meta }` | INSERT, UPDATE, DELETE (no result rows) | +| `.raw()` | `any[][]` (array of arrays) | Raw row arrays without column names | + +```ts +// .first() — single record lookup +const user = await env.DB.prepare( + "SELECT id, name, email FROM users WHERE id = ?" +).bind(userId).first<{ id: string; name: string; email: string }>(); + +// .all() — multiple records +const { results: posts } = await env.DB.prepare( + "SELECT * FROM posts WHERE user_id = ? ORDER BY created_at DESC LIMIT ?" +).bind(userId, 20).all(); + +// .run() — mutations (no result rows needed) +const { meta } = await env.DB.prepare( + "UPDATE users SET name = ?, updated_at = datetime('now') WHERE id = ?" +).bind(newName, userId).run(); +console.log(`Rows changed: ${meta.changes}`); +``` + +## D1 Error Handling + +- D1 operations can throw exceptions — always wrap database calls in `try/catch`. +- Check the `error` property on results for query-level errors. +- Common error scenarios: constraint violations (UNIQUE, NOT NULL, CHECK, FOREIGN KEY), syntax errors, and database-not-found. + +```ts +try { + await env.DB.prepare( + "INSERT INTO users (id, email, name) VALUES (?, ?, ?)" + ).bind(id, email, name).run(); +} catch (err: unknown) { + if (err instanceof Error) { + if (err.message.includes("UNIQUE constraint failed")) { + return Response.json( + { error: "A user with this email already exists" }, + { status: 409 } + ); + } + if (err.message.includes("NOT NULL constraint failed")) { + return Response.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + console.error("D1 error:", err.message); + } + return Response.json( + { error: "Database error" }, + { status: 500 } + ); +} +``` + +- Always check that `.first()` did not return `null` before using the result: + +```ts +const user = await env.DB.prepare( + "SELECT * FROM users WHERE id = ?" +).bind(userId).first(); + +if (!user) { + return Response.json({ error: "User not found" }, { status: 404 }); +} +``` + +## Migrations + +- Use Wrangler's built-in migration system to manage schema changes over time. +- Migrations are numbered SQL files in a `migrations/` directory, executed in order. +- **Never** edit a migration that has already been applied — create a new one instead. +- Test migrations locally before applying to production. + +```bash +# Create a new migration +wrangler d1 migrations create my-db add_comments_table + +# Apply migrations locally (development) +wrangler d1 migrations apply my-db --local + +# List pending migrations (remote) +wrangler d1 migrations list my-db --remote + +# Apply migrations to production +wrangler d1 migrations apply my-db --remote +``` + +### Migration File Conventions + +```sql +-- migrations/0001_create_users.sql +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + email TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +``` + +- Use `IF NOT EXISTS` / `IF EXISTS` to make migrations idempotent where possible. +- Use `CHECK` constraints for enum-like text columns. +- Use `REFERENCES ... ON DELETE CASCADE` for foreign keys. +- Include indexes in the same migration as the table they reference. + +## Optimize Queries for the Edge + +D1 runs on Cloudflare's edge network. Keep queries small and efficient: + +- **Limit result sets** — always use `LIMIT` to prevent unbounded queries: + +```ts +// ✅ Bounded query +const { results } = await env.DB.prepare( + "SELECT id, title, slug FROM posts WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?" +).bind("published", limit, offset).all(); + +// ❌ Unbounded — could return millions of rows +const { results } = await env.DB.prepare( + "SELECT * FROM posts" +).all(); +``` + +- **Select only needed columns** — avoid `SELECT *` in production code: + +```ts +// ✅ Select specific columns +const { results } = await env.DB.prepare( + "SELECT id, name, email FROM users WHERE active = ?" +).bind(1).all(); + +// ❌ Wasteful — fetches all columns including large text/blob +const { results } = await env.DB.prepare( + "SELECT * FROM users WHERE active = ?" +).bind(1).all(); +``` + +- **Use indexes** — add indexes on columns used in `WHERE`, `JOIN`, and `ORDER BY`: + +```sql +-- Index for filtered + sorted queries +CREATE INDEX idx_posts_status_date ON posts(status, created_at DESC); + +-- Partial index — only index the rows you actually query +CREATE INDEX idx_posts_published ON posts(published_at DESC) + WHERE status = 'published'; + +-- Composite index for JOINs +CREATE INDEX idx_post_tags_tag_id ON post_tags(tag_id); +``` + +- **Parallelize independent queries** using `Promise.all()`: + +```ts +const [postsResult, tagsResult, statsResult] = await Promise.all([ + env.DB.prepare("SELECT * FROM posts WHERE status = ? LIMIT 20") + .bind("published").all(), + env.DB.prepare("SELECT * FROM tags ORDER BY name").all(), + env.DB.prepare("SELECT COUNT(*) as total FROM posts WHERE status = ?") + .bind("published").first<{ total: number }>(), +]); +``` + +## D1 Consistency Model + +- D1 provides **strong consistency** within a single database — reads always reflect the latest writes. +- Each D1 database has a **primary location**. Reads and writes are routed to this location. +- D1 uses **read replication** to serve read queries from edge locations closer to the user, reducing latency for read-heavy workloads. +- For write-heavy workloads, consider co-locating your Worker near the D1 database using **Smart Placement**: + +```toml +# wrangler.toml — place Worker near D1 primary +[placement] +mode = "smart" +``` + +- `db.batch()` runs all statements in a single transaction at the primary location, ensuring atomicity and consistency across multiple writes. +- Be aware that D1 is **single-region for writes** — all write operations go to the primary location regardless of where the Worker runs. + +## SQLite-Specific Data Types + +D1 is built on SQLite. Use SQLite-compatible types: + +| Concept | Recommended Type | Notes | +|---|---|---| +| Primary key | `TEXT` with `randomblob` or UUID | SQLite has no native UUID type | +| Timestamps | `TEXT` with `datetime('now')` | ISO 8601 format, stored as text | +| Boolean | `INTEGER` (0 or 1) | SQLite has no native boolean | +| JSON data | `TEXT` | Store serialized JSON, parse in Worker code | +| Enums | `TEXT` with `CHECK` constraint | Enforced at the database level | +| Money/decimals | `INTEGER` (store as cents) | Avoid floating-point precision issues | +| Auto-increment | `INTEGER PRIMARY KEY` | SQLite's built-in rowid alias | + +```sql +CREATE TABLE orders ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + total_cents INTEGER NOT NULL, -- money as cents + metadata TEXT, -- JSON blob + is_paid INTEGER NOT NULL DEFAULT 0, -- boolean + status TEXT NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'processing', 'completed', 'cancelled')), + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +``` + +## Pagination Patterns + +### Offset-Based Pagination + +Simple but less efficient for large offsets: + +```ts +async function listPosts(db: D1Database, page: number, pageSize: number = 20) { + const offset = (page - 1) * pageSize; + + const [data, countRow] = await Promise.all([ + db.prepare( + "SELECT id, title, slug, created_at FROM posts WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?" + ).bind("published", pageSize, offset).all(), + db.prepare( + "SELECT COUNT(*) as total FROM posts WHERE status = ?" + ).bind("published").first<{ total: number }>(), + ]); + + return { + posts: data.results, + total: countRow?.total ?? 0, + page, + pageSize, + totalPages: Math.ceil((countRow?.total ?? 0) / pageSize), + }; +} +``` + +### Cursor-Based Pagination + +More efficient for large datasets — uses the last seen value as a cursor: + +```ts +async function listPostsCursor( + db: D1Database, + cursor?: string, + limit: number = 20 +) { + let query = "SELECT id, title, created_at FROM posts WHERE status = ?"; + const params: any[] = ["published"]; + + if (cursor) { + query += " AND created_at < ?"; + params.push(cursor); + } + + query += " ORDER BY created_at DESC LIMIT ?"; + params.push(limit + 1); // Fetch one extra to detect if there's a next page + + const stmt = db.prepare(query); + const { results } = await stmt.bind(...params).all(); + + const hasMore = results.length > limit; + const items = hasMore ? results.slice(0, limit) : results; + const nextCursor = hasMore ? items[items.length - 1].created_at : null; + + return { posts: items, nextCursor, hasMore }; +} +``` + +## Common Pitfalls + +- **String concatenation in queries** — always use `.bind()` for parameterized queries. +- **Editing applied migrations** — never modify a migration after it has been applied; create a new one. +- **Missing indexes on foreign keys** — SQLite does not auto-index foreign key columns; add explicit indexes. +- **Unbounded queries** — always use `LIMIT` to prevent fetching entire tables. +- **Using `SELECT *`** — select only the columns you need to reduce data transfer. +- **Forgetting `--local` vs. `--remote`** — test migrations locally first before applying to production. +- **Not using `.batch()`** — multiple related writes should use `.batch()` for atomicity and performance. +- **Ignoring error handling** — always wrap D1 operations in `try/catch` and handle constraint violations gracefully. diff --git a/plugins/cloudflare/rules/cloudflare-workers.mdc b/plugins/cloudflare/rules/cloudflare-workers.mdc index 4e8e859..217abe2 100644 --- a/plugins/cloudflare/rules/cloudflare-workers.mdc +++ b/plugins/cloudflare/rules/cloudflare-workers.mdc @@ -3,8 +3,8 @@ description: Best practices for Cloudflare Workers and edge computing globs: - "src/**/*.ts" - "src/**/*.js" - - "worker.*" - - "functions/**/*" + - "worker*.*" + - "**/workers/**" alwaysApply: false --- @@ -269,6 +269,77 @@ export default { // addEventListener("fetch", (event) => { ... }); ``` +## HTMLRewriter — Streaming HTML Transformation + +- Use `HTMLRewriter` to transform HTML responses on the fly without buffering the entire document into memory. +- Common use cases: inject analytics scripts, rewrite links, localize content, add headers/footers, A/B testing. +- Handlers are streaming — they process elements as they are encountered, keeping memory usage constant. + +```ts +// Inject a script tag before +const rewriter = new HTMLRewriter() + .on("head", { + element(element) { + element.append('', { html: true }); + }, + }) + .on("a[href]", { + element(element) { + const href = element.getAttribute("href"); + if (href?.startsWith("http://")) { + element.setAttribute("href", href.replace("http://", "https://")); + } + }, + }) + .on("title", { + text(text) { + // Modify text content within elements + if (text.text) { + text.replace(text.text.toUpperCase()); + } + }, + }); + +// Apply the rewriter to an upstream response +const response = await fetch(request); +return rewriter.transform(response); +``` + +- **Do not** use HTMLRewriter on non-HTML responses — check `Content-Type` first. +- Chain multiple handlers for different selectors on the same `HTMLRewriter` instance. +- Use `element.onEndTag()` for inserting content at the closing tag. + +## Cache API — Edge Caching + +- Use the **Cache API** (`caches.default`) to cache responses at the Cloudflare edge. +- Cache API is separate from browser Cache-Control headers — it stores responses in Cloudflare's CDN. +- Only `GET` requests are cacheable by default. Use a custom cache key for dynamic content. + +```ts +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { + const cache = caches.default; + const cacheKey = new Request(request.url, request); + + // Check cache first + let response = await cache.match(cacheKey); + if (response) { + return response; + } + + // Fetch from origin + response = await fetch(request); + response = new Response(response.body, response); + response.headers.set("Cache-Control", "s-maxage=3600"); + + // Store in cache (non-blocking) + ctx.waitUntil(cache.put(cacheKey, response.clone())); + + return response; + }, +}; +``` + ## Performance Tips - Return responses as early as possible — avoid unnecessary `await` chains. @@ -276,3 +347,5 @@ export default { - Use `HTMLRewriter` for efficient streaming HTML transformations without buffering the entire document. - Prefer `Response.json()` over manually setting `Content-Type` and `JSON.stringify()`. - Minimize global scope code — it runs on every cold start across all isolates. +- Parallelize independent I/O with `Promise.all()` instead of sequential `await`. +- Use streaming responses (`ReadableStream`) for large payloads instead of buffering. -- 2.51.2