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;
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(
83 skip(
84 src,
85 engine,
86 config,
87 auth,
88 storage,
89 integrations,
90 model_map,
91 content_map,
92 action_configs,
93 privileged_components,
94 ),
95 fields(i, nm),
96 err
97 )]
98 #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
99 pub fn new(
100 src: Option<Bytes>,
101 engine: Engine,
102 config: ActionConfig,
103 auth: Arc<Auth>,
104 storage: Arc<Storage>,
105 integrations: Arc<Vec<Integration>>,
106 model_map: &HashMap<String, ModelConfig>,
107 content_map: &HashMap<String, ContentDefinition>,
108 action_configs: &Option<Vec<ActionConfig>>,
109 privileged_components: Option<PrivilegedComponents>,
110 ) -> anyhow::Result<Self> {
111 tracing::Span::current().record("i", config.idx);
112 tracing::Span::current().record("nm", tracing::field::display(&config.name));
113
114 let module = if let Some(src) = src
115 && let Ok(module) = Module::new(&engine, src)
116 {
117 Arc::new(RwLock::new(Some(module)))
118 } else {
119 Arc::new(RwLock::new(None))
120 };
121
122 let mut has_integrations = false;
123 let mut allowed_integrations = HashMap::new();
124
125 let mut has_actions = false;
126 let mut allowed_actions = HashSet::new();
127
128 let mut has_content_defs = false;
129 let mut allowed_content_defs = HashSet::new();
130
131 let mut allowed_model_ops = HashMap::new();
132
133 let mut can_auth_set_token_fields = false;
134
135 let mut can_model_insert = false;
136 let mut can_model_update = false;
137 let mut can_model_delete = false;
138
139 let mut can_model_get = false;
140 let mut can_model_query = false;
141 let mut can_model_search = false;
142
143 for access in &config.access {
144 match access {
145 ActionAccessPermission::Auth { ops } => {
146 for op in ops {
147 match op {
148 ActionAccessAuthOps::SetTokenFields => {
149 can_auth_set_token_fields = true;
150 }
151 }
152 }
153 }
154 ActionAccessPermission::Integration { name } => {
155 has_integrations = true;
156
157 for integration in integrations.iter() {
158 if integration.config.name == *name {
159 allowed_integrations
160 .insert(integration.config.idx, integration.config.send.clone());
161 }
162 }
163 }
164 ActionAccessPermission::Model { name, ops } => {
165 if let Some(model_config) = model_map.get(name)
166 && model_config.name == *name
167 {
168 for op in ops {
169 match op {
170 ActionAccessModelOps::Insert => can_model_insert = true,
171 ActionAccessModelOps::Update => can_model_update = true,
172 ActionAccessModelOps::Delete => can_model_delete = true,
173
174 ActionAccessModelOps::Get => can_model_get = true,
175 ActionAccessModelOps::Query => can_model_query = true,
176 ActionAccessModelOps::Search => can_model_search = true,
177 }
178 }
179
180 let mut fields = model_config.fields.clone();
181 fields.push(Field {
182 idx: 0,
183 name: "uuid".into(),
184 kind: Kind::Uuid,
185 indexed: Some(true),
186 queryable: None,
187 searchable: None,
188 mapping: None,
189 doc: None,
190 encrypted: None,
191 compressed: None,
192 });
193 fields.sort_by(|a, b| a.idx.cmp(&b.idx));
194
195 allowed_model_ops.insert(model_config.idx, (ops.clone(), fields));
196 }
197 }
198 ActionAccessPermission::Content { name } => {
199 has_content_defs = true;
200
201 if let Some(def) = content_map.get(name) {
202 allowed_content_defs.insert(def.idx);
203 }
204 }
205 ActionAccessPermission::Action { name } => {
206 has_actions = true;
207
208 if let Some(action_configs) = &action_configs {
209 for action in action_configs {
210 if action.name == *name {
211 allowed_actions.insert(action.idx);
212 }
213 }
214 }
215 }
216 }
217 }
218
219 Ok(Self {
220 idx: config.idx,
221 config,
222
223 module,
224 engine,
225
226 auth,
227 storage,
228 integrations,
229
230 has_actions,
231 allowed_actions: Arc::new(allowed_actions),
232
233 has_integrations,
234 allowed_integrations: Arc::new(allowed_integrations),
235
236 has_content_defs,
237 allowed_content_defs: Arc::new(allowed_content_defs),
238
239 allowed_model_ops: Arc::new(allowed_model_ops),
240
241 can_auth_set_token_fields,
242
243 can_model_insert,
244 can_model_update,
245 can_model_delete,
246
247 can_model_get,
248 can_model_query,
249 can_model_search,
250
251 privileged_components,
252 })
253 }
254
255 #[instrument(skip(self, src), err)]
256 pub fn set_wasm(&self, src: &[u8]) -> anyhow::Result<()> {
257 let storage_span = tracing::info_span!("storage");
258 let span = storage_span.in_scope(|| tracing::info_span!("artifact"));
259
260 span.in_scope(|| {
261 self.storage
262 .artifact
263 .put(self.idx, ArtifactKind::Action, src)
264 })?;
265
266 let mut lock = self.module.write();
267 let module = Module::new(&self.engine, src)?;
268
269 *lock = Some(module);
270 drop(lock);
271
272 let span = storage_span.in_scope(|| tracing::info_span!("cache"));
273
274 span.in_scope(|| {
275 self.storage
276 .cache
277 .artifact_evict(CacheRead::Action, self.idx)
278 })?;
279
280 Ok(())
281 }
282
283 #[allow(clippy::missing_panics_doc, clippy::too_many_lines)]
284 #[instrument(skip(self, args, actions), err)]
285 pub fn call(&self, args: &[u8], actions: &Arc<Vec<Action>>) -> anyhow::Result<ActionResult> {
286 match self.config.ffi.version {
287 ActionFfiVersion::V1 => ffi::v1::call(self, args, actions),
288 }
289 }
290}