order
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user