feat(backend::api) add post tag filter

This commit is contained in:
lumither committed 2025-10-09 23:26:08 -04:00
1 parent f3cc387013
commit b1a55c363f
3 files changed
+94 -13

No files matched your search

+1
View File
@@ -8,6 +8,7 @@ macros = { path = "../macros" }
markdown = { path = "../markdown" }
axum = { version = "0.8.1", features = ["macros"] }
axum-extra = { version = "0.10.3", features = ["query"] }
tracing = { version = "0.1.41", features = ["log"] }
git2 = "0.20.0"
chrono = { version = "0.4.39", features = ["serde"] }
@@ -4,17 +4,65 @@ use crate::{
utils::axum_response::{err_resp, succ_resp},
};
use axum::{
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
};
use std::collections::HashSet;
use axum::{extract::State, http::StatusCode, response::IntoResponse};
use axum_extra::extract::Query;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::{FromRow, PgPool, query_as};
use sqlx::{query_as, FromRow, PgPool};
use uuid::Uuid;
fn tags_deserializer<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let deserialize_each = |input: String| -> Result<Vec<String>, D::Error> {
Ok(input.split(',').map(|s| s.to_string()).collect())
};
#[derive(Deserialize)]
#[serde(untagged)]
enum Cardinality {
One(String),
Many(Vec<String>),
}
let cardinality: Option<Cardinality> = Deserialize::deserialize(deserializer)?;
let mut tags = HashSet::new();
match cardinality {
Some(Cardinality::One(s)) => {
for item in deserialize_each(s)? {
let item = item.trim().to_string();
if !item.is_empty() {
tags.insert(item);
}
}
let ret = tags.into_iter().collect::<Vec<_>>();
Ok(if ret.is_empty() { None } else { Some(ret) })
}
Some(Cardinality::Many(v)) => {
for items in v
.into_iter()
.map(deserialize_each)
.collect::<Result<Vec<_>, _>>()?
{
for item in items {
let item = item.trim().to_string();
if !item.is_empty() {
tags.insert(item);
}
}
}
let ret = tags.into_iter().collect::<Vec<_>>();
Ok(if ret.is_empty() { None } else { Some(ret) })
}
None => Ok(None),
}
}
#[derive(FromRow, Serialize)]
struct PostBriefModel {
id: Uuid,
@@ -29,9 +77,15 @@ struct PostBriefModel {
}
#[derive(Deserialize, Serialize)]
pub struct Pagination {
pub struct SearchParams {
page: Option<u32>,
page_size: Option<u32>,
#[serde(default, deserialize_with = "tags_deserializer")]
tags: Option<Vec<String>>,
#[serde(default)]
match_all: bool,
}
#[derive(Serialize)]
@@ -53,13 +107,18 @@ struct UuidRow {
}
const GET_POST_WITH_BRIEF_MODEL: &str = include_str!("get_post_with_brief_model.sql");
const SELECT_POST_WITH_TAG_FILTER_PAGINATED: &str =
include_str!("select_post_with_tag_filter_paginated.sql");
pub async fn handler(
State(db): State<PgPool>,
Query(pagination): Query<Pagination>,
Query(search_params): Query<SearchParams>,
) -> impl IntoResponse {
let page = pagination.page.unwrap_or(1);
let page_size = pagination.page_size.unwrap_or(PAGE_SIZE);
dbg!(&search_params.tags);
dbg!(&search_params.match_all);
let page = search_params.page.unwrap_or(1);
let page_size = search_params.page_size.unwrap_or(PAGE_SIZE);
let page_count = match sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM posts")
.fetch_one(&db)
.await
@@ -84,11 +143,11 @@ pub async fn handler(
let offset = page_size * (page - 1);
let ids = match query_as::<_, UuidRow>(
"SELECT id FROM posts ORDER BY date_created DESC LIMIT $1 OFFSET $2",
)
let ids = match query_as::<_, UuidRow>(SELECT_POST_WITH_TAG_FILTER_PAGINATED)
.bind(search_params.tags)
.bind(page_size as i32)
.bind(offset as i32)
.bind(search_params.match_all)
.fetch_all(&db)
.await
{
@@ -0,0 +1,21 @@
SELECT p.id
FROM posts p
WHERE $1::text[] IS NULL
OR (
CASE
WHEN $4 THEN (SELECT COUNT(DISTINCT t.name)
FROM post_tag pt
JOIN tags t ON pt.tag = t.id
WHERE pt.post = p.id
AND t.name = ANY ($1::text[]))
= cardinality($1)
ELSE
EXISTS (SELECT 1
FROM post_tag pt
JOIN tags t ON pt.tag = t.id
WHERE pt.post = p.id
AND t.name = ANY ($1::text[]))
END
)
ORDER BY p.date_created DESC
LIMIT $2 OFFSET $3;