Skip to main content

supabase_client_query/
geojson_select.rs

1use reqwest::header::HeaderValue;
2use serde_json::Value as JsonValue;
3
4use supabase_client_core::SupabaseError;
5
6use crate::backend::QueryBackend;
7use crate::filter::Filterable;
8use crate::modifier::Modifiable;
9use crate::sql::{FilterCondition, ParamStore, SqlParts};
10
11/// Builder for SELECT queries that return GeoJSON.
12///
13/// Created by calling `.geojson()` on a `SelectBuilder`. Returns a
14/// `serde_json::Value` (GeoJSON FeatureCollection). REST-only.
15pub struct GeoJsonSelectBuilder {
16    pub(crate) backend: QueryBackend,
17    pub(crate) parts: SqlParts,
18    pub(crate) params: ParamStore,
19}
20
21impl Filterable for GeoJsonSelectBuilder {
22    fn filters_mut(&mut self) -> &mut Vec<FilterCondition> {
23        &mut self.parts.filters
24    }
25    fn params_mut(&mut self) -> &mut ParamStore {
26        &mut self.params
27    }
28}
29
30impl Modifiable for GeoJsonSelectBuilder {
31    fn parts_mut(&mut self) -> &mut SqlParts {
32        &mut self.parts
33    }
34}
35
36impl GeoJsonSelectBuilder {
37    /// Override the schema for this query.
38    pub fn schema(mut self, schema: &str) -> Self {
39        self.parts.schema_override = Some(schema.to_string());
40        self
41    }
42
43    /// Execute the SELECT query and return the response as GeoJSON.
44    pub async fn execute(self) -> Result<JsonValue, SupabaseError> {
45        match &self.backend {
46            QueryBackend::Rest { http, base_url, api_key, schema } => {
47                let (url, mut headers) = crate::postgrest::build_postgrest_select(
48                    base_url, &self.parts, &self.params,
49                )
50                .map_err(SupabaseError::QueryBuilder)?;
51
52                // Override Accept to GeoJSON
53                headers.insert(
54                    "Accept",
55                    HeaderValue::from_static("application/geo+json"),
56                );
57
58                // Standard auth headers
59                headers.insert("apikey", HeaderValue::from_str(api_key).unwrap());
60                headers.insert(
61                    "Authorization",
62                    HeaderValue::from_str(&format!("Bearer {}", api_key)).unwrap(),
63                );
64
65                // Schema profile
66                if let Some(ref so) = self.parts.schema_override {
67                    headers.insert(
68                        "Accept-Profile",
69                        HeaderValue::from_str(so).unwrap(),
70                    );
71                } else if schema != "public" {
72                    headers.entry("Accept-Profile")
73                        .or_insert_with(|| HeaderValue::from_str(schema).unwrap());
74                }
75
76                let response = http
77                    .get(&url)
78                    .headers(headers)
79                    .send()
80                    .await
81                    .map_err(|e| SupabaseError::Http(e.to_string()))?;
82
83                let status = response.status().as_u16();
84                let body = response
85                    .text()
86                    .await
87                    .map_err(|e| SupabaseError::Http(e.to_string()))?;
88
89                if status >= 400 {
90                    return Err(SupabaseError::postgrest(status, body, None));
91                }
92
93                serde_json::from_str(&body).map_err(|e| {
94                    SupabaseError::Serialization(format!(
95                        "Failed to parse GeoJSON response: {}",
96                        e
97                    ))
98                })
99            }
100            #[cfg(feature = "direct-sql")]
101            QueryBackend::DirectSql { .. } => {
102                Err(SupabaseError::query_builder(
103                    "GeoJSON output is only supported with the REST (PostgREST) backend",
104                ))
105            }
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::sql::{SqlOperation, SqlParts, ParamStore};
114
115    #[test]
116    fn test_geojson_builder_modifiable() {
117        let mut builder = GeoJsonSelectBuilder {
118            backend: QueryBackend::Rest {
119                http: reqwest::Client::new(),
120                base_url: "http://localhost".into(),
121                api_key: "key".into(),
122                schema: "public".to_string(),
123            },
124            parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
125            params: ParamStore::new(),
126        };
127        builder = builder.limit(10);
128        assert_eq!(builder.parts.limit, Some(10));
129    }
130
131    #[test]
132    fn test_geojson_builder_filterable() {
133        let builder = GeoJsonSelectBuilder {
134            backend: QueryBackend::Rest {
135                http: reqwest::Client::new(),
136                base_url: "http://localhost".into(),
137                api_key: "key".into(),
138                schema: "public".to_string(),
139            },
140            parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
141            params: ParamStore::new(),
142        };
143        let builder = builder.eq("name", "Auckland");
144        assert_eq!(builder.parts.filters.len(), 1);
145    }
146
147    #[test]
148    fn test_geojson_accept_header() {
149        let parts = SqlParts::new(SqlOperation::Select, "public", "cities");
150        let params = ParamStore::new();
151        let (_, mut headers) = crate::postgrest::build_postgrest_select(
152            "http://localhost:64321", &parts, &params,
153        ).unwrap();
154        // Simulate what GeoJsonSelectBuilder does
155        headers.insert("Accept", HeaderValue::from_static("application/geo+json"));
156        assert_eq!(headers.get("Accept").unwrap(), "application/geo+json");
157    }
158}