Complete forced template deletion
This commit is contained in:
@@ -338,8 +338,8 @@ pub fn delete_template(
|
||||
let tpl = qt::get_template_by_id(conn, template_id)?;
|
||||
|
||||
// Check references
|
||||
let referencing_posts = count_posts_using_template(conn, &tpl.slug)?;
|
||||
let referencing_tags = count_tags_using_template(conn, &tpl.slug)?;
|
||||
let referencing_posts = count_posts_using_template(conn, &tpl.project_id, &tpl.slug)?;
|
||||
let referencing_tags = count_tags_using_template(conn, &tpl.project_id, &tpl.slug)?;
|
||||
|
||||
if (referencing_posts > 0 || referencing_tags > 0) && !force {
|
||||
return Err(EngineError::Conflict(format!(
|
||||
@@ -348,15 +348,16 @@ pub fn delete_template(
|
||||
)));
|
||||
}
|
||||
|
||||
// Force: null out references
|
||||
if force {
|
||||
if referencing_posts > 0 {
|
||||
null_template_slug_on_posts(conn, &tpl.slug)?;
|
||||
let (affected_posts, affected_tags) = if force {
|
||||
let affected = clear_template_references(conn, &tpl.project_id, &tpl.slug, now_unix_ms())?;
|
||||
for post in &affected.0 {
|
||||
crate::engine::post::rewrite_published_post(conn, data_dir, &post.id)?;
|
||||
}
|
||||
if referencing_tags > 0 {
|
||||
null_template_slug_on_tags(conn, &tpl.slug)?;
|
||||
}
|
||||
}
|
||||
crate::engine::tag::rewrite_tags_json(conn, data_dir, &tpl.project_id)?;
|
||||
affected
|
||||
} else {
|
||||
(Vec::new(), Vec::new())
|
||||
};
|
||||
|
||||
// Delete file if exists
|
||||
if !tpl.file_path.is_empty() {
|
||||
@@ -368,6 +369,22 @@ pub fn delete_template(
|
||||
|
||||
qt::delete_template(conn, template_id)?;
|
||||
emit_template(&tpl, NotificationAction::Deleted);
|
||||
for post in &affected_posts {
|
||||
domain_events::entity_changed(
|
||||
&post.project_id,
|
||||
DomainEntity::Post,
|
||||
&post.id,
|
||||
NotificationAction::Updated,
|
||||
);
|
||||
}
|
||||
for tag in &affected_tags {
|
||||
domain_events::entity_changed(
|
||||
&tag.project_id,
|
||||
DomainEntity::Tag,
|
||||
&tag.id,
|
||||
NotificationAction::Updated,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -595,9 +612,14 @@ fn unquoted_byte_positions(content: &str, target: u8) -> Vec<usize> {
|
||||
positions
|
||||
}
|
||||
|
||||
fn count_posts_using_template(conn: &Connection, slug: &str) -> EngineResult<usize> {
|
||||
fn count_posts_using_template(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
slug: &str,
|
||||
) -> EngineResult<usize> {
|
||||
let count: i64 = conn.with(|c| {
|
||||
posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.filter(posts::template_slug.eq(slug))
|
||||
.count()
|
||||
.get_result(c)
|
||||
@@ -605,9 +627,14 @@ fn count_posts_using_template(conn: &Connection, slug: &str) -> EngineResult<usi
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
fn count_tags_using_template(conn: &Connection, slug: &str) -> EngineResult<usize> {
|
||||
fn count_tags_using_template(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
slug: &str,
|
||||
) -> EngineResult<usize> {
|
||||
let count: i64 = conn.with(|c| {
|
||||
tags::table
|
||||
.filter(tags::project_id.eq(project_id))
|
||||
.filter(tags::post_template_slug.eq(slug))
|
||||
.count()
|
||||
.get_result(c)
|
||||
@@ -673,22 +700,61 @@ fn cascade_template_slug_change(
|
||||
Ok((affected_posts, affected_tags))
|
||||
}
|
||||
|
||||
fn null_template_slug_on_posts(conn: &Connection, slug: &str) -> EngineResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(posts::table.filter(posts::template_slug.eq(slug)))
|
||||
.set(posts::template_slug.eq(None::<String>))
|
||||
.execute(c)
|
||||
fn clear_template_references(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
slug: &str,
|
||||
updated_at: i64,
|
||||
) -> EngineResult<(Vec<Post>, Vec<Tag>)> {
|
||||
let mut affected_posts = conn.with(|c| {
|
||||
posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.filter(posts::template_slug.eq(slug))
|
||||
.select(Post::as_select())
|
||||
.load(c)
|
||||
})?;
|
||||
let mut affected_tags = conn.with(|c| {
|
||||
tags::table
|
||||
.filter(tags::project_id.eq(project_id))
|
||||
.filter(tags::post_template_slug.eq(slug))
|
||||
.select(Tag::as_select())
|
||||
.load(c)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn null_template_slug_on_tags(conn: &Connection, slug: &str) -> EngineResult<()> {
|
||||
conn.with(|c| {
|
||||
diesel::update(tags::table.filter(tags::post_template_slug.eq(slug)))
|
||||
.set(tags::post_template_slug.eq(None::<String>))
|
||||
.execute(c)
|
||||
diesel::update(
|
||||
posts::table
|
||||
.filter(posts::project_id.eq(project_id))
|
||||
.filter(posts::template_slug.eq(slug)),
|
||||
)
|
||||
.set((
|
||||
posts::template_slug.eq(None::<String>),
|
||||
posts::updated_at.eq(updated_at),
|
||||
))
|
||||
.execute(c)
|
||||
})?;
|
||||
Ok(())
|
||||
conn.with(|c| {
|
||||
diesel::update(
|
||||
tags::table
|
||||
.filter(tags::project_id.eq(project_id))
|
||||
.filter(tags::post_template_slug.eq(slug)),
|
||||
)
|
||||
.set((
|
||||
tags::post_template_slug.eq(None::<String>),
|
||||
tags::updated_at.eq(updated_at),
|
||||
))
|
||||
.execute(c)
|
||||
})?;
|
||||
|
||||
for post in &mut affected_posts {
|
||||
post.template_slug = None;
|
||||
post.updated_at = updated_at;
|
||||
}
|
||||
for tag in &mut affected_tags {
|
||||
tag.post_template_slug = None;
|
||||
tag.updated_at = updated_at;
|
||||
}
|
||||
Ok((affected_posts, affected_tags))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1118,6 +1184,139 @@ mod tests {
|
||||
assert!(qt::get_template_by_id(db.conn(), &tpl.id).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_delete_clears_and_flushes_only_its_projects_references() {
|
||||
use crate::db::queries::{post as post_q, tag as tag_q};
|
||||
use crate::engine::{post, tag};
|
||||
|
||||
let (db, dir) = setup();
|
||||
crate::db::fts::ensure_fts_tables(db.conn()).unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p2", "other-blog")).unwrap();
|
||||
let p1_dir = dir.path().join("p1");
|
||||
let p2_dir = dir.path().join("p2");
|
||||
fs::create_dir_all(&p1_dir).unwrap();
|
||||
fs::create_dir_all(&p2_dir).unwrap();
|
||||
|
||||
let p1_template = create_template(
|
||||
db.conn(),
|
||||
"p1",
|
||||
"Article View",
|
||||
TemplateKind::Post,
|
||||
"{{ content }}",
|
||||
)
|
||||
.unwrap();
|
||||
let p2_template = create_template(
|
||||
db.conn(),
|
||||
"p2",
|
||||
"Article View",
|
||||
TemplateKind::Post,
|
||||
"{{ content }}",
|
||||
)
|
||||
.unwrap();
|
||||
let p1_template = publish_template(db.conn(), &p1_dir, &p1_template.id).unwrap();
|
||||
let p2_template = publish_template(db.conn(), &p2_dir, &p2_template.id).unwrap();
|
||||
|
||||
let p1_post = post::create_post(
|
||||
db.conn(),
|
||||
&p1_dir,
|
||||
"p1",
|
||||
"P1 Post",
|
||||
Some("P1 body"),
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
Some("en"),
|
||||
Some(&p1_template.slug),
|
||||
)
|
||||
.unwrap();
|
||||
let p2_post = post::create_post(
|
||||
db.conn(),
|
||||
&p2_dir,
|
||||
"p2",
|
||||
"P2 Post",
|
||||
Some("P2 body"),
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
Some("en"),
|
||||
Some(&p2_template.slug),
|
||||
)
|
||||
.unwrap();
|
||||
let mut p1_post = post::publish_post(db.conn(), &p1_dir, &p1_post.id).unwrap();
|
||||
let p2_post = post::publish_post(db.conn(), &p2_dir, &p2_post.id).unwrap();
|
||||
|
||||
let p1_tag = tag::create_tag(db.conn(), &p1_dir, "p1", "Feature", None).unwrap();
|
||||
tag::update_tag(
|
||||
db.conn(),
|
||||
&p1_dir,
|
||||
&p1_tag.id,
|
||||
None,
|
||||
None,
|
||||
Some(&p1_template.slug),
|
||||
)
|
||||
.unwrap();
|
||||
let p2_tag = tag::create_tag(db.conn(), &p2_dir, "p2", "Feature", None).unwrap();
|
||||
tag::update_tag(
|
||||
db.conn(),
|
||||
&p2_dir,
|
||||
&p2_tag.id,
|
||||
None,
|
||||
None,
|
||||
Some(&p2_template.slug),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
p1_post.updated_at = 1;
|
||||
post_q::update_post(db.conn(), &p1_post).unwrap();
|
||||
let mut p1_tag = tag_q::get_tag_by_id(db.conn(), &p1_tag.id).unwrap();
|
||||
p1_tag.updated_at = 1;
|
||||
tag_q::update_tag(db.conn(), &p1_tag).unwrap();
|
||||
|
||||
assert!(delete_template(db.conn(), &p1_dir, &p1_template.id, false).is_err());
|
||||
delete_template(db.conn(), &p1_dir, &p1_template.id, true).unwrap();
|
||||
|
||||
let reloaded_p1_post = post_q::get_post_by_id(db.conn(), &p1_post.id).unwrap();
|
||||
let reloaded_p2_post = post_q::get_post_by_id(db.conn(), &p2_post.id).unwrap();
|
||||
let reloaded_p1_tag = tag_q::get_tag_by_id(db.conn(), &p1_tag.id).unwrap();
|
||||
let reloaded_p2_tag = tag_q::get_tag_by_id(db.conn(), &p2_tag.id).unwrap();
|
||||
assert_eq!(reloaded_p1_post.template_slug, None);
|
||||
assert_eq!(reloaded_p1_tag.post_template_slug, None);
|
||||
assert_eq!(
|
||||
reloaded_p2_post.template_slug.as_deref(),
|
||||
Some(p2_template.slug.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
reloaded_p2_tag.post_template_slug.as_deref(),
|
||||
Some(p2_template.slug.as_str())
|
||||
);
|
||||
assert!(reloaded_p1_post.updated_at > p1_post.updated_at);
|
||||
assert!(reloaded_p1_tag.updated_at > p1_tag.updated_at);
|
||||
|
||||
let p1_post_file = fs::read_to_string(p1_dir.join(&p1_post.file_path)).unwrap();
|
||||
let p2_post_file = fs::read_to_string(p2_dir.join(&p2_post.file_path)).unwrap();
|
||||
let (p1_frontmatter, p1_body) =
|
||||
crate::util::frontmatter::read_post_file(&p1_post_file).unwrap();
|
||||
let (p2_frontmatter, p2_body) =
|
||||
crate::util::frontmatter::read_post_file(&p2_post_file).unwrap();
|
||||
assert_eq!(p1_frontmatter.template_slug, None);
|
||||
assert_eq!(p1_body.trim_end(), "P1 body");
|
||||
assert_eq!(
|
||||
p2_frontmatter.template_slug.as_deref(),
|
||||
Some(p2_template.slug.as_str())
|
||||
);
|
||||
assert_eq!(p2_body.trim_end(), "P2 body");
|
||||
|
||||
let p1_tags = crate::engine::meta::read_tags_json(&p1_dir).unwrap();
|
||||
let p2_tags = crate::engine::meta::read_tags_json(&p2_dir).unwrap();
|
||||
assert_eq!(p1_tags[0].post_template_slug, None);
|
||||
assert_eq!(
|
||||
p2_tags[0].post_template_slug.as_deref(),
|
||||
Some(p2_template.slug.as_str())
|
||||
);
|
||||
assert!(!p1_dir.join(&p1_template.file_path).exists());
|
||||
assert!(p2_dir.join(&p2_template.file_path).exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_valid_liquid() {
|
||||
assert!(validate_template("<div>{{ title }}</div>").is_ok());
|
||||
|
||||
@@ -42,6 +42,7 @@ surface TemplateManagementSurface {
|
||||
UpdateTemplateRequested(template, changes)
|
||||
PublishTemplateRequested(template)
|
||||
DeleteTemplateRequested(template)
|
||||
ForceDeleteTemplateRequested(template)
|
||||
RebuildTemplatesFromFilesRequested(project)
|
||||
}
|
||||
|
||||
@@ -171,6 +172,24 @@ rule DeleteTemplate {
|
||||
ensures: TemplateFileDeleted(template)
|
||||
}
|
||||
|
||||
rule ForceDeleteTemplate {
|
||||
when: ForceDeleteTemplateRequested(template)
|
||||
-- Force deletion clears every project-scoped post and tag reference before
|
||||
-- removing the template. A same-slug template in another project is unrelated.
|
||||
for post in template.referencing_posts:
|
||||
ensures: post.template_slug = null
|
||||
ensures: post.updated_at = now
|
||||
if post.status = published:
|
||||
ensures: PublishedPostFrontmatterRewritten(post)
|
||||
-- Published files drop templateSlug while retaining their body.
|
||||
for tag in template.referencing_tags:
|
||||
ensures: tag.post_template_slug = null
|
||||
ensures: tag.updated_at = now
|
||||
ensures: TagsMetadataWritten(template.project_id)
|
||||
ensures: not exists template
|
||||
ensures: TemplateFileDeleted(template)
|
||||
}
|
||||
|
||||
rule CascadeSlugUpdate {
|
||||
when: template: Template.slug transitions_to new_slug
|
||||
-- When a template slug changes, update all references
|
||||
|
||||
Reference in New Issue
Block a user