···1064106410651065/// Container to hold blob data and DID for URL generation
10661066#[derive(Clone)]
10671067-struct BlobContainer {
10681068- blob_ref: String, // CID reference
10691069- mime_type: String, // MIME type
10701070- size: i64, // Size in bytes
10711071- did: String, // DID for CDN URL generation
10671067+pub struct BlobContainer {
10681068+ pub blob_ref: String, // CID reference
10691069+ pub mime_type: String, // MIME type
10701070+ pub size: i64, // Size in bytes
10711071+ pub did: String, // DID for CDN URL generation
10721072}
1073107310741074/// Creates a GraphQL Object type for a record collection
+29-18
api/src/graphql/schema_ext/blob_upload.rs
···11//! GraphQL schema extension for blob uploads
2233use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, Object, TypeRef};
44-use async_graphql::{Error, Value as GraphQLValue};
44+use async_graphql::Error;
55use base64::engine::general_purpose;
66use base64::Engine;
7788use crate::atproto_extensions::upload_blob as atproto_upload_blob;
99use crate::auth;
1010-1111-/// Container for blob upload response
1212-#[derive(Clone)]
1313-struct BlobUploadContainer {
1414- blob: serde_json::Value,
1515-}
1010+use crate::graphql::schema_builder::BlobContainer;
16111712/// Creates the BlobUploadResponse GraphQL type
1813pub fn create_blob_upload_response_type() -> Object {
1914 let mut response = Object::new("BlobUploadResponse");
20152121- response = response.field(Field::new("blob", TypeRef::named_nn("JSON"), |ctx| {
1616+ // Return the Blob type instead of JSON to ensure consistent ref field handling
1717+ response = response.field(Field::new("blob", TypeRef::named_nn("Blob"), |ctx| {
2218 FieldFuture::new(async move {
2323- let container = ctx.parent_value.try_downcast_ref::<BlobUploadContainer>()?;
2424- // Convert serde_json::Value to async_graphql::Value
2525- let graphql_value: GraphQLValue = serde_json::from_value(container.blob.clone())
2626- .map_err(|e| async_graphql::Error::new(format!("Failed to convert blob to GraphQL value: {}", e)))?;
2727- Ok(Some(graphql_value))
1919+ // The BlobContainer is passed through from the mutation resolver
2020+ // The Blob type resolver will handle extracting the fields
2121+ let container = ctx.parent_value.try_downcast_ref::<BlobContainer>()?;
2222+ Ok(Some(FieldValue::owned_any(container.clone())))
2823 })
2924 }));
3025···7065 .decode(data_base64)
7166 .map_err(|e| Error::new(format!("Invalid base64 data: {}", e)))?;
72676868+ // Verify OAuth token to get user info (needed for DID)
6969+ let user_info = auth::verify_oauth_token_cached(
7070+ token,
7171+ &auth_base,
7272+ gql_ctx.auth_cache.clone(),
7373+ )
7474+ .await
7575+ .map_err(|e| Error::new(format!("Invalid token: {}", e)))?;
7676+7377 // Get ATProto DPoP auth and PDS URL for this user
7478 let (dpop_auth, pds_url) = auth::get_atproto_auth_for_user_cached(
7579 token,
···9195 .await
9296 .map_err(|e| Error::new(format!("Failed to upload blob: {}", e)))?;
93979494- // Convert blob to JSON value
9595- let blob_json = serde_json::to_value(&upload_result.blob)
9696- .map_err(|e| Error::new(format!("Failed to serialize blob: {}", e)))?;
9898+ // Extract the DID from user info
9999+ let did = user_info.did.unwrap_or(user_info.sub);
100100+101101+ // Create BlobContainer with flattened ref field (CID string)
102102+ // This ensures the GraphQL Blob type returns ref as a String, not an object
103103+ let blob_container = BlobContainer {
104104+ blob_ref: upload_result.blob.r#ref.link.clone(), // Extract CID from ref.$link
105105+ mime_type: upload_result.blob.mime_type.clone(),
106106+ size: upload_result.blob.size as i64,
107107+ did,
108108+ };
971099898- let container = BlobUploadContainer { blob: blob_json };
9999- Ok(Some(FieldValue::owned_any(container)))
110110+ Ok(Some(FieldValue::owned_any(blob_container)))
100111 })
101112 },
102113 )