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