use super::types::RoleInput;
use crate::plugins::json_body;
pub(super) use crate::plugins::json_body::native_invalid_type;
use better_auth_core::{
AuthError, AuthResult, FieldMap, FieldValue, SchemaValue, user_fields::UserConfig,
};
use serde_json::{Map, Value};
pub(super) fn string_operation<'a>(
value: &'a SchemaValue<String>,
operation: &str,
) -> AuthResult<&'a str> {
value.typed().map(String::as_str).or_else(|error| {
better_auth_core::observability::logger::current().error(
"Organization string operation failed",
&[
better_auth_core::observability::LogArgument::Error(&error),
better_auth_core::observability::LogArgument::Value(&serde_json::json!(operation)),
],
)?;
Err(better_auth_core::AuthResponse::new(500).into())
})
}
pub(super) enum BaseField {
String,
NonemptyString,
NullableString,
Boolean,
Roles,
CoercedString,
Record,
Permissions,
}
pub(super) fn finish(errors: Vec<String>) -> AuthResult<()> {
if errors.is_empty() {
Ok(())
} else {
Err(AuthError::RequestValidation {
message: errors.join("; "),
})
}
}
pub(super) fn object<'a>(
value: Option<&'a Value>,
prefix: &str,
errors: &mut Vec<String>,
) -> Option<&'a Map<String, Value>> {
match value.and_then(Value::as_object) {
Some(value) => Some(value),
None => {
errors.push(json_body::invalid_type(prefix, "object", value));
None
}
}
}
pub(super) fn fields(
schema: &UserConfig,
body: &Map<String, Value>,
base: &[(&str, BaseField, bool)],
prefix: &str,
partial: bool,
base_wins: bool,
errors: &mut Vec<String>,
) -> AuthResult<FieldMap> {
native_fields(
schema,
&FieldMap::from_json(body.clone())?,
base,
prefix,
partial,
base_wins,
errors,
)
}
pub(super) fn native_fields(
schema: &UserConfig,
body: &FieldMap,
base: &[(&str, BaseField, bool)],
prefix: &str,
partial: bool,
base_wins: bool,
errors: &mut Vec<String>,
) -> AuthResult<FieldMap> {
let mut names = indexmap::IndexSet::new();
let configured = schema
.fields()
.iter()
.filter(|(_, field)| field.input())
.map(|(name, _)| name.as_str());
if base_wins {
names.extend(configured);
names.extend(base.iter().map(|(name, _, _)| *name));
} else {
names.extend(base.iter().map(|(name, _, _)| *name));
names.extend(configured);
}
let mut output = FieldMap::new();
for name in names {
let builtin = base.iter().find(|(key, _, _)| *key == name);
let field = schema.fields().get(name).filter(|field| field.input());
let location = format!("{prefix}.{name}");
if let Some(field) = field.filter(|_| !base_wins || builtin.is_none()) {
match field.organization_input_outcome(body.get(name), &location, partial)? {
Ok(Some(value)) => {
let _ = output.insert(name.into(), value);
}
Ok(None) => {}
Err(message) => errors.push(message),
}
} else if let Some((_, kind, required)) = builtin
&& let Some(value) = field_value(
body.get(name),
kind,
*required && !partial,
&location,
errors,
)?
{
let _ = output.insert(name.into(), value);
}
}
Ok(output)
}
fn field_value(
value: Option<&FieldValue>,
kind: &BaseField,
required: bool,
location: &str,
errors: &mut Vec<String>,
) -> AuthResult<Option<FieldValue>> {
if value.is_none_or(FieldValue::is_undefined) && !required {
return Ok(value.cloned());
}
if matches!(kind, BaseField::CoercedString) {
return Ok(Some(
SchemaValue::<FieldValue>::from_field(value.cloned().unwrap_or_default())
.display_string()?
.into(),
));
}
let expected = match kind {
BaseField::Boolean => "boolean",
BaseField::Record | BaseField::Permissions => "record",
_ => "string",
};
let valid = match kind {
BaseField::String | BaseField::NonemptyString | BaseField::CoercedString => {
value.is_some_and(FieldValue::is_string)
}
BaseField::NullableString => {
value.is_some_and(|value| value.is_null() || value.is_string())
}
BaseField::Boolean => value.is_some_and(|value| matches!(value, FieldValue::Bool(_))),
BaseField::Roles => match value {
Some(value) if value.is_string() => true,
Some(FieldValue::Array(values)) => values.every(|value| Ok(value.is_string()))?,
_ => false,
},
BaseField::Record | BaseField::Permissions => value.is_some_and(FieldValue::is_object),
};
if !valid {
errors.push(if matches!(kind, BaseField::Roles) {
format!("[{location}] Invalid input")
} else {
native_invalid_type(location, expected, value)
});
return Ok(None);
}
if matches!(kind, BaseField::NonemptyString) && value.and_then(FieldValue::as_str) == Some("") {
errors.push(format!(
"[{location}] Too small: expected string to have >=1 characters"
));
}
if matches!(kind, BaseField::Permissions)
&& let Some(values) = value.and_then(FieldValue::as_object)
{
for (key, value) in &values.snapshot_fields()? {
let path = format!("{location}.{key}");
if let Some(values) = value.as_array() {
for (index, value) in values.iter()?.enumerate() {
let value = value?;
if !value.is_string() {
errors.push(native_invalid_type(
&format!("{path}.{index}"),
"string",
Some(&value),
));
}
}
} else {
errors.push(native_invalid_type(&path, "array", Some(value)));
}
}
}
Ok(value.cloned())
}
pub(super) fn parse_roles(value: &SchemaValue<RoleInput>) -> AuthResult<SchemaValue<String>> {
match value {
SchemaValue::Typed(value) => Ok(value.joined().into()),
SchemaValue::Dynamic(value @ FieldValue::Array(_)) => {
SchemaValue::<FieldValue>::Dynamic(value.clone())
.display_string()
.map(Into::into)
}
SchemaValue::Dynamic(value) => Ok(SchemaValue::Dynamic(value.clone())),
SchemaValue::Undefined => Ok(SchemaValue::Undefined),
}
}
pub(super) fn invitation_team_ids(value: FieldValue) -> SchemaValue<Vec<String>> {
match value {
FieldValue::String(value) => vec![value].into(),
FieldValue::Undefined | FieldValue::Null => Vec::new().into(),
value => SchemaValue::from_field(value),
}
}
fn has_team_length(value: &FieldValue) -> AuthResult<bool> {
match value {
FieldValue::Undefined | FieldValue::Null => {
Err(better_auth_core::AuthResponse::new(500).into())
}
FieldValue::Array(values) => Ok(!values.is_empty()?),
FieldValue::String(value) => Ok(!value.is_empty()),
FieldValue::Utf16String(value) => Ok(!value.as_utf16().is_empty()),
FieldValue::Object(value) => {
better_auth_core::query::field_number(&value.get("length")?.unwrap_or_default())
.map(|length| length > 0.0)
}
_ => Ok(false),
}
}
pub(super) fn invitation_team_alias(
teams: &SchemaValue<Vec<String>>,
) -> AuthResult<SchemaValue<String>> {
let value = teams.field_value();
if !has_team_length(&value)? {
return Ok(SchemaValue::Undefined);
}
Ok(SchemaValue::from_field(match value {
FieldValue::Array(values) => values.first()?.unwrap_or_default(),
FieldValue::Object(values) => values.get("0")?.unwrap_or_default(),
_ => FieldValue::Undefined,
}))
}
pub(super) fn join_invitation_teams(
teams: &SchemaValue<Vec<String>>,
) -> AuthResult<Option<String>> {
let value = teams.field_value();
if !has_team_length(&value)? {
return Ok(None);
}
if !value.is_array() {
return Err(better_auth_core::AuthResponse::new(500).into());
}
SchemaValue::<FieldValue>::Dynamic(value)
.display_string()
.map(Some)
}