chore: remove field_type and value_len from secrets schema
Some checks failed
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 3s
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 2m34s
Secrets CLI - Build & Release / Build (macOS aarch64 + x86_64) (push) Successful in 1m3s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 1m15s
Secrets CLI - Build & Release / 发布草稿 Release (push) Has been cancelled
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Some checks failed
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 3s
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 2m34s
Secrets CLI - Build & Release / Build (macOS aarch64 + x86_64) (push) Successful in 1m3s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 1m15s
Secrets CLI - Build & Release / 发布草稿 Release (push) Has been cancelled
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
- Drop field_type, value_len from secrets and secrets_history tables - Remove infer_field_type, compute_value_len from add.rs - Simplify search output to field names only - Update AGENTS.md, README.md documentation Bump version to 0.9.4 Made-with: Cursor
This commit is contained in:
@@ -161,28 +161,6 @@ pub(crate) fn remove_path(map: &mut Map<String, Value>, path: &[String]) -> Resu
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
// ── field_type inference and value_len ──────────────────────────────────────
|
||||
|
||||
/// Infer the field type string from a JSON value.
|
||||
pub(crate) fn infer_field_type(v: &Value) -> &'static str {
|
||||
match v {
|
||||
Value::String(_) => "string",
|
||||
Value::Number(_) => "number",
|
||||
Value::Bool(_) => "boolean",
|
||||
Value::Null => "string",
|
||||
Value::Array(_) | Value::Object(_) => "json",
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the plaintext length of a JSON value (chars for string, serialized length otherwise).
|
||||
pub(crate) fn compute_value_len(v: &Value) -> i32 {
|
||||
match v {
|
||||
Value::String(s) => s.chars().count() as i32,
|
||||
Value::Null => 0,
|
||||
other => other.to_string().chars().count() as i32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Flatten a (potentially nested) JSON object into dot-separated field entries.
|
||||
/// e.g. `{"credentials": {"type": "ssh", "content": "..."}}` →
|
||||
/// `[("credentials.type", "ssh"), ("credentials.content", "...")]`
|
||||
@@ -291,12 +269,10 @@ pub async fn run(pool: &PgPool, args: AddArgs<'_>, master_key: &[u8; 32]) -> Res
|
||||
struct ExistingField {
|
||||
id: uuid::Uuid,
|
||||
field_name: String,
|
||||
field_type: String,
|
||||
value_len: i32,
|
||||
encrypted: Vec<u8>,
|
||||
}
|
||||
let existing_fields: Vec<ExistingField> = sqlx::query_as(
|
||||
"SELECT id, field_name, field_type, value_len, encrypted \
|
||||
"SELECT id, field_name, encrypted \
|
||||
FROM secrets WHERE entry_id = $1",
|
||||
)
|
||||
.bind(entry_id)
|
||||
@@ -311,8 +287,6 @@ pub async fn run(pool: &PgPool, args: AddArgs<'_>, master_key: &[u8; 32]) -> Res
|
||||
secret_id: f.id,
|
||||
entry_version: new_entry_version - 1,
|
||||
field_name: &f.field_name,
|
||||
field_type: &f.field_type,
|
||||
value_len: f.value_len,
|
||||
encrypted: &f.encrypted,
|
||||
action: "add",
|
||||
},
|
||||
@@ -333,18 +307,14 @@ pub async fn run(pool: &PgPool, args: AddArgs<'_>, master_key: &[u8; 32]) -> Res
|
||||
// Insert new secret fields.
|
||||
let flat_fields = flatten_json_fields("", &secret_json);
|
||||
for (field_name, field_value) in &flat_fields {
|
||||
let field_type = infer_field_type(field_value);
|
||||
let value_len = compute_value_len(field_value);
|
||||
let encrypted = crypto::encrypt_json(master_key, field_value)?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO secrets (entry_id, field_name, field_type, value_len, encrypted) \
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
"INSERT INTO secrets (entry_id, field_name, encrypted) \
|
||||
VALUES ($1, $2, $3)",
|
||||
)
|
||||
.bind(entry_id)
|
||||
.bind(field_name)
|
||||
.bind(field_type)
|
||||
.bind(value_len)
|
||||
.bind(&encrypted)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -399,10 +369,7 @@ pub async fn run(pool: &PgPool, args: AddArgs<'_>, master_key: &[u8; 32]) -> Res
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
build_json, compute_value_len, flatten_json_fields, infer_field_type, key_path_to_string,
|
||||
parse_kv, remove_path,
|
||||
};
|
||||
use super::{build_json, flatten_json_fields, key_path_to_string, parse_kv, remove_path};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@@ -489,19 +456,4 @@ mod tests {
|
||||
assert_eq!(fields[1].0, "credentials.type");
|
||||
assert_eq!(fields[2].0, "username");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infer_field_types() {
|
||||
assert_eq!(infer_field_type(&Value::String("x".into())), "string");
|
||||
assert_eq!(infer_field_type(&serde_json::json!(42)), "number");
|
||||
assert_eq!(infer_field_type(&Value::Bool(true)), "boolean");
|
||||
assert_eq!(infer_field_type(&serde_json::json!(["a"])), "json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_value_len_string() {
|
||||
assert_eq!(compute_value_len(&Value::String("root".into())), 4);
|
||||
assert_eq!(compute_value_len(&Value::Null), 0);
|
||||
assert_eq!(compute_value_len(&serde_json::json!(1234)), 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ async fn snapshot_and_delete(
|
||||
}
|
||||
|
||||
let fields: Vec<SecretFieldRow> = sqlx::query_as(
|
||||
"SELECT id, field_name, field_type, value_len, encrypted \
|
||||
"SELECT id, field_name, encrypted \
|
||||
FROM secrets WHERE entry_id = $1",
|
||||
)
|
||||
.bind(row.id)
|
||||
@@ -272,8 +272,6 @@ async fn snapshot_and_delete(
|
||||
secret_id: f.id,
|
||||
entry_version: row.version,
|
||||
field_name: &f.field_name,
|
||||
field_type: &f.field_type,
|
||||
value_len: f.value_len,
|
||||
encrypted: &f.encrypted,
|
||||
action: "delete",
|
||||
},
|
||||
|
||||
@@ -71,14 +71,12 @@ pub async fn run(pool: &PgPool, args: RollbackArgs<'_>, master_key: &[u8; 32]) -
|
||||
struct SecretHistoryRow {
|
||||
secret_id: Uuid,
|
||||
field_name: String,
|
||||
field_type: String,
|
||||
value_len: i32,
|
||||
encrypted: Vec<u8>,
|
||||
action: String,
|
||||
}
|
||||
|
||||
let field_snaps: Vec<SecretHistoryRow> = sqlx::query_as(
|
||||
"SELECT secret_id, field_name, field_type, value_len, encrypted, action \
|
||||
"SELECT secret_id, field_name, encrypted, action \
|
||||
FROM secrets_history \
|
||||
WHERE entry_id = $1 AND entry_version = $2 \
|
||||
ORDER BY field_name",
|
||||
@@ -145,12 +143,10 @@ pub async fn run(pool: &PgPool, args: RollbackArgs<'_>, master_key: &[u8; 32]) -
|
||||
struct LiveField {
|
||||
id: Uuid,
|
||||
field_name: String,
|
||||
field_type: String,
|
||||
value_len: i32,
|
||||
encrypted: Vec<u8>,
|
||||
}
|
||||
let live_fields: Vec<LiveField> = sqlx::query_as(
|
||||
"SELECT id, field_name, field_type, value_len, encrypted \
|
||||
"SELECT id, field_name, encrypted \
|
||||
FROM secrets WHERE entry_id = $1",
|
||||
)
|
||||
.bind(lr.id)
|
||||
@@ -165,8 +161,6 @@ pub async fn run(pool: &PgPool, args: RollbackArgs<'_>, master_key: &[u8; 32]) -
|
||||
secret_id: f.id,
|
||||
entry_version: lr.version,
|
||||
field_name: &f.field_name,
|
||||
field_type: &f.field_type,
|
||||
value_len: f.value_len,
|
||||
encrypted: &f.encrypted,
|
||||
action: "rollback",
|
||||
},
|
||||
@@ -212,11 +206,9 @@ pub async fn run(pool: &PgPool, args: RollbackArgs<'_>, master_key: &[u8; 32]) -
|
||||
continue;
|
||||
}
|
||||
sqlx::query(
|
||||
"INSERT INTO secrets (id, entry_id, field_name, field_type, value_len, encrypted) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6) \
|
||||
"INSERT INTO secrets (id, entry_id, field_name, encrypted) \
|
||||
VALUES ($1, $2, $3, $4) \
|
||||
ON CONFLICT (entry_id, field_name) DO UPDATE SET \
|
||||
field_type = EXCLUDED.field_type, \
|
||||
value_len = EXCLUDED.value_len, \
|
||||
encrypted = EXCLUDED.encrypted, \
|
||||
version = secrets.version + 1, \
|
||||
updated_at = NOW()",
|
||||
@@ -224,8 +216,6 @@ pub async fn run(pool: &PgPool, args: RollbackArgs<'_>, master_key: &[u8; 32]) -
|
||||
.bind(f.secret_id)
|
||||
.bind(snap.entry_id)
|
||||
.bind(&f.field_name)
|
||||
.bind(&f.field_type)
|
||||
.bind(f.value_len)
|
||||
.bind(&f.encrypted)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -250,8 +250,8 @@ async fn fetch_entries_paged(pool: &PgPool, a: PagedFetchArgs<'_>) -> Result<Vec
|
||||
|
||||
// ── Secret schema fetching (no master key) ───────────────────────────────────
|
||||
|
||||
/// Fetch secret field schemas (field_name, field_type, value_len) for a set of entry ids.
|
||||
/// Returns a map from entry_id to list of SecretField (encrypted field not used here).
|
||||
/// Fetch secret field names for a set of entry ids.
|
||||
/// Returns a map from entry_id to list of SecretField.
|
||||
async fn fetch_secret_schemas(
|
||||
pool: &PgPool,
|
||||
entry_ids: &[uuid::Uuid],
|
||||
@@ -423,8 +423,6 @@ fn to_json(entry: &Entry, summary: bool, schema: Option<&[SecretField]>) -> Valu
|
||||
.map(|f| {
|
||||
json!({
|
||||
"field_name": f.field_name,
|
||||
"field_type": f.field_type,
|
||||
"value_len": f.value_len,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -474,10 +472,7 @@ fn print_text(entry: &Entry, summary: bool, schema: Option<&[SecretField]>) -> R
|
||||
}
|
||||
match schema {
|
||||
Some(fields) if !fields.is_empty() => {
|
||||
let schema_str: Vec<String> = fields
|
||||
.iter()
|
||||
.map(|f| format!("{}: {}({})", f.field_name, f.field_type, f.value_len))
|
||||
.collect();
|
||||
let schema_str: Vec<String> = fields.iter().map(|f| f.field_name.clone()).collect();
|
||||
println!(" secrets: {}", schema_str.join(", "));
|
||||
println!(" (use `secrets inject` or `secrets run` to get values)");
|
||||
}
|
||||
@@ -556,8 +551,6 @@ mod tests {
|
||||
id: Uuid::nil(),
|
||||
entry_id: Uuid::nil(),
|
||||
field_name: "token".to_string(),
|
||||
field_type: "string".to_string(),
|
||||
value_len: 6,
|
||||
encrypted: enc,
|
||||
version: 1,
|
||||
created_at: Utc::now(),
|
||||
@@ -597,8 +590,6 @@ mod tests {
|
||||
let secrets = v.get("secrets").unwrap().as_array().unwrap();
|
||||
assert_eq!(secrets.len(), 1);
|
||||
assert_eq!(secrets[0]["field_name"], "token");
|
||||
assert_eq!(secrets[0]["field_type"], "string");
|
||||
assert_eq!(secrets[0]["value_len"], 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,8 +4,8 @@ use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::add::{
|
||||
collect_field_paths, collect_key_paths, compute_value_len, flatten_json_fields,
|
||||
infer_field_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::crypto;
|
||||
use crate::db;
|
||||
@@ -130,20 +130,16 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>, master_key: &[u8; 32]) ->
|
||||
});
|
||||
|
||||
for (field_name, fv) in &flat {
|
||||
let field_type = infer_field_type(fv);
|
||||
let value_len = compute_value_len(fv);
|
||||
let encrypted = crypto::encrypt_json(master_key, fv)?;
|
||||
|
||||
// Snapshot existing field before replacing.
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct ExistingField {
|
||||
id: Uuid,
|
||||
field_type: String,
|
||||
value_len: i32,
|
||||
encrypted: Vec<u8>,
|
||||
}
|
||||
let existing_field: Option<ExistingField> = sqlx::query_as(
|
||||
"SELECT id, field_type, value_len, encrypted \
|
||||
"SELECT id, encrypted \
|
||||
FROM secrets WHERE entry_id = $1 AND field_name = $2",
|
||||
)
|
||||
.bind(row.id)
|
||||
@@ -159,8 +155,6 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>, master_key: &[u8; 32]) ->
|
||||
secret_id: ef.id,
|
||||
entry_version: row.version,
|
||||
field_name,
|
||||
field_type: &ef.field_type,
|
||||
value_len: ef.value_len,
|
||||
encrypted: &ef.encrypted,
|
||||
action: "update",
|
||||
},
|
||||
@@ -171,19 +165,15 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>, master_key: &[u8; 32]) ->
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO secrets (entry_id, field_name, field_type, value_len, encrypted) \
|
||||
VALUES ($1, $2, $3, $4, $5) \
|
||||
"INSERT INTO secrets (entry_id, field_name, encrypted) \
|
||||
VALUES ($1, $2, $3) \
|
||||
ON CONFLICT (entry_id, field_name) DO UPDATE SET \
|
||||
field_type = EXCLUDED.field_type, \
|
||||
value_len = EXCLUDED.value_len, \
|
||||
encrypted = EXCLUDED.encrypted, \
|
||||
version = secrets.version + 1, \
|
||||
updated_at = NOW()",
|
||||
)
|
||||
.bind(row.id)
|
||||
.bind(field_name)
|
||||
.bind(field_type)
|
||||
.bind(value_len)
|
||||
.bind(&encrypted)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -200,12 +190,10 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>, master_key: &[u8; 32]) ->
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct FieldToDelete {
|
||||
id: Uuid,
|
||||
field_type: String,
|
||||
value_len: i32,
|
||||
encrypted: Vec<u8>,
|
||||
}
|
||||
let field: Option<FieldToDelete> = sqlx::query_as(
|
||||
"SELECT id, field_type, value_len, encrypted \
|
||||
"SELECT id, encrypted \
|
||||
FROM secrets WHERE entry_id = $1 AND field_name = $2",
|
||||
)
|
||||
.bind(row.id)
|
||||
@@ -221,8 +209,6 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>, master_key: &[u8; 32]) ->
|
||||
secret_id: f.id,
|
||||
entry_version: new_version,
|
||||
field_name: &field_name,
|
||||
field_type: &f.field_type,
|
||||
value_len: f.value_len,
|
||||
encrypted: &f.encrypted,
|
||||
action: "delete",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user