Skip to main content

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}