1use std::any::{Any, TypeId};
2use std::collections::{BTreeMap, HashMap};
3use std::future::Future;
4
5use std::pin::Pin;
6use std::sync::Mutex;
7use std::time::{Duration, SystemTime};
8
9use teaql_core::{EntityDescriptor, Record, UpdateCommand, Value};
10use teaql_sql::{CompiledQuery, DatabaseKind};
11
12use crate::{
13 CheckResults, CheckerRegistry, ContextError, EntityDataServiceBehavior,
14 EntityDataServiceBehaviorRegistry, EntityRegistry, GraphNode, InternalIdGenerator, Language,
15 MetadataStore, ObjectLocation, RawAuditEvent, RawAuditEventSink, RequestPolicy, RuntimeError,
16 local_id_generator, translate_check_result,
17};
18use crate::{DataServiceError, EntityRoot};
19
20#[derive(Debug, Clone, PartialEq)]
21pub struct ContinuousPageCursor {
22 pub cursor_id: String,
23 pub query_key: String,
24 pub entity: String,
25 pub direction: teaql_core::SortDirection,
26 pub boundary: Value,
27 pub page_size: u64,
28 pub next_offset: u64,
29 pub expires_at: SystemTime,
30}
31
32#[async_trait::async_trait]
33pub trait ContinuousPageCursorStore: Send + Sync + 'static {
34 async fn get(
35 &self,
36 query_key: &str,
37 target_offset: u64,
38 ) -> Result<Option<ContinuousPageCursor>, String>;
39 async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String>;
40 async fn invalidate(&self, query_key: &str) -> Result<(), String>;
41}
42
43pub struct InMemoryContinuousPageCursorStore {
44 cursors: Mutex<HashMap<String, ContinuousPageCursor>>,
45 max_entries: usize,
46}
47
48impl Default for InMemoryContinuousPageCursorStore {
49 fn default() -> Self {
50 Self {
51 cursors: Mutex::new(HashMap::new()),
52 max_entries: 4096,
53 }
54 }
55}
56
57#[async_trait::async_trait]
58impl ContinuousPageCursorStore for InMemoryContinuousPageCursorStore {
59 async fn get(
60 &self,
61 query_key: &str,
62 target_offset: u64,
63 ) -> Result<Option<ContinuousPageCursor>, String> {
64 let key = format!("{query_key}:{target_offset}");
65 let mut cursors = self.cursors.lock().map_err(|e| e.to_string())?;
66 if cursors
67 .get(&key)
68 .is_some_and(|cursor| cursor.expires_at <= SystemTime::now())
69 {
70 cursors.remove(&key);
71 }
72 Ok(cursors.get(&key).cloned())
73 }
74
75 async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String> {
76 let key = format!("{}:{}", cursor.query_key, cursor.next_offset);
77 let mut cursors = self.cursors.lock().map_err(|e| e.to_string())?;
78 if cursors.len() >= self.max_entries {
79 if let Some(expired_or_oldest) = cursors
80 .iter()
81 .min_by_key(|(_, value)| value.expires_at)
82 .map(|(key, _)| key.clone())
83 {
84 cursors.remove(&expired_or_oldest);
85 }
86 }
87 cursors.insert(key, cursor);
88 Ok(())
89 }
90
91 async fn invalidate(&self, query_key: &str) -> Result<(), String> {
92 let prefix = format!("{query_key}:");
93 self.cursors
94 .lock()
95 .map_err(|e| e.to_string())?
96 .retain(|key, _| !key.starts_with(&prefix));
97 Ok(())
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum SqlLogOperation {
103 Select,
104 Insert,
105 Update,
106 Delete,
107 Recover,
108}
109
110impl SqlLogOperation {
111 pub fn is_select(self) -> bool {
112 matches!(self, Self::Select)
113 }
114
115 pub fn is_mutation(self) -> bool {
116 !self.is_select()
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
121pub struct SqlLogOptions {
122 pub select: bool,
123 pub mutation: bool,
124}
125
126impl SqlLogOptions {
127 pub fn disabled() -> Self {
128 Self {
129 select: false,
130 mutation: false,
131 }
132 }
133
134 pub fn select_only() -> Self {
135 Self {
136 select: true,
137 mutation: false,
138 }
139 }
140
141 pub fn mutation_only() -> Self {
142 Self {
143 select: false,
144 mutation: true,
145 }
146 }
147
148 pub fn all() -> Self {
149 Self {
150 select: true,
151 mutation: true,
152 }
153 }
154
155 pub fn enabled_for(self, operation: SqlLogOperation) -> bool {
156 match operation.is_select() {
157 true => self.select,
158 false => self.mutation,
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq)]
164pub struct SqlLogEntry {
165 pub operation: SqlLogOperation,
166 pub sql: String,
167 pub params: Vec<Value>,
168 pub debug_sql: String,
169 pub pretty_sql: String,
170 pub started_at: SystemTime,
171 pub ended_at: SystemTime,
172 pub elapsed: Duration,
173 pub result_count: Option<usize>,
174 pub result_type: Option<String>,
175 pub affected_rows: Option<u64>,
176 pub result_summary: String,
177}
178
179#[derive(Debug, Clone, PartialEq)]
180pub struct UnifiedLogEntry {
181 pub timestamp: SystemTime,
182 pub user_identifier: Option<String>,
183 pub trace_chain: Vec<teaql_core::TraceNode>,
184 pub payload: LogPayload,
185}
186
187#[derive(Debug, Clone, PartialEq)]
188pub enum LogPayload {
189 Sql(SqlLogEntry),
190 Info(InfoLogEntry),
191}
192
193#[derive(Debug, Clone, PartialEq)]
194pub struct InfoLogEntry {
195 pub message: String,
196}
197
198#[derive(Clone, Default)]
199pub struct UnifiedLogBuffer {
200 pub entries: std::sync::Arc<Mutex<Vec<UnifiedLogEntry>>>,
201}
202
203pub trait SchemaProvider: Send + Sync {
204 fn ensure_schema<'a>(
205 &'a self,
206 ctx: &'a UserContext,
207 ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>>;
208}
209
210pub struct UserContext {
211 pub(crate) metadata: Option<Box<dyn MetadataStore>>,
212 pub(crate) entity_registry: Option<Box<dyn EntityRegistry>>,
213 pub(crate) entity_data_service_behavior_registry:
214 Option<Box<dyn EntityDataServiceBehaviorRegistry>>,
215 pub(crate) request_policy: Option<Box<dyn RequestPolicy>>,
216 pub(crate) checker_registry: Option<Box<dyn CheckerRegistry>>,
217 pub(crate) event_sink: Option<Box<dyn RawAuditEventSink>>,
218 pub(crate) custom_event_sink: Option<Box<dyn crate::SafeAuditEventSink>>,
219 pub(crate) internal_id_generator: Option<Box<dyn InternalIdGenerator>>,
220 schema_provider: Option<Box<dyn SchemaProvider>>,
221 language: Language,
222 typed_resources: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
223 named_resources: BTreeMap<String, Box<dyn Any + Send + Sync>>,
224 locals: BTreeMap<String, Value>,
225 pub(crate) initial_graphs: Vec<GraphNode>,
226 entity_root: EntityRoot,
227 sql_log_options: SqlLogOptions,
228 sql_log_entries: Mutex<Vec<SqlLogEntry>>,
229 user_identifier: Option<String>,
230 timezone: Option<String>,
231 trace_id: String,
232 continuous_page_cursor_store: std::sync::Arc<dyn ContinuousPageCursorStore>,
233 continuous_page_observation: Mutex<(String, Option<String>)>,
234}
235
236impl Default for UserContext {
237 fn default() -> Self {
238 let pid = std::process::id();
239 let thread_id_str = format!("{:?}", std::thread::current().id());
240 let numeric_thread_id = thread_id_str
241 .strip_prefix("ThreadId(")
242 .and_then(|s| s.strip_suffix(")"))
243 .unwrap_or(&thread_id_str);
244 let os_user = std::env::var("USER")
245 .or_else(|_| std::env::var("USERNAME"))
246 .unwrap_or_else(|_| "main".to_owned());
247 let user_id = format!("{os_user}@pid-{pid}.tid-{numeric_thread_id}");
248 Self {
249 metadata: None,
250 entity_registry: None,
251 entity_data_service_behavior_registry: None,
252 request_policy: None,
253 checker_registry: None,
254 event_sink: None,
255 custom_event_sink: None,
256 internal_id_generator: None,
257 schema_provider: None,
258 language: Language::default(),
259 typed_resources: HashMap::new(),
260 named_resources: BTreeMap::new(),
261 locals: BTreeMap::new(),
262 initial_graphs: Vec::new(),
263 entity_root: EntityRoot::default(),
264 sql_log_options: SqlLogOptions::all(),
265 sql_log_entries: Mutex::new(Vec::new()),
266 user_identifier: Some(user_id),
267 timezone: Some("UTC".to_owned()),
268 trace_id: format!(
269 "req-{pid}-{numeric_thread_id}-{:x}",
270 std::time::SystemTime::now()
271 .duration_since(std::time::UNIX_EPOCH)
272 .unwrap_or_default()
273 .as_micros()
274 ),
275 continuous_page_cursor_store: std::sync::Arc::new(
276 InMemoryContinuousPageCursorStore::default(),
277 ),
278 continuous_page_observation: Mutex::new(("DISABLED".to_owned(), None)),
279 }
280 }
281}
282
283#[async_trait::async_trait]
284pub trait DataStore: Send + Sync + 'static {
285 async fn get(&self, key: &str) -> Option<Value>;
286 async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>);
287 async fn remove(&self, key: &str);
288}
289
290#[derive(Default)]
291pub struct InMemoryDataStore {
292 cache: std::sync::RwLock<HashMap<String, (Value, Option<std::time::Instant>)>>,
293}
294
295#[async_trait::async_trait]
296impl DataStore for InMemoryDataStore {
297 async fn get(&self, key: &str) -> Option<Value> {
298 let lock = self.cache.read().unwrap();
299 if let Some((val, expires_at)) = lock.get(key) {
300 if let Some(exp) = expires_at {
301 if std::time::Instant::now() > *exp {
302 return None;
303 }
304 }
305 return Some(val.clone());
306 }
307 None
308 }
309
310 async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>) {
311 let mut lock = self.cache.write().unwrap();
312 let expires_at = timeout_seconds
313 .map(|secs| std::time::Instant::now() + std::time::Duration::from_secs(secs));
314 lock.insert(key.to_string(), (value, expires_at));
315 }
316
317 async fn remove(&self, key: &str) {
318 let mut lock = self.cache.write().unwrap();
319 lock.remove(key);
320 }
321}
322
323impl UserContext {
324 pub fn new() -> Self {
325 Self::default()
326 }
327
328 pub fn user_identifier(&self) -> Option<&str> {
329 self.user_identifier.as_deref()
330 }
331
332 pub fn set_user_identifier(&mut self, user_identifier: impl Into<String>) {
333 self.user_identifier = Some(user_identifier.into());
334 }
335
336 pub fn set_continuous_page_cursor_store(
337 &mut self,
338 store: std::sync::Arc<dyn ContinuousPageCursorStore>,
339 ) {
340 self.continuous_page_cursor_store = store;
341 }
342
343 pub fn continuous_page_plan(&self) -> Option<String> {
344 self.continuous_page_observation
345 .lock()
346 .ok()
347 .map(|value| value.0.clone())
348 }
349
350 pub fn continuous_page_cursor_id(&self) -> Option<String> {
351 self.continuous_page_observation
352 .lock()
353 .ok()
354 .and_then(|value| value.1.clone())
355 }
356
357 pub(crate) fn observe_continuous_page(
358 &self,
359 plan: impl Into<String>,
360 cursor_id: Option<String>,
361 ) {
362 if let Ok(mut observation) = self.continuous_page_observation.lock() {
363 *observation = (plan.into(), cursor_id);
364 }
365 }
366
367 pub(crate) fn continuous_page_cursor_store(&self) -> &dyn ContinuousPageCursorStore {
368 self.continuous_page_cursor_store.as_ref()
369 }
370
371 pub fn with_user_identifier(mut self, user_identifier: impl Into<String>) -> Self {
372 self.user_identifier = Some(user_identifier.into());
373 self
374 }
375
376 pub fn set_user_identifier_option(&mut self, user_identifier: Option<String>) {
377 self.user_identifier = user_identifier;
378 }
379
380 pub fn with_user_identifier_option(mut self, user_identifier: Option<String>) -> Self {
381 self.user_identifier = user_identifier;
382 self
383 }
384
385 pub fn timezone(&self) -> Option<&str> {
386 self.timezone.as_deref()
387 }
388
389 pub fn set_timezone(&mut self, timezone: impl Into<String>) {
390 self.timezone = Some(timezone.into());
391 }
392
393 pub fn with_timezone(mut self, timezone: impl Into<String>) -> Self {
394 self.timezone = Some(timezone.into());
395 self
396 }
397
398 pub fn trace_id(&self) -> &str {
399 &self.trace_id
400 }
401
402 pub fn set_trace_id(&mut self, trace_id: impl Into<String>) {
403 self.trace_id = trace_id.into();
404 }
405
406 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
407 self.trace_id = trace_id.into();
408 self
409 }
410
411 pub fn with_module(mut self, module: crate::RuntimeModule) -> Self {
412 module.apply_to(&mut self);
413 self
414 }
415
416 pub fn entity_root(&self) -> EntityRoot {
417 self.entity_root.clone()
418 }
419
420 pub fn initial_graphs(&self) -> &[GraphNode] {
421 &self.initial_graphs
422 }
423
424 pub fn set_initial_graphs(&mut self, graphs: Vec<GraphNode>) {
425 self.initial_graphs = graphs;
426 }
427
428 pub fn with_metadata(mut self, metadata: impl MetadataStore + 'static) -> Self {
429 self.metadata = Some(Box::new(metadata));
430 self
431 }
432
433 pub fn set_metadata(&mut self, metadata: impl MetadataStore + 'static) {
434 self.metadata = Some(Box::new(metadata));
435 }
436
437 pub fn with_entity_registry(mut self, registry: impl EntityRegistry + 'static) -> Self {
438 self.entity_registry = Some(Box::new(registry));
439 self
440 }
441
442 pub fn set_entity_registry(&mut self, registry: impl EntityRegistry + 'static) {
443 self.entity_registry = Some(Box::new(registry));
444 }
445
446 pub fn with_entity_data_service_behavior_registry(
447 mut self,
448 registry: impl EntityDataServiceBehaviorRegistry + 'static,
449 ) -> Self {
450 self.entity_data_service_behavior_registry = Some(Box::new(registry));
451 self
452 }
453
454 pub fn set_entity_data_service_behavior_registry(
455 &mut self,
456 registry: impl EntityDataServiceBehaviorRegistry + 'static,
457 ) {
458 self.entity_data_service_behavior_registry = Some(Box::new(registry));
459 }
460
461 pub fn with_request_policy(mut self, policy: impl RequestPolicy + 'static) -> Self {
462 self.request_policy = Some(Box::new(policy));
463 self
464 }
465
466 pub fn set_request_policy(&mut self, policy: impl RequestPolicy + 'static) {
467 self.request_policy = Some(Box::new(policy));
468 }
469
470 pub fn clear_request_policy(&mut self) {
471 self.request_policy = None;
472 }
473
474 pub fn with_checker_registry(mut self, registry: impl CheckerRegistry + 'static) -> Self {
475 self.checker_registry = Some(Box::new(registry));
476 self
477 }
478
479 pub fn set_checker_registry(&mut self, registry: impl CheckerRegistry + 'static) {
480 self.checker_registry = Some(Box::new(registry));
481 }
482
483 pub(crate) fn with_event_sink(mut self, sink: impl RawAuditEventSink + 'static) -> Self {
484 self.event_sink = Some(Box::new(sink));
485 self
486 }
487
488 pub(crate) fn set_event_sink(&mut self, sink: impl RawAuditEventSink + 'static) {
489 self.event_sink = Some(Box::new(sink));
490 }
491
492 pub fn with_custom_event_sink(
493 mut self,
494 sink: impl crate::SafeAuditEventSink + 'static,
495 ) -> Self {
496 self.custom_event_sink = Some(Box::new(sink));
497 self
498 }
499
500 pub fn set_custom_event_sink(&mut self, sink: impl crate::SafeAuditEventSink + 'static) {
501 self.custom_event_sink = Some(Box::new(sink));
502 }
503
504 pub fn with_internal_id_generator(
505 mut self,
506 generator: impl InternalIdGenerator + 'static,
507 ) -> Self {
508 self.internal_id_generator = Some(Box::new(generator));
509 self
510 }
511
512 pub fn set_internal_id_generator(&mut self, generator: impl InternalIdGenerator + 'static) {
513 self.internal_id_generator = Some(Box::new(generator));
514 }
515
516 pub fn with_schema_provider(mut self, provider: impl SchemaProvider + 'static) -> Self {
517 self.schema_provider = Some(Box::new(provider));
518 self
519 }
520
521 pub fn set_schema_provider(&mut self, provider: impl SchemaProvider + 'static) {
522 self.schema_provider = Some(Box::new(provider));
523 }
524
525 pub async fn ensure_schema(&self) -> Result<(), RuntimeError> {
526 let provider = self
527 .schema_provider
528 .as_ref()
529 .ok_or_else(|| RuntimeError::Schema("missing schema provider".to_owned()))?;
530 provider.ensure_schema(self).await
531 }
532
533 pub fn with_language(mut self, language: Language) -> Self {
534 self.language = language;
535 self
536 }
537
538 pub fn set_language(&mut self, language: Language) {
539 self.language = language;
540 }
541
542 pub fn with_sql_log_options(mut self, options: SqlLogOptions) -> Self {
543 self.sql_log_options = options;
544 self
545 }
546
547 pub fn set_sql_log_options(&mut self, options: SqlLogOptions) {
548 self.sql_log_options = options;
549 }
550
551 pub fn enable_select_sql_log(&mut self) {
552 self.sql_log_options.select = true;
553 }
554
555 pub fn enable_mutation_sql_log(&mut self) {
556 self.sql_log_options.mutation = true;
557 }
558
559 pub fn enable_all_sql_log(&mut self) {
560 self.sql_log_options = SqlLogOptions::all();
561 }
562
563 pub fn disable_sql_log(&mut self) {
564 self.sql_log_options = SqlLogOptions::disabled();
565 self.clear_sql_logs();
566 }
567
568 pub fn sql_log_options(&self) -> SqlLogOptions {
569 self.sql_log_options
570 }
571
572 pub fn sql_logs(&self) -> Vec<SqlLogEntry> {
573 self.sql_log_entries
574 .lock()
575 .map(|entries| entries.clone())
576 .unwrap_or_default()
577 }
578
579 pub fn clear_sql_logs(&self) {
580 if let Ok(mut entries) = self.sql_log_entries.lock() {
581 entries.clear();
582 }
583 }
584
585 pub(crate) fn record_sql_log(
586 &self,
587 operation: SqlLogOperation,
588 query: &CompiledQuery,
589 database_kind: DatabaseKind,
590 started_at: SystemTime,
591 ended_at: SystemTime,
592 elapsed: Duration,
593 result_count: Option<usize>,
594 result_type: Option<String>,
595 affected_rows: Option<u64>,
596 trace_chain: Vec<teaql_core::TraceNode>,
597 ) {
598 if !self.sql_log_options.enabled_for(operation) {
599 return;
600 }
601 let debug_sql = query.debug_sql(database_kind);
602 let result_summary = sql_result_summary(
603 operation,
604 result_count,
605 result_type.as_deref(),
606 affected_rows,
607 &debug_sql,
608 );
609
610 let sql_log_entry = SqlLogEntry {
611 operation,
612 sql: query.sql.clone(),
613 params: query.params.clone(),
614 pretty_sql: pretty_sql(&debug_sql),
615 debug_sql: debug_sql.clone(),
616 started_at,
617 ended_at,
618 elapsed,
619 result_summary: result_summary.clone(),
620 result_count,
621 result_type,
622 affected_rows,
623 };
624
625 if let Ok(mut entries) = self.sql_log_entries.lock() {
626 entries.push(sql_log_entry.clone());
630 }
631
632 if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
633 if let Ok(mut entries) = buf.entries.lock() {
634 entries.push(UnifiedLogEntry {
635 timestamp: started_at,
636 user_identifier: self.user_identifier.clone(),
637 trace_chain: trace_chain.clone(),
638 payload: LogPayload::Sql(sql_log_entry.clone()),
639 });
640 }
641 }
642
643 crate::log_formatter::LogManager::write_sql_log(&trace_chain, &sql_log_entry);
644 }
645
646 pub(crate) fn record_metadata_log(&self, metadata: &teaql_data_service::ExecutionMetadata) {
647 if let Some(debug_sql) = &metadata.debug_query {
648 let sql_log_entry = SqlLogEntry {
649 operation: match metadata.operation {
650 teaql_data_service::DataServiceOperation::Query => SqlLogOperation::Select,
651 teaql_data_service::DataServiceOperation::Insert => SqlLogOperation::Insert,
652 teaql_data_service::DataServiceOperation::Update => SqlLogOperation::Update,
653 teaql_data_service::DataServiceOperation::Delete => SqlLogOperation::Delete,
654 teaql_data_service::DataServiceOperation::Recover => SqlLogOperation::Update, teaql_data_service::DataServiceOperation::Batch => SqlLogOperation::Update,
656 teaql_data_service::DataServiceOperation::Schema => SqlLogOperation::Update,
657 },
658 sql: String::new(), params: Vec::new(), pretty_sql: pretty_sql(debug_sql),
661 debug_sql: debug_sql.clone(),
662 started_at: metadata.started_at,
663 ended_at: metadata.ended_at,
664 elapsed: metadata
665 .ended_at
666 .duration_since(metadata.started_at)
667 .unwrap_or_default(),
668 result_count: metadata.result_count,
669 result_type: None, affected_rows: metadata.affected_rows,
671 result_summary: String::new(), };
673
674 let mut summary = String::new();
676 if let Some(c) = metadata.result_count {
677 summary = format!("{} rows returned", c);
678 } else if let Some(a) = metadata.affected_rows {
679 summary = format!("{} rows affected", a);
680 }
681
682 let mut final_entry = sql_log_entry;
683 final_entry.result_summary = summary;
684
685 if let Ok(mut entries) = self.sql_log_entries.lock() {
686 entries.push(final_entry.clone());
687 }
688
689 if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
690 if let Ok(mut entries) = buf.entries.lock() {
691 entries.push(UnifiedLogEntry {
692 timestamp: metadata.started_at,
693 user_identifier: self.user_identifier.clone(),
694 trace_chain: metadata.trace_chain.clone(),
695 payload: LogPayload::Sql(final_entry.clone()),
696 });
697 }
698 }
699
700 crate::log_formatter::LogManager::write_sql_log(&metadata.trace_chain, &final_entry);
701 }
702 }
703
704 pub fn language(&self) -> Language {
705 self.language
706 }
707
708 pub fn set_language_code(&mut self, code: &str) -> Result<(), RuntimeError> {
709 let Some(language) = Language::from_code(code) else {
710 return Err(RuntimeError::Language(format!(
711 "unsupported language code: {code}"
712 )));
713 };
714 self.language = language;
715 Ok(())
716 }
717
718 pub fn generate_id(&self, entity: &str) -> Result<Option<u64>, RuntimeError> {
719 self.internal_id_generator
720 .as_ref()
721 .map(|generator| generator.generate_id(entity))
722 .transpose()
723 }
724
725 pub fn next_id(&self, entity: &str) -> Result<u64, RuntimeError> {
726 match self.generate_id(entity)? {
727 Some(id) => Ok(id),
728 None => local_id_generator().generate_id(entity),
729 }
730 }
731
732 pub fn entity(&self, name: &str) -> Option<&EntityDescriptor> {
733 self.metadata
734 .as_ref()
735 .and_then(|metadata| metadata.entity(name))
736 }
737
738 pub fn all_entities(&self) -> Vec<&EntityDescriptor> {
739 self.metadata
740 .as_ref()
741 .map(|metadata| metadata.all_entities())
742 .unwrap_or_default()
743 }
744
745 pub fn require_entity(&self, name: &str) -> Result<&EntityDescriptor, RuntimeError> {
746 self.entity(name)
747 .ok_or_else(|| RuntimeError::MissingEntity(name.to_owned()))
748 }
749
750 pub fn insert_resource<T>(&mut self, resource: T)
751 where
752 T: Send + Sync + 'static,
753 {
754 self.typed_resources
755 .insert(TypeId::of::<T>(), Box::new(resource));
756 }
757
758 pub fn get_resource<T>(&self) -> Option<&T>
759 where
760 T: Send + Sync + 'static,
761 {
762 self.typed_resources
763 .get(&TypeId::of::<T>())
764 .and_then(|value| value.downcast_ref::<T>())
765 }
766
767 pub fn require_resource<T>(&self) -> Result<&T, ContextError>
768 where
769 T: Send + Sync + 'static,
770 {
771 self.get_resource::<T>()
772 .ok_or(ContextError::MissingTypedResource(
773 std::any::type_name::<T>(),
774 ))
775 }
776
777 pub fn insert_named_resource<T>(&mut self, name: impl Into<String>, resource: T)
778 where
779 T: Send + Sync + 'static,
780 {
781 self.named_resources.insert(name.into(), Box::new(resource));
782 }
783
784 pub fn get_named_resource<T>(&self, name: &str) -> Option<&T>
785 where
786 T: Send + Sync + 'static,
787 {
788 self.named_resources
789 .get(name)
790 .and_then(|value| value.downcast_ref::<T>())
791 }
792
793 pub fn require_named_resource<T>(&self, name: &str) -> Result<&T, ContextError>
794 where
795 T: Send + Sync + 'static,
796 {
797 self.get_named_resource::<T>(name)
798 .ok_or_else(|| ContextError::MissingResource(name.to_owned()))
799 }
800
801 pub fn put_local(&mut self, key: impl Into<String>, value: impl Into<Value>) {
802 self.locals.insert(key.into(), value.into());
803 }
804
805 pub fn local(&self, key: &str) -> Option<&Value> {
806 self.locals.get(key)
807 }
808
809 pub fn remove_local(&mut self, key: &str) -> Option<Value> {
810 self.locals.remove(key)
811 }
812
813 pub fn has_entity_data_service(&self, entity: &str) -> bool {
814 let in_registry = self
815 .entity_registry
816 .as_ref()
817 .map(|registry| registry.contains(entity))
818 .unwrap_or(false);
819 in_registry || self.entity(entity).is_some()
820 }
821
822 pub fn entity_data_service_behavior(
823 &self,
824 entity: &str,
825 ) -> Option<std::sync::Arc<dyn EntityDataServiceBehavior>> {
826 self.entity_data_service_behavior_registry
827 .as_ref()
828 .and_then(|registry| registry.behavior(entity))
829 }
830
831 pub fn has_checker(&self, entity: &str) -> bool {
832 self.checker_registry
833 .as_ref()
834 .and_then(|registry| registry.checker(entity))
835 .is_some()
836 }
837
838 pub fn check_and_fix_record(
839 &self,
840 entity: &str,
841 record: &mut Record,
842 ) -> Result<(), RuntimeError> {
843 self.check_and_fix_record_at(entity, record, &ObjectLocation::root())
844 }
845
846 pub fn check_and_fix_record_at(
847 &self,
848 entity: &str,
849 record: &mut Record,
850 location: &ObjectLocation,
851 ) -> Result<(), RuntimeError> {
852 let Some(checker) = self
853 .checker_registry
854 .as_ref()
855 .and_then(|registry| registry.checker(entity))
856 else {
857 return Ok(());
858 };
859 let mut results = CheckResults::new();
860 checker.check_and_fix(self, record, location, &mut results);
861 if results.is_empty() {
862 return Ok(());
863 }
864 self.translate_check_results(&mut results);
865 Err(RuntimeError::Check(results))
866 }
867
868 pub fn translate_check_results(&self, results: &mut CheckResults) {
869 for result in results {
870 result.message = Some(translate_check_result(self.language, result));
871 }
872 }
873
874 pub fn send_event(&self, event: RawAuditEvent) -> Result<(), RuntimeError> {
875 if let Some(sink) = self.event_sink.as_ref() {
876 sink.on_event(self, &event)?;
877 }
878 if let Some(sink) = self.custom_event_sink.as_ref() {
879 let (mask_fields, max_len) = self
880 .metadata
881 .as_ref()
882 .and_then(|metadata| metadata.entity(&event.entity))
883 .map(|desc| (desc.audit_mask_fields.clone(), desc.audit_value_max_len))
884 .unwrap_or_else(|| (vec![], None));
885
886 let safe_event = event.build_safe_event(&mask_fields, max_len);
887 sink.on_safe_event(self, &safe_event)?;
888 }
889
890 crate::log_formatter::LogManager::write_audit_log(&event);
891
892 Ok(())
893 }
894
895 pub(crate) async fn commit_changes_internal<E>(&self) -> Result<(), DataServiceError<E::Error>>
896 where
897 E: teaql_data_service::MutationExecutor + Send + Sync + 'static,
898 {
899 let executor = self.require_resource::<E>().map_err(|err| {
900 DataServiceError::Runtime(RuntimeError::Graph(format!(
901 "cannot commit changes without executor: {err}"
902 )))
903 })?;
904 let change_set = self.entity_root.current_change_set();
905
906 for (key, changes) in change_set.changes() {
907 if changes.is_empty() {
908 continue;
909 }
910 let _entity = self
911 .require_entity(&key.entity)
912 .map_err(DataServiceError::Runtime)?;
913 let mut command = UpdateCommand::new(&key.entity, key.id.clone());
914 for (field, value) in changes {
915 command = command.value(field.clone(), value.clone());
916 }
917 let request = teaql_data_service::MutationRequest::Update(command);
918 executor
919 .mutate(request)
920 .await
921 .map_err(DataServiceError::Executor)?;
922 }
923
924 self.entity_root.clear_current_change_set();
925 Ok(())
926 }
927
928 pub async fn get_in_store(&self, key: &str) -> Option<Value> {
929 let store = self.get_resource::<Box<dyn DataStore>>()?;
930 store.get(key).await
931 }
932
933 pub async fn put_in_store(
934 &self,
935 key: &str,
936 value: impl Into<Value>,
937 timeout_seconds: Option<u64>,
938 ) {
939 if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
940 store.put(key, value.into(), timeout_seconds).await;
941 }
942 }
943
944 pub async fn clear_in_store(&self, key: &str) {
945 if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
946 store.remove(key).await;
947 }
948 }
949}
950
951fn extract_id_from_sql(sql: &str) -> Option<String> {
952 let sql_lower = sql.to_lowercase();
953 let where_idx = sql_lower.find("where")?;
954 let where_clause = &sql_lower[where_idx + 5..];
955
956 let bytes = where_clause.as_bytes();
957 let mut i = 0;
958 while i < bytes.len() {
959 if i + 1 < bytes.len() && &bytes[i..i + 2] == b"id" {
960 let prev_ok = i == 0 || {
962 let prev_char = bytes[i - 1] as char;
963 !prev_char.is_ascii_alphanumeric() && prev_char != '_' && prev_char != '.'
964 };
965 let next_ok = i + 2 == bytes.len() || {
967 let next_char = bytes[i + 2] as char;
968 !next_char.is_ascii_alphanumeric() && next_char != '_'
969 };
970
971 if prev_ok && next_ok {
972 let mut j = i + 2;
975 while j < bytes.len() && (bytes[j] as char).is_whitespace() {
976 j += 1;
977 }
978 if j < bytes.len() && bytes[j] == b'=' {
979 j += 1;
980 while j < bytes.len() && (bytes[j] as char).is_whitespace() {
981 j += 1;
982 }
983 let mut val_str = String::new();
985 if j < bytes.len() && bytes[j] == b'\'' {
986 j += 1; while j < bytes.len() && bytes[j] != b'\'' {
988 val_str.push(bytes[j] as char);
989 j += 1;
990 }
991 return Some(val_str);
992 }
993 while j < bytes.len() {
995 let c = bytes[j] as char;
996 if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
997 break;
998 }
999 val_str.push(c);
1000 j += 1;
1001 }
1002 if !val_str.is_empty() {
1003 return Some(val_str);
1004 }
1005 }
1006 }
1007 }
1008 i += 1;
1009 }
1010 None
1011}
1012
1013fn sql_result_summary(
1014 operation: SqlLogOperation,
1015 result_count: Option<usize>,
1016 result_type: Option<&str>,
1017 affected_rows: Option<u64>,
1018 debug_sql: &str,
1019) -> String {
1020 match operation {
1021 SqlLogOperation::Select => {
1022 let count = result_count.unwrap_or(0);
1023 match count {
1024 0 => "MISS".to_owned(),
1025 1 => match result_type {
1026 Some(result_type) => extract_id_from_sql(debug_sql)
1027 .map(|id| format!("{result_type}({id})"))
1028 .unwrap_or_else(|| result_type.to_owned()),
1029 None => "row".to_owned(),
1030 },
1031 _ => match result_type {
1032 Some(result_type) => format!("{count}*{result_type}"),
1033 None => format!("{count}*rows"),
1034 },
1035 }
1036 }
1037 _ => {
1038 let affected = affected_rows.unwrap_or(0);
1039 format!("{affected} UPDATED")
1040 }
1041 }
1042}
1043
1044fn pretty_sql(sql: &str) -> String {
1045 let mut pretty = sql.to_owned();
1046 for keyword in [
1047 " FROM ",
1048 " WHERE ",
1049 " GROUP BY ",
1050 " HAVING ",
1051 " ORDER BY ",
1052 " LIMIT ",
1053 " OFFSET ",
1054 " RETURNING ",
1055 ] {
1056 pretty = pretty.replace(keyword, &format!("\n{}", keyword.trim_start()));
1057 }
1058 pretty.replace(" AND ", "\n AND ")
1059}