+++ title = "Implement D1Repository for Cloudflare Workers" priority = 7 status = "done" ticket_type = "feature" dependencies = [] +++ ## Goal Implement all 7 stub methods in `src/bin/api/db/d1.rs` so the API works against Cloudflare D1 in production. ## Changes Required ### `src/bin/api/db/d1.rs` **1. Add unsafe Send/Sync** (wasm32 is single-threaded; safe in practice): ```rust // SAFETY: wasm32-unknown-unknown is single-threaded. unsafe impl Send for D1Repository {} unsafe impl Sync for D1Repository {} ``` These are required so `D1Repository` satisfies the `Arc` bound used in the Axum state. **2. Helper row structs** (serde::Deserialize, field names = SQL column names): - `QuoteRow { id, text, author, source: Option, date: Option, created_at, updated_at }` - `AuthRow { auth_code: String }` - `TagRow { tag: String }` - `CountRow { count: u32 }` **3. Helper method** `fetch_tags(&self, id: &str) -> Result, DbError>`: `SELECT tag FROM quote_tags WHERE quote_id = ?1 ORDER BY tag`, bind with `JsValue::from_str(id)`, deserialise as `Vec`. **4. Implement each QuoteRepository method:** - **run_migrations**: Call `self.db.exec()` for each migration constant from `migrations::` in sequence (CREATE_QUOTES, CREATE_QUOTE_TAGS, CREATE_TAG_INDEX, CREATE_AUTHOR_INDEX). - **list_quotes(page, author, tag)**: Dynamic SQL with positional params (mirror native.rs logic). Run COUNT query for total_count, then data query with LIMIT 10 / OFFSET. Fetch tags per quote via helper. Page size = 10. - **get_quote(id)**: `SELECT ... FROM quotes WHERE id = ?1`. Use `.first::(None)`. Fetch tags. Return `Ok(None)` if missing. - **get_random_quote**: `SELECT ... FROM quotes ORDER BY RANDOM() LIMIT 1`. Use `.first::(None)`. - **create_quote(input)**: 1. `generate_id()`, `auth_code = input.auth_code.unwrap_or_else(generate_auth_code)` 2. INSERT quotes row (bind NULL for optional source/date with `JsValue::NULL`) 3. Batch INSERT tags via `db.batch()` 4. SELECT back the row to get timestamps 5. Return `(quote, auth_code)` - **update_quote(id, input, auth_code)**: 1. SELECT auth_code — return Forbidden on mismatch 2. Build dynamic SET clause for non-None fields + `updated_at = datetime('now')` 3. Execute UPDATE 4. If tags provided: DELETE existing, batch INSERT new 5. SELECT updated row; return it - **delete_quote(id, auth_code)**: 1. SELECT auth_code — return NotFound if absent, Forbidden on mismatch 2. DELETE FROM quotes (tags cascade) 3. Return `DeleteResult::Deleted` **JsValue bindings**: `JsValue::from_str(s)` for strings, `JsValue::from_f64(n as f64)` for integers, `JsValue::NULL` for SQL NULL. ## Validation ```sh cargo build --release --bin api --target wasm32-unknown-unknown cargo fmt && cargo check && cargo clippy && cargo test ```