use std::sync::Arc;
use bson::Document;
use bson::oid::ObjectId;
use indexmap::IndexMap;
use vantage_core::{Context, Result, error};
use vantage_table::column::core::Column as TableColumn;
use vantage_table::column::flags::ColumnFlag;
use vantage_table::table::{Orderable, Table, VistaMetadataOptions};
use vantage_table::traits::column_like::ColumnLike;
use vantage_types::{EmptyEntity, Entity};
use vantage_vista::{SpecResolver, Vista, VistaCapabilities, VistaFactory};
use crate::mongodb::impls::joins::table_joins;
use crate::mongodb::{MongoDB, MongoView};
use crate::types::AnyMongoType;
use crate::vista::source::MongoTableShell;
use crate::vista::spec::{
MongoBlock, MongoColumnExtras, MongoReferenceExtras, MongoTableExtras, MongoVistaSpec,
};
mod ref_keys;
pub type MongoSpecResolver = SpecResolver<MongoVistaSpec>;
#[derive(Clone)]
pub struct MongoVistaFactory {
pub(crate) mongo: MongoDB,
resolver: Option<MongoSpecResolver>,
pub(crate) change_streams: Option<bool>,
}
impl MongoVistaFactory {
pub fn new(mongo: MongoDB) -> Self {
Self {
mongo,
resolver: None,
change_streams: None,
}
}
pub fn with_change_streams(mut self, enabled: bool) -> Self {
self.change_streams = Some(enabled);
self
}
pub(crate) fn live_enabled(&self) -> bool {
self.change_streams
.unwrap_or_else(|| self.mongo.supports_change_streams())
}
pub fn with_resolver(mut self, resolver: MongoSpecResolver) -> Self {
self.resolver = Some(resolver);
self
}
fn resolver_with_fallback(&self) -> MongoSpecResolver {
let user = self.resolver.clone();
Arc::new(move |name: &str| {
user.as_ref()
.and_then(|resolve| resolve(name))
.or_else(|| Some(bare_spec(name)))
})
}
#[cfg(feature = "rhai")]
pub(crate) fn script_spec(&self, name: &str) -> Result<MongoVistaSpec> {
match &self.resolver {
Some(resolve) => {
resolve(name).ok_or_else(|| error!("script table(): unknown table", table = name))
}
None => Ok(bare_spec(name)),
}
}
pub fn from_table<E>(&self, table: Table<MongoDB, E>) -> Result<Vista>
where
E: Entity<AnyMongoType> + 'static,
{
let name = table.table_name().to_string();
let any_table = table.into_entity::<EmptyEntity>();
table_joins(&any_table)?;
ref_keys::check_table(&any_table)?;
let column_paths = if is_bare(&any_table) {
IndexMap::new()
} else {
paths_from_table_columns(&any_table)
};
Ok(self.wrap_with_paths(any_table, column_paths, name, true, IndexMap::new()))
}
fn wrap_with_paths(
&self,
table: Table<MongoDB, EmptyEntity>,
column_paths: IndexMap<String, Vec<String>>,
name: String,
search_unflagged_columns: bool,
ref_scripts: IndexMap<String, String>,
) -> Vista {
let mut metadata = table.vista_metadata(VistaMetadataOptions {
orderable: Orderable::All,
references: true,
contained: true,
column_flags: true,
..VistaMetadataOptions::default()
});
for (relation, script) in ref_scripts {
if let Some(reference) = metadata.references.get_mut(&relation) {
reference.build_script = Some(script);
}
}
let writable = table.data_source().view().is_none();
let live = writable && self.live_enabled();
let source = MongoTableShell::new(
table,
VistaCapabilities {
can_count: true,
can_insert: writable,
can_update: writable,
can_delete: writable,
can_order: true,
can_search: true,
can_set_page_size: true,
can_fetch_page: true,
can_fetch_next: true,
can_fetch_window: true,
can_traverse_to_record: writable,
can_traverse_to_set: writable,
can_traverse_in_columns: writable,
can_filter_operators: true,
can_confine_writes: writable,
can_subscribe: live,
can_import: live,
can_build_ref_via_script: cfg!(feature = "rhai"),
..VistaCapabilities::default()
},
metadata,
column_paths,
self.resolver.clone(),
search_unflagged_columns,
self.change_streams,
);
Vista::new(name, Box::new(source))
}
pub fn table_from_spec(&self, spec: &MongoVistaSpec) -> Result<Table<MongoDB, EmptyEntity>> {
let resolver = self.resolver_with_fallback();
ref_keys::check_spec(spec, Some(&resolver))?;
build_mongo_table(spec, self.mongo.clone(), Some(resolver))
}
}
fn bare_spec(name: &str) -> MongoVistaSpec {
MongoVistaSpec {
name: name.to_string(),
datasource: None,
id_column: None,
columns: IndexMap::new(),
references: IndexMap::new(),
contained: IndexMap::new(),
driver: MongoTableExtras::default(),
}
}
fn is_bare<E: Entity<AnyMongoType>>(table: &Table<MongoDB, E>) -> bool {
table.columns().len() == 1 && table.columns().contains_key("_id")
}
pub(crate) fn build_mongo_table(
spec: &MongoVistaSpec,
db: MongoDB,
resolver: Option<MongoSpecResolver>,
) -> Result<Table<MongoDB, EmptyEntity>> {
let block = spec.driver.mongo.as_ref();
let collection = block
.and_then(|m| m.collection.clone())
.unwrap_or_else(|| spec.name.clone());
let stages = block
.map(view_stages)
.transpose()
.with_context(|| error!("pipeline view can't be built", table = spec.name.as_str()))?
.flatten();
let db = match stages {
Some(stages) => db.with_view(MongoView::new(collection.clone(), stages)),
None => db.without_view(),
};
let joined: Vec<&str> = spec
.columns
.keys()
.filter(|name| name.contains('.'))
.map(String::as_str)
.collect();
check_joined_columns(spec, &joined, db.view().is_some())?;
let mut table = Table::<MongoDB, EmptyEntity>::new(collection, db);
if spec.columns.is_empty() {
table.add_column(TableColumn::<AnyMongoType>::new("_id"));
table.set_spec_id_field("_id")?;
} else {
table.add_spec_columns(&spec.columns, build_column)?;
table.set_spec_id_field(&spec.resolve_id_column_or("_id"))?;
}
let mut table = table.with_spec_references(&spec.references, resolver, build_mongo_table);
for name in joined {
table.add_imported_column(name)?;
}
table_joins(&table)?;
table.with_contained_specs(&spec.contained, build_column)
}
fn view_stages(block: &MongoBlock) -> Result<Option<Vec<Document>>> {
match &block.rhai {
Some(_) if block.pipeline.is_some() => Err(error!(
"a mongo block takes either `pipeline:` or `rhai:`, not both"
)),
Some(code) => pipeline_from_rhai(code, &block.args).map(Some),
None => block.pipeline_stages(),
}
}
#[cfg(feature = "rhai")]
fn pipeline_from_rhai(code: &str, args: &[(String, String)]) -> Result<Vec<Document>> {
crate::vista::scripting::eval_pipeline(code, args)
}
#[cfg(not(feature = "rhai"))]
fn pipeline_from_rhai(_code: &str, _args: &[(String, String)]) -> Result<Vec<Document>> {
Err(missing_rhai("rhai"))
}
#[cfg_attr(feature = "rhai", allow(dead_code))]
pub(crate) fn missing_rhai(slot: &str) -> vantage_core::VantageError {
error!(
"vista declares a Rhai script but vantage-mongodb was built without the `rhai` feature",
slot = slot
)
}
fn check_joined_columns(spec: &MongoVistaSpec, joined: &[&str], is_view: bool) -> Result<()> {
let Some(first) = joined.first() else {
return Ok(());
};
if is_view {
return Err(error!(
"a pipeline view cannot declare a joined (dotted) column",
table = spec.name.as_str(),
column = *first
));
}
for name in joined {
if spec.columns[*name].driver.mongo.is_some() {
return Err(error!(
"a joined (dotted) column cannot set a mongo path",
table = spec.name.as_str(),
column = *name
));
}
}
Ok(())
}
pub(crate) fn build_column(
name: &str,
col_spec: &vantage_vista::ColumnSpec<MongoColumnExtras>,
) -> Result<TableColumn<AnyMongoType>> {
let alias = match col_spec.driver.mongo.as_ref() {
Some(block) => block.resolved_path(name)?.map(|path| path.join(".")),
None => None,
};
let column = TableColumn::from_spec(name, col_spec, alias, column_for_type)?;
Ok(if col_spec.has_flag(vantage_vista::flags::SEARCHABLE) {
column.with_flag(ColumnFlag::Searchable)
} else {
column
})
}
pub(crate) fn column_for_type(name: &str, ty: &str) -> Result<TableColumn<AnyMongoType>> {
let col: TableColumn<AnyMongoType> = match ty {
"int" | "integer" | "i64" | "i32" => {
TableColumn::from_column(TableColumn::<i64>::new(name))
}
"float" | "double" | "f64" => TableColumn::from_column(TableColumn::<f64>::new(name)),
"bool" | "boolean" => TableColumn::from_column(TableColumn::<bool>::new(name)),
"string" | "text" | "str" => TableColumn::from_column(TableColumn::<String>::new(name)),
"object_id" | "objectid" | "oid" => {
TableColumn::from_column(TableColumn::<ObjectId>::new(name))
}
"datetime" | "date" | "timestamp" => {
TableColumn::from_column(TableColumn::<bson::DateTime>::new(name))
}
"decimal" => TableColumn::from_column(TableColumn::<bson::Decimal128>::new(name)),
other => {
return Err(error!(
"Unknown YAML column type",
column = name,
ty = other.to_string()
));
}
};
Ok(col)
}
pub(crate) fn paths_from_table_columns<T, E>(table: &Table<T, E>) -> IndexMap<String, Vec<String>>
where
T: vantage_table::traits::table_source::TableSource,
E: Entity<T::Value>,
T::Column<T::AnyType>: ColumnLike<T::AnyType>,
{
let mut paths = IndexMap::new();
for (name, col) in table.columns() {
let path = match col.alias() {
Some(a) => a.split('.').map(str::to_string).collect(),
None => vec![name.clone()],
};
paths.insert(name.clone(), path);
}
paths
}
impl VistaFactory for MongoVistaFactory {
type TableExtras = MongoTableExtras;
type ColumnExtras = MongoColumnExtras;
type ReferenceExtras = MongoReferenceExtras;
fn build_from_spec(&self, spec: MongoVistaSpec) -> Result<Vista> {
let modify = spec.driver.mongo.as_ref().and_then(|b| b.modify.clone());
#[cfg(not(feature = "rhai"))]
if modify.is_some() {
return Err(missing_rhai("modify"));
}
let table = self.table_from_spec(&spec)?;
let column_paths = paths_from_table_columns(&table);
let ref_scripts = spec
.references
.iter()
.filter_map(|(name, r)| {
let script = r.driver.mongo.as_ref()?.rhai.clone()?;
Some((name.clone(), script))
})
.collect();
let vista = self.wrap_with_paths(table, column_paths, spec.name, false, ref_scripts);
#[cfg(feature = "rhai")]
if let Some(code) = modify {
return crate::vista::scripting::apply_modify(self, vista, &code);
}
Ok(vista)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_rhai_names_the_feature_and_slot() {
let msg = missing_rhai("modify").to_string();
assert!(msg.contains("`rhai` feature"), "{msg}");
assert!(msg.contains("modify"), "{msg}");
}
}