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}