diff --git a/migrations/postgres/20260527000000_script_description_limit.sql b/migrations/postgres/20260527000000_script_description_limit.sql new file mode 100644 index 0000000..6dd1ff9 --- /dev/null +++ b/migrations/postgres/20260527000000_script_description_limit.sql @@ -0,0 +1,2 @@ +ALTER TABLE scripts + ADD CONSTRAINT scripts_description_length CHECK (length(description) <= 300); diff --git a/migrations/sqlite/20260527000000_script_description_limit.sql b/migrations/sqlite/20260527000000_script_description_limit.sql new file mode 100644 index 0000000..4c880aa --- /dev/null +++ b/migrations/sqlite/20260527000000_script_description_limit.sql @@ -0,0 +1,19 @@ +-- SQLite doesn't support ADD CONSTRAINT on existing tables, so we +-- recreate with the check inline. + +CREATE TABLE scripts_new ( + id TEXT PRIMARY KEY, + body TEXT NOT NULL, + description TEXT CHECK (length(description) <= 300), + script_type TEXT NOT NULL DEFAULT 'lua', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +INSERT INTO scripts_new (id, body, description, script_type, created_at, updated_at) +SELECT id, body, substr(description, 1, 300), script_type, created_at, updated_at +FROM scripts; + +DROP TABLE scripts; + +ALTER TABLE scripts_new RENAME TO scripts; diff --git a/src/admin/scripts.rs b/src/admin/scripts.rs index aed2800..e8cca63 100644 --- a/src/admin/scripts.rs +++ b/src/admin/scripts.rs @@ -31,6 +31,8 @@ use crate::lua::{ParsedTrigger, ScriptLanguage}; use super::auth::UserAuth; use super::permissions::Permission; +const MAX_DESCRIPTION_LEN: usize = 300; + // --------------------------------------------------------------------------- // Wire types // --------------------------------------------------------------------------- @@ -162,6 +164,14 @@ pub(super) async fn upsert( // Validate the trigger id grammar up-front (400 with a clear message). let _trigger = ParsedTrigger::parse(&body.id).map_err(AppError::BadRequest)?; + if let Some(ref desc) = body.description + && desc.len() > MAX_DESCRIPTION_LEN + { + return Err(AppError::BadRequest(format!( + "description must be at most {MAX_DESCRIPTION_LEN} characters" + ))); + } + let script_type = body.script_type.unwrap_or_default(); validate_body_for_type(&body.body, script_type)?; @@ -257,6 +267,13 @@ pub(super) async fn patch( let lang = body.script_type.unwrap_or_default(); validate_body_for_type(new_body, lang)?; } + if let Some(Some(ref desc)) = body.description + && desc.len() > MAX_DESCRIPTION_LEN + { + return Err(AppError::BadRequest(format!( + "description must be at most {MAX_DESCRIPTION_LEN} characters" + ))); + } // Existence check + fetch current values. let existing = fetch_one(&state, &id).await?; diff --git a/web/src/app/dashboard/lexicons/[id]/lexicon-detail.tsx b/web/src/app/dashboard/lexicons/[id]/lexicon-detail.tsx index aad8c9e..0dd4721 100644 --- a/web/src/app/dashboard/lexicons/[id]/lexicon-detail.tsx +++ b/web/src/app/dashboard/lexicons/[id]/lexicon-detail.tsx @@ -288,11 +288,11 @@ function ScriptsTargetingPanel({ canManage: boolean; }) { const byId = new Map(scripts.map((s) => [s.id, s])); - const slots = entries.map((e) => ({ - ...e, - triggerId: `${e.kind}:${lexiconId}`, - exists: byId.has(`${e.kind}:${lexiconId}`), - })); + const slots = entries.map((e) => { + const triggerId = `${e.kind}:${lexiconId}`; + const script = byId.get(triggerId); + return { ...e, triggerId, script }; + }); return (