Release secrets-mcp 0.3.0: folder/type schema and MCP folder disambiguation
- Rename namespace/kind to folder/type on entries, audit_log, and history tables; add notes. Unique key is (user_id, folder, name). - Service layer and MCP tools support name-first lookup with optional folder when multiple entries share the same name. - secrets_delete dry_run uses the same disambiguation as real deletes. - Add scripts/migrate-v0.3.0.sql for manual DB migration. Refresh README and AGENTS.md. Made-with: Cursor
This commit is contained in:
@@ -13,9 +13,10 @@ use crate::service::add::{
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct UpdateResult {
|
||||
pub namespace: String,
|
||||
pub kind: String,
|
||||
pub name: String,
|
||||
pub folder: String,
|
||||
#[serde(rename = "type")]
|
||||
pub entry_type: String,
|
||||
pub add_tags: Vec<String>,
|
||||
pub remove_tags: Vec<String>,
|
||||
pub meta_keys: Vec<String>,
|
||||
@@ -25,9 +26,10 @@ pub struct UpdateResult {
|
||||
}
|
||||
|
||||
pub struct UpdateParams<'a> {
|
||||
pub namespace: &'a str,
|
||||
pub kind: &'a str,
|
||||
pub name: &'a str,
|
||||
/// Optional folder for disambiguation when multiple entries share the same name.
|
||||
pub folder: Option<&'a str>,
|
||||
pub notes: Option<&'a str>,
|
||||
pub add_tags: &'a [String],
|
||||
pub remove_tags: &'a [String],
|
||||
pub meta_entries: &'a [String],
|
||||
@@ -44,45 +46,76 @@ pub async fn run(
|
||||
) -> Result<UpdateResult> {
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
let row: Option<EntryRow> = if let Some(uid) = params.user_id {
|
||||
// Fetch matching rows with FOR UPDATE; use folder when provided to resolve ambiguity.
|
||||
let rows: Vec<EntryRow> = if let Some(uid) = params.user_id {
|
||||
if let Some(folder) = params.folder {
|
||||
sqlx::query_as(
|
||||
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
|
||||
WHERE user_id = $1 AND folder = $2 AND name = $3 FOR UPDATE",
|
||||
)
|
||||
.bind(uid)
|
||||
.bind(folder)
|
||||
.bind(params.name)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
|
||||
WHERE user_id = $1 AND name = $2 FOR UPDATE",
|
||||
)
|
||||
.bind(uid)
|
||||
.bind(params.name)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
}
|
||||
} else if let Some(folder) = params.folder {
|
||||
sqlx::query_as(
|
||||
"SELECT id, version, tags, metadata FROM entries \
|
||||
WHERE user_id = $1 AND namespace = $2 AND kind = $3 AND name = $4 FOR UPDATE",
|
||||
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
|
||||
WHERE user_id IS NULL AND folder = $1 AND name = $2 FOR UPDATE",
|
||||
)
|
||||
.bind(uid)
|
||||
.bind(params.namespace)
|
||||
.bind(params.kind)
|
||||
.bind(folder)
|
||||
.bind(params.name)
|
||||
.fetch_optional(&mut *tx)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
"SELECT id, version, tags, metadata FROM entries \
|
||||
WHERE user_id IS NULL AND namespace = $1 AND kind = $2 AND name = $3 FOR UPDATE",
|
||||
"SELECT id, version, folder, type, tags, metadata, notes FROM entries \
|
||||
WHERE user_id IS NULL AND name = $1 FOR UPDATE",
|
||||
)
|
||||
.bind(params.namespace)
|
||||
.bind(params.kind)
|
||||
.bind(params.name)
|
||||
.fetch_optional(&mut *tx)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
};
|
||||
|
||||
let row = row.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Not found: [{}/{}] {}. Use `add` to create it first.",
|
||||
params.namespace,
|
||||
params.kind,
|
||||
params.name
|
||||
)
|
||||
})?;
|
||||
let row = match rows.len() {
|
||||
0 => {
|
||||
tx.rollback().await?;
|
||||
anyhow::bail!(
|
||||
"Not found: '{}'. Use `add` to create it first.",
|
||||
params.name
|
||||
)
|
||||
}
|
||||
1 => rows.into_iter().next().unwrap(),
|
||||
_ => {
|
||||
tx.rollback().await?;
|
||||
let folders: Vec<&str> = rows.iter().map(|r| r.folder.as_str()).collect();
|
||||
anyhow::bail!(
|
||||
"Ambiguous: {} entries named '{}' found in folders: [{}]. \
|
||||
Specify 'folder' to disambiguate.",
|
||||
rows.len(),
|
||||
params.name,
|
||||
folders.join(", ")
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = db::snapshot_entry_history(
|
||||
&mut tx,
|
||||
db::EntrySnapshotParams {
|
||||
entry_id: row.id,
|
||||
user_id: params.user_id,
|
||||
namespace: params.namespace,
|
||||
kind: params.kind,
|
||||
folder: &row.folder,
|
||||
entry_type: &row.entry_type,
|
||||
name: params.name,
|
||||
version: row.version,
|
||||
action: "update",
|
||||
@@ -117,12 +150,16 @@ pub async fn run(
|
||||
}
|
||||
let metadata = Value::Object(meta_map);
|
||||
|
||||
let new_notes = params.notes.unwrap_or(&row.notes);
|
||||
|
||||
let result = sqlx::query(
|
||||
"UPDATE entries SET tags = $1, metadata = $2, version = version + 1, updated_at = NOW() \
|
||||
WHERE id = $3 AND version = $4",
|
||||
"UPDATE entries SET tags = $1, metadata = $2, notes = $3, \
|
||||
version = version + 1, updated_at = NOW() \
|
||||
WHERE id = $4 AND version = $5",
|
||||
)
|
||||
.bind(&tags)
|
||||
.bind(&metadata)
|
||||
.bind(new_notes)
|
||||
.bind(row.id)
|
||||
.bind(row.version)
|
||||
.execute(&mut *tx)
|
||||
@@ -131,9 +168,7 @@ pub async fn run(
|
||||
if result.rows_affected() == 0 {
|
||||
tx.rollback().await?;
|
||||
anyhow::bail!(
|
||||
"Concurrent modification detected for [{}/{}] {}. Please retry.",
|
||||
params.namespace,
|
||||
params.kind,
|
||||
"Concurrent modification detected for '{}'. Please retry.",
|
||||
params.name
|
||||
);
|
||||
}
|
||||
@@ -243,8 +278,8 @@ pub async fn run(
|
||||
&mut tx,
|
||||
params.user_id,
|
||||
"update",
|
||||
params.namespace,
|
||||
params.kind,
|
||||
"",
|
||||
"",
|
||||
params.name,
|
||||
serde_json::json!({
|
||||
"add_tags": params.add_tags,
|
||||
@@ -260,9 +295,9 @@ pub async fn run(
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(UpdateResult {
|
||||
namespace: params.namespace.to_string(),
|
||||
kind: params.kind.to_string(),
|
||||
name: params.name.to_string(),
|
||||
folder: row.folder.clone(),
|
||||
entry_type: row.entry_type.clone(),
|
||||
add_tags: params.add_tags.to_vec(),
|
||||
remove_tags: params.remove_tags.to_vec(),
|
||||
meta_keys,
|
||||
|
||||
Reference in New Issue
Block a user