shopify_api 0.10.0

Shopify API client
Documentation
use serde::Deserialize;
use std::time::Duration;

use shopify_api::{
    BulkConcurrencyOptions, BulkOperationsFilter, BulkWaitOptions, Shopify, ShopifyAPIError,
    ShopifyAuth, ShopifyBulkStatus, ShopifyConfig,
};

fn live_client() -> Option<Shopify> {
    let shop = std::env::var("SHOPIFY_TEST_SHOP").ok()?;
    let token = std::env::var("SHOPIFY_TEST_ACCESS_TOKEN").ok()?;
    Shopify::new(
        shop,
        ShopifyAuth::AccessToken(token),
        ShopifyConfig::default(),
    )
    .ok()
}

fn live_client_credentials_client() -> Option<Shopify> {
    let shop = std::env::var("SHOPIFY_TEST_SHOP").ok()?;
    let client_id = std::env::var("SHOPIFY_TEST_CLIENT_ID").ok()?;
    let client_secret = std::env::var("SHOPIFY_TEST_CLIENT_SECRET").ok()?;
    Shopify::new(
        shop,
        ShopifyAuth::client_credentials(client_id, client_secret),
        ShopifyConfig::default(),
    )
    .ok()
}

#[derive(Debug, Deserialize)]
struct ShopQuery {
    shop: Shop,
}

#[derive(Debug, Deserialize)]
struct Shop {
    name: String,
}

#[tokio::test]
async fn live_graphql_shop_query_when_env_is_present() -> Result<(), ShopifyAPIError> {
    let Some(shopify) = live_client() else {
        return Ok(());
    };

    let data: ShopQuery = shopify
        .graphql("query { shop { name } }", &serde_json::json!({}))
        .await?;

    assert!(!data.shop.name.is_empty());
    Ok(())
}

#[tokio::test]
async fn live_schema_download_when_env_is_present() -> Result<(), ShopifyAPIError> {
    let Some(shopify) = live_client() else {
        return Ok(());
    };

    let schema = shopify.download_admin_schema().await?;

    assert!(schema.pointer("/data/__schema").is_some());
    Ok(())
}

#[tokio::test]
async fn live_client_credentials_when_env_is_present() -> Result<(), ShopifyAPIError> {
    let Some(shopify) = live_client_credentials_client() else {
        return Ok(());
    };
    let token = shopify.access_token().await?;

    assert!(!token.is_empty());
    Ok(())
}

#[tokio::test]
async fn live_bulk_query_wait_list_and_download_when_env_is_present() -> Result<(), ShopifyAPIError>
{
    let Some(shopify) = live_client_credentials_client() else {
        return Ok(());
    };

    let payload = shopify
        .run_bulk_query(
            r#"{
                products {
                    edges {
                        node {
                            id
                            title
                        }
                    }
                }
            }"#,
        )
        .await?;

    assert!(
        payload.user_errors.is_empty(),
        "bulk query returned user errors: {:?}",
        payload.user_errors
    );

    let operation = payload
        .bulk_operation
        .expect("bulkOperationRunQuery should return a bulk operation");

    let listed = shopify
        .list_bulk_operations(BulkOperationsFilter {
            first: 5,
            ..BulkOperationsFilter::default()
        })
        .await?;
    assert!(listed.edges.iter().any(|edge| edge.node.id == operation.id));

    let operation = shopify
        .wait_for_bulk(
            &operation.id,
            BulkWaitOptions {
                poll_interval: Duration::from_secs(5),
                timeout: Some(Duration::from_secs(180)),
            },
        )
        .await?;

    assert_eq!(operation.status, ShopifyBulkStatus::Completed);

    if let Some(url) = operation.url {
        let rows: Vec<serde_json::Value> = Shopify::download_bulk_jsonl(&url).await?;
        for row in rows {
            assert!(row.get("id").is_some());
        }
    }

    Ok(())
}

#[tokio::test]
async fn live_five_concurrent_bulk_queries_when_enabled() -> Result<(), ShopifyAPIError> {
    if std::env::var("SHOPIFY_TEST_BULK_CONCURRENCY")
        .ok()
        .as_deref()
        != Some("1")
    {
        return Ok(());
    }

    let Some(shopify) = live_client_credentials_client() else {
        return Ok(());
    };

    let query = r#"{
        products(first: 1) {
            edges {
                node {
                    id
                    title
                }
            }
        }
    }"#;
    let payloads = shopify
        .run_bulk_queries(vec![query; 5], BulkConcurrencyOptions::default())
        .await?;
    let operations = payloads
        .into_iter()
        .map(|payload| {
            assert!(
                payload.user_errors.is_empty(),
                "bulk query returned user errors: {:?}",
                payload.user_errors
            );
            payload
                .bulk_operation
                .expect("bulkOperationRunQuery should return a bulk operation")
        })
        .collect::<Vec<_>>();

    assert_eq!(operations.len(), 5);

    let running_or_recent = shopify
        .list_bulk_operations(BulkOperationsFilter {
            first: 10,
            query: Some("type:QUERY".to_string()),
            ..BulkOperationsFilter::default()
        })
        .await?;
    for operation in &operations {
        assert!(running_or_recent
            .edges
            .iter()
            .any(|edge| edge.node.id == operation.id));
    }

    for operation in operations {
        let operation = shopify
            .wait_for_bulk(
                &operation.id,
                BulkWaitOptions {
                    poll_interval: Duration::from_secs(5),
                    timeout: Some(Duration::from_secs(180)),
                },
            )
            .await?;

        assert_eq!(operation.status, ShopifyBulkStatus::Completed);
    }

    Ok(())
}