postrust_graphql/subscription/
mod.rs1pub mod broker;
34
35pub use broker::{
36 create_notify_trigger_sql, drop_notify_trigger_sql, table_channel_name, BrokerError,
37 NotifyBroker, PgNotification,
38};
39
40use crate::schema::GeneratedSchema;
41use postrust_core::schema_cache::SchemaCache;
42use std::sync::atomic::{AtomicI64, Ordering};
43
44static ACTIVE_SUBSCRIPTIONS: AtomicI64 = AtomicI64::new(0);
53
54pub fn active_subscriptions() -> i64 {
56 ACTIVE_SUBSCRIPTIONS.load(Ordering::Relaxed)
57}
58
59pub(crate) struct ActiveSubscription(());
61
62impl ActiveSubscription {
63 pub(crate) fn open() -> Self {
64 ACTIVE_SUBSCRIPTIONS.fetch_add(1, Ordering::Relaxed);
65 Self(())
66 }
67}
68
69impl Drop for ActiveSubscription {
70 fn drop(&mut self) {
71 ACTIVE_SUBSCRIPTIONS.fetch_sub(1, Ordering::Relaxed);
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct SubscriptionField {
78 pub name: String,
80 pub table_name: String,
82 pub schema_name: String,
84 pub return_type: String,
86 pub description: Option<String>,
88}
89
90impl SubscriptionField {
91 pub fn for_table(schema: &str, table: &str, type_name: &str) -> Self {
93 Self {
94 name: table.to_string(),
95 table_name: table.to_string(),
96 schema_name: schema.to_string(),
97 return_type: type_name.to_string(),
98 description: Some(format!("Subscribe to changes on the {} table", table)),
99 }
100 }
101
102 pub fn channel_name(&self) -> String {
104 table_channel_name(&self.schema_name, &self.table_name)
105 }
106}
107
108pub fn generate_subscription_fields(
110 _schema_cache: &SchemaCache,
111 generated: &GeneratedSchema,
112) -> Vec<SubscriptionField> {
113 let mut fields = Vec::new();
114
115 for (type_name, obj_type) in &generated.object_types {
116 let table = &obj_type.table;
117
118 if !table.is_view {
120 fields.push(SubscriptionField::for_table(
121 &table.schema,
122 &table.name,
123 type_name,
124 ));
125 }
126 }
127
128 fields
129}
130
131#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
133pub struct TableChangePayload {
134 pub operation: String,
136 pub table: String,
138 pub schema: String,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub old: Option<serde_json::Value>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub new: Option<serde_json::Value>,
146}
147
148impl TableChangePayload {
149 pub fn from_payload(payload: &str) -> Result<Self, serde_json::Error> {
151 serde_json::from_str(payload)
152 }
153
154 pub fn data(&self) -> Option<&serde_json::Value> {
159 match self.operation.as_str() {
160 "DELETE" => self.old.as_ref(),
161 _ => self.new.as_ref(),
162 }
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_subscription_field_channel_name() {
172 let field = SubscriptionField::for_table("public", "users", "Users");
173 assert_eq!(field.channel_name(), "postrust_public_users");
174 }
175
176 #[test]
177 fn test_table_change_payload_parsing() {
178 let json = r#"{
179 "operation": "INSERT",
180 "table": "users",
181 "schema": "public",
182 "new": {"id": 1, "name": "Alice"}
183 }"#;
184
185 let payload = TableChangePayload::from_payload(json).unwrap();
186 assert_eq!(payload.operation, "INSERT");
187 assert_eq!(payload.table, "users");
188 assert!(payload.new.is_some());
189 assert!(payload.old.is_none());
190 }
191
192 #[test]
193 fn test_table_change_payload_data() {
194 let insert_payload = TableChangePayload {
195 operation: "INSERT".to_string(),
196 table: "users".to_string(),
197 schema: "public".to_string(),
198 old: None,
199 new: Some(serde_json::json!({"id": 1})),
200 };
201 assert!(insert_payload.data().is_some());
202
203 let delete_payload = TableChangePayload {
204 operation: "DELETE".to_string(),
205 table: "users".to_string(),
206 schema: "public".to_string(),
207 old: Some(serde_json::json!({"id": 1})),
208 new: None,
209 };
210 assert!(delete_payload.data().is_some());
211 }
212}