chore(release): secrets-mcp 0.4.0
Bump version for the N:N entry_secrets data model and related MCP/Web changes. Remove superseded SQL migration artifacts; rely on auto-migrate. Add structured errors, taxonomy normalization, and web i18n helpers. Made-with: Cursor
This commit is contained in:
@@ -5,11 +5,13 @@ use uuid::Uuid;
|
||||
|
||||
use crate::crypto;
|
||||
use crate::db;
|
||||
use crate::error::{AppError, DbErrorContext};
|
||||
use crate::models::{EntryRow, EntryWriteRow};
|
||||
use crate::service::add::{
|
||||
collect_field_paths, collect_key_paths, flatten_json_fields, infer_secret_type, insert_path,
|
||||
parse_key_path, parse_kv, remove_path,
|
||||
collect_field_paths, collect_key_paths, flatten_json_fields, insert_path, parse_key_path,
|
||||
parse_kv, remove_path,
|
||||
};
|
||||
use crate::taxonomy;
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct UpdateResult {
|
||||
@@ -35,6 +37,7 @@ pub struct UpdateParams<'a> {
|
||||
pub meta_entries: &'a [String],
|
||||
pub remove_meta: &'a [String],
|
||||
pub secret_entries: &'a [String],
|
||||
pub secret_types: &'a std::collections::HashMap<String, String>,
|
||||
pub remove_secrets: &'a [String],
|
||||
pub user_id: Option<Uuid>,
|
||||
}
|
||||
@@ -90,10 +93,7 @@ pub async fn run(
|
||||
let row = match rows.len() {
|
||||
0 => {
|
||||
tx.rollback().await?;
|
||||
anyhow::bail!(
|
||||
"Not found: '{}'. Use `add` to create it first.",
|
||||
params.name
|
||||
)
|
||||
return Err(AppError::NotFoundEntry.into());
|
||||
}
|
||||
1 => rows.into_iter().next().unwrap(),
|
||||
_ => {
|
||||
@@ -167,10 +167,7 @@ pub async fn run(
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
tx.rollback().await?;
|
||||
anyhow::bail!(
|
||||
"Concurrent modification detected for '{}'. Please retry.",
|
||||
params.name
|
||||
);
|
||||
return Err(AppError::ConcurrentModification.into());
|
||||
}
|
||||
|
||||
for entry in params.secret_entries {
|
||||
@@ -224,15 +221,21 @@ pub async fn run(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
} else {
|
||||
let secret_type = params
|
||||
.secret_types
|
||||
.get(field_name)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("text");
|
||||
let secret_id: Uuid = sqlx::query_scalar(
|
||||
"INSERT INTO secrets (user_id, name, type, encrypted) VALUES ($1, $2, $3, $4) RETURNING id",
|
||||
)
|
||||
.bind(params.user_id)
|
||||
.bind(field_name)
|
||||
.bind(infer_secret_type(field_name))
|
||||
.bind(field_name.to_string())
|
||||
.bind(secret_type)
|
||||
.bind(&encrypted)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| AppError::from_db_error(e, DbErrorContext::secret_name(field_name)))?;
|
||||
sqlx::query("INSERT INTO entry_secrets (entry_id, secret_id) VALUES ($1, $2)")
|
||||
.bind(row.id)
|
||||
.bind(secret_id)
|
||||
@@ -347,13 +350,13 @@ pub async fn update_fields_by_id(
|
||||
user_id: Uuid,
|
||||
params: UpdateEntryFieldsByIdParams<'_>,
|
||||
) -> Result<()> {
|
||||
if params.folder.len() > 128 {
|
||||
if params.folder.chars().count() > 128 {
|
||||
anyhow::bail!("folder must be at most 128 characters");
|
||||
}
|
||||
if params.entry_type.len() > 64 {
|
||||
if params.entry_type.chars().count() > 64 {
|
||||
anyhow::bail!("type must be at most 64 characters");
|
||||
}
|
||||
if params.name.len() > 256 {
|
||||
if params.name.chars().count() > 256 {
|
||||
anyhow::bail!("name must be at most 256 characters");
|
||||
}
|
||||
|
||||
@@ -372,7 +375,7 @@ pub async fn update_fields_by_id(
|
||||
Some(r) => r,
|
||||
None => {
|
||||
tx.rollback().await?;
|
||||
anyhow::bail!("Entry not found");
|
||||
return Err(AppError::NotFoundEntry.into());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -395,17 +398,25 @@ pub async fn update_fields_by_id(
|
||||
tracing::warn!(error = %e, "failed to snapshot entry history before web update");
|
||||
}
|
||||
|
||||
let mut metadata_map = match params.metadata {
|
||||
Value::Object(m) => m.clone(),
|
||||
_ => Map::new(),
|
||||
};
|
||||
let normalized_type =
|
||||
taxonomy::normalize_entry_type_and_metadata(params.entry_type, &mut metadata_map);
|
||||
let normalized_metadata = Value::Object(metadata_map);
|
||||
|
||||
let res = sqlx::query(
|
||||
"UPDATE entries SET folder = $1, type = $2, name = $3, notes = $4, tags = $5, metadata = $6, \
|
||||
version = version + 1, updated_at = NOW() \
|
||||
WHERE id = $7 AND version = $8",
|
||||
)
|
||||
.bind(params.folder)
|
||||
.bind(params.entry_type)
|
||||
.bind(&normalized_type)
|
||||
.bind(params.name)
|
||||
.bind(params.notes)
|
||||
.bind(params.tags)
|
||||
.bind(params.metadata)
|
||||
.bind(&normalized_metadata)
|
||||
.bind(row.id)
|
||||
.bind(row.version)
|
||||
.execute(&mut *tx)
|
||||
@@ -414,16 +425,17 @@ pub async fn update_fields_by_id(
|
||||
if let sqlx::Error::Database(ref d) = e
|
||||
&& d.code().as_deref() == Some("23505")
|
||||
{
|
||||
return anyhow::anyhow!(
|
||||
"An entry with this folder and name already exists for your account."
|
||||
);
|
||||
return AppError::ConflictEntryName {
|
||||
folder: params.folder.to_string(),
|
||||
name: params.name.to_string(),
|
||||
};
|
||||
}
|
||||
e.into()
|
||||
AppError::Internal(e.into())
|
||||
})?;
|
||||
|
||||
if res.rows_affected() == 0 {
|
||||
tx.rollback().await?;
|
||||
anyhow::bail!("Concurrent modification detected. Please refresh and try again.");
|
||||
return Err(AppError::ConcurrentModification.into());
|
||||
}
|
||||
|
||||
crate::audit::log_tx(
|
||||
@@ -431,7 +443,7 @@ pub async fn update_fields_by_id(
|
||||
Some(user_id),
|
||||
"update",
|
||||
params.folder,
|
||||
params.entry_type,
|
||||
&normalized_type,
|
||||
params.name,
|
||||
serde_json::json!({
|
||||
"source": "web",
|
||||
|
||||
Reference in New Issue
Block a user