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    /// A user request — a missing ambient ability is a bug, so `Repo` fails closed.
32    Request,
33    /// System work (cron/queue) — no principal is expected, so reads are unscoped.
34    Job,
35}
36
37tokio::task_local! {
38    static EXECUTOR: Arc<dyn Executor>;
39    static EXECUTOR_SCOPE: ExecutorScope;
40}
41
42/// The installed ambient executor, or `None` outside any scope. An
43/// ORM-specific `Repo` calls this and downcasts via [`Executor::as_any`].
44pub fn current_executor() -> Option<Arc<dyn Executor>> {
45    EXECUTOR.try_with(Arc::clone).ok()
46}
47
48/// The installed ambient executor scope, or `None` outside any scope.
49pub fn current_executor_scope() -> Option<ExecutorScope> {
50    EXECUTOR_SCOPE.try_with(Clone::clone).ok()
51}
52
53/// Install `executor` without tagging a scope. Prefer the request/job
54/// variants at framework boundaries so authorization can distinguish the
55/// two paths. An untagged (unset) scope is treated as **fail-closed** by a
56/// scope-aware `Repo`: with no ambient ability it denies every row, exactly
57/// like a request — only [`with_job_executor`] grants unscoped reads.
58pub async fn with_executor<F: Future>(executor: Arc<dyn Executor>, fut: F) -> F::Output {
59    EXECUTOR.scope(executor, fut).await
60}
61
62/// Install `executor` and tag the scope as a request — the path on which a
63/// `Repo` fails closed when no ambient authorization context is present.
64pub async fn with_request_executor<F: Future>(executor: Arc<dyn Executor>, fut: F) -> F::Output {
65    EXECUTOR
66        .scope(executor, EXECUTOR_SCOPE.scope(ExecutorScope::Request, fut))
67        .await
68}
69
70/// Install `executor` and tag the scope as a worker job — the path on
71/// which a `Repo` runs unscoped (no principal ⇒ system work).
72pub async fn with_job_executor<F: Future>(executor: Arc<dyn Executor>, fut: F) -> F::Output {
73    EXECUTOR
74        .scope(executor, EXECUTOR_SCOPE.scope(ExecutorScope::Job, fut))
75        .await
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    struct StubExecutor;
83    impl Executor for StubExecutor {
84        fn as_any(&self) -> &dyn Any {
85            self
86        }
87    }
88
89    fn stub() -> Arc<dyn Executor> {
90        Arc::new(StubExecutor)
91    }
92
93    #[tokio::test]
94    async fn no_ambient_state_outside_any_scope() {
95        assert!(current_executor().is_none());
96        assert!(current_executor_scope().is_none());
97    }
98
99    #[tokio::test]
100    async fn with_executor_installs_but_does_not_tag() {
101        with_executor(stub(), async {
102            assert!(current_executor().is_some());
103            assert!(current_executor_scope().is_none());
104        })
105        .await;
106    }
107
108    #[tokio::test]
109    async fn with_request_executor_tags_request() {
110        with_request_executor(stub(), async {
111            assert_eq!(current_executor_scope(), Some(ExecutorScope::Request));
112            assert!(current_executor().is_some());
113        })
114        .await;
115    }
116
117    #[tokio::test]
118    async fn with_job_executor_tags_job() {
119        with_job_executor(stub(), async {
120            assert_eq!(current_executor_scope(), Some(ExecutorScope::Job));
121            assert!(current_executor().is_some());
122        })
123        .await;
124    }
125
126    #[tokio::test]
127    async fn scope_unwinds_on_exit() {
128        with_request_executor(stub(), async {}).await;
129        assert!(current_executor().is_none());
130        assert!(current_executor_scope().is_none());
131    }
132
133    #[tokio::test]
134    async fn nested_scope_shadows_outer() {
135        with_request_executor(stub(), async {
136            assert_eq!(current_executor_scope(), Some(ExecutorScope::Request));
137            with_job_executor(stub(), async {
138                assert_eq!(current_executor_scope(), Some(ExecutorScope::Job));
139            })
140            .await;
141            assert_eq!(current_executor_scope(), Some(ExecutorScope::Request));
142        })
143        .await;
144    }
145
146    #[tokio::test]
147    async fn downcast_round_trips_the_concrete_type() {
148        with_request_executor(stub(), async {
149            let e = current_executor().expect("installed");
150            assert!(e.as_any().is::<StubExecutor>());
151        })
152        .await;
153    }
154}