···591591 collection: &str,
592592 query: &str,
593593 field: Option<&str>,
594594+ _authors: Option<&Vec<String>>, // TODO: Authors filtering for search not yet implemented
594595 limit: Option<i32>,
595596 cursor: Option<&str>,
596597 sort: Option<&str>
···600601 // For lexicon collection, we filter by slice directly
601602 if collection == "social.slices.lexicon" {
602603 let order_by = parse_sort_parameter(sort); // Use simple parsing for lexicon collections
604604+605605+ // Note: For lexicon searches, authors filtering is not commonly used as lexicons are typically not user-specific
606606+ // But we include it for completeness
603607 let records = match (cursor, field) {
604608 (Some(cursor_str), Some(field_name)) => {
605609 // Try to parse compound cursor, fallback to old cursor format
···793797 slice_uri: &str,
794798 collection: &str,
795799 repo: Option<&str>,
800800+ authors: Option<&Vec<String>>,
796801 limit: Option<i32>,
797802 cursor: Option<&str>,
798803 sort: Option<&str>
···802807 // For lexicon collection, we filter by slice directly
803808 if collection == "social.slices.lexicon" {
804809 let order_by = parse_sort_parameter(sort); // Use simple parsing for lexicon collections
810810+811811+812812+ // Determine the author list to use
813813+ let author_list: Option<Vec<String>> = if let Some(authors_list) = authors {
814814+ Some(authors_list.clone())
815815+ } else if let Some(repo_did) = repo {
816816+ Some(vec![repo_did.to_string()])
817817+ } else {
818818+ None
819819+ };
820820+805821 // For lexicon records, filter by slice
806806- let records = match (cursor, repo) {
807807- (Some(cursor_time), Some(repo_did)) => {
822822+ let records = match (cursor, author_list.as_ref()) {
823823+ (Some(cursor_time), Some(author_list)) => {
808824 let cursor_dt = cursor_time.parse::<chrono::DateTime<chrono::Utc>>()
809825 .unwrap_or_else(|_| chrono::Utc::now());
810826 sqlx::query_as!(
···812828 r#"
813829 SELECT uri, cid, did, collection, json, "indexed_at" as indexed_at
814830 FROM record
815815- WHERE collection = $1 AND json->>'slice' = $2 AND "indexed_at" < $3 AND did = $4
831831+ WHERE collection = $1 AND json->>'slice' = $2 AND "indexed_at" < $3 AND did = ANY($4)
816832 ORDER BY "indexed_at" DESC
817833 LIMIT $5
818834 "#,
819835 collection,
820836 slice_uri,
821837 cursor_dt,
822822- repo_did,
838838+ &author_list[..],
823839 limit as i64
824840 )
825841 .fetch_all(&self.pool)
···845861 .fetch_all(&self.pool)
846862 .await?
847863 },
848848- (None, Some(repo_did)) => {
864864+ (None, Some(author_list)) => {
849865 let sql = format!(
850866 r#"
851867 SELECT uri, cid, did, collection, json, indexed_at
852868 FROM record
853853- WHERE collection = $1 AND json->>'slice' = $2 AND did = $3
869869+ WHERE collection = $1 AND json->>'slice' = $2 AND did = ANY($3)
854870 ORDER BY {}
855871 LIMIT $4
856872 "#,
···859875 sqlx::query_as::<_, Record>(&sql)
860876 .bind(collection)
861877 .bind(slice_uri)
862862- .bind(repo_did)
878878+ .bind(author_list)
863879 .bind(limit as i64)
864880 .fetch_all(&self.pool)
865881 .await?
···915931 // Get lexicon-aware ORDER BY clause for non-lexicon collections
916932 let order_by = parse_sort_parameter_with_lexicon(&self.pool, slice_uri, collection, sort).await;
917933934934+ // Determine the author list to use
935935+ let author_list: Option<Vec<String>> = if let Some(authors_list) = authors {
936936+ Some(authors_list.clone())
937937+ } else if let Some(repo_did) = repo {
938938+ Some(vec![repo_did.to_string()])
939939+ } else {
940940+ None
941941+ };
942942+918943 // Now fetch the records with cursor-based pagination
919919- let records = match (cursor, repo) {
920920- (Some(cursor_time), Some(repo_did)) => {
944944+ let records = match (cursor, author_list.as_ref()) {
945945+ (Some(cursor_time), Some(author_list)) => {
921946 let cursor_dt = cursor_time.parse::<chrono::DateTime<chrono::Utc>>()
922947 .unwrap_or_else(|_| chrono::Utc::now());
923948 let query = format!(
924924- "SELECT uri, cid, did, collection, json, indexed_at FROM record WHERE collection = $1 AND indexed_at < $2 AND did = $3 ORDER BY {} LIMIT $4",
949949+ "SELECT uri, cid, did, collection, json, indexed_at FROM record WHERE collection = $1 AND indexed_at < $2 AND did = ANY($3) ORDER BY {} LIMIT $4",
925950 order_by
926951 );
927952 sqlx::query_as::<_, Record>(&query)
928953 .bind(collection)
929954 .bind(cursor_dt)
930930- .bind(repo_did)
955955+ .bind(author_list)
931956 .bind(limit as i64)
932957 .fetch_all(&self.pool)
933958 .await?
···946971 .fetch_all(&self.pool)
947972 .await?
948973 },
949949- (None, Some(repo_did)) => {
974974+ (None, Some(author_list)) => {
950975 let query = format!(
951951- "SELECT uri, cid, did, collection, json, indexed_at FROM record WHERE collection = $1 AND did = $2 ORDER BY {} LIMIT $3",
976976+ "SELECT uri, cid, did, collection, json, indexed_at FROM record WHERE collection = $1 AND did = ANY($2) ORDER BY {} LIMIT $3",
952977 order_by
953978 );
954979 sqlx::query_as::<_, Record>(&query)
955980 .bind(collection)
956956- .bind(repo_did)
981981+ .bind(author_list)
957982 .bind(limit as i64)
958983 .fetch_all(&self.pool)
959984 .await?
···2626 ¶ms.slice,
2727 ¶ms.collection,
2828 params.repo.as_deref(),
2929+ None, // No authors parameter for this endpoint yet
2930 params.limit,
3031 params.cursor.as_deref(),
3132 None, // No sort parameter for this endpoint yet
+49-6
api/src/handler_xrpc_dynamic.rs
···33 http::{HeaderMap, StatusCode},
44 response::Json,
55};
66-use serde::Deserialize;
66+use serde::{Deserialize, Deserializer};
77use chrono::Utc;
88use atproto_client::com::atproto::repo::{CreateRecordRequest, PutRecordRequest, DeleteRecordRequest, create_record, put_record, delete_record, CreateRecordResponse, PutRecordResponse};
99···1111use crate::models::{ListRecordsOutput, Record};
1212use crate::AppState;
13131414+// Custom deserializer to handle comma-separated strings as Vec<String>
1515+fn deserialize_comma_separated<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
1616+where
1717+ D: Deserializer<'de>,
1818+{
1919+ let opt_str: Option<String> = Option::deserialize(deserializer)?;
2020+ Ok(opt_str.map(|s| s.split(',').map(|s| s.trim().to_string()).collect()))
2121+}
2222+1423#[derive(Deserialize)]
1524pub struct DynamicListParams {
1625 pub author: Option<String>,
2626+ #[serde(deserialize_with = "deserialize_comma_separated")]
2727+ pub authors: Option<Vec<String>>,
1728 pub limit: Option<i32>,
1829 pub cursor: Option<String>,
1930 pub slice: String,
···4758 // Parse the XRPC method (e.g., "social.grain.gallery.list")
4859 if method.ends_with(".list") {
4960 let collection = method.trim_end_matches(".list").to_string();
5050- dynamic_list_records_impl(collection, state, params).await
6161+ dynamic_list_records_handler(collection, state, params).await
5162 } else if method.ends_with(".get") {
5263 let collection = method.trim_end_matches(".get").to_string();
5364 dynamic_get_record_impl(collection, state, params).await
···8293 }
8394}
84959696+// Handler for list records that properly deserializes query parameters
9797+async fn dynamic_list_records_handler(
9898+ collection: String,
9999+ state: AppState,
100100+ params: serde_json::Value,
101101+) -> Result<Json<serde_json::Value>, StatusCode> {
102102+ // Handle manual parameter extraction for authors since Query<serde_json::Value> doesn't use custom deserializers
103103+ let mut manual_params = DynamicListParams {
104104+ author: params.get("author").and_then(|v| v.as_str()).map(|s| s.to_string()),
105105+ authors: None,
106106+ limit: params.get("limit").and_then(|v| v.as_i64()).map(|i| i as i32),
107107+ cursor: params.get("cursor").and_then(|v| v.as_str()).map(|s| s.to_string()),
108108+ slice: params.get("slice").and_then(|v| v.as_str()).ok_or(StatusCode::BAD_REQUEST)?.to_string(),
109109+ sort: params.get("sort").and_then(|v| v.as_str()).map(|s| s.to_string()),
110110+ };
111111+112112+ // Handle authors parameter manually - split comma-separated string
113113+ if let Some(authors_str) = params.get("authors").and_then(|v| v.as_str()) {
114114+ manual_params.authors = Some(authors_str.split(',').map(|s| s.trim().to_string()).collect());
115115+ }
116116+117117+ dynamic_list_records_impl(collection, state, manual_params).await
118118+}
8511986120// Implementation for list records
87121async fn dynamic_list_records_impl(
88122 collection: String,
89123 state: AppState,
9090- params: serde_json::Value,
124124+ dynamic_params: DynamicListParams,
91125) -> Result<Json<serde_json::Value>, StatusCode> {
9292- let dynamic_params: DynamicListParams = serde_json::from_value(params)
9393- .map_err(|_| StatusCode::BAD_REQUEST)?;
126126+127127+ // Convert author or authors to the authors parameter
128128+ let authors_param = if let Some(authors) = &dynamic_params.authors {
129129+ Some(authors)
130130+ } else if let Some(author) = &dynamic_params.author {
131131+ Some(&vec![author.clone()])
132132+ } else {
133133+ None
134134+ };
9413595136 // Use slice-aware method that filters by collection belonging to the slice
96137 match state.database.get_slice_collection_records(
97138 &dynamic_params.slice,
98139 &collection,
9999- dynamic_params.author.as_deref(),
140140+ None, // repo parameter is replaced by authors
141141+ authors_param,
100142 dynamic_params.limit,
101143 dynamic_params.cursor.as_deref(),
102144 dynamic_params.sort.as_deref(),
···138180 &collection,
139181 &search_params.query,
140182 search_params.field.as_deref(),
183183+ None, // authors parameter not yet supported for search
141184 search_params.limit,
142185 search_params.cursor.as_deref(),
143186 search_params.sort.as_deref(),