Normalize template and script slug changes.

This commit is contained in:
2026-07-21 23:18:12 +02:00
parent e28d42d345
commit 8a6274ef6a
8 changed files with 312 additions and 41 deletions

View File

@@ -10,7 +10,7 @@ The project is under active development. Core blogging workflows are broadly ava
- Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, live link/backlink graphs, media, and batch gallery-image import. - Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, live link/backlink graphs, media, and batch gallery-image import.
- Media import, thumbnails, metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors. - Media import, thumbnails, metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors.
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping. - WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.
- Post, Liquid template, and Lua script editing with dedicated syntax highlighting, explicit syntax-check feedback, reference-safe template slug changes, and publish-time enforcement of the bDS2 Liquid tag/filter/operator subset, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms, including airplane-gated Git sync. - Post, Liquid template, and Lua script editing with dedicated syntax highlighting, explicit syntax-check feedback, normalized collision-safe template/script slug changes, reference-safe template renames, and publish-time enforcement of the bDS2 Liquid tag/filter/operator subset, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms, including airplane-gated Git sync.
- SQLite and filesystem persistence with frontmatter, relationship-safe media sidecars, rebuild, metadata diff/repair, stale post-path cleanup on republish, bDS2-compatible checksums and NFD slug generation, and FTS5 search. - SQLite and filesystem persistence with frontmatter, relationship-safe media sidecars, rebuild, metadata diff/repair, stale post-path cleanup on republish, bDS2-compatible checksums and NFD slug generation, and FTS5 search.
- Optional on-device multilingual semantic search and similar-post tag suggestions backed by a persistent USearch index, plus always-available partial-name tag autocomplete and duplicate-post review in the desktop workspace. - Optional on-device multilingual semantic search and similar-post tag suggestions backed by a persistent USearch index, plus always-available partial-name tag autocomplete and duplicate-post review in the desktop workspace.
- Read-only in-app browsers in the Help menu for the bundled global `DOCUMENTATION.md`, the generated Lua API reference with public types and runnable examples, the [CLI/server/TUI documentation](CLI.md), and the [MCP server documentation](MCP.md), with safe GFM rendering and confirmed external links. - Read-only in-app browsers in the Help menu for the bundled global `DOCUMENTATION.md`, the generated Lua API reference with public types and runnable examples, the [CLI/server/TUI documentation](CLI.md), and the [MCP server documentation](MCP.md), with safe GFM rendering and confirmed external links.

View File

@@ -8,6 +8,7 @@ use crate::db::queries::script as qs;
use crate::engine::{EngineError, EngineResult, domain_events}; use crate::engine::{EngineError, EngineResult, domain_events};
use crate::model::{DomainEntity, NotificationAction, Script, ScriptKind, ScriptStatus}; use crate::model::{DomainEntity, NotificationAction, Script, ScriptKind, ScriptStatus};
use crate::util::frontmatter::{ScriptFrontmatter, write_script_file}; use crate::util::frontmatter::{ScriptFrontmatter, write_script_file};
use crate::util::paths::script_file_path;
use crate::util::{atomic_write_str, ensure_unique, now_unix_ms, slugify}; use crate::util::{atomic_write_str, ensure_unique, now_unix_ms, slugify};
/// Create a new draft script. Content stored in DB, no file written. /// Create a new draft script. Content stored in DB, no file written.
@@ -64,6 +65,7 @@ pub fn create_script(
)] )]
pub fn update_script( pub fn update_script(
conn: &Connection, conn: &Connection,
data_dir: &Path,
script_id: &str, script_id: &str,
project_id: &str, project_id: &str,
title: Option<&str>, title: Option<&str>,
@@ -74,23 +76,28 @@ pub fn update_script(
content: Option<&str>, content: Option<&str>,
) -> EngineResult<Script> { ) -> EngineResult<Script> {
let mut script = qs::get_script_by_id(conn, script_id)?; let mut script = qs::get_script_by_id(conn, script_id)?;
if script.project_id != project_id {
return Err(EngineError::NotFound(format!("script {script_id}")));
}
let original_slug = script.slug.clone();
let original_file_path = script.file_path.clone();
// Slug uniqueness if let Some(requested_slug) = slug {
if let Some(new_slug) = slug let slug = slugify(requested_slug);
&& new_slug != script.slug let slug = if slug.is_empty() {
&& qs::get_script_by_slug(conn, project_id, new_slug).is_ok() "script".to_string()
{ } else {
return Err(EngineError::Conflict(format!( slug
"script slug '{new_slug}' already exists" };
))); script.slug = ensure_unique(&slug, |candidate| {
qs::get_script_by_slug(conn, project_id, candidate)
.is_ok_and(|existing| existing.id != script_id)
});
} }
if let Some(t) = title { if let Some(t) = title {
script.title = t.to_string(); script.title = t.to_string();
} }
if let Some(s) = slug {
script.slug = s.to_string();
}
if let Some(k) = kind { if let Some(k) = kind {
script.kind = k; script.kind = k;
} }
@@ -104,6 +111,16 @@ pub fn update_script(
script.content = Some(c.to_string()); script.content = Some(c.to_string());
} }
let slug_changed = script.slug != original_slug;
let published_body = if slug_changed && !original_file_path.is_empty() {
Some(read_published_script_body(data_dir, &original_file_path))
} else {
None
};
if published_body.is_some() {
script.file_path = script_file_path(&script.slug);
}
// If published, transition back to draft on edit // If published, transition back to draft on edit
if script.status == ScriptStatus::Published { if script.status == ScriptStatus::Published {
script.status = ScriptStatus::Draft; script.status = ScriptStatus::Draft;
@@ -112,6 +129,9 @@ pub fn update_script(
script.version += 1; script.version += 1;
script.updated_at = now_unix_ms(); script.updated_at = now_unix_ms();
qs::update_script(conn, &script)?; qs::update_script(conn, &script)?;
if let Some(body) = published_body {
rewrite_renamed_script_file(data_dir, &original_file_path, &script, &body)?;
}
emit_script(&script, NotificationAction::Updated); emit_script(&script, NotificationAction::Updated);
Ok(script) Ok(script)
} }
@@ -276,6 +296,46 @@ fn script_kind_to_frontmatter(kind: &ScriptKind) -> String {
} }
} }
fn read_published_script_body(data_dir: &Path, file_path: &str) -> String {
fs::read_to_string(data_dir.join(file_path))
.ok()
.and_then(|file| crate::util::frontmatter::read_script_file(&file).ok())
.map(|(_, body)| body)
.unwrap_or_default()
}
fn rewrite_renamed_script_file(
data_dir: &Path,
original_file_path: &str,
script: &Script,
body: &str,
) -> EngineResult<()> {
let frontmatter = ScriptFrontmatter {
id: script.id.clone(),
project_id: Some(script.project_id.clone()),
slug: script.slug.clone(),
title: script.title.clone(),
kind: script_kind_to_frontmatter(&script.kind),
entrypoint: script.entrypoint.clone(),
enabled: script.enabled,
version: script.version,
created_at: script.created_at,
updated_at: script.updated_at,
};
atomic_write_str(
&data_dir.join(&script.file_path),
&write_script_file(&frontmatter, body),
)?;
if original_file_path != script.file_path {
match fs::remove_file(data_dir.join(original_file_path)) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -339,10 +399,11 @@ mod tests {
#[test] #[test]
fn update_script_bumps_version() { fn update_script_bumps_version() {
let (db, _dir) = setup(); let (db, dir) = setup();
let s = create_script(db.conn(), "p1", "S", ScriptKind::Macro, "old", None).unwrap(); let s = create_script(db.conn(), "p1", "S", ScriptKind::Macro, "old", None).unwrap();
let updated = update_script( let updated = update_script(
db.conn(), db.conn(),
dir.path(),
&s.id, &s.id,
"p1", "p1",
Some("New"), Some("New"),
@@ -358,6 +419,83 @@ mod tests {
assert_eq!(updated.version, 2); assert_eq!(updated.version, 2);
} }
#[test]
fn update_script_slug_normalizes_and_uniquifies() {
let (db, dir) = setup();
create_script(db.conn(), "p1", "Alpha", ScriptKind::Utility, "", None).unwrap();
let script = create_script(db.conn(), "p1", "Beta", ScriptKind::Utility, "", None).unwrap();
let updated = update_script(
db.conn(),
dir.path(),
&script.id,
"p1",
None,
Some(" Alpha! "),
None,
None,
None,
None,
)
.unwrap();
assert_eq!(updated.slug, "alpha-2");
assert!(updated.file_path.is_empty());
let unchanged = update_script(
db.conn(),
dir.path(),
&script.id,
"p1",
None,
Some(" Alpha 2 "),
None,
None,
None,
None,
)
.unwrap();
assert_eq!(unchanged.slug, "alpha-2");
}
#[test]
fn update_published_script_slug_rewrites_and_renames_file() {
let (db, dir) = setup();
let script = create_script(
db.conn(),
"p1",
"Published Script",
ScriptKind::Utility,
"function main()\nend",
None,
)
.unwrap();
let published = publish_script(db.conn(), dir.path(), &script.id).unwrap();
let old_path = dir.path().join(&published.file_path);
let updated = update_script(
db.conn(),
dir.path(),
&script.id,
"p1",
None,
Some(" Renamed Script! "),
None,
None,
None,
None,
)
.unwrap();
assert_eq!(updated.slug, "renamed-script");
assert_eq!(updated.file_path, "scripts/renamed-script.lua");
assert!(!old_path.exists());
let new_file = fs::read_to_string(dir.path().join(&updated.file_path)).unwrap();
let (frontmatter, body) = crate::util::frontmatter::read_script_file(&new_file).unwrap();
assert_eq!(frontmatter.slug, "renamed-script");
assert_eq!(body, "function main()\nend");
}
#[test] #[test]
fn save_script_updates_content() { fn save_script_updates_content() {
let (db, _dir) = setup(); let (db, _dir) = setup();

View File

@@ -12,6 +12,7 @@ use crate::model::{
DomainEntity, NotificationAction, Post, Tag, Template, TemplateKind, TemplateStatus, DomainEntity, NotificationAction, Post, Tag, Template, TemplateKind, TemplateStatus,
}; };
use crate::util::frontmatter::{TemplateFrontmatter, write_template_file}; use crate::util::frontmatter::{TemplateFrontmatter, write_template_file};
use crate::util::paths::template_file_path;
use crate::util::{atomic_write_str, ensure_unique, now_unix_ms, slugify}; use crate::util::{atomic_write_str, ensure_unique, now_unix_ms, slugify};
const ALLOWED_LIQUID_TAGS: &[&str] = &[ const ALLOWED_LIQUID_TAGS: &[&str] = &[
@@ -89,23 +90,24 @@ pub fn update_template(
return Err(EngineError::NotFound(format!("template {template_id}"))); return Err(EngineError::NotFound(format!("template {template_id}")));
} }
let original_slug = tpl.slug.clone(); let original_slug = tpl.slug.clone();
let original_file_path = tpl.file_path.clone();
// Slug uniqueness check if let Some(requested_slug) = slug {
if let Some(new_slug) = slug let slug = slugify(requested_slug);
&& new_slug != tpl.slug let slug = if slug.is_empty() {
&& qt::get_template_by_slug(conn, project_id, new_slug).is_ok() "template".to_string()
{ } else {
return Err(EngineError::Conflict(format!( slug
"template slug '{new_slug}' already exists" };
))); tpl.slug = ensure_unique(&slug, |candidate| {
qt::get_template_by_slug(conn, project_id, candidate)
.is_ok_and(|existing| existing.id != template_id)
});
} }
if let Some(t) = title { if let Some(t) = title {
tpl.title = t.to_string(); tpl.title = t.to_string();
} }
if let Some(s) = slug {
tpl.slug = s.to_string();
}
if let Some(k) = kind { if let Some(k) = kind {
tpl.kind = k; tpl.kind = k;
} }
@@ -127,6 +129,14 @@ pub fn update_template(
} }
let slug_changed = tpl.slug != original_slug; let slug_changed = tpl.slug != original_slug;
let published_body = if slug_changed && !original_file_path.is_empty() {
Some(read_published_template_body(data_dir, &original_file_path))
} else {
None
};
if published_body.is_some() {
tpl.file_path = template_file_path(&tpl.slug);
}
tpl.version += 1; tpl.version += 1;
tpl.updated_at = now_unix_ms(); tpl.updated_at = now_unix_ms();
conn.begin_savepoint()?; conn.begin_savepoint()?;
@@ -155,6 +165,16 @@ pub fn update_template(
} }
}; };
for post in &affected_posts {
crate::engine::post::rewrite_published_post(conn, data_dir, &post.id)?;
}
if slug_changed {
crate::engine::tag::rewrite_tags_json(conn, data_dir, &tpl.project_id)?;
}
if let Some(body) = published_body {
rewrite_renamed_template_file(data_dir, &original_file_path, &tpl, &body)?;
}
emit_template(&tpl, NotificationAction::Updated); emit_template(&tpl, NotificationAction::Updated);
for post in &affected_posts { for post in &affected_posts {
domain_events::entity_changed( domain_events::entity_changed(
@@ -172,13 +192,6 @@ pub fn update_template(
NotificationAction::Updated, NotificationAction::Updated,
); );
} }
for post in &affected_posts {
crate::engine::post::rewrite_published_post(conn, data_dir, &post.id)?;
}
if slug_changed {
crate::engine::tag::rewrite_tags_json(conn, data_dir, &tpl.project_id)?;
}
Ok(tpl) Ok(tpl)
} }
@@ -361,6 +374,45 @@ fn template_kind_to_frontmatter(kind: &TemplateKind) -> String {
} }
} }
fn read_published_template_body(data_dir: &Path, file_path: &str) -> String {
fs::read_to_string(data_dir.join(file_path))
.ok()
.and_then(|file| crate::util::frontmatter::read_template_file(&file).ok())
.map(|(_, body)| body)
.unwrap_or_default()
}
fn rewrite_renamed_template_file(
data_dir: &Path,
original_file_path: &str,
template: &Template,
body: &str,
) -> EngineResult<()> {
let frontmatter = TemplateFrontmatter {
id: template.id.clone(),
project_id: Some(template.project_id.clone()),
slug: template.slug.clone(),
title: template.title.clone(),
kind: template_kind_to_frontmatter(&template.kind),
enabled: template.enabled,
version: template.version,
created_at: template.created_at,
updated_at: template.updated_at,
};
atomic_write_str(
&data_dir.join(&template.file_path),
&write_template_file(&frontmatter, body),
)?;
if original_file_path != template.file_path {
match fs::remove_file(data_dir.join(original_file_path)) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
}
Ok(())
}
fn validate_liquid_subset(content: &str) -> Result<(), String> { fn validate_liquid_subset(content: &str) -> Result<(), String> {
let mut cursor = 0; let mut cursor = 0;
while let Some((offset, is_tag)) = next_liquid_markup(&content[cursor..]) { while let Some((offset, is_tag)) = next_liquid_markup(&content[cursor..]) {
@@ -689,22 +741,74 @@ mod tests {
} }
#[test] #[test]
fn update_template_slug_conflict() { fn update_template_slug_normalizes_and_uniquifies() {
let (db, dir) = setup(); let (db, dir) = setup();
create_template(db.conn(), "p1", "Alpha", TemplateKind::Post, "").unwrap(); create_template(db.conn(), "p1", "Alpha", TemplateKind::Post, "").unwrap();
let t2 = create_template(db.conn(), "p1", "Beta", TemplateKind::Post, "").unwrap(); let t2 = create_template(db.conn(), "p1", "Beta", TemplateKind::Post, "").unwrap();
let result = update_template( let updated = update_template(
db.conn(), db.conn(),
dir.path(), dir.path(),
&t2.id, &t2.id,
"p1", "p1",
None, None,
Some("alpha"), Some(" Alpha! "),
None, None,
None, None,
None, None,
); )
assert!(result.is_err()); .unwrap();
assert_eq!(updated.slug, "alpha-2");
assert!(updated.file_path.is_empty());
let unchanged = update_template(
db.conn(),
dir.path(),
&t2.id,
"p1",
None,
Some(" Alpha 2 "),
None,
None,
None,
)
.unwrap();
assert_eq!(unchanged.slug, "alpha-2");
}
#[test]
fn update_published_template_slug_rewrites_and_renames_file() {
let (db, dir) = setup();
let template = create_template(
db.conn(),
"p1",
"Published Template",
TemplateKind::Post,
"<article>{{ title }}</article>",
)
.unwrap();
let published = publish_template(db.conn(), dir.path(), &template.id).unwrap();
let old_path = dir.path().join(&published.file_path);
let updated = update_template(
db.conn(),
dir.path(),
&template.id,
"p1",
None,
Some(" Renamed Template! "),
None,
None,
None,
)
.unwrap();
assert_eq!(updated.slug, "renamed-template");
assert_eq!(updated.file_path, "templates/renamed-template.liquid");
assert!(!old_path.exists());
let new_file = fs::read_to_string(dir.path().join(&updated.file_path)).unwrap();
let (frontmatter, body) = crate::util::frontmatter::read_template_file(&new_file).unwrap();
assert_eq!(frontmatter.slug, "renamed-template");
assert_eq!(body, "<article>{{ title }}</article>");
} }
#[test] #[test]

View File

@@ -894,6 +894,7 @@ impl CoreHost {
let data = object_arg(args, 1)?; let data = object_arg(args, 1)?;
public_script(engine::script::update_script( public_script(engine::script::update_script(
db.conn(), db.conn(),
&self.data_dir,
id, id,
&self.project_id, &self.project_id,
optional_string_field(data, "title"), optional_string_field(data, "title"),

View File

@@ -1246,6 +1246,7 @@ impl TuiApp {
let project_id = self.project_id()?.to_owned(); let project_id = self.project_id()?.to_owned();
engine::script::update_script( engine::script::update_script(
db.conn(), db.conn(),
self.data_dir()?,
&id, &id,
&project_id, &project_id,
Some(&title), Some(&title),

View File

@@ -818,12 +818,14 @@ fn save_template_editor_state_impl(
fn save_script_editor_state_impl( fn save_script_editor_state_impl(
db: &Database, db: &Database,
data_dir: &Path,
project_id: &str, project_id: &str,
state: &ScriptEditorState, state: &ScriptEditorState,
) -> Result<Script, String> { ) -> Result<Script, String> {
engine::script::validate_script_syntax(&state.content)?; engine::script::validate_script_syntax(&state.content)?;
engine::script::update_script( engine::script::update_script(
db.conn(), db.conn(),
data_dir,
&state.script_id, &state.script_id,
project_id, project_id,
Some(&state.title), Some(&state.title),
@@ -6660,7 +6662,11 @@ impl BdsApp {
return Task::none(); return Task::none();
}; };
match save_script_editor_state_impl(db, &project.id, state) { let Some(ref data_dir) = self.data_dir else {
return Task::none();
};
match save_script_editor_state_impl(db, data_dir, &project.id, state) {
Ok(script) => { Ok(script) => {
let s = self.script_editors.get_mut(script_id).unwrap(); let s = self.script_editors.get_mut(script_id).unwrap();
s.is_dirty = false; s.is_dirty = false;
@@ -11342,15 +11348,18 @@ mod tests {
bds_core::db::queries::template::get_template_by_id(db.conn(), &created.id).unwrap(); bds_core::db::queries::template::get_template_by_id(db.conn(), &created.id).unwrap();
let mut editor = TemplateEditorState::from_template(&template_record); let mut editor = TemplateEditorState::from_template(&template_record);
editor.title = "Updated Template".to_string(); editor.title = "Updated Template".to_string();
editor.slug = " Updated Template! ".to_string();
editor.content = "<main>{{ title }}</main>".to_string(); editor.content = "<main>{{ title }}</main>".to_string();
let saved_template = let saved_template =
save_template_editor_state_impl(&db, tmp.path(), &project.id, &editor).unwrap(); save_template_editor_state_impl(&db, tmp.path(), &project.id, &editor).unwrap();
assert_eq!(saved_template.title, "Updated Template"); assert_eq!(saved_template.title, "Updated Template");
assert_eq!(saved_template.slug, "updated-template");
let saved = let saved =
bds_core::db::queries::template::get_template_by_id(db.conn(), &created.id).unwrap(); bds_core::db::queries::template::get_template_by_id(db.conn(), &created.id).unwrap();
assert_eq!(saved.title, "Updated Template"); assert_eq!(saved.title, "Updated Template");
assert_eq!(saved.slug, "updated-template");
assert_eq!(saved.content.as_deref(), Some("<main>{{ title }}</main>")); assert_eq!(saved.content.as_deref(), Some("<main>{{ title }}</main>"));
} }
@@ -11385,7 +11394,7 @@ mod tests {
#[test] #[test]
fn script_editor_save_flow_persists_changes() { fn script_editor_save_flow_persists_changes() {
let (db, project, _tmp) = setup(); let (db, project, tmp) = setup();
let created = script::create_script( let created = script::create_script(
db.conn(), db.conn(),
&project.id, &project.id,
@@ -11400,15 +11409,19 @@ mod tests {
bds_core::db::queries::script::get_script_by_id(db.conn(), &created.id).unwrap(); bds_core::db::queries::script::get_script_by_id(db.conn(), &created.id).unwrap();
let mut editor = ScriptEditorState::from_script(&script_record); let mut editor = ScriptEditorState::from_script(&script_record);
editor.title = "Updated Script".to_string(); editor.title = "Updated Script".to_string();
editor.slug = " Updated Script! ".to_string();
editor.content = "function main()\n return 'lua'\nend".to_string(); editor.content = "function main()\n return 'lua'\nend".to_string();
editor.entrypoint = "main".to_string(); editor.entrypoint = "main".to_string();
let saved_script = save_script_editor_state_impl(&db, &project.id, &editor).unwrap(); let saved_script =
save_script_editor_state_impl(&db, tmp.path(), &project.id, &editor).unwrap();
assert_eq!(saved_script.title, "Updated Script"); assert_eq!(saved_script.title, "Updated Script");
assert_eq!(saved_script.slug, "updated-script");
let saved = let saved =
bds_core::db::queries::script::get_script_by_id(db.conn(), &created.id).unwrap(); bds_core::db::queries::script::get_script_by_id(db.conn(), &created.id).unwrap();
assert_eq!(saved.title, "Updated Script"); assert_eq!(saved.title, "Updated Script");
assert_eq!(saved.slug, "updated-script");
assert_eq!( assert_eq!(
saved.content.as_deref(), saved.content.as_deref(),
Some("function main()\n return 'lua'\nend") Some("function main()\n return 'lua'\nend")
@@ -11417,7 +11430,7 @@ mod tests {
#[test] #[test]
fn script_editor_save_rejects_invalid_content() { fn script_editor_save_rejects_invalid_content() {
let (db, project, _tmp) = setup(); let (db, project, tmp) = setup();
let created = script::create_script( let created = script::create_script(
db.conn(), db.conn(),
&project.id, &project.id,
@@ -11433,7 +11446,8 @@ mod tests {
let mut editor = ScriptEditorState::from_script(&script_record); let mut editor = ScriptEditorState::from_script(&script_record);
editor.content = "function main()\n return 'oops'".to_string(); editor.content = "function main()\n return 'oops'".to_string();
let error = save_script_editor_state_impl(&db, &project.id, &editor).unwrap_err(); let error =
save_script_editor_state_impl(&db, tmp.path(), &project.id, &editor).unwrap_err();
assert!(error.contains("end") || error.contains("unclosed") || error.contains("missing")); assert!(error.contains("end") || error.contains("unclosed") || error.contains("missing"));
let saved = let saved =
@@ -12775,6 +12789,7 @@ mod tests {
script::update_script( script::update_script(
app.db.as_ref().unwrap().conn(), app.db.as_ref().unwrap().conn(),
tmp.path(),
&created.id, &created.id,
&project.id, &project.id,
Some("Remote title"), Some("Remote title"),

View File

@@ -224,6 +224,12 @@ rule CreateScript {
rule UpdateScript { rule UpdateScript {
when: UpdateScriptRequested(script, changes) when: UpdateScriptRequested(script, changes)
ensures: ScriptFieldsUpdated(script, changes) ensures: ScriptFieldsUpdated(script, changes)
if changes.slug != null:
ensures: script.slug = unique_slug(slugify(changes.slug), excluding: script)
if script.file_path != "":
ensures: script.file_path = format("scripts/{slug}.lua", slug: script.slug)
ensures: ScriptFileWritten(script)
ensures: PreviousScriptFileDeleted(script)
ensures: script.updated_at = now ensures: script.updated_at = now
ensures: script.version = script.version + 1 ensures: script.version = script.version + 1
} }

View File

@@ -129,6 +129,12 @@ rule CreateAndPublishTemplate {
rule UpdateTemplate { rule UpdateTemplate {
when: UpdateTemplateRequested(template, changes) when: UpdateTemplateRequested(template, changes)
ensures: TemplateFieldsUpdated(template, changes) ensures: TemplateFieldsUpdated(template, changes)
if changes.slug != null:
ensures: template.slug = unique_slug(slugify(changes.slug), excluding: template)
if template.file_path != "":
ensures: template.file_path = format("templates/{slug}.liquid", slug: template.slug)
ensures: TemplateFileWritten(template)
ensures: PreviousTemplateFileDeleted(template)
ensures: template.updated_at = now ensures: template.updated_at = now
ensures: template.version = template.version + 1 ensures: template.version = template.version + 1
} }