diff options
author | Federico Igne <git@federicoigne.com> | 2022-08-22 22:16:26 +0100 |
---|---|---|
committer | Federico Igne <git@federicoigne.com> | 2022-08-22 22:16:26 +0100 |
commit | 9b5d490d3be3533a236323a0f74415e322f94550 (patch) | |
tree | d2a58e39791b5486af4646b1c2e4c9fe07ab5707 | |
parent | 9b7077a7696d0ac9e649abeff8e6f469807402aa (diff) | |
download | joyce-9b5d490d3be3533a236323a0f74415e322f94550.tar.gz joyce-9b5d490d3be3533a236323a0f74415e322f94550.zip |
feat(GET /tag/{tags}): reenable querying by tag(s)
This reintroduces the feature that allows to query the database by tag(s);
this time the feature is implemented using the `LIKE` operator of SQL.
-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 |