Normalize template and script slug changes.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user