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