aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFederico Igne <git@federicoigne.com>2022-08-22 22:16:26 +0100
committerFederico Igne <git@federicoigne.com>2022-08-22 22:16:26 +0100
commit9b5d490d3be3533a236323a0f74415e322f94550 (patch)
treed2a58e39791b5486af4646b1c2e4c9fe07ab5707
parent9b7077a7696d0ac9e649abeff8e6f469807402aa (diff)
downloadjoyce-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.md69
1 files changed, 58 insertions, 11 deletions
diff --git a/README.md b/README.md
index 4366979..c2f62bc 100644
--- a/README.md
+++ b/README.md
@@ -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. 186Notes can be retrieved by (sets of) tags.
187One 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
188This handler allows to query the set of notes for specific tags. 189The 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.
189One or more tags separated by `+` can be passed to the request. 190
191Passing 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}")]
193pub async fn get_tags(tags: web::Path<String>) -> HttpResponse { 195pub 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
320We can query for specific tags by using the `LIKE` SQL operator.
321Such an SQL query will look like this
322
323 SELECT * FROM notes
324 WHERE tags LIKE '%<tag>%'
325 [ AND tags LIKE '%<tag>%' ...]
326
327where the escaped expression `'%<tag>%'` matches anything containing the word `<tag>`, i.e., the `%` operator matches a (possibly empty) sequence of characters.
328
329A query needs to be dynamically generated for the specific number of provided tags.
330
331```{#build_query_tags .rust}
332let mut query = String::with_capacity(32 + 16 * tags.len());
333query.push_str("SELECT * FROM notes WHERE tags LIKE ? ");
334for _ in 0..(tags.len()-1) {
335 query.push_str("AND tags LIKE ? ");
336}
337```
338
339Once built, we execute the query with the sequence of tags as parameters.
340Note 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}
343pub 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
315The main service will instantiate a new `App` running within a `HttpServer` bound to *localhost* on port 8080. 359The 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