diff options
| -rw-r--r-- | README.md | 69 |
1 files changed, 58 insertions, 11 deletions
| @@ -183,21 +183,26 @@ pub async fn post_notes(pool: web::Data<Pool>, req: web::Json<Vec<NoteRequest>>) | |||
| 183 | 183 | ||
| 184 | ## GET /tag/{tags} | 184 | ## GET /tag/{tags} |
| 185 | 185 | ||
| 186 | **Deprecated:** this is currently not implemented for the new SQLite backend. | 186 | Notes can be retrieved by (sets of) tags. |
| 187 | One or more tags separated by `+` can be passed to the request and `joyce` will retrieve those notes marked with **all** the provided tags. | ||
| 187 | 188 | ||
| 188 | This handler allows to query the set of notes for specific tags. | 189 | The function takes 2 parameters (the connection pool to the underlying SQLite database and the sequence of tags) and returns a collection of notes as a JSON array. |
| 189 | One or more tags separated by `+` can be passed to the request. | 190 | |
| 191 | Passing an empty collection of tags results in an error. | ||
| 190 | 192 | ||
| 191 | ```{#req_get_tags .rust} | 193 | ```{#req_get_tags .rust} |
| 192 | #[get("/tag/{tags}")] | 194 | #[get("/tag/{tags}")] |
| 193 | pub async fn get_tags(tags: web::Path<String>) -> HttpResponse { | 195 | pub async fn get_tags(pool: web::Data<Pool>, tags: web::Path<String>) -> HttpResponse { |
| 196 | <<get_connection>> | ||
| 194 | let tags = tags.split('+').map(|t| t.to_string()).collect::<Vec<_>>(); | 197 | let tags = tags.split('+').map(|t| t.to_string()).collect::<Vec<_>>(); |
| 195 | 198 | if !tags.is_empty() { | |
| 196 | todo(); | 199 | let notes: Vec<Note> = db::get_tags(&mut conn, &tags); |
| 197 | 200 | HttpResponse::Ok() | |
| 198 | HttpResponse::Ok() | 201 | .content_type(ContentType::json()) |
| 199 | .content_type(ContentType::json()) | 202 | .json(notes) |
| 200 | .json(tagged) | 203 | } else { |
| 204 | todo!() | ||
| 205 | } | ||
| 201 | } | 206 | } |
| 202 | ``` | 207 | ``` |
| 203 | 208 | ||
| @@ -310,6 +315,45 @@ pub fn post_notes(conn: &mut Connection, reqs: Vec<NoteRequest>) -> usize { | |||
| 310 | 315 | ||
| 311 | ``` | 316 | ``` |
| 312 | 317 | ||
| 318 | ### Retrieving tagged notes | ||
| 319 | |||
| 320 | We can query for specific tags by using the `LIKE` SQL operator. | ||
| 321 | Such an SQL query will look like this | ||
| 322 | |||
| 323 | SELECT * FROM notes | ||
| 324 | WHERE tags LIKE '%<tag>%' | ||
| 325 | [ AND tags LIKE '%<tag>%' ...] | ||
| 326 | |||
| 327 | where the escaped expression `'%<tag>%'` matches anything containing the word `<tag>`, i.e., the `%` operator matches a (possibly empty) sequence of characters. | ||
| 328 | |||
| 329 | A query needs to be dynamically generated for the specific number of provided tags. | ||
| 330 | |||
| 331 | ```{#build_query_tags .rust} | ||
| 332 | let mut query = String::with_capacity(32 + 16 * tags.len()); | ||
| 333 | query.push_str("SELECT * FROM notes WHERE tags LIKE ? "); | ||
| 334 | for _ in 0..(tags.len()-1) { | ||
| 335 | query.push_str("AND tags LIKE ? "); | ||
| 336 | } | ||
| 337 | ``` | ||
| 338 | |||
| 339 | Once built, we execute the query with the sequence of tags as parameters. | ||
| 340 | Note that we need to use [`rusqlite::params_from_iter`](https://docs.rs/rusqlite/latest/rusqlite/fn.params_from_iter.html) in order to handle a dynamic set of parameters. | ||
| 341 | |||
| 342 | ```{#db_operations .rust} | ||
| 343 | pub fn get_tags(conn: &mut Connection, tags: &[String]) -> Vec<Note> { | ||
| 344 | <<build_query_tags>> | ||
| 345 | let mut query = conn.prepare_cached(&query) | ||
| 346 | .expect("Failed to prepare SELECT query"); | ||
| 347 | let params = tags.iter().map(|tag| format!("%\"{}\"%", tag)); | ||
| 348 | query | ||
| 349 | .query_map(rusqlite::params_from_iter(params), |row| Ok(Note::from(row))) | ||
| 350 | .and_then(Iterator::collect) | ||
| 351 | .expect("Failed to collect query results") | ||
| 352 | } | ||
| 353 | |||
| 354 | ``` | ||
| 355 | |||
| 356 | |||
| 313 | # Main service | 357 | # Main service |
| 314 | 358 | ||
| 315 | The main service will instantiate a new `App` running within a `HttpServer` bound to *localhost* on port 8080. | 359 | The main service will instantiate a new `App` running within a `HttpServer` bound to *localhost* on port 8080. |
| @@ -328,6 +372,7 @@ async fn main() -> std::io::Result<()> { | |||
| 328 | App::new() | 372 | App::new() |
| 329 | .app_data(web::Data::new(db_pool.clone())) | 373 | .app_data(web::Data::new(db_pool.clone())) |
| 330 | .service(note::get_notes) | 374 | .service(note::get_notes) |
| 375 | .service(note::get_tags) | ||
| 331 | .service(note::post_notes) | 376 | .service(note::post_notes) |
| 332 | }) | 377 | }) |
| 333 | .bind(("127.0.0.1", 8080))? | 378 | .bind(("127.0.0.1", 8080))? |
| @@ -364,6 +409,8 @@ Notes, along with their REST API are defined in their own `note` module. | |||
| 364 | 409 | ||
| 365 | <<req_get_notes>> | 410 | <<req_get_notes>> |
| 366 | 411 | ||
| 412 | <<req_get_tags>> | ||
| 413 | |||
| 367 | <<req_post_notes>> | 414 | <<req_post_notes>> |
| 368 | ``` | 415 | ``` |
| 369 | 416 | ||
| @@ -381,7 +428,7 @@ Communication with SQLite is grouped under the `db` module. | |||
| 381 | 428 | ||
| 382 | # TODOs | 429 | # TODOs |
| 383 | 430 | ||
| 384 | - Better error handling with `anyhow` | 431 | - Better error handling |
| 385 | - CLI with `clap` | 432 | - CLI with `clap` |
| 386 | 433 | ||
| 387 | ## Open questions | 434 | ## Open questions |
