fraiseql_functions/triggers/storage.rs
1//! Storage event triggers.
2//!
3//! Handles `after:storage:<bucket>:<operation>` triggers that fire when objects
4//! are uploaded or deleted from object storage.
5//!
6//! ## Operations
7//!
8//! - `upload`: Fires after successful file upload
9//! - `delete`: Fires after successful file deletion
10//! - `all`: Fires for both upload and delete operations
11//!
12//! ## Event Payload
13//!
14//! The function receives metadata about the storage operation:
15//! - Bucket name
16//! - Object key (path)
17//! - File size
18//! - Content type (MIME type)
19//! - Owner (user ID or service account)
20//! - Operation type
21//!
22//! ## Async Dispatch
23//!
24//! Storage triggers fire asynchronously after the storage operation completes.
25//! Failures in the trigger function do not affect the storage operation result.
26use serde::{Deserialize, Serialize};
27
28use crate::types::EventPayload;
29
30/// Storage operation type.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[non_exhaustive]
33pub enum StorageOperation {
34 /// Upload/put operation.
35 Upload,
36 /// Delete operation.
37 Delete,
38 /// Any storage operation.
39 Any,
40}
41
42impl StorageOperation {
43 /// Convert to string for trigger type.
44 #[must_use]
45 pub const fn as_str(&self) -> &str {
46 match self {
47 StorageOperation::Upload => "upload",
48 StorageOperation::Delete => "delete",
49 StorageOperation::Any => "any",
50 }
51 }
52}
53
54/// Event data for storage operations.
55#[derive(Debug, Clone)]
56pub struct StorageEventPayload {
57 /// Bucket name.
58 pub bucket: String,
59 /// Object key/path.
60 pub key: String,
61 /// Object size in bytes.
62 pub size_bytes: i64,
63 /// MIME type of the object.
64 pub content_type: String,
65 /// User ID of the owner (if applicable).
66 pub owner_id: Option<String>,
67 /// Operation that triggered the event.
68 pub operation: StorageOperation,
69}
70
71/// A trigger that fires after storage operations.
72#[derive(Debug, Clone)]
73pub struct StorageTrigger {
74 /// Name of the function to invoke.
75 pub function_name: String,
76 /// Bucket name to listen on.
77 pub bucket: String,
78 /// Operation filter (Upload, Delete, or Any).
79 pub operation: StorageOperation,
80}
81
82impl StorageTrigger {
83 /// Check if this trigger matches the given storage event.
84 ///
85 /// Matches if:
86 /// - Bucket name matches exactly
87 /// - Operation matches (Upload/Delete/Any)
88 /// - Key doesn't have `_transforms/` prefix (internal cache operations)
89 #[must_use]
90 pub fn matches(&self, event: &StorageEventPayload) -> bool {
91 // Bucket must match
92 if self.bucket != event.bucket {
93 return false;
94 }
95
96 // Operation must match
97 let op_matches = match self.operation {
98 StorageOperation::Any => true,
99 _ => self.operation == event.operation,
100 };
101
102 if !op_matches {
103 return false;
104 }
105
106 // Exclude internal transform cache operations
107 if event.key.starts_with("_transforms/") {
108 return false;
109 }
110
111 true
112 }
113
114 /// Check if this trigger should fire for the given event.
115 ///
116 /// This is an explicit check (same as `matches` but with a different name
117 /// for clarity in tests).
118 #[must_use]
119 pub fn should_fire(&self, event: &StorageEventPayload) -> bool {
120 self.matches(event)
121 }
122
123 /// Build an `EventPayload` from a storage event.
124 #[must_use]
125 pub fn build_payload(&self, event: &StorageEventPayload) -> EventPayload {
126 let trigger_type = format!("after:storage:{}:{}", event.bucket, event.operation.as_str());
127
128 let mut data = serde_json::json!({
129 "bucket": event.bucket,
130 "key": event.key,
131 "size_bytes": event.size_bytes,
132 "content_type": event.content_type,
133 "operation": event.operation.as_str(),
134 });
135
136 if let Some(owner_id) = &event.owner_id {
137 data["owner_id"] = serde_json::Value::String(owner_id.clone());
138 }
139
140 EventPayload {
141 trigger_type,
142 entity: event.bucket.clone(),
143 event_kind: event.operation.as_str().to_string(),
144 data,
145 timestamp: chrono::Utc::now(),
146 }
147 }
148}