|
|
+++
|
|
|
title = "Filter quotes by date range (before/after with year/month/day granularity)"
|
|
|
priority = 5
|
|
|
status = "done"
|
|
|
ticket_type = "feature"
|
|
|
dependencies = []
|
|
|
+++
|
|
|
## Feature
|
|
|
|
|
|
Extend `GET /api/quotes` with date range filtering. Users can specify a "before" and/or "after" bound, with year/month/day granularity for each. Quotes without a date (`date IS NULL`) are excluded when a date filter is active.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Query parameter design
|
|
|
|
|
|
Six optional query params are added:
|
|
|
|
|
|
| Param | Type | Description |
|
|
|
|---|---|---|
|
|
|
| `date_after_year` | u16 | Only include quotes dated on or after this year |
|
|
|
| `date_after_month` | u8 (1–12) | Narrows after-bound to this month |
|
|
|
| `date_after_day` | u8 (1–31) | Narrows after-bound to this day |
|
|
|
| `date_before_year` | u16 | Only include quotes dated on or before this year |
|
|
|
| `date_before_month` | u8 (1–12) | Narrows before-bound to this month |
|
|
|
| `date_before_day` | u8 (1–31) | Narrows before-bound to this day |
|
|
|
|
|
|
The handler constructs two partial ISO strings from these fields before calling `list_quotes`:
|
|
|
|
|
|
- **after bound** (`>=`): `YYYY`, `YYYY-MM`, or `YYYY-MM-DD` — string comparison anchors to the start of the specified period (e.g., `2020` means ≥ `2020-01-01` in text comparison, but actually `>= '2020'` which works correctly since any 2020 date starts with `2020`).
|
|
|
- **before bound** (`<=`): `YYYY-MM-DD` where missing month defaults to `12` and missing day defaults to `31` — so `date_before_year=2020` means `<= '2020-12-31'`.
|
|
|
|
|
|
**Validation rules:**
|
|
|
- Month must be 1–12 if provided (without its year, it is an error).
|
|
|
- Day must be 1–31 if provided (without its year+month, it is an error).
|
|
|
- Return `400 Bad Request` for invalid combinations.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Part 1 — API: `handlers/mod.rs`
|
|
|
|
|
|
**Extend `ListParams`:**
|
|
|
```rust
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
struct ListParams {
|
|
|
#[serde(default = "default_page")]
|
|
|
page: u32,
|
|
|
author: Option<String>,
|
|
|
tag: Option<String>,
|
|
|
// Date range filter
|
|
|
date_after_year: Option<u16>,
|
|
|
date_after_month: Option<u8>,
|
|
|
date_after_day: Option<u8>,
|
|
|
date_before_year: Option<u16>,
|
|
|
date_before_month: Option<u8>,
|
|
|
date_before_day: Option<u8>,
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**In `list_handler`**, construct the `date_after` and `date_before` strings before calling `repo.list_quotes`:
|
|
|
```rust
|
|
|
fn build_date_bound(year: Option<u16>, month: Option<u8>, day: Option<u8>) -> Option<String> {
|
|
|
match (year, month, day) {
|
|
|
(None, _, _) => None,
|
|
|
(Some(y), None, _) => Some(format!("{y:04}")),
|
|
|
(Some(y), Some(m), None) => Some(format!("{y:04}-{m:02}")),
|
|
|
(Some(y), Some(m), Some(d)) => Some(format!("{y:04}-{m:02}-{d:02}")),
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Return `400` if month is present without year, or day is present without year+month.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Part 2 — Repository trait: `db/mod.rs`
|
|
|
|
|
|
Extend `list_quotes` signature:
|
|
|
```rust
|
|
|
async fn list_quotes(
|
|
|
&self,
|
|
|
page: u32,
|
|
|
author: Option<&str>,
|
|
|
tag: Option<&str>,
|
|
|
date_after: Option<&str>, // new
|
|
|
date_before: Option<&str>, // new
|
|
|
) -> Result<ListResult, DbError>;
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
## Part 3 — Native implementation: `db/native.rs`
|
|
|
|
|
|
In `NativeRepository::list_quotes`, build the WHERE clause dynamically. Current query filters on `author` and `tag` via subquery. Extend it:
|
|
|
|
|
|
```sql
|
|
|
-- Additional clauses appended when filters are present:
|
|
|
AND q.date IS NOT NULL
|
|
|
AND q.date >= ? -- when date_after is Some
|
|
|
AND q.date <= ? -- when date_before is Some
|
|
|
```
|
|
|
|
|
|
Use string comparison — ISO YYYY-MM-DD format sorts lexicographically, so `>=`/`<=` on the `date` TEXT column is correct. Prefix matching (e.g. `date >= '2020'` with `date = '2020-06-15'`) works because `'2020-06-15' >= '2020'` is true in SQLite string comparison.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Part 4 — D1 implementation: `db/d1.rs`
|
|
|
|
|
|
Apply the same WHERE clause extensions to the D1 query builder.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Part 5 — UI: Browse page
|
|
|
|
|
|
File: `src/bin/ui/pages/browse.rs`
|
|
|
|
|
|
Add date filter controls to the filter panel alongside author and tag filters:
|
|
|
|
|
|
- "After:" year input (`<input type="number" min="0" max="9999">`), optional month select (1–12), optional day input.
|
|
|
- "Before:" same.
|
|
|
|
|
|
On filter apply, include the populated fields as query params. When fields are empty, omit them.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Part 6 — Mock repo in tests: `handlers/mod.rs`
|
|
|
|
|
|
Update `MockRepo::list_quotes` to accept the two new `date_after` / `date_before` params (ignore them in the mock — just extend the signature).
|
|
|
|
|
|
---
|
|
|
|
|
|
## Part 7 — OpenAPI spec: `api/openapi.yaml`
|
|
|
|
|
|
Add the six new query parameters to the `GET /api/quotes` operation with descriptions and `schema: {type: integer}`.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Files touched
|
|
|
|
|
|
- `src/bin/api/handlers/mod.rs` — `ListParams` + `list_handler` + `MockRepo`
|
|
|
- `src/bin/api/db/mod.rs` — `QuoteRepository::list_quotes` signature
|
|
|
- `src/bin/api/db/native.rs` — SQL query extension
|
|
|
- `src/bin/api/db/d1.rs` — SQL query extension
|
|
|
- `src/bin/ui/pages/browse.rs` — date filter UI
|
|
|
- `api/openapi.yaml` — new query params
|
|
|
|
|
|
## Validation
|
|
|
|
|
|
```sh
|
|
|
# From quotesdb/ root
|
|
|
cargo fmt && cargo check && cargo clippy && cargo test
|
|
|
trunk build
|
|
|
redocly lint api/openapi.yaml
|
|
|
```
|
|
|
|
|
|
Test manually:
|
|
|
- `GET /api/quotes?date_after_year=1900&date_before_year=2000` — should return only quotes with dates in the 20th century.
|
|
|
- `GET /api/quotes?date_after_year=2020&date_after_month=6` — on or after June 2020.
|
|
|
- `GET /api/quotes?date_after_month=3` — should return 400 (month without year).
|
|
|
|
|
|
## Commit scope
|
|
|
|
|
|
`feat(quotesdb): date range filter for quotes list` |