mirror of
https://github.com/Lumither/nocturne.git
synced 2026-10-17 10:26:21 +00:00
feat(backend::api) add post tag filter
This commit is contained in:
3 files changed
+94
-13
No files matched your search
@@ -8,6 +8,7 @@ macros = { path = "../macros" }
|
|||||||
markdown = { path = "../markdown" }
|
markdown = { path = "../markdown" }
|
||||||
|
|
||||||
axum = { version = "0.8.1", features = ["macros"] }
|
axum = { version = "0.8.1", features = ["macros"] }
|
||||||
|
axum-extra = { version = "0.10.3", features = ["query"] }
|
||||||
tracing = { version = "0.1.41", features = ["log"] }
|
tracing = { version = "0.1.41", features = ["log"] }
|
||||||
git2 = "0.20.0"
|
git2 = "0.20.0"
|
||||||
chrono = { version = "0.4.39", features = ["serde"] }
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
|
|||||||
@@ -4,17 +4,65 @@ use crate::{
|
|||||||
utils::axum_response::{err_resp, succ_resp},
|
utils::axum_response::{err_resp, succ_resp},
|
||||||
};
|
};
|
||||||
|
|
||||||
use axum::{
|
use std::collections::HashSet;
|
||||||
extract::{Query, State},
|
|
||||||
http::StatusCode,
|
use axum::{extract::State, http::StatusCode, response::IntoResponse};
|
||||||
response::IntoResponse,
|
use axum_extra::extract::Query;
|
||||||
};
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sqlx::{FromRow, PgPool, query_as};
|
use sqlx::{query_as, FromRow, PgPool};
|
||||||
use uuid::Uuid;
|
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)]
|
#[derive(FromRow, Serialize)]
|
||||||
struct PostBriefModel {
|
struct PostBriefModel {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
@@ -29,9 +77,15 @@ struct PostBriefModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Pagination {
|
pub struct SearchParams {
|
||||||
page: Option<u32>,
|
page: Option<u32>,
|
||||||
page_size: Option<u32>,
|
page_size: Option<u32>,
|
||||||
|
|
||||||
|
#[serde(default, deserialize_with = "tags_deserializer")]
|
||||||
|
tags: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
match_all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -53,13 +107,18 @@ struct UuidRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GET_POST_WITH_BRIEF_MODEL: &str = include_str!("get_post_with_brief_model.sql");
|
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(
|
pub async fn handler(
|
||||||
State(db): State<PgPool>,
|
State(db): State<PgPool>,
|
||||||
Query(pagination): Query<Pagination>,
|
Query(search_params): Query<SearchParams>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let page = pagination.page.unwrap_or(1);
|
dbg!(&search_params.tags);
|
||||||
let page_size = pagination.page_size.unwrap_or(PAGE_SIZE);
|
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")
|
let page_count = match sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM posts")
|
||||||
.fetch_one(&db)
|
.fetch_one(&db)
|
||||||
.await
|
.await
|
||||||
@@ -84,11 +143,11 @@ pub async fn handler(
|
|||||||
|
|
||||||
let offset = page_size * (page - 1);
|
let offset = page_size * (page - 1);
|
||||||
|
|
||||||
let ids = match query_as::<_, UuidRow>(
|
let ids = match query_as::<_, UuidRow>(SELECT_POST_WITH_TAG_FILTER_PAGINATED)
|
||||||
"SELECT id FROM posts ORDER BY date_created DESC LIMIT $1 OFFSET $2",
|
.bind(search_params.tags)
|
||||||
)
|
|
||||||
.bind(page_size as i32)
|
.bind(page_size as i32)
|
||||||
.bind(offset as i32)
|
.bind(offset as i32)
|
||||||
|
.bind(search_params.match_all)
|
||||||
.fetch_all(&db)
|
.fetch_all(&db)
|
||||||
.await
|
.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;
|
||||||
Reference in new issue
Block a user