Skip to main content

forge_core/function/
traits.rs

1use std::future::Future;
2use std::pin::Pin;
3
4use serde::{Serialize, de::DeserializeOwned};
5
6use super::context::{MutationContext, QueryContext};
7use crate::error::Result;
8
9/// Information about a registered function.
10#[derive(Debug, Clone)]
11pub struct FunctionInfo {
12    /// Function name (used for routing).
13    pub name: &'static str,
14    /// Human-readable description.
15    pub description: Option<&'static str>,
16    /// Kind of function.
17    pub kind: FunctionKind,
18    /// Required role (if any, implies auth required).
19    pub required_role: Option<&'static str>,
20    /// Whether this function is public (no auth).
21    pub is_public: bool,
22    /// Cache TTL in seconds (for queries).
23    pub cache_ttl: Option<u64>,
24    /// Timeout in seconds.
25    pub timeout: Option<u64>,
26    /// Default timeout in seconds for outbound HTTP requests made via the
27    /// circuit-breaker client. `None` means no request timeout is applied.
28    pub http_timeout: Option<u64>,
29    /// Rate limit: requests per time window.
30    pub rate_limit_requests: Option<u32>,
31    /// Rate limit: time window in seconds.
32    pub rate_limit_per_secs: Option<u64>,
33    /// Rate limit: bucket key type (user, ip, tenant, global).
34    pub rate_limit_key: Option<&'static str>,
35    /// Log level for access logging: "trace", "debug", "info", "warn", "error", "off".
36    /// Defaults to "trace" if not specified.
37    pub log_level: Option<&'static str>,
38    /// Table dependencies extracted at compile time for reactive subscriptions.
39    /// Empty slice means tables could not be determined (dynamic SQL).
40    pub table_dependencies: &'static [&'static str],
41    /// Columns referenced in SELECT clauses, extracted at compile time.
42    /// Used for fine-grained invalidation: skip re-execution when changed columns
43    /// don't intersect with selected columns. Empty means unknown (invalidate always).
44    pub selected_columns: &'static [&'static str],
45    /// Whether this mutation should be wrapped in a database transaction.
46    /// Only applies to mutations. When true, jobs are buffered and inserted
47    /// atomically with the mutation via the outbox pattern.
48    pub transactional: bool,
49    /// Force this query to read from the primary database instead of replicas.
50    /// Use for read-after-write consistency (e.g., post-mutation confirmation,
51    /// permission checks depending on just-written state).
52    pub consistent: bool,
53    /// Whether the function signature has input arguments beyond the context.
54    /// When false, identity scope enforcement is skipped since there are no
55    /// args to carry scope fields. Auth is still enforced via the JWT.
56    pub has_input_args: bool,
57}
58
59/// The kind of function.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum FunctionKind {
62    Query,
63    Mutation,
64}
65
66impl std::fmt::Display for FunctionKind {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            FunctionKind::Query => write!(f, "query"),
70            FunctionKind::Mutation => write!(f, "mutation"),
71        }
72    }
73}
74
75/// A query function (read-only, cacheable, subscribable).
76///
77/// Queries:
78/// - Can only read from the database
79/// - Are automatically cached based on arguments
80/// - Can be subscribed to for real-time updates
81/// - Should be deterministic (same inputs → same outputs)
82/// - Should not have side effects
83pub trait ForgeQuery: Send + Sync + 'static {
84    /// The input arguments type.
85    type Args: DeserializeOwned + Serialize + Send + Sync;
86    /// The output type.
87    type Output: Serialize + Send;
88
89    /// Function metadata.
90    fn info() -> FunctionInfo;
91
92    /// Execute the query.
93    fn execute(
94        ctx: &QueryContext,
95        args: Self::Args,
96    ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
97}
98
99/// A mutation function (transactional write).
100///
101/// Mutations:
102/// - Run within a database transaction
103/// - Can read and write to the database
104/// - Should NOT call external APIs (use Actions)
105/// - Are atomic: all changes commit or none do
106pub trait ForgeMutation: Send + Sync + 'static {
107    /// The input arguments type.
108    type Args: DeserializeOwned + Serialize + Send + Sync;
109    /// The output type.
110    type Output: Serialize + Send;
111
112    /// Function metadata.
113    fn info() -> FunctionInfo;
114
115    /// Execute the mutation within a transaction.
116    fn execute(
117        ctx: &MutationContext,
118        args: Self::Args,
119    ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
120}
121
122#[cfg(test)]
123#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_function_kind_display() {
129        assert_eq!(format!("{}", FunctionKind::Query), "query");
130        assert_eq!(format!("{}", FunctionKind::Mutation), "mutation");
131    }
132
133    #[test]
134    fn test_function_info() {
135        let info = FunctionInfo {
136            name: "get_user",
137            description: Some("Get a user by ID"),
138            kind: FunctionKind::Query,
139            required_role: None,
140            is_public: false,
141            cache_ttl: Some(300),
142            timeout: Some(30),
143            http_timeout: Some(5),
144            rate_limit_requests: Some(100),
145            rate_limit_per_secs: Some(60),
146            rate_limit_key: Some("user"),
147            log_level: Some("debug"),
148            table_dependencies: &["users"],
149            selected_columns: &["id", "name", "email"],
150            transactional: false,
151            consistent: false,
152            has_input_args: true,
153        };
154
155        assert_eq!(info.name, "get_user");
156        assert_eq!(info.kind, FunctionKind::Query);
157        assert_eq!(info.cache_ttl, Some(300));
158        assert_eq!(info.http_timeout, Some(5));
159        assert_eq!(info.rate_limit_requests, Some(100));
160        assert_eq!(info.log_level, Some("debug"));
161        assert_eq!(info.table_dependencies, &["users"]);
162    }
163}