use crate::braze::BrazeClient;
use crate::config::ResolvedConfig;
use crate::diff::catalog::CatalogSchemaDiff;
use crate::diff::{DiffOp, DiffSummary, ResourceDiff};
use crate::error::Error;
use crate::format::OutputFormat;
use crate::resource::ResourceKind;
use anyhow::{anyhow, Context as _};
use clap::Args;
use std::path::Path;
use super::diff::compute_catalog_schema_diffs;
use super::{selected_kinds, warn_unimplemented};
#[derive(Args, Debug)]
pub struct ApplyArgs {
#[arg(long, value_enum)]
pub resource: Option<ResourceKind>,
#[arg(long, requires = "resource")]
pub name: Option<String>,
#[arg(long)]
pub confirm: bool,
#[arg(long)]
pub allow_destructive: bool,
#[arg(long)]
pub archive_orphans: bool,
}
pub async fn run(
args: &ApplyArgs,
resolved: ResolvedConfig,
config_dir: &Path,
format: OutputFormat,
) -> anyhow::Result<()> {
let catalogs_root = config_dir.join(&resolved.resources.catalog_schema.path);
let client = BrazeClient::from_resolved(&resolved);
let kinds = selected_kinds(args.resource, &resolved.resources);
let mut summary = DiffSummary::default();
for kind in kinds {
match kind {
ResourceKind::CatalogSchema => {
let diffs =
compute_catalog_schema_diffs(&client, &catalogs_root, args.name.as_deref())
.await
.context("computing catalog_schema plan")?;
summary.diffs.extend(diffs);
}
other => warn_unimplemented(other),
}
}
let mode_label = if args.confirm {
"Plan:"
} else {
"Plan (dry-run, pass --confirm to apply):"
};
eprintln!("{mode_label}");
print!("{}", format.formatter().format(&summary));
if summary.changed_count() == 0 {
eprintln!("No changes to apply.");
return Ok(());
}
if !args.confirm {
eprintln!("DRY RUN — pass --confirm to apply these changes.");
return Ok(());
}
if summary.destructive_count() > 0 && !args.allow_destructive {
return Err(Error::DestructiveBlocked.into());
}
check_for_unsupported_ops(&summary)?;
let mut applied = 0;
for diff in &summary.diffs {
if let ResourceDiff::CatalogSchema(d) = diff {
applied += apply_catalog_schema(&client, d).await?;
}
}
eprintln!("✓ Applied {applied} change(s).");
Ok(())
}
fn check_for_unsupported_ops(summary: &DiffSummary) -> anyhow::Result<()> {
for diff in &summary.diffs {
if let ResourceDiff::CatalogSchema(d) = diff {
match &d.op {
DiffOp::Added(_) => {
return Err(anyhow!(
"creating a new catalog '{}' is not supported in v0.1.0; \
create the catalog in the Braze dashboard first, then run \
`braze-sync export` to populate the local schema",
d.name
));
}
DiffOp::Removed(_) => {
return Err(anyhow!(
"deleting catalog '{}' (top-level) is not supported in v0.1.0; \
only field-level changes can be applied",
d.name
));
}
_ => {}
}
for fd in &d.field_diffs {
if let DiffOp::Modified { from, to } = fd {
return Err(anyhow!(
"modifying field '{}' on catalog '{}' (type {} → {}) \
is not supported in v0.1.0; the change would be \
data-losing on the field. Drop the field manually \
in the Braze dashboard and re-run `braze-sync apply`",
to.name,
d.name,
from.field_type.as_str(),
to.field_type.as_str(),
));
}
}
}
}
Ok(())
}
async fn apply_catalog_schema(
client: &BrazeClient,
d: &CatalogSchemaDiff,
) -> anyhow::Result<usize> {
let mut count = 0;
for fd in &d.field_diffs {
match fd {
DiffOp::Added(f) => {
tracing::info!(
catalog = %d.name,
field = %f.name,
field_type = f.field_type.as_str(),
"adding catalog field"
);
client.add_catalog_field(&d.name, f).await?;
count += 1;
}
DiffOp::Removed(f) => {
tracing::info!(
catalog = %d.name,
field = %f.name,
"deleting catalog field"
);
client.delete_catalog_field(&d.name, &f.name).await?;
count += 1;
}
DiffOp::Modified { .. } => {
return Err(anyhow!(
"internal: Modified field op should have been rejected \
by check_for_unsupported_ops"
));
}
DiffOp::Unchanged => {}
}
}
Ok(count)
}