Skip to main content

nest_rs_database/
executor.rs

1use std::any::Any;
2use std::future::Future;
3use std::sync::Arc;
4
5/// An ambient handle to a unit of database work, installed in the
6/// task-local for the lifetime of a request or a worker job.
7///
8/// The trait is **object-safe** so the engine can carry it as
9/// `Arc<dyn Executor>` without naming a concrete ORM. The concrete handle
10/// (a SeaORM `Executor` enum, a `sqlx::Pool`, a `diesel_async::Connection`,
11/// …) implements this trait; an ORM-specific `Repo` recovers the concrete
12/// type via [`Executor::as_any`] when it needs to issue a query.
13///
14/// Downcasting is the documented seam: this crate stays free of every
15/// candidate ORM's query API, and each `Repo` knows exactly which executor
16/// shape its `Module` installs. A downcast miss is a framework bug
17/// (mismatched `Module` + `Repo`) and should `panic!` with a clear message
18/// during boot tests, never surface as a runtime "no rows".
19pub trait Executor: Any + Send + Sync + 'static {
20    /// Downcast handle. Used by an ORM-specific `Repo` to recover its
21    /// concrete executor type from the ambient `Arc<dyn Executor>`.
22    fn as_any(&self) -> &dyn Any;
23}
24
25/// Whether the ambient executor belongs to a request or a worker job. An
26/// ORM's `Repo` reads this back to fail closed when a request path lacks
27/// an ambient authorization context (a missing principal on a worker is
28/// expected — it's system work; on a request it's a bug).
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum ExecutorScope {
31    Request,
32    Job,
33}
34
35tokio::task_local! {
36    static EXECUTOR: Arc<dyn Executor>;
37    static EXECUTOR_SCOPE: ExecutorScope;
38}
39
40/// The installed ambient executor, or `None` outside any scope. An
41/// ORM-specific `Repo` calls this and downcasts via [`Executor::as_any`].
42pub fn current_executor() -> Option<Arc<dyn Executor>> {
43    EXECUTOR.try_with(Arc::clone).ok()
44}
45
46/// The installed ambient executor scope, or `None` outside any scope.
47pub fn current_executor_scope() -> Option<ExecutorScope> {
48    EXECUTOR_SCOPE.try_with(Clone::clone).ok()
49}
50
51/// Install `executor` without tagging a scope. Prefer the request/job
52/// variants at framework boundaries so authorization can distinguish the
53/// two paths. An untagged (unset) scope is treated as **fail-closed** by a
54/// scope-aware `Repo`: with no ambient ability it denies every row, exactly
55/// like a request — only [`with_job_executor`] grants unscoped reads.
56pub async fn with_executor<F: Future>(executor: Arc<dyn Executor>, fut: F) -> F::Output {
57    EXECUTOR.scope(executor, fut).await
58}
59
60/// Install `executor` and tag the scope as a request — the path on which a
61/// `Repo` fails closed when no ambient authorization context is present.
62pub async fn with_request_executor<F: Future>(executor: Arc<dyn Executor>, fut: F) -> F::Output {
63    EXECUTOR
64        .scope(executor, EXECUTOR_SCOPE.scope(ExecutorScope::Request, fut))
65        .await
66}
67
68/// Install `executor` and tag the scope as a worker job — the path on
69/// which a `Repo` runs unscoped (no principal ⇒ system work).
70pub async fn with_job_executor<F: Future>(executor: Arc<dyn Executor>, fut: F) -> F::Output {
71    EXECUTOR
72        .scope(executor, EXECUTOR_SCOPE.scope(ExecutorScope::Job, fut))
73        .await
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    struct StubExecutor;
81    impl Executor for StubExecutor {
82        fn as_any(&self) -> &dyn Any {
83            self
84        }
85    }
86
87    fn stub() -> Arc<dyn Executor> {
88        Arc::new(StubExecutor)
89    }
90
91    #[tokio::test]
92    async fn no_ambient_state_outside_any_scope() {
93        assert!(current_executor().is_none());
94        assert!(current_executor_scope().is_none());
95    }
96
97    #[tokio::test]
98    async fn with_executor_installs_but_does_not_tag() {
99        with_executor(stub(), async {
100            assert!(current_executor().is_some());
101            assert!(current_executor_scope().is_none());
102        })
103        .await;
104    }
105
106    #[tokio::test]
107    async fn with_request_executor_tags_request() {
108        with_request_executor(stub(), async {
109            assert_eq!(current_executor_scope(), Some(ExecutorScope::Request));
110            assert!(current_executor().is_some());
111        })
112        .await;
113    }
114
115    #[tokio::test]
116    async fn with_job_executor_tags_job() {
117        with_job_executor(stub(), async {
118            assert_eq!(current_executor_scope(), Some(ExecutorScope::Job));
119            assert!(current_executor().is_some());
120        })
121        .await;
122    }
123
124    #[tokio::test]
125    async fn scope_unwinds_on_exit() {
126        with_request_executor(stub(), async {}).await;
127        assert!(current_executor().is_none());
128        assert!(current_executor_scope().is_none());
129    }
130
131    #[tokio::test]
132    async fn nested_scope_shadows_outer() {
133        with_request_executor(stub(), async {
134            assert_eq!(current_executor_scope(), Some(ExecutorScope::Request));
135            with_job_executor(stub(), async {
136                assert_eq!(current_executor_scope(), Some(ExecutorScope::Job));
137            })
138            .await;
139            assert_eq!(current_executor_scope(), Some(ExecutorScope::Request));
140        })
141        .await;
142    }
143
144    #[tokio::test]
145    async fn downcast_round_trips_the_concrete_type() {
146        with_request_executor(stub(), async {
147            let e = current_executor().expect("installed");
148            assert!(e.as_any().is::<StubExecutor>());
149        })
150        .await;
151    }
152}