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:
@@ -8,10 +8,23 @@ use crate::models::{Entry, SecretField};
|
||||
|
||||
pub const FETCH_ALL_LIMIT: u32 = 100_000;
|
||||
|
||||
/// Build an ILIKE pattern for fuzzy matching, escaping `%` and `_` literals.
|
||||
pub fn ilike_pattern(value: &str) -> String {
|
||||
format!(
|
||||
"%{}%",
|
||||
value
|
||||
.replace('\\', "\\\\")
|
||||
.replace('%', "\\%")
|
||||
.replace('_', "\\_")
|
||||
)
|
||||
}
|
||||
|
||||
pub struct SearchParams<'a> {
|
||||
pub folder: Option<&'a str>,
|
||||
pub entry_type: Option<&'a str>,
|
||||
pub name: Option<&'a str>,
|
||||
/// Fuzzy match on `entries.name` only (ILIKE with escaped `%`/`_`).
|
||||
pub name_query: Option<&'a str>,
|
||||
pub tags: &'a [String],
|
||||
pub query: Option<&'a str>,
|
||||
pub sort: &'a str,
|
||||
@@ -51,11 +64,15 @@ pub async fn count_entries(pool: &PgPool, a: &SearchParams<'_>) -> Result<i64> {
|
||||
if let Some(v) = a.name {
|
||||
q = q.bind(v);
|
||||
}
|
||||
if let Some(v) = a.name_query {
|
||||
let pattern = ilike_pattern(v);
|
||||
q = q.bind(pattern);
|
||||
}
|
||||
for tag in a.tags {
|
||||
q = q.bind(tag);
|
||||
}
|
||||
if let Some(v) = a.query {
|
||||
let pattern = format!("%{}%", v.replace('%', "\\%").replace('_', "\\_"));
|
||||
let pattern = ilike_pattern(v);
|
||||
q = q.bind(pattern);
|
||||
}
|
||||
let n = q.fetch_one(pool).await?;
|
||||
@@ -86,6 +103,10 @@ fn entry_where_clause_and_next_idx(a: &SearchParams<'_>) -> (String, i32) {
|
||||
conditions.push(format!("name = ${}", idx));
|
||||
idx += 1;
|
||||
}
|
||||
if a.name_query.is_some() {
|
||||
conditions.push(format!("name ILIKE ${} ESCAPE '\\'", idx));
|
||||
idx += 1;
|
||||
}
|
||||
if !a.tags.is_empty() {
|
||||
let placeholders: Vec<String> = a
|
||||
.tags
|
||||
@@ -135,6 +156,7 @@ pub async fn run(pool: &PgPool, params: SearchParams<'_>) -> Result<SearchResult
|
||||
}
|
||||
|
||||
/// Fetch entries matching the given filters — returns all matching entries up to FETCH_ALL_LIMIT.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn fetch_entries(
|
||||
pool: &PgPool,
|
||||
folder: Option<&str>,
|
||||
@@ -148,6 +170,7 @@ pub async fn fetch_entries(
|
||||
folder,
|
||||
entry_type,
|
||||
name,
|
||||
name_query: None,
|
||||
tags,
|
||||
query,
|
||||
sort: "name",
|
||||
@@ -189,11 +212,15 @@ async fn fetch_entries_paged(pool: &PgPool, a: &SearchParams<'_>) -> Result<Vec<
|
||||
if let Some(v) = a.name {
|
||||
q = q.bind(v);
|
||||
}
|
||||
if let Some(v) = a.name_query {
|
||||
let pattern = ilike_pattern(v);
|
||||
q = q.bind(pattern);
|
||||
}
|
||||
for tag in a.tags {
|
||||
q = q.bind(tag);
|
||||
}
|
||||
if let Some(v) = a.query {
|
||||
let pattern = format!("%{}%", v.replace('%', "\\%").replace('_', "\\_"));
|
||||
let pattern = ilike_pattern(v);
|
||||
q = q.bind(pattern);
|
||||
}
|
||||
q = q.bind(a.limit as i64).bind(a.offset as i64);
|
||||
@@ -384,3 +411,13 @@ impl EntrySecretRow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ilike_pattern_escapes_backslash_percent_and_underscore() {
|
||||
assert_eq!(ilike_pattern(r"hello\_100%"), r"%hello\\\_100\%%");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user