pub mod assets;
pub mod database;
pub mod functions;
pub mod secrets;
use crate::cmds::accounts::get_current_account;
use crate::cmds::logs::{Logs, Sync, print_logs_metadata_table};
use crate::{GENERATOR, HostCallFlags};
use anyhow::bail;
use assets::Assets;
use clap::Subcommand;
use clap::builder::PossibleValue;
use clio::ClioPath;
use database::Database;
use functions::Functions;
use ordinary_build::traverse;
use ordinary_config::OrdinaryConfig;
use ordinaryd::{AppHost, GlobalArgs};
use secrets::Secrets;
use std::path::Path;
use tracing::instrument;
#[derive(Subcommand, Debug)]
pub enum App {
#[clap(visible_aliases(["create", "init"]))]
New {
domain: String,
#[arg(long, default_value = ".")]
path: String,
},
Build {
#[arg(short, long, default_value_t = false)]
force: bool,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
#[clap(visible_aliases(["run", "preview"]))]
Start {
#[command(flatten)]
app_api: AppHost,
#[command(flatten)]
global_args: GlobalArgs,
#[arg(short, long, default_value_t = false)]
disable_defaults: bool,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
Publish {
#[command(flatten)]
host: HostCallFlags,
#[arg(short, long, default_value_t = false)]
tls_staging: bool,
#[arg(long, default_value_t = false)]
no_seed: bool,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
Deploy {
#[command(flatten)]
host: HostCallFlags,
#[arg(short, long, default_value_t = false)]
json_to_stdout: bool,
#[arg(short, long, default_value_t = false)]
tls_staging: bool,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
Kill {
#[command(flatten)]
host: HostCallFlags,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
Restart {
#[command(flatten)]
host: HostCallFlags,
#[arg(short, long, default_value_t = false)]
json_to_stdout: bool,
#[arg(short, long, default_value_t = false)]
tls_staging: bool,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
Erase {
#[command(flatten)]
host: HostCallFlags,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
Logs {
#[command(flatten)]
host: HostCallFlags,
#[command(subcommand)]
logs: Logs,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
Assets {
#[command(subcommand)]
assets: Assets,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
#[clap(visible_aliases(["account", "accts", "acct"]))]
Accounts {
#[command(subcommand)]
accounts: Accounts,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
#[clap(visible_aliases(["fns", "fn", "function"]))]
Functions {
#[command(subcommand)]
functions: Functions,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
#[clap(visible_aliases(["db"]))]
Database {
#[command(subcommand)]
database: Database,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
Secrets {
#[command(subcommand)]
secrets: Secrets,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
Config {
#[command(subcommand)]
config: Config,
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
Routes {
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
},
}
#[derive(Subcommand, Debug)]
pub enum Accounts {
#[clap(visible_aliases(["ls"]))]
List {
#[command(flatten)]
host: HostCallFlags,
},
}
#[derive(Subcommand, Debug)]
pub enum Config {
View {
#[arg(short, long, default_value = "build")]
r#for: For,
},
Validate {
#[arg(short, long, default_value = "build")]
r#for: For,
},
}
#[derive(Clone, Debug, PartialOrd, PartialEq)]
pub enum For {
Build,
Deploy,
}
impl clap::ValueEnum for For {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Build, Self::Deploy]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
match self {
Self::Build => Some(PossibleValue::new("build")),
Self::Deploy => Some(PossibleValue::new("deploy")),
}
}
}
impl App {
#[allow(clippy::missing_panics_doc, clippy::too_many_lines)]
#[instrument(skip_all, name = "app")]
pub async fn handle(&self) -> anyhow::Result<()> {
match self {
Self::New { path, domain } => ordinary_modify::project::new(path, domain)?,
Self::Build {
force: ignore_cache,
project,
} => {
let project = project.to_str().expect("failed to get string from project");
let env_file = Path::new(project).join(".env");
if env_file.exists() {
dotenvy::from_path(env_file)?;
}
let env_file = Path::new(project).join(".env");
if env_file.exists() {
dotenvy::from_path(env_file)?;
}
ordinary_build::build(project, *ignore_cache, "ordinary")?;
}
Self::Publish {
tls_staging,
no_seed,
project,
host,
} => {
let project = project.to_str().expect("failed to get string from project");
let env_file = Path::new(project).join(".env");
if env_file.exists() {
dotenvy::from_path(env_file)?;
}
ordinary_build::build(project, true, GENERATOR)?;
let account = get_current_account(host.insecure)?;
let client = host.client(&account)?;
let config = OrdinaryConfig::get(project, true)?;
client.app_deploy(project, *tls_staging).await?;
if !no_seed {
client.database_items_seed(project).await?;
}
if config.assets.is_some() {
client.assets_write_all(project).await?;
}
if config.functions.is_some() {
client.functions_upload_all(project).await?;
}
}
Self::Deploy {
json_to_stdout,
tls_staging,
project,
host,
} => {
let project = project.to_str().expect("failed to get string from project");
let account = get_current_account(host.insecure)?;
let client = host.client(&account)?;
let env_file = Path::new(project).join(".env");
if env_file.exists() {
dotenvy::from_path(env_file)?;
}
let ports_res = client.app_deploy(project, *tls_staging).await?;
tracing::info!("app deployed");
if *json_to_stdout {
println!("{}", serde_json::to_string(&ports_res)?);
}
}
Self::Kill { project, host } => {
let project = project.to_str().expect("failed to get string from project");
let account = get_current_account(host.insecure)?;
let client = host.client(&account)?;
client.app_kill(project).await?;
}
Self::Restart {
json_to_stdout,
tls_staging,
project,
host,
} => {
let project = project.to_str().expect("failed to get string from project");
let account = get_current_account(host.insecure)?;
let client = host.client(&account)?;
let ports_res = client.app_restart(project, *tls_staging).await?;
tracing::info!("app restarted");
if *json_to_stdout {
println!("{}", serde_json::to_string(&ports_res)?);
}
}
Self::Erase { project, host } => {
let project = project.to_str().expect("failed to get string from project");
let account = get_current_account(host.insecure)?;
let client = host.client(&account)?;
client.app_erase(project).await?;
}
Self::Logs {
logs,
project,
host,
} => {
let project = project.to_str().expect("failed to get string from project");
let account = get_current_account(host.insecure)?;
let client = host.client(&account)?;
match logs {
Logs::Query {
query,
json_columns,
sync,
} => {
if sync == &Some(true) {
client.app_logs_sync(project, None, None).await?;
}
let res = client.app_logs_query(project, query, json_columns.as_ref())?;
print!("{res}");
}
Logs::Search {
format,
query,
limit,
sync,
} => {
if sync == &Some(true) {
client.app_logs_sync(project, None, None).await?;
}
let res =
client.app_logs_search(project, query, format.as_str(), *limit)?;
print!("{res}");
}
Logs::List => client.app_logs_file_list(project)?,
Logs::Read { name } => {
if let Some(name) = name {
client.app_logs_file_read(project, name)?;
} else {
client.app_logs_file_read_all(project)?;
}
}
Logs::Sync { sync } => match sync {
Sync::Info => {
let remote_metadata = client.app_logs_remote_metadata(project).await?;
let local_metadata = client.app_logs_local_metadata(project)?;
print_logs_metadata_table(remote_metadata, local_metadata);
}
Sync::All { force } => {
client.app_logs_sync(project, *force, None).await?;
}
Sync::File { name } => {
client.app_logs_sync(project, None, Some(name)).await?;
}
},
}
}
Self::Accounts { accounts, project } => {
let project = project.to_str().expect("failed to get string from project");
match accounts {
Accounts::List { host } => {
let account = get_current_account(host.insecure)?;
let client = host.client(&account)?;
let res = client.app_accounts_list(project).await?;
print!("{res}");
}
}
}
Self::Routes { project } => {
let project = project.to_str().expect("failed to get string from project");
let config = OrdinaryConfig::get(project, true)?;
println!("ROUTES\n");
if let Some(assets) = config.assets {
let Some(assets_dir_path) = &assets.dir_path else {
bail!("assets.dir_path cannot be unset");
};
println!("assets:");
let base_route = assets.base_route.as_str();
let assets_dir = Path::new(project).join(assets_dir_path);
traverse(&assets_dir, &|entry| {
let path = entry.path();
let path = path.strip_prefix(&assets_dir)?;
if base_route == "/" {
println!("- GET /{}", path.display());
} else {
println!("- GET {base_route}/{}", path.display());
}
Ok(())
})?;
println!();
}
if let Some(http_config) = &config.http {
if let Some(routes) = &http_config.routes {
println!("functions:");
for http_route in routes {
println!("- {} {}", http_route.method, http_route.path);
}
println!();
}
if let Some(redirects) = &http_config.redirects
&& let Some(redirects) = &redirects.route
{
println!("redirects:");
for redirect in redirects {
println!(
"- {} {} [ {} -> {} ]",
redirect.method,
redirect.condition,
redirect.rule.0,
redirect.rule.1
);
}
println!();
}
if let Some(proxies) = &http_config.proxies {
println!("proxies:");
for proxy in proxies {
if let Some(path) = &proxy.path {
println!("- ANY {path} -> {}", proxy.target);
} else if let Some(domain) = &proxy.domain {
println!("- ANY http(s)://{domain}/{{*path}} -> {}", proxy.target);
}
}
println!();
}
}
}
Self::Assets { assets, project } => {
let project = project.to_str().expect("failed to get string from project");
assets.handle(project).await?;
}
Self::Functions { functions, project } => {
let project = project.to_str().expect("failed to get string from project");
functions.handle(project).await?;
}
Self::Database {
database: models,
project,
} => {
let project = project.to_str().expect("failed to get string from project");
models.handle(project).await?;
}
Self::Secrets { secrets, project } => {
let project = project.to_str().expect("failed to get string from project");
secrets.handle(project).await?;
}
Self::Config { config, project } => {
let project = project.to_str().expect("failed to get string from project");
let app_config = OrdinaryConfig::get(project, true)?;
match config {
Config::View { r#for } => match r#for {
For::Build => {
println!("{}", serde_json::to_string_pretty(&app_config)?);
}
For::Deploy => {
println!("{}", serde_json::to_string_pretty(&app_config.for_send()?)?);
}
},
Config::Validate { r#for } => match r#for {
For::Build => {
app_config.validate()?;
println!("valid");
}
For::Deploy => {
app_config.for_send()?.validate()?;
println!("valid");
}
},
}
}
Self::Start { .. } => unreachable!("handled in main.rs"),
}
Ok(())
}
}