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    /// Per-function maximum upload size in bytes. Overrides gateway max_body_size.
54    pub max_upload_size_bytes: Option<usize>,
55}
56
57/// The kind of function.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum FunctionKind {
60    Query,
61    Mutation,
62}
63
64impl std::fmt::Display for FunctionKind {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        match self {
67            FunctionKind::Query => write!(f, "query"),
68            FunctionKind::Mutation => write!(f, "mutation"),
69        }
70    }
71}
72
73/// A query function (read-only, cacheable, subscribable).
74///
75/// Queries:
76/// - Can only read from the database
77/// - Are automatically cached based on arguments
78/// - Can be subscribed to for real-time updates
79/// - Should be deterministic (same inputs → same outputs)
80/// - Should not have side effects
81pub trait ForgeQuery: Send + Sync + 'static {
82    /// The input arguments type.
83    type Args: DeserializeOwned + Serialize + Send + Sync;
84    /// The output type.
85    type Output: Serialize + Send;
86
87    /// Function metadata.
88    fn info() -> FunctionInfo;
89
90    /// Execute the query.
91    fn execute(
92        ctx: &QueryContext,
93        args: Self::Args,
94    ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
95}
96
97/// A mutation function (transactional write).
98///
99/// Mutations:
100/// - Run within a database transaction
101/// - Can read and write to the database
102/// - Should NOT call external APIs (use Actions)
103/// - Are atomic: all changes commit or none do
104pub trait ForgeMutation: Send + Sync + 'static {
105    /// The input arguments type.
106    type Args: DeserializeOwned + Serialize + Send + Sync;
107    /// The output type.
108    type Output: Serialize + Send;
109
110    /// Function metadata.
111    fn info() -> FunctionInfo;
112
113    /// Execute the mutation within a transaction.
114    fn execute(
115        ctx: &MutationContext,
116        args: Self::Args,
117    ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
118}
119
120#[cfg(test)]
121#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_function_kind_display() {
127        assert_eq!(format!("{}", FunctionKind::Query), "query");
128        assert_eq!(format!("{}", FunctionKind::Mutation), "mutation");
129    }
130
131    #[test]
132    fn test_function_info() {
133        let info = FunctionInfo {
134            name: "get_user",
135            description: Some("Get a user by ID"),
136            kind: FunctionKind::Query,
137            required_role: None,
138            is_public: false,
139            cache_ttl: Some(300),
140            timeout: Some(30),
141            http_timeout: Some(5),
142            rate_limit_requests: Some(100),
143            rate_limit_per_secs: Some(60),
144            rate_limit_key: Some("user"),
145            log_level: Some("debug"),
146            table_dependencies: &["users"],
147            selected_columns: &["id", "name", "email"],
148            transactional: false,
149            consistent: false,
150            max_upload_size_bytes: None,
151        };
152
153        assert_eq!(info.name, "get_user");
154        assert_eq!(info.kind, FunctionKind::Query);
155        assert_eq!(info.cache_ttl, Some(300));
156        assert_eq!(info.http_timeout, Some(5));
157        assert_eq!(info.rate_limit_requests, Some(100));
158        assert_eq!(info.log_level, Some("debug"));
159        assert_eq!(info.table_dependencies, &["users"]);
160    }
161}