You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vibed/quotesdb/.beans/quotesdb-k5rx--implement-d1...

2.9 KiB

title status type priority created_at updated_at
Implement D1Repository for Cloudflare Workers completed feature high 2026-03-10T23:32:10Z 2026-03-10T23:32:10Z

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):

// 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<dyn QuoteRepository + Send + Sync> bound used in the Axum state.

2. Helper row structs (serde::Deserialize, field names = SQL column names):

  • QuoteRow { id, text, author, source: Option<String>, date: Option<String>, created_at, updated_at }
  • AuthRow { auth_code: String }
  • TagRow { tag: String }
  • CountRow { count: u32 }

3. Helper method fetch_tags(&self, id: &str) -> Result<Vec<String>, DbError>: SELECT tag FROM quote_tags WHERE quote_id = ?1 ORDER BY tag, bind with JsValue::from_str(id), deserialise as Vec<TagRow>.

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::<QuoteRow>(None). Fetch tags. Return Ok(None) if missing.

  • get_random_quote: SELECT ... FROM quotes ORDER BY RANDOM() LIMIT 1. Use .first::<QuoteRow>(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

cargo build --release --bin api --target wasm32-unknown-unknown
cargo fmt && cargo check && cargo clippy && cargo test