1#![doc = include_str!("../README.md")]
2#![warn(clippy::all, clippy::pedantic)]
3#![allow(clippy::missing_errors_doc, clippy::cast_sign_loss)]
4
5mod ffi;
10
11use bytes::Bytes;
12use hashbrown::{HashMap, HashSet};
13use http::StatusCode;
14use parking_lot::RwLock;
15use std::path::PathBuf;
16use std::sync::Arc;
17use tracing::{Instrument, instrument};
18
19use ordinary_auth::Auth;
20use ordinary_config::{
21 ActionAccessAuthOps, ActionAccessModelOps, ActionAccessPermission, ActionConfig,
22 ActionFfiVersion, ContentDefinition, ModelConfig,
23};
24use ordinary_integration::Integration;
25use ordinary_storage::{ArtifactKind, CacheRead, Storage};
26use ordinary_types::{Field, Kind};
27
28pub use wasmtime::Engine;
29use wasmtime::Module;
30
31#[derive(Clone, Debug, PartialEq)]
32pub enum ActionResult {
33 Result(Bytes),
34 StatusCode(StatusCode),
35}
36
37#[derive(Clone)]
38pub struct PrivilegedComponents {
39 pub auth: Arc<Auth>,
40 pub app_domains: Arc<Vec<String>>,
41 pub apps_dir: PathBuf,
42}
43
44#[allow(clippy::struct_excessive_bools)]
45#[derive(Clone)]
46pub struct Action {
47 pub idx: u8,
48 pub config: ActionConfig,
49
50 module: Arc<RwLock<Option<Module>>>,
51 engine: Engine,
52
53 auth: Arc<Auth>,
54 storage: Arc<Storage>,
55 integrations: Arc<Vec<Integration>>,
56
57 has_integrations: bool,
58 allowed_integrations: Arc<HashMap<u8, Kind>>,
59 has_actions: bool,
60 allowed_actions: Arc<HashSet<u8>>,
61
62 has_content_defs: bool,
63 allowed_content_defs: Arc<HashSet<u8>>,
64
65 #[allow(clippy::type_complexity)]
66 allowed_model_ops: Arc<HashMap<u8, (Vec<ActionAccessModelOps>, Vec<Field>)>>,
67
68 can_auth_set_token_fields: bool,
69
70 can_model_insert: bool,
71 can_model_update: bool,
72 can_model_delete: bool,
73
74 can_model_get: bool,
75 can_model_query: bool,
76 can_model_search: bool,
77
78 privileged_components: Option<PrivilegedComponents>,
79}
80
81impl Action {
82 #[instrument(skip_all, fields(i, nm), err)]
83 #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
84 pub fn new(
85 src: Option<Bytes>,
86 engine: Engine,
87 config: ActionConfig,
88 auth: Arc<Auth>,
89 storage: Arc<Storage>,
90 integrations: Arc<Vec<Integration>>,
91 model_map: &HashMap<String, ModelConfig>,
92 content_map: &HashMap<String, ContentDefinition>,
93 action_configs: &Option<Vec<ActionConfig>>,
94 privileged_components: Option<PrivilegedComponents>,
95 ) -> anyhow::Result<Self> {
96 tracing::Span::current().record("i", config.idx);
97 tracing::Span::current().record("nm", tracing::field::display(&config.name));
98
99 let module = if let Some(src) = src
100 && let Ok(module) = Module::new(&engine, src)
101 {
102 Arc::new(RwLock::new(Some(module)))
103 } else {
104 Arc::new(RwLock::new(None))
105 };
106
107 let mut has_integrations = false;
108 let mut allowed_integrations = HashMap::new();
109
110 let mut has_actions = false;
111 let mut allowed_actions = HashSet::new();
112
113 let mut has_content_defs = false;
114 let mut allowed_content_defs = HashSet::new();
115
116 let mut allowed_model_ops = HashMap::new();
117
118 let mut can_auth_set_token_fields = false;
119
120 let mut can_model_insert = false;
121 let mut can_model_update = false;
122 let mut can_model_delete = false;
123
124 let mut can_model_get = false;
125 let mut can_model_query = false;
126 let mut can_model_search = false;
127
128 for access in &config.access {
129 match access {
130 ActionAccessPermission::Auth { ops } => {
131 for op in ops {
132 match op {
133 ActionAccessAuthOps::SetTokenFields => {
134 can_auth_set_token_fields = true;
135 }
136 }
137 }
138 }
139 ActionAccessPermission::Integration { name } => {
140 has_integrations = true;
141
142 for integration in integrations.iter() {
143 if integration.config.name == *name {
144 allowed_integrations
145 .insert(integration.config.idx, integration.config.send.clone());
146 }
147 }
148 }
149 ActionAccessPermission::Model { name, ops } => {
150 if let Some(model_config) = model_map.get(name)
151 && model_config.name == *name
152 {
153 for op in ops {
154 match op {
155 ActionAccessModelOps::Insert => can_model_insert = true,
156 ActionAccessModelOps::Update => can_model_update = true,
157 ActionAccessModelOps::Delete => can_model_delete = true,
158
159 ActionAccessModelOps::Get => can_model_get = true,
160 ActionAccessModelOps::Query => can_model_query = true,
161 ActionAccessModelOps::Search => can_model_search = true,
162 }
163 }
164
165 let mut fields = model_config.fields.clone();
166 fields.push(Field {
167 idx: 0,
168 name: "uuid".into(),
169 kind: Kind::Uuid,
170 indexed: Some(true),
171 queryable: None,
172 searchable: None,
173 mapping: None,
174 doc: None,
175 encrypted: None,
176 compressed: None,
177 });
178 fields.sort_by_key(|a| a.idx);
179
180 allowed_model_ops.insert(model_config.idx, (ops.clone(), fields));
181 }
182 }
183 ActionAccessPermission::Content { name } => {
184 has_content_defs = true;
185
186 if let Some(def) = content_map.get(name) {
187 allowed_content_defs.insert(def.idx);
188 }
189 }
190 ActionAccessPermission::Action { name } => {
191 has_actions = true;
192
193 if let Some(action_configs) = &action_configs {
194 for action in action_configs {
195 if action.name == *name {
196 allowed_actions.insert(action.idx);
197 }
198 }
199 }
200 }
201 }
202 }
203
204 Ok(Self {
205 idx: config.idx,
206 config,
207
208 module,
209 engine,
210
211 auth,
212 storage,
213 integrations,
214
215 has_actions,
216 allowed_actions: Arc::new(allowed_actions),
217
218 has_integrations,
219 allowed_integrations: Arc::new(allowed_integrations),
220
221 has_content_defs,
222 allowed_content_defs: Arc::new(allowed_content_defs),
223
224 allowed_model_ops: Arc::new(allowed_model_ops),
225
226 can_auth_set_token_fields,
227
228 can_model_insert,
229 can_model_update,
230 can_model_delete,
231
232 can_model_get,
233 can_model_query,
234 can_model_search,
235
236 privileged_components,
237 })
238 }
239
240 #[instrument(skip_all, err)]
241 pub async fn set_wasm(&self, src: &[u8]) -> anyhow::Result<()> {
242 let storage_span = tracing::info_span!("storage");
243 let span = storage_span.in_scope(|| tracing::info_span!("artifact"));
244
245 span.in_scope(|| {
246 self.storage
247 .artifact
248 .put(self.idx, ArtifactKind::Action, src)
249 })?;
250
251 {
252 let mut lock = self.module.write();
253 let module = Module::new(&self.engine, src)?;
254
255 *lock = Some(module);
256 }
257
258 let span = storage_span.in_scope(|| tracing::info_span!("cache"));
259
260 async {
261 self.storage
262 .cache
263 .artifact_evict(CacheRead::Action, self.idx)
264 .await
265 }
266 .instrument(span)
267 .await?;
268
269 Ok(())
270 }
271
272 #[allow(clippy::missing_panics_doc, clippy::too_many_lines)]
273 #[instrument(skip_all, err)]
274 pub fn call(&self, args: &[u8], actions: &Arc<Vec<Action>>) -> anyhow::Result<ActionResult> {
275 match self.config.ffi.version {
276 ActionFfiVersion::V1 => ffi::v1::call(self, args, actions),
277 }
278 }
279}