//! Hello-World seed for fresh installs. //! //! Idempotent. Runs after migrations and after admin bootstrap. Only //! seeds when the default app is empty (no scripts, no routes); on //! upgrades it does nothing so existing content isn't polluted. use std::sync::Arc; use picloud_shared::{App, AppId, HostKind, PathKind}; use crate::app_repo::AppRepository; use crate::repo::{NewScript, ScriptRepository, ScriptRepositoryError}; use crate::route_repo::{NewRoute, RouteRepository}; const HELLO_RHAI_SOURCE: &str = include_str!("../seeds/hello.rhai"); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HelloWorldOutcome { /// Default app already has scripts (or doesn't exist) — left alone. SkippedExisting, /// Inserted the hello.rhai script and the `/hello` route. Seeded, } #[derive(Debug, thiserror::Error)] pub enum SeedError { #[error("default app not found — did the migration run?")] MissingDefaultApp, #[error("repository error: {0}")] Repo(#[from] ScriptRepositoryError), } pub async fn seed_hello_world_if_fresh( apps: Arc, scripts: Arc, routes: Arc, ) -> Result { let default = apps .get_by_slug("default") .await? .ok_or(SeedError::MissingDefaultApp)?; // Idempotence: only seed when both scripts AND routes are empty. // (Either alone is suspicious enough to skip — the operator may have // already started shaping the default app.) let existing_scripts = scripts.list_for_app(default.id).await?; let existing_routes = routes.list_for_app(default.id).await?; if !existing_scripts.is_empty() || !existing_routes.is_empty() { return Ok(HelloWorldOutcome::SkippedExisting); } seed_into(&*scripts, &*routes, &default).await?; Ok(HelloWorldOutcome::Seeded) } async fn seed_into( scripts: &dyn ScriptRepository, routes: &dyn RouteRepository, default: &App, ) -> Result<(), ScriptRepositoryError> { let script = scripts .create(NewScript { app_id: default.id, name: "hello".to_string(), description: Some("Reference example: returns a greeting at GET /hello.".to_string()), source: HELLO_RHAI_SOURCE.to_string(), timeout_seconds: Some(5), memory_limit_mb: None, sandbox: None, }) .await?; routes .create(NewRoute { app_id: default.id, script_id: script.id, host_kind: HostKind::Any, host: String::new(), host_param_name: None, path_kind: PathKind::Exact, path: "/hello".to_string(), // Accept any method so both `curl /hello` and // `curl -d '{"name":"X"}' /hello` work out of the box. method: None, }) .await?; Ok(()) } #[allow(dead_code)] fn _typecheck(_id: AppId) {} // suppress unused-import warnings if reshuffled