chore: reorganize project into monorepo
This commit is contained in:
368
backend/src/app.rs
Normal file
368
backend/src/app.rs
Normal file
@@ -0,0 +1,368 @@
|
||||
use async_trait::async_trait;
|
||||
use axum::{http::Method, Router as AxumRouter};
|
||||
use loco_rs::{
|
||||
app::{AppContext, Hooks, Initializer},
|
||||
bgworker::{BackgroundWorker, Queue},
|
||||
boot::{create_app, BootResult, StartMode},
|
||||
config::Config,
|
||||
controller::AppRoutes,
|
||||
db::{self, truncate_table},
|
||||
environment::Environment,
|
||||
task::Tasks,
|
||||
Result,
|
||||
};
|
||||
use migration::Migrator;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder, Set,
|
||||
};
|
||||
use std::path::Path;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::{
|
||||
controllers, initializers,
|
||||
models::_entities::{categories, comments, friend_links, posts, reviews, site_settings, tags, users},
|
||||
tasks,
|
||||
workers::downloader::DownloadWorker,
|
||||
};
|
||||
|
||||
pub struct App;
|
||||
#[async_trait]
|
||||
impl Hooks for App {
|
||||
fn app_name() -> &'static str {
|
||||
env!("CARGO_CRATE_NAME")
|
||||
}
|
||||
|
||||
fn app_version() -> String {
|
||||
format!(
|
||||
"{} ({})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
option_env!("BUILD_SHA")
|
||||
.or(option_env!("GITHUB_SHA"))
|
||||
.unwrap_or("dev")
|
||||
)
|
||||
}
|
||||
|
||||
async fn boot(
|
||||
mode: StartMode,
|
||||
environment: &Environment,
|
||||
config: Config,
|
||||
) -> Result<BootResult> {
|
||||
create_app::<Self, Migrator>(mode, environment, config).await
|
||||
}
|
||||
|
||||
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
|
||||
Ok(vec![
|
||||
Box::new(initializers::content_sync::ContentSyncInitializer),
|
||||
Box::new(initializers::view_engine::ViewEngineInitializer),
|
||||
])
|
||||
}
|
||||
|
||||
fn routes(_ctx: &AppContext) -> AppRoutes {
|
||||
AppRoutes::with_default_routes() // controller routes below
|
||||
.add_route(controllers::admin::routes())
|
||||
.add_route(controllers::review::routes())
|
||||
.add_route(controllers::category::routes())
|
||||
.add_route(controllers::friend_link::routes())
|
||||
.add_route(controllers::tag::routes())
|
||||
.add_route(controllers::comment::routes())
|
||||
.add_route(controllers::post::routes())
|
||||
.add_route(controllers::search::routes())
|
||||
.add_route(controllers::site_settings::routes())
|
||||
.add_route(controllers::auth::routes())
|
||||
}
|
||||
async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::PATCH, Method::DELETE])
|
||||
.allow_headers(Any);
|
||||
|
||||
Ok(router.layer(cors))
|
||||
}
|
||||
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
|
||||
queue.register(DownloadWorker::build(ctx)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn register_tasks(tasks: &mut Tasks) {
|
||||
// tasks-inject (do not remove)
|
||||
}
|
||||
async fn truncate(ctx: &AppContext) -> Result<()> {
|
||||
truncate_table(&ctx.db, users::Entity).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn seed(ctx: &AppContext, base: &Path) -> Result<()> {
|
||||
// Seed users - use loco's default seed which handles duplicates
|
||||
let users_file = base.join("users.yaml");
|
||||
if users_file.exists() {
|
||||
if let Err(e) =
|
||||
db::seed::<users::ActiveModel>(&ctx.db, &users_file.display().to_string()).await
|
||||
{
|
||||
tracing::warn!("Users seed skipped or failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed tags first (no foreign key dependencies) - use Unchanged to ignore conflicts
|
||||
if let Ok(seed_data) = std::fs::read_to_string(base.join("tags.yaml")) {
|
||||
let tags_data: Vec<serde_json::Value> =
|
||||
serde_yaml::from_str(&seed_data).unwrap_or_default();
|
||||
for tag in tags_data {
|
||||
let id = tag["id"].as_i64().unwrap_or(0) as i32;
|
||||
let name = tag["name"].as_str().unwrap_or("").to_string();
|
||||
let slug = tag["slug"].as_str().unwrap_or("").to_string();
|
||||
|
||||
let existing = tags::Entity::find_by_id(id).one(&ctx.db).await?;
|
||||
if existing.is_none() {
|
||||
let new_tag = tags::ActiveModel {
|
||||
id: Set(id),
|
||||
name: Set(Some(name)),
|
||||
slug: Set(slug),
|
||||
..Default::default()
|
||||
};
|
||||
let _ = new_tag.insert(&ctx.db).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seed posts
|
||||
if let Ok(seed_data) = std::fs::read_to_string(base.join("posts.yaml")) {
|
||||
let posts_data: Vec<serde_json::Value> =
|
||||
serde_yaml::from_str(&seed_data).unwrap_or_default();
|
||||
for post in posts_data {
|
||||
let pid = post["pid"].as_i64().unwrap_or(0) as i32;
|
||||
let title = post["title"].as_str().unwrap_or("").to_string();
|
||||
let slug = post["slug"].as_str().unwrap_or("").to_string();
|
||||
let content = post["content"].as_str().unwrap_or("").to_string();
|
||||
let excerpt = post["excerpt"].as_str().unwrap_or("").to_string();
|
||||
let category = post["category"].as_str().unwrap_or("").to_string();
|
||||
let pinned = post["pinned"].as_bool().unwrap_or(false);
|
||||
let post_type = post["post_type"].as_str().unwrap_or("article").to_string();
|
||||
let tags_vec = post["tags"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let tags_json = if tags_vec.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(serde_json::json!(tags_vec))
|
||||
};
|
||||
|
||||
let existing = posts::Entity::find_by_id(pid).one(&ctx.db).await?;
|
||||
let has_existing = existing.is_some();
|
||||
let mut post_model = existing
|
||||
.map(|model| model.into_active_model())
|
||||
.unwrap_or_else(|| posts::ActiveModel {
|
||||
id: Set(pid),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
post_model.title = Set(Some(title));
|
||||
post_model.slug = Set(slug);
|
||||
post_model.content = Set(Some(content));
|
||||
post_model.description = Set(Some(excerpt));
|
||||
post_model.category = Set(Some(category));
|
||||
post_model.tags = Set(tags_json);
|
||||
post_model.pinned = Set(Some(pinned));
|
||||
post_model.post_type = Set(Some(post_type));
|
||||
|
||||
if has_existing {
|
||||
let _ = post_model.update(&ctx.db).await;
|
||||
} else {
|
||||
let _ = post_model.insert(&ctx.db).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seed comments
|
||||
if let Ok(seed_data) = std::fs::read_to_string(base.join("comments.yaml")) {
|
||||
let comments_data: Vec<serde_json::Value> =
|
||||
serde_yaml::from_str(&seed_data).unwrap_or_default();
|
||||
for comment in comments_data {
|
||||
let id = comment["id"].as_i64().unwrap_or(0) as i32;
|
||||
let pid = comment["pid"].as_i64().unwrap_or(0) as i32;
|
||||
let author = comment["author"].as_str().unwrap_or("").to_string();
|
||||
let email = comment["email"].as_str().unwrap_or("").to_string();
|
||||
let content_text = comment["content"].as_str().unwrap_or("").to_string();
|
||||
let approved = comment["approved"].as_bool().unwrap_or(false);
|
||||
let post_slug = posts::Entity::find_by_id(pid)
|
||||
.one(&ctx.db)
|
||||
.await?
|
||||
.map(|post| post.slug);
|
||||
|
||||
let existing = comments::Entity::find_by_id(id).one(&ctx.db).await?;
|
||||
let has_existing = existing.is_some();
|
||||
let mut comment_model = existing
|
||||
.map(|model| model.into_active_model())
|
||||
.unwrap_or_else(|| comments::ActiveModel {
|
||||
id: Set(id),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
comment_model.author = Set(Some(author));
|
||||
comment_model.email = Set(Some(email));
|
||||
comment_model.content = Set(Some(content_text));
|
||||
comment_model.approved = Set(Some(approved));
|
||||
comment_model.post_slug = Set(post_slug);
|
||||
|
||||
if has_existing {
|
||||
let _ = comment_model.update(&ctx.db).await;
|
||||
} else {
|
||||
let _ = comment_model.insert(&ctx.db).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seed friend links
|
||||
if let Ok(seed_data) = std::fs::read_to_string(base.join("friend_links.yaml")) {
|
||||
let links_data: Vec<serde_json::Value> =
|
||||
serde_yaml::from_str(&seed_data).unwrap_or_default();
|
||||
for link in links_data {
|
||||
let site_name = link["site_name"].as_str().unwrap_or("").to_string();
|
||||
let site_url = link["site_url"].as_str().unwrap_or("").to_string();
|
||||
let avatar_url = link["avatar_url"].as_str().map(|s: &str| s.to_string());
|
||||
let description = link["description"].as_str().unwrap_or("").to_string();
|
||||
let category = link["category"].as_str().unwrap_or("").to_string();
|
||||
let status = link["status"].as_str().unwrap_or("pending").to_string();
|
||||
|
||||
let existing = friend_links::Entity::find()
|
||||
.filter(friend_links::Column::SiteUrl.eq(&site_url))
|
||||
.one(&ctx.db)
|
||||
.await?;
|
||||
if existing.is_none() {
|
||||
let new_link = friend_links::ActiveModel {
|
||||
site_name: Set(Some(site_name)),
|
||||
site_url: Set(site_url),
|
||||
avatar_url: Set(avatar_url),
|
||||
description: Set(Some(description)),
|
||||
category: Set(Some(category)),
|
||||
status: Set(Some(status)),
|
||||
..Default::default()
|
||||
};
|
||||
let _ = new_link.insert(&ctx.db).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seed site settings
|
||||
if let Ok(seed_data) = std::fs::read_to_string(base.join("site_settings.yaml")) {
|
||||
let settings_data: Vec<serde_json::Value> =
|
||||
serde_yaml::from_str(&seed_data).unwrap_or_default();
|
||||
if let Some(settings) = settings_data.first() {
|
||||
let existing = site_settings::Entity::find()
|
||||
.order_by_asc(site_settings::Column::Id)
|
||||
.one(&ctx.db)
|
||||
.await?;
|
||||
|
||||
if existing.is_none() {
|
||||
let tech_stack = settings["tech_stack"]
|
||||
.as_array()
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| item.as_str())
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|items| !items.is_empty())
|
||||
.map(|items| serde_json::json!(items));
|
||||
|
||||
let item = site_settings::ActiveModel {
|
||||
id: Set(settings["id"].as_i64().unwrap_or(1) as i32),
|
||||
site_name: Set(settings["site_name"].as_str().map(ToString::to_string)),
|
||||
site_short_name: Set(
|
||||
settings["site_short_name"].as_str().map(ToString::to_string),
|
||||
),
|
||||
site_url: Set(settings["site_url"].as_str().map(ToString::to_string)),
|
||||
site_title: Set(settings["site_title"].as_str().map(ToString::to_string)),
|
||||
site_description: Set(
|
||||
settings["site_description"].as_str().map(ToString::to_string),
|
||||
),
|
||||
hero_title: Set(settings["hero_title"].as_str().map(ToString::to_string)),
|
||||
hero_subtitle: Set(
|
||||
settings["hero_subtitle"].as_str().map(ToString::to_string),
|
||||
),
|
||||
owner_name: Set(settings["owner_name"].as_str().map(ToString::to_string)),
|
||||
owner_title: Set(
|
||||
settings["owner_title"].as_str().map(ToString::to_string),
|
||||
),
|
||||
owner_bio: Set(settings["owner_bio"].as_str().map(ToString::to_string)),
|
||||
owner_avatar_url: Set(
|
||||
settings["owner_avatar_url"].as_str().and_then(|value| {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
}),
|
||||
),
|
||||
social_github: Set(
|
||||
settings["social_github"].as_str().map(ToString::to_string),
|
||||
),
|
||||
social_twitter: Set(
|
||||
settings["social_twitter"].as_str().map(ToString::to_string),
|
||||
),
|
||||
social_email: Set(
|
||||
settings["social_email"].as_str().map(ToString::to_string),
|
||||
),
|
||||
location: Set(settings["location"].as_str().map(ToString::to_string)),
|
||||
tech_stack: Set(tech_stack),
|
||||
..Default::default()
|
||||
};
|
||||
let _ = item.insert(&ctx.db).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seed reviews
|
||||
if let Ok(seed_data) = std::fs::read_to_string(base.join("reviews.yaml")) {
|
||||
let reviews_data: Vec<serde_json::Value> =
|
||||
serde_yaml::from_str(&seed_data).unwrap_or_default();
|
||||
for review in reviews_data {
|
||||
let title = review["title"].as_str().unwrap_or("").to_string();
|
||||
let review_type = review["review_type"].as_str().unwrap_or("").to_string();
|
||||
let rating = review["rating"].as_i64().unwrap_or(0) as i32;
|
||||
let review_date = review["review_date"].as_str().unwrap_or("").to_string();
|
||||
let status = review["status"].as_str().unwrap_or("completed").to_string();
|
||||
let description = review["description"].as_str().unwrap_or("").to_string();
|
||||
let cover = review["cover"].as_str().unwrap_or("📝").to_string();
|
||||
let tags_vec = review["tags"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let existing = reviews::Entity::find()
|
||||
.filter(reviews::Column::Title.eq(&title))
|
||||
.one(&ctx.db)
|
||||
.await?;
|
||||
if existing.is_none() {
|
||||
let new_review = reviews::ActiveModel {
|
||||
title: Set(Some(title)),
|
||||
review_type: Set(Some(review_type)),
|
||||
rating: Set(Some(rating)),
|
||||
review_date: Set(Some(review_date)),
|
||||
status: Set(Some(status)),
|
||||
description: Set(Some(description)),
|
||||
cover: Set(Some(cover)),
|
||||
tags: Set(Some(serde_json::to_string(&tags_vec).unwrap_or_default())),
|
||||
..Default::default()
|
||||
};
|
||||
let _ = new_review.insert(&ctx.db).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user