use super::super::{
DbAdapter, DbField, DbRecord, DbSchema, DbTable, DbValue, FindMany, JoinRelation, Where,
WhereOperator,
};
use crate::error::OpenAuthError;
#[derive(Debug, Clone)]
pub(super) struct FallbackJoin {
model: String,
from: String,
to: String,
relation: JoinRelation,
limit: usize,
}
pub(super) async fn attach_joins<A>(
adapter: &A,
records: &mut [&mut DbRecord],
joins: &[FallbackJoin],
) -> Result<(), OpenAuthError>
where
A: DbAdapter,
{
for join in joins {
for record in records.iter_mut() {
initialize_join_value(record, join);
}
let values = records
.iter()
.filter_map(|record| record.get(&join.from))
.cloned()
.collect::<Vec<_>>();
let Some(where_value) = in_value(values) else {
continue;
};
let related =
adapter
.find_many(FindMany::new(join.model.clone()).where_clause(
Where::new(join.to.clone(), where_value).operator(WhereOperator::In),
))
.await?;
for record in records.iter_mut() {
let Some(base_value) = record.get(&join.from).cloned() else {
continue;
};
let mut matching = related
.iter()
.filter(|related| related.get(&join.to) == Some(&base_value))
.cloned()
.collect::<Vec<_>>();
if join.relation == JoinRelation::OneToOne {
let value = matching
.into_iter()
.next()
.map(DbValue::Record)
.unwrap_or(DbValue::Null);
record.insert(join.model.clone(), value);
} else {
matching.truncate(join.limit);
record.insert(join.model.clone(), DbValue::RecordArray(matching));
}
}
}
Ok(())
}
pub(super) fn trim_joined_record(
record: &mut DbRecord,
original_select: &[String],
joins: &[FallbackJoin],
) {
if original_select.is_empty() {
return;
}
record.retain(|field, _| {
original_select.contains(field) || joins.iter().any(|join| join.model == *field)
});
}
pub(super) fn extend_select_for_joins(select: &mut Vec<String>, joins: &[FallbackJoin]) {
if select.is_empty() {
return;
}
for join in joins {
if !select.contains(&join.from) {
select.push(join.from.clone());
}
}
}
pub(super) fn resolve_fallback_joins(
schema: &DbSchema,
base_model: &str,
joins: &indexmap::IndexMap<String, super::super::JoinOption>,
default_limit: usize,
) -> Result<Vec<FallbackJoin>, OpenAuthError> {
let (_, base_table) =
find_table(schema, base_model).ok_or_else(|| OpenAuthError::TableNotFound {
table: base_model.to_owned(),
})?;
let mut resolved = Vec::new();
for (join_model, option) in joins {
if !option.enabled {
continue;
}
let (join_logical, join_table) =
find_table(schema, join_model).ok_or_else(|| OpenAuthError::TableNotFound {
table: join_model.clone(),
})?;
let mut foreign_keys = foreign_keys_to_table(join_table, &base_table.name);
let is_forward_join = !foreign_keys.is_empty();
if foreign_keys.is_empty() {
foreign_keys = foreign_keys_to_table(base_table, &join_table.name);
}
let [(foreign_key, field)] =
foreign_keys
.as_slice()
.try_into()
.map_err(|_| match foreign_keys.len() {
0 => OpenAuthError::JoinForeignKeyNotFound {
base_model: base_model.to_owned(),
join_model: join_model.clone(),
},
_ => OpenAuthError::JoinForeignKeyAmbiguous {
base_model: base_model.to_owned(),
join_model: join_model.clone(),
},
})?;
let reference =
field
.foreign_key
.as_ref()
.ok_or_else(|| OpenAuthError::JoinForeignKeyNotFound {
base_model: base_model.to_owned(),
join_model: join_model.clone(),
})?;
let (from, to, relation_field) = if is_forward_join {
(
logical_field_name(base_table, &reference.field)?,
(*foreign_key).to_owned(),
field,
)
} else {
(
(*foreign_key).to_owned(),
logical_field_name(join_table, &reference.field)?,
field,
)
};
let relation = if to == "id" || relation_field.unique {
JoinRelation::OneToOne
} else {
JoinRelation::OneToMany
};
let limit = if relation == JoinRelation::OneToOne {
1
} else {
option.limit.unwrap_or(default_limit)
};
resolved.push(FallbackJoin {
model: join_logical.to_owned(),
from,
to,
relation,
limit,
});
}
Ok(resolved)
}
fn initialize_join_value(record: &mut DbRecord, join: &FallbackJoin) {
let value = if join.relation == JoinRelation::OneToOne {
DbValue::Null
} else {
DbValue::RecordArray(Vec::new())
};
record.insert(join.model.clone(), value);
}
fn in_value(values: Vec<DbValue>) -> Option<DbValue> {
let mut strings = Vec::new();
let mut numbers = Vec::new();
for value in values {
match value {
DbValue::String(value) if !strings.contains(&value) => strings.push(value),
DbValue::Number(value) if !numbers.contains(&value) => numbers.push(value),
_ => {}
}
}
if !strings.is_empty() {
Some(DbValue::StringArray(strings))
} else if !numbers.is_empty() {
Some(DbValue::NumberArray(numbers))
} else {
None
}
}
fn find_table<'a>(schema: &'a DbSchema, model: &str) -> Option<(&'a str, &'a DbTable)> {
schema
.tables()
.find(|(logical_name, table)| *logical_name == model || table.name == model)
}
fn foreign_keys_to_table<'a>(
table: &'a DbTable,
target_table: &str,
) -> Vec<(&'a str, &'a DbField)> {
table
.fields
.iter()
.filter_map(|(logical_name, field)| {
field
.foreign_key
.as_ref()
.filter(|foreign_key| foreign_key.table == target_table)
.map(|_| (logical_name.as_str(), field))
})
.collect()
}
fn logical_field_name(table: &DbTable, field: &str) -> Result<String, OpenAuthError> {
table
.fields
.iter()
.find_map(|(logical_name, metadata)| {
(logical_name == field || metadata.name == field).then(|| logical_name.clone())
})
.ok_or_else(|| OpenAuthError::FieldNotFound {
table: table.name.clone(),
field: field.to_owned(),
})
}