arlyon / async-stripe

Async (and blocking!) Rust bindings for the Stripe API
https://payments.rs
Apache License 2.0
464 stars 130 forks source link

Query using metadata #345

Closed onx2 closed 1 year ago

onx2 commented 1 year ago

Is your feature request related to a problem? Please describe.

I'm currently trying to query stripe service but I don't have the customer ID and would like to avoid querying the entire list then filtering in code. Is there currently a way to do this that I'm missing or is it not available yet?

Describe the solution you'd like

In the Javascript SDK I can use something like this:

await this.stripeClient.customers
        .search({ query: `metadata["memberId"]:"${memberId}"` })

So I was hoping there would be an equivalent in Rust, maybe like:

let mut customer_params = ListCustomers::new();
customer_params.expand = &vec!["metadata[\"member_id\"]"];
customer_params.query = format!("metadata[\"member_id\"]:{member_id}");
let customers = Customer::list(&self.client, &customer_params).await?;

Describe alternatives you've considered

The alternative is to filter after I query the entire list using an expanded metadata object.

Additional context

No response

onx2 commented 1 year ago

Currently my code looks like what I've pasted below because I need to query for a metadata field called member_id. It would be great if I could query for the customers by metadata fields and subscriptions by a vec of customer IDs.

async fn get_subscriptions(&self, member_id: &Uuid) -> Result<Vec<Subscription>> {
      let customers = Customer::list(
          &self.client,
          &ListCustomers {
              expand: &vec!["metadata"],
              ..ListCustomers::default()
          },
      )
      .await?;

      let customer_ids = customers
          .data
          .into_iter()
          .filter_map(|customer| {
              let m_id = match customer.metadata.get("member_id") {
                  Some(m_id) => m_id,
                  _ => return None,
              };

              if m_id == &member_id.to_string() {
                  Some(customer.id.clone());
              }

              None
          })
          .collect::<Vec<CustomerId>>();

      let futures = FuturesUnordered::new();

      for id in customer_ids {
          futures.push(Subscription::list(&self.client, &ListSubscriptions {
              customer: Some(id),
              ..ListSubscriptions::default()
          }))
      }

      let subscriptions: Vec<Subscription> = join_all(futures)
          .await
          .into_iter()
          .filter_map(|x| match x {
              Ok(sub_list) => Some(sub_list),
              _ => None,
          })
          .map(|sub_list| sub_list.data)
          .flatten()
          .collect();

      Ok(subscriptions)
  }
AlbertMarashi commented 1 year ago

Need this!!

Searching by metadata is really important

AlbertMarashi commented 1 year ago

Workaround:

#[derive(Serialize, Default, Debug)]
pub(crate) struct SearchParams {
    pub query: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub page: Option<u64>,
}

pub(crate) async fn stripe_search<R: DeserializeOwned + 'static + Send>(
    client: &stripe::Client,
    resource: &str,
    params: SearchParams,
) -> Result<List<R>, stripe::StripeError> {
    client
        .get_query(&format!("/{}/search", resource), &params)
        .await
}
onx2 commented 1 year ago

I ended up not using Rust as this was a POC and the company decided to use JS (unrelated to this issue). But seeing this brought up again made me think.

Perhaps this feature isn't possible or not likely to be on the roadmap... If I were to implement this again I'd probably just end up storing the customer ID internally or in whatever system needs to access the subscriptions. Seems like the most efficient solution without writing custom application layer workarounds IMO but I suppose each scenario is unique. 😄

AlbertMarashi commented 1 year ago

@onx2 I also realized it was not an option for me because I learnt that the meta data is not instantly queryable. It can take up to a minute for the search index to update

My code was creating multiple customers instead of one so I had to resort to storing the customer ID internally.

However the code above does work for any resource, but note that search results are not safe for read-after-write operations

arlyon commented 1 year ago

I am going to move this to discussions, thanks for opening! :)