diff --git a/deepwell/src/api.rs b/deepwell/src/api.rs index e36df5b1d3..819aa555bf 100644 --- a/deepwell/src/api.rs +++ b/deepwell/src/api.rs @@ -462,6 +462,7 @@ async fn build_module(app_state: ServerState) -> Result> register!("import_wikidot_user", import_wikidot_user); register!("import_wikidot_site", import_wikidot_site); register!("import_wikidot_page", import_wikidot_page); + register!("import_wikidot_page_revision", import_wikidot_page_revision); // Return Ok(module) diff --git a/deepwell/src/endpoints/import.rs b/deepwell/src/endpoints/import.rs index 0ee5cc7bf3..e40b32d3cb 100644 --- a/deepwell/src/endpoints/import.rs +++ b/deepwell/src/endpoints/import.rs @@ -20,8 +20,8 @@ use super::prelude::*; use crate::services::import::{ - ImportPage, ImportPageOutput, ImportService, ImportSite, ImportSiteOutput, - ImportUser, ImportUserOutput, + ImportPage, ImportPageOutput, ImportPageRevision, ImportPageRevisionOutput, + ImportService, ImportSite, ImportSiteOutput, ImportUser, ImportUserOutput, }; pub async fn import_wikidot_user( @@ -56,3 +56,18 @@ pub async fn import_wikidot_page( Error::new("failed to import wikidot page", ErrorType::DatabaseImport) }) } + +pub async fn import_wikidot_page_revision( + ctx: &ServiceContext<'_>, + params: Params<'static>, +) -> Result { + let input: ImportPageRevision = parse!(params, DatabaseImport); + ImportService::add_page_revision(ctx, input) + .await + .or_raise(|| { + Error::new( + "failed to import wikidot page revision", + ErrorType::DatabaseImport, + ) + }) +} diff --git a/deepwell/src/services/import/service.rs b/deepwell/src/services/import/service.rs index 3841726edf..6ee7ab3c6e 100644 --- a/deepwell/src/services/import/service.rs +++ b/deepwell/src/services/import/service.rs @@ -31,14 +31,24 @@ use crate::constants::SYSTEM_USER_ID; use crate::models::known_user::{self, Model as KnownUserModel}; use crate::models::page::{self, Entity as Page}; use crate::models::page_category::Model as PageCategoryModel; +use crate::models::page_revision::{ + self, Entity as PageRevision, Model as PageRevisionModel, +}; use crate::models::site::{self, Entity as Site}; use crate::models::wikidot_user::{self, Entity as WikidotUser}; use crate::services::audit::{AuditEvent, AuditService}; use crate::services::blob::{BlobService, FinalizeBlobUploadOutput}; use crate::services::page_lock::{CreatePageLockInput, PageLockService}; -use crate::services::{CategoryService, UserService}; -use crate::types::PageLockType; +use crate::services::page_revision::{ + CreateFirstPageRevision, CreateFirstPageRevisionOutput, CreatePageRevision, + CreatePageRevisionBody, CreatePageRevisionOutput, PageRevisionService, +}; +use crate::services::{CategoryService, TextService, UserService}; +use crate::types::{PageId, PageLockType}; use crate::utils::get_category_name; +use ftml::layout::Layout; +use sea_orm::UpdateResult; +use sea_query::{Expr, Query}; #[derive(Debug)] pub struct ImportService; @@ -277,7 +287,205 @@ impl ImportService { Ok(ImportPageOutput { site_id, page_id }) } - // TODO page_revision + pub async fn add_page_revision( + ctx: &ServiceContext<'_>, + ImportPageRevision { + revision_id, + revision_type, + created_at, + updated_at, + revision_number, + page_id, + site_id, + user_id, + wikitext, + comments, + title, + slug, + tags, + }: ImportPageRevision, + ) -> Result { + info!( + "Creating page revision ID {} (number {}) on page ID {} on site ID {}", + revision_id, revision_number, page_id, site_id, + ); + + let txn = ctx.transaction(); + let make_error = || { + Error::new( + format!( + "failed to import page revision ID {} (number {}) on page ID {} in site ID {}", + revision_id, revision_number, page_id, site_id, + ), + ErrorType::DatabaseImport, + ) + }; + + // Get page category + let PageCategoryModel { category_id, .. } = + CategoryService::get_or_create(ctx, site_id, get_category_name(&slug)) + .await + .or_raise(make_error)?; + + // Get prior revision + // + // Import operations don't require an initial page revision on page import, + // so it's possible that this is None. + // + // Then we check that the revision_number being inserted is one more than the + // prior revision (or 0 if None, i.e. this is the first). + let prev_revision = PageRevision::find() + .filter( + Condition::all() + .add(page_revision::Column::SiteId.eq(site_id)) + .add(page_revision::Column::PageId.eq(page_id)), + ) + .order_by_desc(page_revision::Column::RevisionNumber) + .one(txn) + .await + .or_raise(make_error)?; + + let output_revision_id = match prev_revision { + // No prior revisions + // First revision for the page + None => { + if revision_number != 0 { + bail!(Error::new( + format!( + "failed to import page revision ID {} (number {}), because there are no prior revisions (should've been 0)", + revision_id, revision_number, + ), + ErrorType::DatabaseImport, + )); + } + + let CreateFirstPageRevisionOutput { + revision_id: output_revision_id, + parser_errors: _, + } = PageRevisionService::create_first( + ctx, + PageId { + site_id, + category_id, + page_id, + }, + CreateFirstPageRevision { + user_id, + comments, + wikitext, + title, + alt_title: None, + slug, + layout: Some(Layout::Wikidot), + }, + ) + .await + .or_raise(make_error)?; + + output_revision_id + } + + // Prior revisions + // Second revision or later + Some(prev_revision) => { + if revision_number != prev_revision.revision_number + 1 { + bail!(Error::new( + format!( + "failed to import page revision ID {} (number {}), because the prior revision was {} (should've been {})", + revision_id, + revision_number, + prev_revision.revision_number, + prev_revision.revision_number + 1, + ), + ErrorType::DatabaseImport + )); + } + + let output = PageRevisionService::create( + ctx, + PageId { + site_id, + category_id, + page_id, + }, + CreatePageRevision { + user_id, + comments, + revision_type, + body: CreatePageRevisionBody { + wikitext: Maybe::Set(wikitext), + title: Maybe::Set(title), + alt_title: Maybe::Unset, + slug: Maybe::Set(slug), + // NOTE: We set tags here so the "changes" value + // is correct for this revision. + tags: Maybe::Set(tags.clone()), + }, + }, + prev_revision, + ) + .await + .or_raise(make_error)?; + + match output { + // There should always be an output, if not, raise an error + None => bail!(Error::new( + format!( + "failed to import page revision ID {} (number {}), because no data was changed from the last revision", + revision_id, revision_number, + ), + ErrorType::DatabaseImport + )), + + // Extract revision ID and validate revision number + Some(CreatePageRevisionOutput { + revision_number: output_revision_number, + revision_id: output_revision_id, + parser_errors: _, + }) => { + assert_eq!( + revision_number, output_revision_number, + "Newly-created revision has a revision number of {}, which is not {} as expected", + output_revision_number, revision_number, + ); + + output_revision_id + } + } + } + }; + + // Update database fields that we can't set via PageRevisionService + // + // We need to use update_many since this changes the primary key, + // but only one row should be modified by this. + + let UpdateResult { rows_affected, .. } = PageRevision::update_many() + .col_expr(page_revision::Column::RevisionId, Expr::value(revision_id)) + .col_expr(page_revision::Column::CreatedAt, Expr::value(created_at)) + .col_expr(page_revision::Column::UpdatedAt, Expr::value(updated_at)) + // not settable on page create + .col_expr(page_revision::Column::Tags, Expr::value(tags)) + .filter(page_revision::Column::RevisionId.eq(output_revision_id)) + .exec(txn) + .await + .or_raise(make_error)?; + + assert_eq!( + rows_affected, 1, + "More than one row updated in page_revision after revision creation", + ); + + // Return output + + Ok(ImportPageRevisionOutput { + site_id, + page_id, + page_revision_id: revision_id, + page_revision_number: revision_number, + }) + } + // TODO page_vote // TODO file diff --git a/deepwell/src/services/import/structs.rs b/deepwell/src/services/import/structs.rs index 50abf2a4bc..83c844a9e1 100644 --- a/deepwell/src/services/import/structs.rs +++ b/deepwell/src/services/import/structs.rs @@ -18,6 +18,7 @@ * along with this program. If not, see . */ +use crate::types::PageRevisionType; use ftml::data::KarmaLevel; use std::net::IpAddr; use time::{Date, OffsetDateTime}; @@ -97,3 +98,28 @@ pub struct ImportPageOutput { pub site_id: i64, pub page_id: i64, } + +#[derive(Deserialize, Debug)] +pub struct ImportPageRevision { + pub revision_id: i64, + pub revision_type: PageRevisionType, + pub created_at: OffsetDateTime, + pub updated_at: Option, + pub revision_number: i32, + pub page_id: i64, + pub site_id: i64, + pub user_id: i64, + pub wikitext: String, + pub comments: String, + pub title: String, + pub slug: String, + pub tags: Vec, +} + +#[derive(Serialize, Debug, Copy, Clone)] +pub struct ImportPageRevisionOutput { + pub site_id: i64, + pub page_id: i64, + pub page_revision_id: i64, + pub page_revision_number: i32, +}