Remove history-ui workspace and supporting server endpoints
Splits history-ui out of asch/fix-everything into its own branch. This commit removes from asch/fix-everything: the Svelte workspace under frontend/history-ui, the three dedicated server endpoints (list_vaults, fetch_vault_history, fetch_document_versions) and their router wiring, the SPA asset embedding in index.rs, the rust-embed/mime_guess deps, the build.rs dist-dir creation, the matching response types and database methods (list_vaults, get_vault_stats, get_vault_history, get_document_versions, VaultStats, VaultHistoryRow), and the TS mirror types in sync-client. Note: Cargo.lock, frontend/package-lock.json, and sync-server/.sqlx/ will need regeneration via `cargo build`, `npm install`, and `cargo sqlx prepare` to clean up stale entries. The history-ui mentions in CLAUDE.md and scripts/update-api-types.sh predate this branch (also present on main) and were left as-is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b8f7c0eda2
commit
7dc0f4316e
56 changed files with 6 additions and 4397 deletions
|
|
@ -185,42 +185,6 @@ impl Database {
|
|||
self.epoch.elapsed().as_millis() as u64
|
||||
}
|
||||
|
||||
/// Lists all vault IDs that exist on disk (have a `.sqlite` file).
|
||||
pub async fn list_vaults(&self) -> Result<Vec<VaultId>> {
|
||||
let mut vaults = Vec::new();
|
||||
let mut entries = tokio::fs::read_dir(&self.config.databases_directory_path)
|
||||
.await
|
||||
.context("Failed to read databases directory")?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if let Some(vault) = name.strip_suffix(".sqlite") {
|
||||
vaults.push(vault.to_owned());
|
||||
}
|
||||
}
|
||||
vaults.sort();
|
||||
Ok(vaults)
|
||||
}
|
||||
|
||||
pub async fn get_vault_stats(&self, vault: &VaultId) -> Result<models::VaultStats> {
|
||||
let pool = self.get_connection_pool(vault).await?;
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
(SELECT MIN(updated_date) FROM documents)
|
||||
AS "created_at: chrono::DateTime<Utc>",
|
||||
(SELECT COUNT(DISTINCT document_id) FROM latest_document_versions
|
||||
WHERE is_deleted = false)
|
||||
AS "document_count!: u32"
|
||||
"#,
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
Ok(models::VaultStats {
|
||||
created_at: row.created_at,
|
||||
document_count: row.document_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn try_new(
|
||||
config: &DatabaseConfig,
|
||||
broadcasts: &Broadcasts,
|
||||
|
|
@ -859,145 +823,6 @@ impl Database {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Return all versions (without content) of a specific document, ordered by `vault_update_id`
|
||||
pub async fn get_document_versions(
|
||||
&self,
|
||||
vault: &VaultId,
|
||||
document_id: &DocumentId,
|
||||
connection: Option<&mut SqliteConnection>,
|
||||
) -> Result<Vec<DocumentVersionWithoutContent>> {
|
||||
let document_id = document_id.as_hyphenated();
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
select
|
||||
vault_update_id,
|
||||
creation_vault_update_id,
|
||||
document_id as "document_id: Hyphenated",
|
||||
relative_path,
|
||||
updated_date as "updated_date: chrono::DateTime<Utc>",
|
||||
is_deleted,
|
||||
user_id,
|
||||
device_id,
|
||||
length(content) as "content_size: u64"
|
||||
from documents
|
||||
where document_id = ?
|
||||
order by vault_update_id
|
||||
"#,
|
||||
document_id,
|
||||
);
|
||||
|
||||
if let Some(conn) = connection {
|
||||
query.fetch_all(&mut *conn).await
|
||||
} else {
|
||||
query
|
||||
.fetch_all(&self.get_connection_pool(vault).await?)
|
||||
.await
|
||||
}
|
||||
.with_context(|| format!("Cannot fetch document versions for document `{document_id}`"))
|
||||
.map(|rows| {
|
||||
rows.into_iter()
|
||||
.map(|row| DocumentVersionWithoutContent {
|
||||
vault_update_id: row.vault_update_id,
|
||||
document_id: row.document_id.into(),
|
||||
relative_path: row.relative_path,
|
||||
updated_date: row.updated_date,
|
||||
is_deleted: row.is_deleted,
|
||||
user_id: row.user_id,
|
||||
device_id: row.device_id,
|
||||
content_size: row.content_size.unwrap_or(0),
|
||||
is_new_file: row.creation_vault_update_id == row.vault_update_id,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Return all versions across all documents, paginated, ordered by `vault_update_id` DESC
|
||||
pub async fn get_vault_history(
|
||||
&self,
|
||||
vault: &VaultId,
|
||||
limit: i64,
|
||||
before_update_id: Option<VaultUpdateId>,
|
||||
connection: Option<&mut SqliteConnection>,
|
||||
) -> Result<Vec<DocumentVersionWithoutContent>> {
|
||||
let map_row = |row: models::VaultHistoryRow| DocumentVersionWithoutContent {
|
||||
vault_update_id: row.vault_update_id,
|
||||
document_id: row.document_id,
|
||||
relative_path: row.relative_path,
|
||||
updated_date: row.updated_date,
|
||||
is_deleted: row.is_deleted,
|
||||
user_id: row.user_id,
|
||||
device_id: row.device_id,
|
||||
content_size: row.content_size.unwrap_or(0),
|
||||
is_new_file: row.creation_vault_update_id == row.vault_update_id,
|
||||
};
|
||||
|
||||
if let Some(before) = before_update_id {
|
||||
let query = sqlx::query_as!(
|
||||
models::VaultHistoryRow,
|
||||
r#"
|
||||
select
|
||||
vault_update_id,
|
||||
creation_vault_update_id,
|
||||
document_id as "document_id: Hyphenated",
|
||||
relative_path,
|
||||
updated_date as "updated_date: chrono::DateTime<Utc>",
|
||||
is_deleted,
|
||||
user_id,
|
||||
device_id,
|
||||
length(content) as "content_size: u64"
|
||||
from documents
|
||||
where vault_update_id < ?
|
||||
order by vault_update_id desc
|
||||
limit ?
|
||||
"#,
|
||||
before,
|
||||
limit,
|
||||
);
|
||||
|
||||
let rows = if let Some(conn) = connection {
|
||||
query.fetch_all(&mut *conn).await
|
||||
} else {
|
||||
query
|
||||
.fetch_all(&self.get_connection_pool(vault).await?)
|
||||
.await
|
||||
}
|
||||
.context("Cannot fetch vault history")?;
|
||||
|
||||
Ok(rows.into_iter().map(map_row).collect())
|
||||
} else {
|
||||
let query = sqlx::query_as!(
|
||||
models::VaultHistoryRow,
|
||||
r#"
|
||||
select
|
||||
vault_update_id,
|
||||
creation_vault_update_id,
|
||||
document_id as "document_id: Hyphenated",
|
||||
relative_path,
|
||||
updated_date as "updated_date: chrono::DateTime<Utc>",
|
||||
is_deleted,
|
||||
user_id,
|
||||
device_id,
|
||||
length(content) as "content_size: u64"
|
||||
from documents
|
||||
order by vault_update_id desc
|
||||
limit ?
|
||||
"#,
|
||||
limit,
|
||||
);
|
||||
|
||||
let rows = if let Some(conn) = connection {
|
||||
query.fetch_all(&mut *conn).await
|
||||
} else {
|
||||
query
|
||||
.fetch_all(&self.get_connection_pool(vault).await?)
|
||||
.await
|
||||
}
|
||||
.context("Cannot fetch vault history")?;
|
||||
|
||||
Ok(rows.into_iter().map(map_row).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleanup idle connection pools that haven't been accessed in more than 5 minutes
|
||||
async fn cleanup_idle_pools(&self) {
|
||||
// Collect idle vaults and remove them from the map while holding
|
||||
|
|
|
|||
|
|
@ -83,24 +83,6 @@ pub struct DocumentVersion {
|
|||
pub device_id: DeviceId,
|
||||
}
|
||||
|
||||
/// Row struct for vault history queries (used by `sqlx::query_as!`)
|
||||
#[derive(Debug)]
|
||||
pub struct VaultHistoryRow {
|
||||
pub vault_update_id: VaultUpdateId,
|
||||
pub creation_vault_update_id: VaultUpdateId,
|
||||
pub document_id: DocumentId,
|
||||
pub relative_path: String,
|
||||
pub updated_date: DateTime<Utc>,
|
||||
pub is_deleted: bool,
|
||||
pub user_id: String,
|
||||
pub device_id: String,
|
||||
pub content_size: Option<u64>,
|
||||
}
|
||||
|
||||
pub struct VaultStats {
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub document_count: u32,
|
||||
}
|
||||
|
||||
impl From<StoredDocumentVersion> for DocumentVersion {
|
||||
fn from(value: StoredDocumentVersion) -> Self {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,9 @@ mod delete_document;
|
|||
mod device_id_header;
|
||||
mod fetch_document_version;
|
||||
mod fetch_document_version_content;
|
||||
mod fetch_document_versions;
|
||||
mod fetch_latest_document_version;
|
||||
mod fetch_latest_documents;
|
||||
mod fetch_vault_history;
|
||||
mod index;
|
||||
mod list_vaults;
|
||||
mod ping;
|
||||
mod rate_limit;
|
||||
mod requests;
|
||||
|
|
@ -57,11 +54,8 @@ pub async fn create_server(config: Config) -> Result<()> {
|
|||
let mut app = Router::new()
|
||||
.nest("/", get_authed_routes(app_state.clone()))
|
||||
.route("/", get(index::index))
|
||||
.route("/assets/*path", get(index::spa_assets))
|
||||
.route("/vaults", get(list_vaults::list_vaults))
|
||||
.route("/vaults/:vault_id/ping", get(ping::ping))
|
||||
.route("/vaults/:vault_id/ws", get(websocket::websocket_handler))
|
||||
.fallback(index::spa_fallback);
|
||||
.route("/vaults/:vault_id/ws", get(websocket::websocket_handler));
|
||||
|
||||
let cors_layer = build_cors_layer(&server_config).context("Invalid CORS configuration")?;
|
||||
|
||||
|
|
@ -157,10 +151,6 @@ fn get_authed_routes(app_state: AppState) -> Router<AppState> {
|
|||
"/vaults/:vault_id/documents/:document_id/text",
|
||||
put(update_document::update_text),
|
||||
)
|
||||
.route(
|
||||
"/vaults/:vault_id/documents/:document_id/versions",
|
||||
get(fetch_document_versions::fetch_document_versions),
|
||||
)
|
||||
.route(
|
||||
"/vaults/:vault_id/documents/:document_id/versions/:vault_update_id",
|
||||
get(fetch_document_version::fetch_document_version),
|
||||
|
|
@ -173,10 +163,6 @@ fn get_authed_routes(app_state: AppState) -> Router<AppState> {
|
|||
"/vaults/:vault_id/documents/:document_id",
|
||||
delete(delete_document::delete_document),
|
||||
)
|
||||
.route(
|
||||
"/vaults/:vault_id/history",
|
||||
get(fetch_vault_history::fetch_vault_history),
|
||||
)
|
||||
.layer(middleware::from_fn_with_state(app_state, auth_middleware))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
};
|
||||
use log::debug;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
app_state::{
|
||||
AppState,
|
||||
database::models::{DocumentId, DocumentVersionWithoutContent, VaultId},
|
||||
},
|
||||
errors::{SyncServerError, server_error},
|
||||
utils::normalize::normalize,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FetchDocumentVersionsPathParams {
|
||||
#[serde(deserialize_with = "normalize")]
|
||||
vault_id: VaultId,
|
||||
|
||||
document_id: DocumentId,
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn fetch_document_versions(
|
||||
Path(FetchDocumentVersionsPathParams {
|
||||
vault_id,
|
||||
document_id,
|
||||
}): Path<FetchDocumentVersionsPathParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Vec<DocumentVersionWithoutContent>>, SyncServerError> {
|
||||
debug!("Fetching all versions for document `{document_id}` in vault `{vault_id}`");
|
||||
|
||||
let versions = state
|
||||
.database
|
||||
.get_document_versions(&vault_id, &document_id, None)
|
||||
.await
|
||||
.map_err(server_error)?;
|
||||
|
||||
Ok(Json(versions))
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
};
|
||||
use log::debug;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::responses::VaultHistoryResponse;
|
||||
use crate::{
|
||||
app_state::{
|
||||
AppState,
|
||||
database::models::{VaultId, VaultUpdateId},
|
||||
},
|
||||
errors::{SyncServerError, client_error, server_error},
|
||||
utils::normalize::normalize,
|
||||
};
|
||||
|
||||
const DEFAULT_LIMIT: i64 = 50;
|
||||
const MAX_LIMIT: i64 = 500;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FetchVaultHistoryPathParams {
|
||||
#[serde(deserialize_with = "normalize")]
|
||||
vault_id: VaultId,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct QueryParams {
|
||||
limit: Option<i64>,
|
||||
before_update_id: Option<VaultUpdateId>,
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn fetch_vault_history(
|
||||
Path(FetchVaultHistoryPathParams { vault_id }): Path<FetchVaultHistoryPathParams>,
|
||||
Query(QueryParams {
|
||||
limit,
|
||||
before_update_id,
|
||||
}): Query<QueryParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<VaultHistoryResponse>, SyncServerError> {
|
||||
if let Some(id) = before_update_id
|
||||
&& id <= 0
|
||||
{
|
||||
return Err(client_error(anyhow::anyhow!(
|
||||
"before_update_id must be a positive integer"
|
||||
)));
|
||||
}
|
||||
|
||||
let limit = limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT);
|
||||
|
||||
debug!(
|
||||
"Fetching vault history for vault `{vault_id}` (limit={limit}, before={before_update_id:?})"
|
||||
);
|
||||
|
||||
// Fetch one extra row to determine if there are more results
|
||||
let mut versions = state
|
||||
.database
|
||||
.get_vault_history(&vault_id, limit + 1, before_update_id, None)
|
||||
.await
|
||||
.map_err(server_error)?;
|
||||
|
||||
#[allow(clippy::cast_sign_loss)] // limit is clamped to [1, 500] above
|
||||
let has_more = versions.len() > limit as usize;
|
||||
if has_more {
|
||||
versions.pop();
|
||||
}
|
||||
|
||||
Ok(Json(VaultHistoryResponse { versions, has_more }))
|
||||
}
|
||||
|
|
@ -1,77 +1,6 @@
|
|||
use axum::{
|
||||
body::Body,
|
||||
extract::{Path, State},
|
||||
http::{StatusCode, header},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use log::warn;
|
||||
use rust_embed::Embed;
|
||||
use axum::response::{Html, IntoResponse};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[derive(Embed)]
|
||||
#[folder = "../frontend/history-ui/dist/"]
|
||||
struct HistoryUiAssets;
|
||||
|
||||
pub async fn index(State(_state): State<AppState>) -> impl IntoResponse {
|
||||
if let Some(content) = HistoryUiAssets::get("index.html") {
|
||||
Html(
|
||||
std::str::from_utf8(content.data.as_ref())
|
||||
.inspect_err(|e| warn!("Embedded index.html is not valid UTF-8: {e}"))
|
||||
.unwrap_or("<h1>VaultLink</h1>")
|
||||
.to_owned(),
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
warn!("No embedded index.html found — history UI may not have been built");
|
||||
Html("<h1>VaultLink server</h1>".to_owned()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn spa_assets(Path(path): Path<String>) -> impl IntoResponse {
|
||||
// The route is /assets/*path so path is relative to assets/.
|
||||
// The embedded files include the assets/ prefix from the dist directory.
|
||||
let full_path = format!("assets/{path}");
|
||||
if let Some(content) = HistoryUiAssets::get(&full_path) {
|
||||
let mime = mime_guess::from_path(&full_path).first_or_octet_stream();
|
||||
return Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, mime.as_ref())
|
||||
.body(Body::from(content.data.to_vec()))
|
||||
.unwrap_or_else(|_| {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::empty())
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
});
|
||||
}
|
||||
|
||||
// Asset paths must match an embedded file — no SPA fallback.
|
||||
// Serving index.html here would return 200 with text/html for missing
|
||||
// .css/.js files, causing the browser to silently ignore the content.
|
||||
Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::from("Not found"))
|
||||
.unwrap_or_else(|_| Response::new(Body::from("Not found")))
|
||||
}
|
||||
|
||||
/// SPA fallback for production: serves index.html for client-side routes
|
||||
/// (e.g. `/documents/123`).
|
||||
pub async fn spa_fallback() -> impl IntoResponse {
|
||||
match HistoryUiAssets::get("index.html") {
|
||||
Some(content) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "text/html")
|
||||
.body(Body::from(content.data.to_vec()))
|
||||
.unwrap_or_else(|_| {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::empty())
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
}),
|
||||
None => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::from("Not found"))
|
||||
.unwrap_or_else(|_| Response::new(Body::from("Not found"))),
|
||||
}
|
||||
pub async fn index() -> impl IntoResponse {
|
||||
const HTML_CONTENT: &str = include_str!("./assets/index.html");
|
||||
Html(HTML_CONTENT)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
};
|
||||
use axum_extra::{
|
||||
TypedHeader,
|
||||
headers::{Authorization, authorization::Bearer},
|
||||
};
|
||||
use log::debug;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{
|
||||
auth::authenticate,
|
||||
responses::{ListVaultsResponse, VaultInfo},
|
||||
};
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
config::user_config::{AllowListedVaults, VaultAccess},
|
||||
errors::{SyncServerError, server_error, unauthenticated_error},
|
||||
};
|
||||
|
||||
const DEFAULT_LIMIT: usize = 50;
|
||||
const MAX_LIMIT: usize = 200;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct QueryParams {
|
||||
limit: Option<usize>,
|
||||
after: Option<String>,
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn list_vaults(
|
||||
auth_header: Option<TypedHeader<Authorization<Bearer>>>,
|
||||
Query(QueryParams { limit, after }): Query<QueryParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<ListVaultsResponse>, SyncServerError> {
|
||||
let auth_header = auth_header
|
||||
.ok_or_else(|| unauthenticated_error(anyhow::anyhow!("Missing Authorization header")))?;
|
||||
|
||||
let user = authenticate(&state, auth_header.token().trim())?;
|
||||
|
||||
debug!("User `{}` listing accessible vaults", user.name);
|
||||
|
||||
let existing_vaults = state.database.list_vaults().await.map_err(server_error)?;
|
||||
|
||||
let mut accessible: Vec<String> = match user.vault_access {
|
||||
VaultAccess::AllowAccessToAll => existing_vaults,
|
||||
VaultAccess::AllowList(AllowListedVaults { ref allowed }) => existing_vaults
|
||||
.into_iter()
|
||||
.filter(|v| allowed.contains(v))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
// Cursor-based pagination: skip vaults up to and including `after`
|
||||
if let Some(ref cursor) = after {
|
||||
accessible.retain(|v| v.as_str() > cursor.as_str());
|
||||
}
|
||||
|
||||
let limit = limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT);
|
||||
let has_more = accessible.len() > limit;
|
||||
accessible.truncate(limit);
|
||||
|
||||
let mut vaults = Vec::with_capacity(accessible.len());
|
||||
for name in accessible {
|
||||
let stats = state
|
||||
.database
|
||||
.get_vault_stats(&name)
|
||||
.await
|
||||
.map_err(server_error)?;
|
||||
vaults.push(VaultInfo {
|
||||
name,
|
||||
document_count: stats.document_count,
|
||||
created_at: stats.created_at,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Json(ListVaultsResponse {
|
||||
vaults,
|
||||
has_more,
|
||||
user_name: user.name,
|
||||
}))
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{self, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
|
|
@ -37,35 +36,6 @@ pub struct FetchLatestDocumentsResponse {
|
|||
pub last_update_id: VaultUpdateId,
|
||||
}
|
||||
|
||||
/// Response to a vault history request (paginated).
|
||||
#[derive(TS, Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct VaultHistoryResponse {
|
||||
pub versions: Vec<DocumentVersionWithoutContent>,
|
||||
pub has_more: bool,
|
||||
}
|
||||
|
||||
/// Summary of a single vault returned by the list-vaults endpoint.
|
||||
#[derive(TS, Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct VaultInfo {
|
||||
pub name: String,
|
||||
pub document_count: u32,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Response to listing vaults accessible to the authenticated user.
|
||||
#[derive(TS, Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct ListVaultsResponse {
|
||||
pub vaults: Vec<VaultInfo>,
|
||||
pub has_more: bool,
|
||||
pub user_name: String,
|
||||
}
|
||||
|
||||
/// Response to a create/update document request.
|
||||
#[derive(TS, Debug, Clone, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue