refactor
This commit is contained in:
@@ -0,0 +1 @@
|
||||
pub mod responders;
|
||||
@@ -0,0 +1,149 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
|
||||
/// Helper trait to convert error-centric results into the application's error type.
|
||||
pub trait IntoAppResult<T> {
|
||||
fn into_app_result(self) -> AppResult<T>;
|
||||
}
|
||||
|
||||
impl<T, E> IntoAppResult<T> for Result<T, E>
|
||||
where
|
||||
AppError: From<E>,
|
||||
{
|
||||
fn into_app_result(self) -> AppResult<T> {
|
||||
self.map_err(AppError::from)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension helpers for optional values to map them into `AppResult`.
|
||||
pub trait OptionAppResultExt<T> {
|
||||
fn or_not_found(self) -> AppResult<T>;
|
||||
fn or_bad_request(self, message: impl Into<String>) -> AppResult<T>;
|
||||
}
|
||||
|
||||
impl<T> OptionAppResultExt<T> for Option<T> {
|
||||
fn or_not_found(self) -> AppResult<T> {
|
||||
self.ok_or_else(AppError::not_found)
|
||||
}
|
||||
|
||||
fn or_bad_request(self, message: impl Into<String>) -> AppResult<T> {
|
||||
self.ok_or_else(|| AppError::bad_request(message))
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides helpers for statements returning number of affected rows.
|
||||
pub trait RowsAffectedExt: Sized {
|
||||
fn or_error(self, error: AppError) -> AppResult<usize>;
|
||||
fn or_not_found(self) -> AppResult<usize> {
|
||||
self.or_error(AppError::not_found())
|
||||
}
|
||||
}
|
||||
|
||||
impl RowsAffectedExt for usize {
|
||||
fn or_error(self, error: AppError) -> AppResult<usize> {
|
||||
if self == 0 {
|
||||
Err(error)
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper providing a consistent JSON response with a status code.
|
||||
pub struct JsonResponse<T> {
|
||||
status: StatusCode,
|
||||
payload: T,
|
||||
}
|
||||
|
||||
impl<T> JsonResponse<T> {
|
||||
pub fn new(status: StatusCode, payload: T) -> Self {
|
||||
Self { status, payload }
|
||||
}
|
||||
|
||||
pub fn ok(payload: T) -> Self {
|
||||
Self::new(StatusCode::OK, payload)
|
||||
}
|
||||
|
||||
pub fn created(payload: T) -> Self {
|
||||
Self::new(StatusCode::CREATED, payload)
|
||||
}
|
||||
|
||||
pub fn accepted(payload: T) -> Self {
|
||||
Self::new(StatusCode::ACCEPTED, payload)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for JsonResponse<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self::ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for JsonResponse<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
(self.status, Json(self.payload)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for returning empty responses with a status code.
|
||||
pub fn empty(status: StatusCode) -> AppResult<StatusCode> {
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
/// Helper for returning `204 No Content`.
|
||||
pub fn no_content() -> AppResult<StatusCode> {
|
||||
empty(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// Helper for returning JSON payloads with `200 OK`.
|
||||
pub fn ok_json<T>(value: T) -> AppResult<JsonResponse<T>>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
Ok(JsonResponse::ok(value))
|
||||
}
|
||||
|
||||
/// Helper for returning JSON payloads with `201 Created`.
|
||||
pub fn created_json<T>(value: T) -> AppResult<JsonResponse<T>>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
Ok(JsonResponse::created(value))
|
||||
}
|
||||
|
||||
/// Helper for returning JSON payloads with `202 Accepted`.
|
||||
pub fn accepted_json<T>(value: T) -> AppResult<JsonResponse<T>>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
Ok(JsonResponse::accepted(value))
|
||||
}
|
||||
|
||||
/// Standard wrapper for paginated responses.
|
||||
#[derive(Serialize)]
|
||||
pub struct PaginatedResponse<T, M>
|
||||
where
|
||||
T: Serialize,
|
||||
M: Serialize,
|
||||
{
|
||||
pub data: T,
|
||||
pub meta: M,
|
||||
}
|
||||
|
||||
pub fn paginated_json<T, M>(data: T, meta: M) -> AppResult<JsonResponse<PaginatedResponse<T, M>>>
|
||||
where
|
||||
T: Serialize,
|
||||
M: Serialize,
|
||||
{
|
||||
let payload = PaginatedResponse { data, meta };
|
||||
Ok(JsonResponse::ok(payload))
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pub mod config;
|
||||
pub mod db;
|
||||
pub mod documents;
|
||||
pub mod error;
|
||||
pub mod http;
|
||||
pub mod jobs;
|
||||
pub mod models;
|
||||
pub mod openapi;
|
||||
|
||||
+22
-19
@@ -28,6 +28,7 @@ use crate::{
|
||||
AuthenticatedUser,
|
||||
},
|
||||
error::{AppError, AppResult},
|
||||
http::responders::{ok_json, JsonResponse},
|
||||
models::{
|
||||
MagicToken, MagicTokenKind, NewUser, NewUserSession, TenantStatus, User, UserMembership,
|
||||
UserSession,
|
||||
@@ -224,7 +225,7 @@ pub async fn login(
|
||||
pub async fn api_token_exchange(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ApiTokenExchangeRequest>,
|
||||
) -> AppResult<Json<LoginResponse>> {
|
||||
) -> AppResult<JsonResponse<LoginResponse>> {
|
||||
let secret = payload.api_token.trim();
|
||||
if secret.is_empty() {
|
||||
return Err(AppError::bad_request("api_token must not be empty"));
|
||||
@@ -289,7 +290,7 @@ pub async fn api_token_exchange(
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
ok_json(response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -306,7 +307,7 @@ pub async fn api_token_exchange(
|
||||
pub async fn signup_start(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SignupStartRequest>,
|
||||
) -> AppResult<Json<SignupStartResponse>> {
|
||||
) -> AppResult<JsonResponse<SignupStartResponse>> {
|
||||
let username = payload.username.trim();
|
||||
if username.is_empty() {
|
||||
return Err(AppError::bad_request("username must not be empty"));
|
||||
@@ -334,10 +335,10 @@ pub async fn signup_start(
|
||||
.generate_signup_token(user_id, challenge.challenge_id, username.to_owned())
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
Ok(Json(SignupStartResponse {
|
||||
ok_json(SignupStartResponse {
|
||||
signup_token,
|
||||
challenge,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -590,7 +591,7 @@ pub async fn me(user: AuthenticatedUser) -> Json<AuthenticatedUser> {
|
||||
pub async fn list_tenants(
|
||||
State(state): State<AppState>,
|
||||
auth: Option<TypedHeader<Authorization<Bearer>>>,
|
||||
) -> AppResult<Json<TenantListResponse>> {
|
||||
) -> AppResult<JsonResponse<TenantListResponse>> {
|
||||
let bearer = auth.ok_or_else(AppError::unauthorized)?;
|
||||
let token = bearer.token();
|
||||
|
||||
@@ -630,7 +631,7 @@ pub async fn list_tenants(
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Json(TenantListResponse { tenants }))
|
||||
ok_json(TenantListResponse { tenants })
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -642,7 +643,7 @@ pub async fn list_tenants(
|
||||
pub async fn passkey_register_start(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> AppResult<Json<RegistrationChallengeResponse>> {
|
||||
) -> AppResult<JsonResponse<RegistrationChallengeResponse>> {
|
||||
let service = state
|
||||
.passkeys
|
||||
.as_ref()
|
||||
@@ -651,7 +652,7 @@ pub async fn passkey_register_start(
|
||||
let mut conn = state.db_unscoped()?;
|
||||
let current_user: User = dsl::users.find(user.user_id).first(&mut conn)?;
|
||||
let challenge = service.start_registration(&mut conn, ¤t_user)?;
|
||||
Ok(Json(challenge))
|
||||
ok_json(challenge)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -665,7 +666,7 @@ pub async fn passkey_register_finish(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
Json(payload): Json<PasskeyRegistrationFinishPayload>,
|
||||
) -> AppResult<Json<PasskeySummary>> {
|
||||
) -> AppResult<JsonResponse<PasskeySummary>> {
|
||||
let service = state
|
||||
.passkeys
|
||||
.as_ref()
|
||||
@@ -688,7 +689,7 @@ pub async fn passkey_register_finish(
|
||||
nickname,
|
||||
)?;
|
||||
|
||||
Ok(Json(PasskeySummary::from(passkey)))
|
||||
ok_json(PasskeySummary::from(passkey))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -701,7 +702,7 @@ pub async fn passkey_register_finish(
|
||||
pub async fn passkey_login_start(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<PasskeyLoginStartPayload>,
|
||||
) -> AppResult<Json<AuthenticationChallengeResponse>> {
|
||||
) -> AppResult<JsonResponse<AuthenticationChallengeResponse>> {
|
||||
let service = state
|
||||
.passkeys
|
||||
.as_ref()
|
||||
@@ -718,7 +719,7 @@ pub async fn passkey_login_start(
|
||||
.first(&mut conn)?;
|
||||
|
||||
let challenge = service.start_authentication(&mut conn, &user)?;
|
||||
Ok(Json(challenge))
|
||||
ok_json(challenge)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -799,11 +800,12 @@ fn complete_login(
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Json(TenantSelectionResponse {
|
||||
let response = ok_json(TenantSelectionResponse {
|
||||
access_token: selection_token,
|
||||
tenants,
|
||||
})
|
||||
.into_response())
|
||||
})?;
|
||||
|
||||
Ok(response.into_response())
|
||||
}
|
||||
|
||||
fn magic_token_login(
|
||||
@@ -925,7 +927,7 @@ fn issue_session(
|
||||
.values(&new_session)
|
||||
.execute(conn)?;
|
||||
|
||||
let mut response = Json(LoginResponse {
|
||||
let json = ok_json(LoginResponse {
|
||||
access_token,
|
||||
token_type: "Bearer".to_string(),
|
||||
expires_in: state.config.jwt_expiry_minutes * 60,
|
||||
@@ -933,8 +935,9 @@ fn issue_session(
|
||||
id: tenant_id,
|
||||
name: tenant_name,
|
||||
},
|
||||
})
|
||||
.into_response();
|
||||
})?;
|
||||
|
||||
let mut response = json.into_response();
|
||||
|
||||
response.headers_mut().insert(
|
||||
SET_COOKIE,
|
||||
|
||||
@@ -11,6 +11,9 @@ use crate::{
|
||||
},
|
||||
auth::TenantScopedConn,
|
||||
error::{AppError, AppResult},
|
||||
http::responders::{
|
||||
created_json, no_content, ok_json, IntoAppResult, JsonResponse, RowsAffectedExt,
|
||||
},
|
||||
models::{ApiCapability, CapabilitySet},
|
||||
schema::{
|
||||
api_tokens,
|
||||
@@ -97,7 +100,7 @@ pub async fn list_capability_sets(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<CapabilitySetResponse>>> {
|
||||
) -> AppResult<JsonResponse<Vec<CapabilitySetResponse>>> {
|
||||
let sets = cs_dsl::capability_sets
|
||||
.filter(cs_dsl::tenant_id.eq(tenant_id))
|
||||
.order(cs_dsl::slug.asc())
|
||||
@@ -109,7 +112,7 @@ pub async fn list_capability_sets(
|
||||
responses.push(to_response(set, capabilities));
|
||||
}
|
||||
|
||||
Ok(Json(responses))
|
||||
ok_json(responses)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -120,12 +123,12 @@ pub async fn list_capability_sets(
|
||||
)]
|
||||
pub async fn list_capabilities(
|
||||
TenantScopedConn { .. }: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<ApiCapability>>> {
|
||||
) -> AppResult<JsonResponse<Vec<ApiCapability>>> {
|
||||
let capabilities = ApiCapability::variants()
|
||||
.iter()
|
||||
.map(|value| value.parse::<ApiCapability>().expect("valid capability"))
|
||||
.collect();
|
||||
Ok(Json(capabilities))
|
||||
ok_json(capabilities)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -142,18 +145,15 @@ pub async fn get_capability_set(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> AppResult<Json<CapabilitySetResponse>> {
|
||||
) -> AppResult<JsonResponse<CapabilitySetResponse>> {
|
||||
let set = cs_dsl::capability_sets
|
||||
.filter(cs_dsl::tenant_id.eq(tenant_id))
|
||||
.find(id)
|
||||
.first::<CapabilitySet>(&mut conn)
|
||||
.map_err(|err| match err {
|
||||
diesel::result::Error::NotFound => AppError::not_found(),
|
||||
other => AppError::from(other),
|
||||
})?;
|
||||
.into_app_result()?;
|
||||
|
||||
let capabilities = load_capabilities_for_set(&mut conn, set.id)?;
|
||||
Ok(Json(to_response(set, capabilities)))
|
||||
ok_json(to_response(set, capabilities))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -170,7 +170,7 @@ pub async fn create_capability_set(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<CreateCapabilitySetRequest>,
|
||||
) -> AppResult<(StatusCode, Json<CapabilitySetResponse>)> {
|
||||
) -> AppResult<JsonResponse<CapabilitySetResponse>> {
|
||||
let original_caps = payload.capabilities;
|
||||
let normalized_caps = normalize_capabilities(original_caps.clone())?;
|
||||
if normalized_caps.is_empty() {
|
||||
@@ -196,7 +196,7 @@ pub async fn create_capability_set(
|
||||
let set = create_capability_set_record(&mut conn, tenant_id, &slug, original_caps)?;
|
||||
let response = to_response(set, normalized_caps);
|
||||
|
||||
Ok((StatusCode::CREATED, Json(response)))
|
||||
created_json(response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -215,15 +215,12 @@ pub async fn update_capability_set(
|
||||
}: TenantScopedConn,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateCapabilitySetRequest>,
|
||||
) -> AppResult<Json<CapabilitySetResponse>> {
|
||||
) -> AppResult<JsonResponse<CapabilitySetResponse>> {
|
||||
let set = cs_dsl::capability_sets
|
||||
.filter(cs_dsl::tenant_id.eq(tenant_id))
|
||||
.find(id)
|
||||
.first::<CapabilitySet>(&mut conn)
|
||||
.map_err(|err| match err {
|
||||
diesel::result::Error::NotFound => AppError::not_found(),
|
||||
other => AppError::from(other),
|
||||
})?;
|
||||
.into_app_result()?;
|
||||
|
||||
if set.is_system {
|
||||
if payload.slug.is_some() || payload.capabilities.is_some() {
|
||||
@@ -232,7 +229,7 @@ pub async fn update_capability_set(
|
||||
));
|
||||
}
|
||||
let capabilities = load_capabilities_for_set(&mut conn, set.id)?;
|
||||
return Ok(Json(to_response(set, capabilities)));
|
||||
return ok_json(to_response(set, capabilities));
|
||||
}
|
||||
|
||||
let set = conn.transaction::<CapabilitySet, AppError, _>(|conn| {
|
||||
@@ -250,7 +247,7 @@ pub async fn update_capability_set(
|
||||
.filter(cs_dsl::id.ne(working.id))
|
||||
.first::<CapabilitySet>(conn)
|
||||
.optional()
|
||||
.map_err(AppError::from)?
|
||||
.into_app_result()?
|
||||
.is_some()
|
||||
{
|
||||
return Err(AppError::conflict("slug already exists"));
|
||||
@@ -262,7 +259,7 @@ pub async fn update_capability_set(
|
||||
cs_dsl::updated_at.eq(Utc::now().naive_utc()),
|
||||
))
|
||||
.execute(conn)
|
||||
.map_err(AppError::from)?;
|
||||
.into_app_result()?;
|
||||
|
||||
working.slug = normalized;
|
||||
}
|
||||
@@ -280,11 +277,11 @@ pub async fn update_capability_set(
|
||||
capability_sets::table
|
||||
.find(working.id)
|
||||
.first::<CapabilitySet>(conn)
|
||||
.map_err(AppError::from)
|
||||
.into_app_result()
|
||||
})?;
|
||||
|
||||
let capabilities = load_capabilities_for_set(&mut conn, set.id)?;
|
||||
Ok(Json(to_response(set, capabilities)))
|
||||
ok_json(to_response(set, capabilities))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -306,10 +303,7 @@ pub async fn delete_capability_set(
|
||||
.filter(cs_dsl::tenant_id.eq(tenant_id))
|
||||
.find(id)
|
||||
.first::<CapabilitySet>(&mut conn)
|
||||
.map_err(|err| match err {
|
||||
diesel::result::Error::NotFound => AppError::not_found(),
|
||||
other => AppError::from(other),
|
||||
})?;
|
||||
.into_app_result()?;
|
||||
|
||||
if set.is_system {
|
||||
return Err(AppError::conflict(
|
||||
@@ -339,9 +333,12 @@ pub async fn delete_capability_set(
|
||||
));
|
||||
}
|
||||
|
||||
diesel::delete(cs_dsl::capability_sets.find(set.id)).execute(&mut conn)?;
|
||||
diesel::delete(cs_dsl::capability_sets.find(set.id))
|
||||
.execute(&mut conn)
|
||||
.into_app_result()?
|
||||
.or_not_found()?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
no_content()
|
||||
}
|
||||
|
||||
#[derive(utoipa::OpenApi)]
|
||||
|
||||
@@ -11,10 +11,10 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
auth::TenantScopedConn,
|
||||
error::{AppError, AppResult},
|
||||
http::responders::{no_content, ok_json, IntoAppResult, JsonResponse, RowsAffectedExt},
|
||||
models::{Correspondent, NewCorrespondent},
|
||||
schema::{correspondents, document_correspondents},
|
||||
utils::{
|
||||
db::{no_content, EnsureEntity, IntoJsonResponse},
|
||||
named_entity::{ensure_name_available, normalize_name},
|
||||
time::to_iso,
|
||||
},
|
||||
@@ -71,7 +71,7 @@ pub async fn list_correspondents(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<CorrespondentSummary>>> {
|
||||
) -> AppResult<JsonResponse<Vec<CorrespondentSummary>>> {
|
||||
let correspondents_list: Vec<Correspondent> = correspondents::table
|
||||
.filter(correspondents::tenant_id.eq(tenant_id))
|
||||
.order(correspondents::name.asc())
|
||||
@@ -94,7 +94,7 @@ pub async fn list_correspondents(
|
||||
response.push(build_summary(correspondent, total));
|
||||
}
|
||||
|
||||
response.into_json()
|
||||
ok_json(response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -111,7 +111,7 @@ pub async fn create_correspondent(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<CreateCorrespondentRequest>,
|
||||
) -> AppResult<Json<CorrespondentSummary>> {
|
||||
) -> AppResult<JsonResponse<CorrespondentSummary>> {
|
||||
let name = normalize_name(&payload.name, || {
|
||||
AppError::bad_request("name must not be empty")
|
||||
})?;
|
||||
@@ -140,9 +140,9 @@ pub async fn create_correspondent(
|
||||
.find(new_id)
|
||||
.filter(correspondents::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)
|
||||
.one()?;
|
||||
.into_app_result()?;
|
||||
|
||||
build_summary(correspondent, 0).into_json()
|
||||
ok_json(build_summary(correspondent, 0))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -161,12 +161,12 @@ pub async fn update_correspondent(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<UpdateCorrespondentRequest>,
|
||||
) -> AppResult<Json<CorrespondentSummary>> {
|
||||
) -> AppResult<JsonResponse<CorrespondentSummary>> {
|
||||
let existing: Correspondent = correspondents::table
|
||||
.find(correspondent_id)
|
||||
.filter(correspondents::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)
|
||||
.one()?;
|
||||
.into_app_result()?;
|
||||
|
||||
let mut new_name: Option<String> = None;
|
||||
if let Some(ref candidate) = payload.name {
|
||||
@@ -199,7 +199,7 @@ pub async fn update_correspondent(
|
||||
|
||||
if new_name.is_none() && new_metadata.is_none() {
|
||||
let usage = load_usage_for_correspondent(&mut conn, tenant_id, correspondent_id)?;
|
||||
return build_summary(existing.clone(), usage).into_json();
|
||||
return ok_json(build_summary(existing.clone(), usage));
|
||||
}
|
||||
|
||||
let mut changeset = CorrespondentChangeset::default();
|
||||
@@ -217,15 +217,17 @@ pub async fn update_correspondent(
|
||||
.filter(correspondents::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.set((&changeset, correspondents::updated_at.eq(now)))
|
||||
.execute(&mut conn)?;
|
||||
.execute(&mut conn)
|
||||
.into_app_result()?
|
||||
.or_not_found()?;
|
||||
|
||||
let updated: Correspondent = correspondents::table
|
||||
.find(correspondent_id)
|
||||
.filter(correspondents::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)
|
||||
.one()?;
|
||||
.into_app_result()?;
|
||||
let usage = load_usage_for_correspondent(&mut conn, tenant_id, correspondent_id)?;
|
||||
build_summary(updated, usage).into_json()
|
||||
ok_json(build_summary(updated, usage))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -255,15 +257,14 @@ pub async fn delete_correspondent(
|
||||
));
|
||||
}
|
||||
|
||||
let deleted = diesel::delete(
|
||||
diesel::delete(
|
||||
correspondents::table
|
||||
.filter(correspondents::id.eq(correspondent_id))
|
||||
.filter(correspondents::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.execute(&mut conn)?;
|
||||
if deleted == 0 {
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
.execute(&mut conn)
|
||||
.into_app_result()?
|
||||
.or_not_found()?;
|
||||
no_content()
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ use crate::documents::{
|
||||
tags::{assign_tags as assign_tags_to_document, load_tags_for_documents},
|
||||
};
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::http::responders::{accepted_json, created_json, no_content, ok_json, JsonResponse};
|
||||
use crate::jobs::{
|
||||
enqueue_job, JobQueueError, JOB_ANALYZE_DOCUMENT, JOB_INDEX_DOCUMENT_TEXT, JOB_PURGE_DOCUMENT,
|
||||
};
|
||||
@@ -46,7 +47,7 @@ use crate::schema::{
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::utils::{
|
||||
db::{no_content, validate_bulk_ids, IntoJsonResponse},
|
||||
db::validate_bulk_ids,
|
||||
error::StorageResultExt,
|
||||
http::inline_content_disposition,
|
||||
json::{classify_nullable, NullableValue},
|
||||
@@ -372,7 +373,7 @@ pub async fn list_documents(
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<DocumentResponse>>> {
|
||||
) -> AppResult<JsonResponse<Vec<DocumentResponse>>> {
|
||||
let DocumentListQuery {
|
||||
folder_id,
|
||||
include_descendants,
|
||||
@@ -455,7 +456,7 @@ pub async fn list_documents(
|
||||
})?;
|
||||
|
||||
if ids.is_empty() {
|
||||
return Ok(Json(vec![]));
|
||||
return ok_json(Vec::<DocumentResponse>::new());
|
||||
}
|
||||
|
||||
quickwit_order = Some(ids.clone());
|
||||
@@ -480,7 +481,7 @@ pub async fn list_documents(
|
||||
let docs_set: HashSet<Uuid> = docs_without_tags.into_iter().collect();
|
||||
|
||||
if docs_set.is_empty() {
|
||||
return Ok(Json(vec![]));
|
||||
return ok_json(Vec::<DocumentResponse>::new());
|
||||
}
|
||||
|
||||
let new_filter = match &filter_ids {
|
||||
@@ -519,7 +520,7 @@ pub async fn list_documents(
|
||||
let matching_doc_ids: HashSet<Uuid> = doc_id_set.unwrap_or_default();
|
||||
|
||||
if matching_doc_ids.is_empty() {
|
||||
return Ok(Json(vec![]));
|
||||
return ok_json(Vec::<DocumentResponse>::new());
|
||||
}
|
||||
|
||||
let new_filter = match &filter_ids {
|
||||
@@ -566,7 +567,7 @@ pub async fn list_documents(
|
||||
let matching_doc_ids: HashSet<Uuid> = doc_id_set.unwrap_or_default();
|
||||
|
||||
if matching_doc_ids.is_empty() {
|
||||
return Ok(Json(vec![]));
|
||||
return ok_json(Vec::<DocumentResponse>::new());
|
||||
}
|
||||
|
||||
let new_filter = match &filter_ids {
|
||||
@@ -581,7 +582,7 @@ pub async fn list_documents(
|
||||
|
||||
if let Some(ref set) = filter_ids {
|
||||
if set.is_empty() {
|
||||
return Ok(Json(vec![]));
|
||||
return ok_json(Vec::<DocumentResponse>::new());
|
||||
}
|
||||
|
||||
let ids_vec: Vec<Uuid> = set.iter().copied().collect();
|
||||
@@ -606,7 +607,7 @@ pub async fn list_documents(
|
||||
};
|
||||
|
||||
if relevant_ids.is_empty() {
|
||||
return Ok(Json(vec![]));
|
||||
return ok_json(Vec::<DocumentResponse>::new());
|
||||
}
|
||||
|
||||
let mut fetched: Vec<Document> = docs_query.load(&mut conn)?;
|
||||
@@ -631,7 +632,7 @@ pub async fn list_documents(
|
||||
|
||||
let response = hydrate_documents(&state, &mut conn, tenant_id, user_id, docs)?;
|
||||
|
||||
Ok(Json(response))
|
||||
ok_json(response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -648,7 +649,7 @@ pub async fn check_document(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<DocumentCheckResponse>> {
|
||||
) -> AppResult<JsonResponse<DocumentCheckResponse>> {
|
||||
let checksum_raw = query.checksum.trim();
|
||||
if checksum_raw.is_empty() {
|
||||
return Err(AppError::bad_request("checksum must not be empty"));
|
||||
@@ -672,7 +673,7 @@ pub async fn check_document(
|
||||
.optional()?;
|
||||
|
||||
if let Some((document, version)) = record {
|
||||
Ok(Json(DocumentCheckResponse {
|
||||
ok_json(DocumentCheckResponse {
|
||||
exists: true,
|
||||
document_id: Some(document.id),
|
||||
title: Some(document.title.clone()),
|
||||
@@ -680,9 +681,9 @@ pub async fn check_document(
|
||||
version_id: Some(version.id),
|
||||
version_number: Some(version.version_number),
|
||||
created_at: Some(to_iso(document.created_at)),
|
||||
}))
|
||||
})
|
||||
} else {
|
||||
Ok(Json(DocumentCheckResponse {
|
||||
ok_json(DocumentCheckResponse {
|
||||
exists: false,
|
||||
document_id: None,
|
||||
title: None,
|
||||
@@ -690,7 +691,7 @@ pub async fn check_document(
|
||||
version_id: None,
|
||||
version_number: None,
|
||||
created_at: None,
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,7 +711,7 @@ pub async fn get_document(
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<DocumentDetailResponse>> {
|
||||
) -> AppResult<JsonResponse<DocumentDetailResponse>> {
|
||||
let doc: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
@@ -731,7 +732,7 @@ pub async fn get_document(
|
||||
let assets = load_asset_responses(&state, tenant_id, version_id).await?;
|
||||
let version_response = to_version_response(current_version);
|
||||
|
||||
Ok(Json(DocumentDetailResponse {
|
||||
ok_json(DocumentDetailResponse {
|
||||
document: to_document_response(
|
||||
&state,
|
||||
user_id,
|
||||
@@ -740,7 +741,7 @@ pub async fn get_document(
|
||||
correspondents_map.remove(&document_id).unwrap_or_default(),
|
||||
Some((version_response, assets)),
|
||||
)?,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -760,7 +761,7 @@ pub async fn upload_document(
|
||||
tenant_id, user_id, ..
|
||||
}: TenantScopedConn,
|
||||
mut multipart: Multipart,
|
||||
) -> AppResult<impl IntoResponse> {
|
||||
) -> AppResult<JsonResponse<DocumentDetailResponse>> {
|
||||
let mut file_bytes: Option<Vec<u8>> = None;
|
||||
let mut original_name: Option<String> = None;
|
||||
let mut content_type: Option<String> = None;
|
||||
@@ -938,7 +939,7 @@ pub async fn upload_document(
|
||||
reused_existing = false,
|
||||
"document upload succeeded",
|
||||
);
|
||||
(StatusCode::CREATED, Json(detail)).into_response()
|
||||
created_json(detail)?
|
||||
}
|
||||
UploadOutcome::Reused(detail) => {
|
||||
info!(
|
||||
@@ -948,7 +949,7 @@ pub async fn upload_document(
|
||||
reused_existing = true,
|
||||
"document upload succeeded",
|
||||
);
|
||||
(StatusCode::OK, Json(detail)).into_response()
|
||||
ok_json(detail)?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1012,7 +1013,7 @@ pub async fn reanalyze_selected_documents(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<BulkReanalyzeSelectionRequest>,
|
||||
) -> AppResult<(StatusCode, Json<BulkReanalyzeResponse>)> {
|
||||
) -> AppResult<JsonResponse<BulkReanalyzeResponse>> {
|
||||
let BulkReanalyzeSelectionRequest {
|
||||
mut document_ids,
|
||||
force,
|
||||
@@ -1053,7 +1054,7 @@ pub async fn reanalyze_selected_documents(
|
||||
queued += 1;
|
||||
}
|
||||
|
||||
Ok((StatusCode::ACCEPTED, Json(BulkReanalyzeResponse { queued })))
|
||||
accepted_json(BulkReanalyzeResponse { queued })
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -1071,7 +1072,7 @@ pub async fn list_document_assets(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<DocumentAssetResponse>>> {
|
||||
) -> AppResult<JsonResponse<Vec<DocumentAssetResponse>>> {
|
||||
let document: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
@@ -1084,7 +1085,7 @@ pub async fn list_document_assets(
|
||||
drop(conn);
|
||||
|
||||
let assets = load_asset_responses(&state, tenant_id, version_id).await?;
|
||||
Ok(Json(assets))
|
||||
ok_json(assets)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -1103,7 +1104,7 @@ pub async fn get_document_asset(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<DocumentAssetDetailResponse>> {
|
||||
) -> AppResult<JsonResponse<DocumentAssetDetailResponse>> {
|
||||
let asset: DocumentAsset = match document_assets::table
|
||||
.find(asset_id)
|
||||
.filter(document_assets::tenant_id.eq(tenant_id))
|
||||
@@ -1168,7 +1169,7 @@ pub async fn get_document_asset(
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
|
||||
Ok(Json(to_asset_detail_response(asset, object_responses)))
|
||||
ok_json(to_asset_detail_response(asset, object_responses))
|
||||
}
|
||||
|
||||
fn presign_disposition_for_asset(
|
||||
@@ -1193,7 +1194,7 @@ pub async fn list_document_versions(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<DocumentVersionResponse>>> {
|
||||
) -> AppResult<JsonResponse<Vec<DocumentVersionResponse>>> {
|
||||
let document: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
@@ -1212,7 +1213,7 @@ pub async fn list_document_versions(
|
||||
let versions: Vec<DocumentVersionResponse> =
|
||||
versions.into_iter().map(to_version_response).collect();
|
||||
|
||||
Ok(Json(versions))
|
||||
ok_json(versions)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -1234,7 +1235,7 @@ pub async fn get_document_version(
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<DocumentVersionDetailResponse>> {
|
||||
) -> AppResult<JsonResponse<DocumentVersionDetailResponse>> {
|
||||
let document: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
@@ -1256,11 +1257,11 @@ pub async fn get_document_version(
|
||||
let download_path = build_download_path(&state, &document, user_id)?;
|
||||
let version_core = to_version_response(version);
|
||||
|
||||
Ok(Json(DocumentVersionDetailResponse {
|
||||
ok_json(DocumentVersionDetailResponse {
|
||||
version: version_core,
|
||||
assets,
|
||||
download_path,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -1416,7 +1417,7 @@ pub async fn update_document(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<Value>,
|
||||
) -> AppResult<Json<DocumentDetailResponse>> {
|
||||
) -> AppResult<JsonResponse<DocumentDetailResponse>> {
|
||||
let mut document: Document = documents::table
|
||||
.find(document_id)
|
||||
.filter(documents::tenant_id.eq(tenant_id))
|
||||
@@ -1566,7 +1567,7 @@ pub async fn update_document(
|
||||
let assets = load_asset_responses(&state, tenant_id, version_id).await?;
|
||||
let version_response = to_version_response(current_version);
|
||||
|
||||
Ok(Json(DocumentDetailResponse {
|
||||
ok_json(DocumentDetailResponse {
|
||||
document: to_document_response(
|
||||
&state,
|
||||
user_id,
|
||||
@@ -1575,7 +1576,7 @@ pub async fn update_document(
|
||||
correspondents_map.remove(&document_id).unwrap_or_default(),
|
||||
Some((version_response, assets)),
|
||||
)?,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -1686,7 +1687,7 @@ pub async fn bulk_move_documents(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<BulkMoveRequest>,
|
||||
) -> AppResult<(StatusCode, Json<BulkMoveResponse>)> {
|
||||
) -> AppResult<JsonResponse<BulkMoveResponse>> {
|
||||
let BulkMoveRequest {
|
||||
mut document_ids,
|
||||
folder_id,
|
||||
@@ -1759,7 +1760,7 @@ pub async fn bulk_move_documents(
|
||||
};
|
||||
|
||||
let body = BulkMoveResponse { updated };
|
||||
Ok((StatusCode::OK, body.into_json()?))
|
||||
ok_json(body)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -1866,7 +1867,7 @@ pub async fn bulk_assign_correspondents(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<BulkCorrespondentsRequest>,
|
||||
) -> AppResult<(StatusCode, Json<BulkCorrespondentResponse>)> {
|
||||
) -> AppResult<JsonResponse<BulkCorrespondentResponse>> {
|
||||
if payload.assignments.is_empty() {
|
||||
return Err(AppError::bad_request("assignments must not be empty"));
|
||||
}
|
||||
@@ -1947,7 +1948,7 @@ pub async fn bulk_assign_correspondents(
|
||||
})?;
|
||||
|
||||
let body = BulkCorrespondentResponse { assigned, removed };
|
||||
Ok((StatusCode::OK, body.into_json()?))
|
||||
ok_json(body)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -2052,7 +2053,7 @@ pub async fn bulk_update_tags(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<BulkTagRequest>,
|
||||
) -> AppResult<(StatusCode, Json<BulkTagResponse>)> {
|
||||
) -> AppResult<JsonResponse<BulkTagResponse>> {
|
||||
let BulkTagRequest {
|
||||
mut document_ids,
|
||||
mut tag_ids,
|
||||
@@ -2126,7 +2127,7 @@ pub async fn bulk_update_tags(
|
||||
}
|
||||
};
|
||||
|
||||
Ok((StatusCode::OK, response.into_json()?))
|
||||
ok_json(response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
|
||||
@@ -18,6 +18,9 @@ use crate::state::AppState;
|
||||
use crate::{
|
||||
auth::TenantScopedConn,
|
||||
error::{AppError, AppResult},
|
||||
http::responders::{
|
||||
created_json, no_content, ok_json, IntoAppResult, JsonResponse, RowsAffectedExt,
|
||||
},
|
||||
};
|
||||
|
||||
use super::documents::{hydrate_documents, DocumentResponse};
|
||||
@@ -141,15 +144,15 @@ pub async fn get_folder(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<FolderResponse>> {
|
||||
) -> AppResult<JsonResponse<FolderResponse>> {
|
||||
let folder: Folder = folders::table
|
||||
.find(folder_id)
|
||||
.filter(folders::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)?;
|
||||
|
||||
Ok(Json(FolderResponse {
|
||||
ok_json(FolderResponse {
|
||||
folder: folder_to_info(folder),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -166,7 +169,7 @@ pub async fn ensure_folder_path(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<EnsureFolderPathRequest>,
|
||||
) -> AppResult<Json<FolderResponse>> {
|
||||
) -> AppResult<JsonResponse<FolderResponse>> {
|
||||
if payload.segments.is_empty() {
|
||||
return Err(AppError::bad_request("segments must not be empty"));
|
||||
}
|
||||
@@ -240,9 +243,9 @@ pub async fn ensure_folder_path(
|
||||
last_folder.ok_or_else(|| AppError::internal("failed to resolve folder path"))
|
||||
})?;
|
||||
|
||||
Ok(Json(FolderResponse {
|
||||
ok_json(FolderResponse {
|
||||
folder: folder_to_info(target_folder),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -262,7 +265,7 @@ pub async fn create_folder(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<CreateFolderRequest>,
|
||||
) -> AppResult<(StatusCode, Json<FolderResponse>)> {
|
||||
) -> AppResult<JsonResponse<FolderResponse>> {
|
||||
if payload.name.trim().is_empty() {
|
||||
return Err(AppError::bad_request("name must not be empty"));
|
||||
}
|
||||
@@ -331,14 +334,14 @@ pub async fn create_folder(
|
||||
}
|
||||
};
|
||||
|
||||
let response = Json(FolderResponse {
|
||||
let response = FolderResponse {
|
||||
folder: folder_to_info(folder),
|
||||
});
|
||||
};
|
||||
|
||||
if created {
|
||||
Ok((StatusCode::CREATED, response))
|
||||
created_json(response)
|
||||
} else {
|
||||
Ok((StatusCode::OK, response))
|
||||
ok_json(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +362,7 @@ pub async fn list_folder_contents(
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<FolderContentsResponse>> {
|
||||
) -> AppResult<JsonResponse<FolderContentsResponse>> {
|
||||
let FolderContentsQuery {
|
||||
include_documents,
|
||||
sort,
|
||||
@@ -427,11 +430,11 @@ pub async fn list_folder_contents(
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Ok(Json(FolderContentsResponse {
|
||||
ok_json(FolderContentsResponse {
|
||||
folder,
|
||||
subfolders,
|
||||
documents,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -446,7 +449,7 @@ pub async fn list_folder_tree(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<FolderTreeNode>>> {
|
||||
) -> AppResult<JsonResponse<Vec<FolderTreeNode>>> {
|
||||
let folders: Vec<Folder> = folders::table
|
||||
.filter(folders::tenant_id.eq(tenant_id))
|
||||
.order(sql::<Text>("name COLLATE \"unicode_ci\" ASC"))
|
||||
@@ -499,7 +502,7 @@ pub async fn list_folder_tree(
|
||||
.map(|root_id| build_node(*root_id, &node_map, &children_map))
|
||||
.collect();
|
||||
|
||||
Ok(Json(tree))
|
||||
ok_json(tree)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -555,12 +558,14 @@ pub async fn delete_folder(
|
||||
.filter(folders::id.eq(folder_id))
|
||||
.filter(folders::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.execute(conn)?;
|
||||
.execute(conn)
|
||||
.into_app_result()?
|
||||
.or_not_found()?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
no_content()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -677,12 +682,14 @@ pub async fn update_folder(
|
||||
folders::parent_id.eq(next_parent),
|
||||
folders::name.eq(&new_name),
|
||||
))
|
||||
.execute(conn)?;
|
||||
.execute(conn)
|
||||
.into_app_result()?
|
||||
.or_not_found()?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
no_content()
|
||||
}
|
||||
|
||||
fn folder_to_info(folder: Folder) -> FolderInfo {
|
||||
|
||||
@@ -18,9 +18,10 @@ use crate::auth::{
|
||||
TenantScopedConn,
|
||||
};
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::http::responders::{created_json, no_content, ok_json, JsonResponse};
|
||||
use crate::models::ApiToken;
|
||||
use crate::state::{AppState, PgPooledConnection};
|
||||
use crate::utils::{db::no_content, time::to_iso};
|
||||
use crate::utils::time::to_iso;
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ApiTokenResponse {
|
||||
@@ -71,14 +72,14 @@ pub async fn list_passkeys(
|
||||
TenantScopedConn {
|
||||
mut conn, user_id, ..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<PasskeySummary>>> {
|
||||
) -> AppResult<JsonResponse<Vec<PasskeySummary>>> {
|
||||
let service = state
|
||||
.passkeys
|
||||
.as_ref()
|
||||
.ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;
|
||||
|
||||
let passkeys = service.list_for_user(&mut conn, user_id)?;
|
||||
Ok(Json(passkeys))
|
||||
ok_json(passkeys)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -94,10 +95,10 @@ pub async fn list_api_tokens(
|
||||
user_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<ApiTokenResponse>>> {
|
||||
) -> AppResult<JsonResponse<Vec<ApiTokenResponse>>> {
|
||||
let tokens = load_tokens(&mut conn, user_id, Some(tenant_id))?;
|
||||
let responses = tokens.into_iter().map(api_token_to_response).collect();
|
||||
Ok(Json(responses))
|
||||
ok_json(responses)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -115,7 +116,7 @@ pub async fn create_api_token(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<CreateApiTokenRequest>,
|
||||
) -> AppResult<(StatusCode, Json<ApiTokenCreatedResponse>)> {
|
||||
) -> AppResult<JsonResponse<ApiTokenCreatedResponse>> {
|
||||
let expires_at = match payload.expires_at {
|
||||
Some(ref value) => Some(parse_timestamp(value)?),
|
||||
None => None,
|
||||
@@ -140,7 +141,7 @@ pub async fn create_api_token(
|
||||
token_info,
|
||||
};
|
||||
|
||||
Ok((StatusCode::CREATED, Json(response)))
|
||||
created_json(response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -158,7 +159,7 @@ pub async fn regenerate_api_token(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Path(token_id): Path<Uuid>,
|
||||
) -> AppResult<Json<ApiTokenCreatedResponse>> {
|
||||
) -> AppResult<JsonResponse<ApiTokenCreatedResponse>> {
|
||||
let issued = rotate_token(&mut conn, token_id, user_id, Some(tenant_id))?;
|
||||
let token_info = api_token_to_response(issued.record);
|
||||
let response = ApiTokenCreatedResponse {
|
||||
@@ -166,7 +167,7 @@ pub async fn regenerate_api_token(
|
||||
token_info,
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
ok_json(response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
|
||||
+32
-32
@@ -5,14 +5,16 @@ use std::collections::HashMap;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::TenantScopedConn;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::models::{NewTag, Tag};
|
||||
use crate::schema::{document_tags, tags};
|
||||
use crate::utils::{
|
||||
db::{no_content, EnsureEntity, IntoJsonResponse},
|
||||
json::deserialize_patch_field,
|
||||
named_entity::{ensure_name_available, normalize_name},
|
||||
use crate::{
|
||||
auth::TenantScopedConn,
|
||||
error::{AppError, AppResult},
|
||||
http::responders::{no_content, ok_json, IntoAppResult, JsonResponse, RowsAffectedExt},
|
||||
models::{NewTag, Tag},
|
||||
schema::{document_tags, tags},
|
||||
utils::{
|
||||
json::deserialize_patch_field,
|
||||
named_entity::{ensure_name_available, normalize_name},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
@@ -84,7 +86,7 @@ pub async fn list_tags(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
) -> AppResult<Json<Vec<TagCatalogEntry>>> {
|
||||
) -> AppResult<JsonResponse<Vec<TagCatalogEntry>>> {
|
||||
let tag_list: Vec<Tag> = tags::table
|
||||
.filter(tags::tenant_id.eq(tenant_id))
|
||||
.order(tags::label.asc())
|
||||
@@ -108,7 +110,7 @@ pub async fn list_tags(
|
||||
})
|
||||
.collect();
|
||||
|
||||
response.into_json()
|
||||
ok_json(response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -125,7 +127,7 @@ pub async fn create_tag(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<CreateTagRequest>,
|
||||
) -> AppResult<Json<TagCatalogEntry>> {
|
||||
) -> AppResult<JsonResponse<TagCatalogEntry>> {
|
||||
let label = normalize_name(&payload.label, || {
|
||||
AppError::bad_request("label must not be empty")
|
||||
})?;
|
||||
@@ -155,15 +157,14 @@ pub async fn create_tag(
|
||||
.find(new_tag.id)
|
||||
.filter(tags::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)
|
||||
.one()?;
|
||||
.into_app_result()?;
|
||||
|
||||
TagCatalogEntry {
|
||||
ok_json(TagCatalogEntry {
|
||||
id: tag.id,
|
||||
label: tag.label,
|
||||
color: tag.color,
|
||||
usage_count: 0,
|
||||
}
|
||||
.into_json()
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -182,12 +183,12 @@ pub async fn update_tag(
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(payload): Json<UpdateTagRequest>,
|
||||
) -> AppResult<Json<TagCatalogEntry>> {
|
||||
) -> AppResult<JsonResponse<TagCatalogEntry>> {
|
||||
let existing: Tag = tags::table
|
||||
.find(tag_id)
|
||||
.filter(tags::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)
|
||||
.one()?;
|
||||
.into_app_result()?;
|
||||
let UpdateTagRequest { label, color } = payload;
|
||||
|
||||
if label.is_none() && color.is_none() {
|
||||
@@ -195,13 +196,12 @@ pub async fn update_tag(
|
||||
.filter(document_tags::tag_id.eq(tag_id))
|
||||
.select(count_star())
|
||||
.first(&mut conn)?;
|
||||
return TagCatalogEntry {
|
||||
return ok_json(TagCatalogEntry {
|
||||
id: existing.id,
|
||||
label: existing.label.clone(),
|
||||
color: existing.color.clone(),
|
||||
usage_count,
|
||||
}
|
||||
.into_json();
|
||||
});
|
||||
}
|
||||
|
||||
let mut new_label: Option<String> = None;
|
||||
@@ -258,12 +258,12 @@ pub async fn update_tag(
|
||||
.filter(document_tags::tenant_id.eq(tenant_id))
|
||||
.select(count_star())
|
||||
.first(&mut conn)?;
|
||||
return Ok(Json(TagCatalogEntry {
|
||||
return ok_json(TagCatalogEntry {
|
||||
id: existing.id,
|
||||
label: existing.label.clone(),
|
||||
color: existing.color.clone(),
|
||||
usage_count,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
let changeset = UpdateTagChangeset {
|
||||
@@ -279,26 +279,27 @@ pub async fn update_tag(
|
||||
.filter(tags::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.set(&changeset)
|
||||
.execute(&mut conn)?;
|
||||
.execute(&mut conn)
|
||||
.into_app_result()?
|
||||
.or_not_found()?;
|
||||
|
||||
let updated: Tag = tags::table
|
||||
.find(tag_id)
|
||||
.filter(tags::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)
|
||||
.one()?;
|
||||
.into_app_result()?;
|
||||
let usage_count: i64 = document_tags::table
|
||||
.filter(document_tags::tag_id.eq(tag_id))
|
||||
.filter(document_tags::tenant_id.eq(tenant_id))
|
||||
.select(count_star())
|
||||
.first(&mut conn)?;
|
||||
|
||||
TagCatalogEntry {
|
||||
ok_json(TagCatalogEntry {
|
||||
id: updated.id,
|
||||
label: updated.label,
|
||||
color: updated.color,
|
||||
usage_count,
|
||||
}
|
||||
.into_json()
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -328,15 +329,14 @@ pub async fn delete_tag(
|
||||
));
|
||||
}
|
||||
|
||||
let deleted = diesel::delete(
|
||||
diesel::delete(
|
||||
tags::table
|
||||
.find(tag_id)
|
||||
.filter(tags::tenant_id.eq(tenant_id)),
|
||||
)
|
||||
.execute(&mut conn)?;
|
||||
if deleted == 0 {
|
||||
return Err(AppError::not_found());
|
||||
}
|
||||
.execute(&mut conn)
|
||||
.into_app_result()?
|
||||
.or_not_found()?;
|
||||
|
||||
no_content()
|
||||
}
|
||||
|
||||
+1
-34
@@ -1,4 +1,4 @@
|
||||
use diesel::{pg::PgConnection, result::Error as DieselError};
|
||||
use diesel::pg::PgConnection;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
@@ -6,25 +6,6 @@ use crate::{
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub trait EnsureEntity<T> {
|
||||
fn one(self) -> AppResult<T>;
|
||||
fn maybe(self) -> AppResult<Option<T>>;
|
||||
}
|
||||
|
||||
impl<T> EnsureEntity<T> for Result<T, DieselError> {
|
||||
fn one(self) -> AppResult<T> {
|
||||
self.map_err(AppError::from)
|
||||
}
|
||||
|
||||
fn maybe(self) -> AppResult<Option<T>> {
|
||||
match self {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(DieselError::NotFound) => Ok(None),
|
||||
Err(err) => Err(AppError::from(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn with_tenant_conn<F, T>(&self, tenant_id: Uuid, f: F) -> AppResult<T>
|
||||
where
|
||||
@@ -43,17 +24,3 @@ pub fn validate_bulk_ids(ids: &mut Vec<Uuid>, label: &str) -> AppResult<()> {
|
||||
ids.dedup();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub trait IntoJsonResponse<T> {
|
||||
fn into_json(self) -> AppResult<axum::Json<T>>;
|
||||
}
|
||||
|
||||
impl<T> IntoJsonResponse<T> for T {
|
||||
fn into_json(self) -> AppResult<axum::Json<T>> {
|
||||
Ok(axum::Json(self))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_content() -> AppResult<axum::http::StatusCode> {
|
||||
Ok(axum::http::StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# API response helpers
|
||||
|
||||
The backend now exposes `crate::http::responders`, which wraps common success and
|
||||
error patterns for routes:
|
||||
|
||||
- `ok_json`, `created_json`, `accepted_json` return `JsonResponse<T>` with the
|
||||
respective status codes.
|
||||
- `no_content`/`empty` provide shared empty responses.
|
||||
- `JsonResponse<T>` implements `IntoResponse`, so any handler can return
|
||||
`AppResult<JsonResponse<T>>` without pairing tuples manually.
|
||||
- `IntoAppResult`, `RowsAffectedExt`, and friends convert Diesel results into
|
||||
`AppResult<T>` with consistent `AppError` handling.
|
||||
|
||||
When adding new routes, import from `crate::http::responders` instead of
|
||||
constructing `(StatusCode, Json<T>)` tuples directly. The folders, documents,
|
||||
auth, capability-set, correspondent, tag, and profile routers now all share
|
||||
these helpers; WebDAV keeps its bespoke streaming responses for now.
|
||||
Reference in New Issue
Block a user