This commit is contained in:
2025-11-05 23:01:42 +01:00
parent c193b30c8e
commit 911051e75d
11 changed files with 275 additions and 68 deletions
+2 -1
View File
@@ -37,7 +37,8 @@ pub fn create_api_token(
expires_at: Option<NaiveDateTime>,
capability_set_id: Uuid,
) -> Result<IssuedApiToken, AppError> {
let capability_set = validate_capability_set_belongs_to_tenant(conn, capability_set_id, tenant_id)?;
let capability_set =
validate_capability_set_belongs_to_tenant(conn, capability_set_id, tenant_id)?;
let raw_secret = generate_secret()?;
let token_prefix = raw_secret[..TOKEN_PREFIX_LENGTH].to_string();
+3
View File
@@ -2,5 +2,8 @@ pub mod asset;
pub mod correspondents;
pub mod folders;
pub mod metadata;
pub mod ordering;
pub mod search;
pub mod tags;
pub use ordering::{DocumentSortField, SortDirection};
+58
View File
@@ -0,0 +1,58 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const UNICODE_COLLATION_NAME: &str = "unicode_ci";
pub const UNICODE_COLLATION_LOCALE: &str = "und-u-ks-level2";
const TITLE_ASC: &str = "title COLLATE \"unicode_ci\" ASC";
const TITLE_DESC: &str = "title COLLATE \"unicode_ci\" DESC";
const ISSUED_AT_ASC: &str = "issued_at ASC NULLS LAST";
const ISSUED_AT_DESC: &str = "issued_at DESC NULLS LAST";
const CREATED_AT_ASC: &str = "created_at ASC";
const CREATED_AT_DESC: &str = "created_at DESC";
const UPDATED_AT_ASC: &str = "updated_at ASC";
const UPDATED_AT_DESC: &str = "updated_at DESC";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum DocumentSortField {
Title,
IssuedAt,
CreatedAt,
UpdatedAt,
}
impl Default for DocumentSortField {
fn default() -> Self {
DocumentSortField::Title
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum SortDirection {
Asc,
Desc,
}
impl Default for SortDirection {
fn default() -> Self {
SortDirection::Asc
}
}
pub fn ordering_clauses(
field: DocumentSortField,
direction: SortDirection,
) -> (&'static str, Option<&'static str>) {
match (field, direction) {
(DocumentSortField::Title, SortDirection::Asc) => (TITLE_ASC, None),
(DocumentSortField::Title, SortDirection::Desc) => (TITLE_DESC, None),
(DocumentSortField::IssuedAt, SortDirection::Asc) => (ISSUED_AT_ASC, Some(TITLE_ASC)),
(DocumentSortField::IssuedAt, SortDirection::Desc) => (ISSUED_AT_DESC, Some(TITLE_ASC)),
(DocumentSortField::CreatedAt, SortDirection::Asc) => (CREATED_AT_ASC, Some(TITLE_ASC)),
(DocumentSortField::CreatedAt, SortDirection::Desc) => (CREATED_AT_DESC, Some(TITLE_ASC)),
(DocumentSortField::UpdatedAt, SortDirection::Asc) => (UPDATED_AT_ASC, Some(TITLE_ASC)),
(DocumentSortField::UpdatedAt, SortDirection::Desc) => (UPDATED_AT_DESC, Some(TITLE_ASC)),
}
}
+29 -19
View File
@@ -1,14 +1,11 @@
use std::{
collections::{HashMap, HashSet},
time::Duration,
};
use std::{collections::HashSet, time::Duration};
use axum::extract::{Json, Multipart, Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use chrono::{DateTime, NaiveDateTime, Utc};
use diesel::dsl::exists;
use diesel::{prelude::*, result::DatabaseErrorKind, select, OptionalExtension};
use diesel::dsl::{exists, sql};
use diesel::{prelude::*, result::DatabaseErrorKind, select, sql_types::Text, OptionalExtension};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
@@ -31,6 +28,7 @@ use crate::documents::{
},
folders::ensure_folder_exists_on_conn,
metadata::merge_document_metadata,
ordering::{ordering_clauses, DocumentSortField, SortDirection},
search::quickwit_search,
tags::{assign_tags as assign_tags_to_document, load_tags_for_documents},
};
@@ -72,6 +70,12 @@ pub struct DocumentListQuery {
#[serde(default = "default_document_status_filter")]
#[schema(default = "active")]
pub status: DocumentStatusFilter,
#[serde(default)]
#[schema(default = "title")]
pub sort: DocumentSortField,
#[serde(default)]
#[schema(default = "asc")]
pub dir: SortDirection,
}
fn default_document_status_filter() -> DocumentStatusFilter {
@@ -376,7 +380,11 @@ pub async fn list_documents(
tags,
correspondents,
status,
sort,
dir,
} = params;
let sort_field = sort;
let sort_direction = dir;
let mut docs_query = documents::table
.filter(documents::tenant_id.eq(tenant_id))
@@ -554,6 +562,12 @@ pub async fn list_documents(
docs_query = docs_query.filter(documents::id.eq_any(ids_vec));
}
let (primary_sql, secondary_sql) = ordering_clauses(sort_field, sort_direction);
docs_query = docs_query.order(sql::<Text>(primary_sql));
if let Some(second) = secondary_sql {
docs_query = docs_query.then_order_by(sql::<Text>(second));
}
let docs: Vec<Document> = if let Some(order_ids) = quickwit_order.as_ref() {
let relevant_ids: Vec<Uuid> = if let Some(filter_set) = filter_ids.as_ref() {
order_ids
@@ -569,28 +583,24 @@ pub async fn list_documents(
return Ok(Json(vec![]));
}
let fetched: Vec<Document> = docs_query.load(&mut conn)?;
let mut by_id: HashMap<Uuid, Document> =
fetched.into_iter().map(|doc| (doc.id, doc)).collect();
let mut fetched: Vec<Document> = docs_query.load(&mut conn)?;
let mut ordered = Vec::with_capacity(fetched.len());
let mut ordered = Vec::with_capacity(by_id.len());
for id in relevant_ids {
if let Some(doc) = by_id.remove(&id) {
ordered.push(doc);
if let Some(pos) = fetched.iter().position(|doc| doc.id == id) {
ordered.push(fetched.remove(pos));
}
}
if !by_id.is_empty() {
let mut remaining: Vec<Document> = by_id.into_values().collect();
remaining.sort_by(|a, b| b.created_at.cmp(&a.created_at));
ordered.extend(remaining);
// Any documents not referenced by the search results remain in the
// order supplied by the database (title ASC or the chosen sorting).
if !fetched.is_empty() {
ordered.extend(fetched);
}
ordered
} else {
docs_query
.order(documents::created_at.desc())
.load(&mut conn)?
docs_query.load(&mut conn)?
};
let response = hydrate_documents(&state, &mut conn, tenant_id, user_id, docs)?;
+26 -5
View File
@@ -16,7 +16,10 @@ use crate::{
};
use super::documents::{hydrate_documents, DocumentResponse};
use crate::documents::ordering::{ordering_clauses, DocumentSortField, SortDirection};
use crate::utils::{json::deserialize_patch_field, time::to_iso};
use diesel::dsl::sql;
use diesel::sql_types::Text;
#[derive(Deserialize, ToSchema)]
pub struct CreateFolderRequest {
@@ -51,6 +54,12 @@ pub struct FolderContentsQuery {
#[serde(default = "default_include_documents")]
#[schema(default = true)]
pub include_documents: bool,
#[serde(default)]
#[schema(default = "title")]
pub sort: DocumentSortField,
#[serde(default)]
#[schema(default = "asc")]
pub dir: SortDirection,
}
const fn default_include_documents() -> bool {
@@ -335,6 +344,12 @@ pub async fn list_folder_contents(
..
}: TenantScopedConn,
) -> AppResult<Json<FolderContentsResponse>> {
let FolderContentsQuery {
include_documents,
sort,
dir,
} = query;
let folder_id = if folder_identifier.eq_ignore_ascii_case("root") {
None
} else {
@@ -358,22 +373,28 @@ pub async fn list_folder_contents(
folders::table
.filter(folders::parent_id.eq(parent_id))
.filter(folders::tenant_id.eq(tenant_id))
.order(folders::name.asc())
.order(sql::<Text>("name COLLATE \"unicode_ci\" ASC"))
.load(&mut conn)?
} else {
folders::table
.filter(folders::parent_id.is_null())
.filter(folders::tenant_id.eq(tenant_id))
.order(folders::name.asc())
.order(sql::<Text>("name COLLATE \"unicode_ci\" ASC"))
.load(&mut conn)?
};
let subfolders = child_folders.into_iter().map(folder_to_info).collect();
let documents = if query.include_documents {
let docs_query = documents::table
let documents = if include_documents {
let mut docs_query = documents::table
.filter(documents::deleted_at.is_null())
.filter(documents::tenant_id.eq(tenant_id))
.order(documents::created_at.desc());
.into_boxed();
let (primary_sql, secondary_sql) = ordering_clauses(sort, dir);
docs_query = docs_query.order(sql::<Text>(primary_sql));
if let Some(second) = secondary_sql {
docs_query = docs_query.then_order_by(sql::<Text>(second));
}
let docs: Vec<Document> = if let Some(current_folder) = folder_id {
docs_query
+2 -1
View File
@@ -121,7 +121,8 @@ pub async fn create_api_token(
None => None,
};
let capability_set_id = validate_capability_set(&mut conn, tenant_id, payload.capability_set_id)?;
let capability_set_id =
validate_capability_set(&mut conn, tenant_id, payload.capability_set_id)?;
let issued = issue_token(
&mut conn,