#![doc = include_str!("../README.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc, clippy::cast_sign_loss)]
mod ffi;
use bytes::Bytes;
use hashbrown::{HashMap, HashSet};
use http::StatusCode;
use parking_lot::RwLock;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{Instrument, instrument};
use ordinary_auth::Auth;
use ordinary_config::{
ActionAccessAuthOps, ActionAccessModelOps, ActionAccessPermission, ActionConfig,
ActionFfiVersion, ContentDefinition, ModelConfig,
};
use ordinary_integration::Integration;
use ordinary_storage::{ArtifactKind, CacheRead, Storage};
use ordinary_types::{Field, Kind};
pub use wasmtime::Engine;
use wasmtime::Module;
#[derive(Clone, Debug, PartialEq)]
pub enum ActionResult {
Result(Bytes),
StatusCode(StatusCode),
}
#[derive(Clone)]
pub struct PrivilegedComponents {
pub auth: Arc<Auth>,
pub app_domains: Arc<Vec<String>>,
pub apps_dir: PathBuf,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone)]
pub struct Action {
pub idx: u8,
pub config: ActionConfig,
module: Arc<RwLock<Option<Module>>>,
engine: Engine,
auth: Arc<Auth>,
storage: Arc<Storage>,
integrations: Arc<Vec<Integration>>,
has_integrations: bool,
allowed_integrations: Arc<HashMap<u8, Kind>>,
has_actions: bool,
allowed_actions: Arc<HashSet<u8>>,
has_content_defs: bool,
allowed_content_defs: Arc<HashSet<u8>>,
#[allow(clippy::type_complexity)]
allowed_model_ops: Arc<HashMap<u8, (Vec<ActionAccessModelOps>, Vec<Field>)>>,
can_auth_set_token_fields: bool,
can_model_insert: bool,
can_model_update: bool,
can_model_delete: bool,
can_model_get: bool,
can_model_query: bool,
can_model_search: bool,
privileged_components: Option<PrivilegedComponents>,
}
impl Action {
#[instrument(skip_all, fields(i, nm), err)]
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
pub fn new(
src: Option<Bytes>,
engine: Engine,
config: ActionConfig,
auth: Arc<Auth>,
storage: Arc<Storage>,
integrations: Arc<Vec<Integration>>,
model_map: &HashMap<String, ModelConfig>,
content_map: &HashMap<String, ContentDefinition>,
action_configs: &Option<Vec<ActionConfig>>,
privileged_components: Option<PrivilegedComponents>,
) -> anyhow::Result<Self> {
tracing::Span::current().record("i", config.idx);
tracing::Span::current().record("nm", tracing::field::display(&config.name));
let module = if let Some(src) = src
&& let Ok(module) = Module::new(&engine, src)
{
Arc::new(RwLock::new(Some(module)))
} else {
Arc::new(RwLock::new(None))
};
let mut has_integrations = false;
let mut allowed_integrations = HashMap::new();
let mut has_actions = false;
let mut allowed_actions = HashSet::new();
let mut has_content_defs = false;
let mut allowed_content_defs = HashSet::new();
let mut allowed_model_ops = HashMap::new();
let mut can_auth_set_token_fields = false;
let mut can_model_insert = false;
let mut can_model_update = false;
let mut can_model_delete = false;
let mut can_model_get = false;
let mut can_model_query = false;
let mut can_model_search = false;
for access in &config.access {
match access {
ActionAccessPermission::Auth { ops } => {
for op in ops {
match op {
ActionAccessAuthOps::SetTokenFields => {
can_auth_set_token_fields = true;
}
}
}
}
ActionAccessPermission::Integration { name } => {
has_integrations = true;
for integration in integrations.iter() {
if integration.config.name == *name {
allowed_integrations
.insert(integration.config.idx, integration.config.send.clone());
}
}
}
ActionAccessPermission::Model { name, ops } => {
if let Some(model_config) = model_map.get(name)
&& model_config.name == *name
{
for op in ops {
match op {
ActionAccessModelOps::Insert => can_model_insert = true,
ActionAccessModelOps::Update => can_model_update = true,
ActionAccessModelOps::Delete => can_model_delete = true,
ActionAccessModelOps::Get => can_model_get = true,
ActionAccessModelOps::Query => can_model_query = true,
ActionAccessModelOps::Search => can_model_search = true,
}
}
let mut fields = model_config.fields.clone();
fields.push(Field {
idx: 0,
name: "uuid".into(),
kind: Kind::Uuid,
indexed: Some(true),
queryable: None,
searchable: None,
mapping: None,
doc: None,
encrypted: None,
compressed: None,
});
fields.sort_by_key(|a| a.idx);
allowed_model_ops.insert(model_config.idx, (ops.clone(), fields));
}
}
ActionAccessPermission::Content { name } => {
has_content_defs = true;
if let Some(def) = content_map.get(name) {
allowed_content_defs.insert(def.idx);
}
}
ActionAccessPermission::Action { name } => {
has_actions = true;
if let Some(action_configs) = &action_configs {
for action in action_configs {
if action.name == *name {
allowed_actions.insert(action.idx);
}
}
}
}
}
}
Ok(Self {
idx: config.idx,
config,
module,
engine,
auth,
storage,
integrations,
has_actions,
allowed_actions: Arc::new(allowed_actions),
has_integrations,
allowed_integrations: Arc::new(allowed_integrations),
has_content_defs,
allowed_content_defs: Arc::new(allowed_content_defs),
allowed_model_ops: Arc::new(allowed_model_ops),
can_auth_set_token_fields,
can_model_insert,
can_model_update,
can_model_delete,
can_model_get,
can_model_query,
can_model_search,
privileged_components,
})
}
#[instrument(skip_all, err)]
pub async fn set_wasm(&self, src: &[u8]) -> anyhow::Result<()> {
let storage_span = tracing::info_span!("storage");
let span = storage_span.in_scope(|| tracing::info_span!("artifact"));
span.in_scope(|| {
self.storage
.artifact
.put(self.idx, ArtifactKind::Action, src)
})?;
{
let mut lock = self.module.write();
let module = Module::new(&self.engine, src)?;
*lock = Some(module);
}
let span = storage_span.in_scope(|| tracing::info_span!("cache"));
async {
self.storage
.cache
.artifact_evict(CacheRead::Action, self.idx)
.await
}
.instrument(span)
.await?;
Ok(())
}
#[allow(clippy::missing_panics_doc, clippy::too_many_lines)]
#[instrument(skip_all, err)]
pub fn call(&self, args: &[u8], actions: &Arc<Vec<Action>>) -> anyhow::Result<ActionResult> {
match self.config.ffi.version {
ActionFfiVersion::V1 => ffi::v1::call(self, args, actions),
}
}
}