Skip to main content

shopify_client/admin/bulk_operation/
remote.rs

1use crate::common::ServiceContext;
2use crate::{
3    common::{http::execute_graphql, types::APIError},
4    types::bulk_operation::{
5        CancelResp, GetBulkOperationResp, ListBulkOperationsParams, ListBulkOperationsResp,
6        RunMutationResp, RunQueryResp, StagedUploadInput, StagedUploadsCreateResp,
7    },
8};
9
10use serde_json::json;
11
12// region: Core Operations
13
14pub async fn run_query(
15    ctx: &ServiceContext,
16    query_string: &str,
17    group_objects: Option<bool>,
18) -> Result<RunQueryResp, APIError> {
19    let query = r#"
20        mutation bulkOperationRunQuery($query: String!) {
21            bulkOperationRunQuery(query: $query) {
22                bulkOperation {
23                    id
24                    status
25                    errorCode
26                    createdAt
27                    objectCount
28                    rootObjectCount
29                    url
30                    query
31                    type
32                }
33                userErrors {
34                    code
35                    field
36                    message
37                }
38            }
39        }
40    "#;
41
42    let mut query_with_group = query_string.to_string();
43    if let Some(true) = group_objects {
44        // Wrap the query to enable object grouping if not already present
45        if !query_with_group.contains("groupObjects") {
46            query_with_group = query_string.to_string();
47        }
48    }
49
50    let variables = json!({
51        "query": query_with_group
52    });
53
54    execute_graphql(ctx, query, variables).await
55}
56
57pub async fn run_mutation(
58    ctx: &ServiceContext,
59    mutation: &str,
60    staged_upload_path: &str,
61    client_identifier: Option<&str>,
62) -> Result<RunMutationResp, APIError> {
63    let query = r#"
64        mutation bulkOperationRunMutation($mutation: String!, $stagedUploadPath: String!, $clientIdentifier: String) {
65            bulkOperationRunMutation(mutation: $mutation, stagedUploadPath: $stagedUploadPath, clientIdentifier: $clientIdentifier) {
66                bulkOperation {
67                    id
68                    status
69                    errorCode
70                    createdAt
71                    objectCount
72                    rootObjectCount
73                    url
74                    type
75                }
76                userErrors {
77                    code
78                    field
79                    message
80                }
81            }
82        }
83    "#;
84
85    let variables = json!({
86        "mutation": mutation,
87        "stagedUploadPath": staged_upload_path,
88        "clientIdentifier": client_identifier
89    });
90
91    execute_graphql(ctx, query, variables).await
92}
93
94pub async fn cancel(ctx: &ServiceContext, id: &str) -> Result<CancelResp, APIError> {
95    let query = r#"
96        mutation bulkOperationCancel($id: ID!) {
97            bulkOperationCancel(id: $id) {
98                bulkOperation {
99                    id
100                    status
101                    errorCode
102                }
103                userErrors {
104                    field
105                    message
106                }
107            }
108        }
109    "#;
110
111    let variables = json!({ "id": id });
112
113    execute_graphql(ctx, query, variables).await
114}
115
116pub async fn get(ctx: &ServiceContext, id: &str) -> Result<GetBulkOperationResp, APIError> {
117    let query = format!(
118        r#"
119        query {{
120            node(id: "{}") {{
121                ... on BulkOperation {{
122                    id
123                    status
124                    errorCode
125                    createdAt
126                    completedAt
127                    objectCount
128                    rootObjectCount
129                    fileSize
130                    url
131                    partialDataUrl
132                    query
133                    type
134                }}
135            }}
136        }}
137    "#,
138        id
139    );
140
141    let variables = json!({});
142
143    execute_graphql(ctx, &query, variables).await
144}
145
146pub async fn list(
147    ctx: &ServiceContext,
148    params: &ListBulkOperationsParams,
149) -> Result<ListBulkOperationsResp, APIError> {
150    let mut args = Vec::new();
151
152    if let Some(first) = params.first {
153        args.push(format!("first: {}", first));
154    }
155    if let Some(after) = &params.after {
156        args.push(format!("after: \"{}\"", after));
157    }
158    if let Some(last) = params.last {
159        args.push(format!("last: {}", last));
160    }
161    if let Some(before) = &params.before {
162        args.push(format!("before: \"{}\"", before));
163    }
164    if let Some(reverse) = params.reverse {
165        args.push(format!("reverse: {}", reverse));
166    }
167    if let Some(sort_key) = &params.sort_key {
168        let key_str = serde_json::to_string(sort_key).unwrap_or_default();
169        args.push(format!("sortKey: {}", key_str.trim_matches('"')));
170    }
171
172    let mut query_filters = Vec::new();
173    if let Some(status) = &params.status {
174        let status_str = serde_json::to_string(status).unwrap_or_default();
175        query_filters.push(format!("status:{}", status_str.trim_matches('"')));
176    }
177    if let Some(op_type) = &params.operation_type {
178        let type_str = serde_json::to_string(op_type).unwrap_or_default();
179        query_filters.push(format!("type:{}", type_str.trim_matches('"')));
180    }
181    if !query_filters.is_empty() {
182        args.push(format!("query: \"{}\"", query_filters.join(" ")));
183    }
184
185    let args_str = if args.is_empty() {
186        String::new()
187    } else {
188        format!("({})", args.join(", "))
189    };
190
191    let query = format!(
192        r#"
193        query {{
194            bulkOperations{} {{
195                nodes {{
196                    id
197                    status
198                    errorCode
199                    createdAt
200                    completedAt
201                    objectCount
202                    rootObjectCount
203                    fileSize
204                    url
205                    query
206                    type
207                }}
208                pageInfo {{
209                    hasNextPage
210                    hasPreviousPage
211                    startCursor
212                    endCursor
213                }}
214            }}
215        }}
216    "#,
217        args_str
218    );
219
220    let variables = json!({});
221
222    execute_graphql(ctx, &query, variables).await
223}
224
225pub async fn create_staged_upload(
226    ctx: &ServiceContext,
227    input: &[StagedUploadInput],
228) -> Result<StagedUploadsCreateResp, APIError> {
229    let query = r#"
230        mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
231            stagedUploadsCreate(input: $input) {
232                stagedTargets {
233                    url
234                    resourceUrl
235                    parameters {
236                        name
237                        value
238                    }
239                }
240                userErrors {
241                    field
242                    message
243                }
244            }
245        }
246    "#;
247
248    let variables = json!({ "input": input });
249
250    execute_graphql(ctx, query, variables).await
251}
252
253// endregion
254
255// region: Export Template Queries
256
257fn build_export_query(resource: &str, query_body: &str, filter: Option<&str>) -> String {
258    let mut args = Vec::new();
259
260    if resource == "inventoryItems" {
261        args.push("first: 1".to_string());
262    }
263    if let Some(f) = filter {
264        args.push(format!("query: \"{}\"", f));
265    }
266
267    let args_str = if args.is_empty() {
268        String::new()
269    } else {
270        format!("({})", args.join(", "))
271    };
272
273    format!(
274        r#"{{
275  {}{} {{
276    edges {{
277      node {{
278        {}
279      }}
280    }}
281  }}
282}}"#,
283        resource, args_str, query_body
284    )
285}
286
287pub fn products_query(filter: Option<&str>) -> String {
288    let body = r#"id
289        title
290        handle
291        descriptionHtml
292        status
293        vendor
294        productType
295        tags
296        isGiftCard
297        hasOnlyDefaultVariant
298        templateSuffix
299        onlineStoreUrl
300        createdAt
301        updatedAt
302        publishedAt
303        seo {
304          title
305          description
306        }
307        priceRangeV2 {
308          minVariantPrice {
309            amount
310            currencyCode
311          }
312          maxVariantPrice {
313            amount
314            currencyCode
315          }
316        }
317        options {
318          id
319          name
320          values
321        }
322        category {
323          id
324          name
325          fullName
326          isLeaf
327          isRoot
328          isArchived
329          level
330          parentId
331          ancestorIds
332          childrenIds
333        }
334        featuredMedia {
335          preview {
336            image {
337              url
338              altText
339              width
340              height
341            }
342          }
343        }
344        variants {
345          edges {
346            node {
347              id
348              title
349              sku
350              price
351              compareAtPrice
352              barcode
353              position
354              inventoryQuantity
355              taxable
356              taxCode
357              availableForSale
358              selectedOptions {
359                name
360                value
361              }
362              inventoryItem {
363                id
364                tracked
365                requiresShipping
366                unitCost {
367                  amount
368                  currencyCode
369                }
370              }
371              image {
372                url
373                altText
374                width
375                height
376              }
377            }
378          }
379        }
380        media {
381          edges {
382            node {
383              mediaContentType
384              status
385              alt
386              preview {
387                image {
388                  url
389                  altText
390                  width
391                  height
392                }
393              }
394              ... on MediaImage {
395                id
396                mimeType
397                image {
398                  url
399                  altText
400                  width
401                  height
402                }
403              }
404              ... on Video {
405                id
406                filename
407                sources {
408                  url
409                  format
410                  width
411                  height
412                  mimeType
413                }
414              }
415              ... on ExternalVideo {
416                id
417                embedUrl
418                host
419              }
420              ... on Model3d {
421                id
422                filename
423                originalSource {
424                  url
425                  format
426                  filesize
427                }
428              }
429            }
430          }
431        }"#;
432
433    build_export_query("products", body, filter)
434}
435
436pub fn orders_query(filter: Option<&str>) -> String {
437    let body = r#"id
438        name
439        email
440        phone
441        note
442        tags
443        displayFinancialStatus
444        displayFulfillmentStatus
445        cancelledAt
446        cancelReason
447        closedAt
448        createdAt
449        updatedAt
450        processedAt
451        test
452        confirmed
453        taxesIncluded
454        currencyCode
455        presentmentCurrencyCode
456        subtotalPriceSet {
457          shopMoney { amount currencyCode }
458          presentmentMoney { amount currencyCode }
459        }
460        totalPriceSet {
461          shopMoney { amount currencyCode }
462          presentmentMoney { amount currencyCode }
463        }
464        totalDiscountsSet {
465          shopMoney { amount currencyCode }
466          presentmentMoney { amount currencyCode }
467        }
468        totalTaxSet {
469          shopMoney { amount currencyCode }
470          presentmentMoney { amount currencyCode }
471        }
472        totalShippingPriceSet {
473          shopMoney { amount currencyCode }
474          presentmentMoney { amount currencyCode }
475        }
476        totalRefundedSet {
477          shopMoney { amount currencyCode }
478          presentmentMoney { amount currencyCode }
479        }
480        totalWeight
481        customer {
482          id
483          email
484          firstName
485          lastName
486        }
487        shippingAddress {
488          address1 address2 city province provinceCode
489          country countryCodeV2 zip phone
490          firstName lastName company name
491          latitude longitude
492        }
493        billingAddress {
494          address1 address2 city province provinceCode
495          country countryCodeV2 zip phone
496          firstName lastName company name
497          latitude longitude
498        }
499        sourceName
500        fulfillable
501        requiresShipping
502        riskLevel
503        discountCodes
504        lineItems {
505          edges {
506            node {
507              id
508              title
509              name
510              sku
511              quantity
512              variantTitle
513              vendor
514              product { id }
515              variant { id }
516              originalUnitPriceSet {
517                shopMoney { amount currencyCode }
518                presentmentMoney { amount currencyCode }
519              }
520              discountedUnitPriceSet {
521                shopMoney { amount currencyCode }
522                presentmentMoney { amount currencyCode }
523              }
524              discountedTotalSet {
525                shopMoney { amount currencyCode }
526                presentmentMoney { amount currencyCode }
527              }
528              totalDiscountSet {
529                shopMoney { amount currencyCode }
530                presentmentMoney { amount currencyCode }
531              }
532              taxLines {
533                title
534                rate
535                ratePercentage
536                priceSet {
537                  shopMoney { amount currencyCode }
538                  presentmentMoney { amount currencyCode }
539                }
540              }
541              requiresShipping
542              taxable
543              fulfillableQuantity
544              fulfillmentStatus
545              customAttributes { key value }
546              duties {
547                id
548                harmonizedSystemCode
549                price {
550                  shopMoney { amount currencyCode }
551                  presentmentMoney { amount currencyCode }
552                }
553                taxLines {
554                  title rate ratePercentage
555                  priceSet {
556                    shopMoney { amount currencyCode }
557                    presentmentMoney { amount currencyCode }
558                  }
559                }
560              }
561            }
562          }
563        }"#;
564
565    build_export_query("orders", body, filter)
566}
567
568pub fn collections_query(filter: Option<&str>) -> String {
569    let body = r#"id
570        title
571        handle
572        descriptionHtml
573        sortOrder
574        templateSuffix
575        productsCount {
576          count
577          precision
578        }
579        updatedAt
580        seo {
581          title
582          description
583        }
584        image {
585          url
586          altText
587          width
588          height
589        }
590        products {
591          edges {
592            node {
593              id
594              title
595              handle
596              status
597              vendor
598              productType
599            }
600          }
601        }"#;
602
603    build_export_query("collections", body, filter)
604}
605
606pub fn customers_query(filter: Option<&str>) -> String {
607    let body = r#"id
608        defaultEmailAddress {
609          emailAddress
610        }
611        firstName
612        lastName
613        displayName
614        defaultPhoneNumber {
615          phoneNumber
616        }
617        note
618        tags
619        state
620        taxExempt
621        verifiedEmail
622        locale
623        numberOfOrders
624        amountSpent {
625          amount
626          currencyCode
627        }
628        createdAt
629        updatedAt
630        defaultAddress {
631          address1 address2 city province provinceCode
632          country countryCodeV2 zip phone
633          firstName lastName company name
634          latitude longitude
635        }
636        image {
637          url
638          altText
639          width
640          height
641        }
642        addresses {
643          id
644          address1 address2 city province provinceCode
645          country countryCodeV2 zip phone
646          firstName lastName company name
647        }"#;
648
649    build_export_query("customers", body, filter)
650}
651
652pub fn inventory_items_query(filter: Option<&str>) -> String {
653    let body = r#"id
654        sku
655        tracked
656        requiresShipping
657        countryCodeOfOrigin
658        provinceCodeOfOrigin
659        harmonizedSystemCode
660        createdAt
661        updatedAt
662        unitCost {
663          amount
664          currencyCode
665        }
666        inventoryLevels {
667          edges {
668            node {
669              id
670              quantities(names: ["available"]) {
671                name
672                quantity
673              }
674              location {
675                id
676                name
677              }
678              updatedAt
679            }
680          }
681        }"#;
682
683    build_export_query("inventoryItems", body, filter)
684}
685
686pub fn draft_orders_query(filter: Option<&str>) -> String {
687    let body = r#"id
688        name
689        email
690        phone
691        note2
692        tags
693        status
694        currencyCode
695        taxExempt
696        taxesIncluded
697        createdAt
698        updatedAt
699        completedAt
700        invoiceSentAt
701        subtotalPriceSet {
702          shopMoney { amount currencyCode }
703          presentmentMoney { amount currencyCode }
704        }
705        totalPriceSet {
706          shopMoney { amount currencyCode }
707          presentmentMoney { amount currencyCode }
708        }
709        totalTaxSet {
710          shopMoney { amount currencyCode }
711          presentmentMoney { amount currencyCode }
712        }
713        totalDiscountsSet {
714          shopMoney { amount currencyCode }
715          presentmentMoney { amount currencyCode }
716        }
717        totalShippingPriceSet {
718          shopMoney { amount currencyCode }
719          presentmentMoney { amount currencyCode }
720        }
721        customer {
722          id
723          email
724          firstName
725          lastName
726        }
727        shippingAddress {
728          address1 address2 city province provinceCode
729          country countryCodeV2 zip phone
730          firstName lastName company name
731          latitude longitude
732        }
733        billingAddress {
734          address1 address2 city province provinceCode
735          country countryCodeV2 zip phone
736          firstName lastName company name
737          latitude longitude
738        }
739        order { id }
740        lineItems {
741          edges {
742            node {
743              id
744              title
745              name
746              sku
747              quantity
748              variantTitle
749              vendor
750              product { id }
751              variant { id }
752              originalUnitPriceSet {
753                shopMoney { amount currencyCode }
754                presentmentMoney { amount currencyCode }
755              }
756              discountedUnitPriceSet {
757                shopMoney { amount currencyCode }
758                presentmentMoney { amount currencyCode }
759              }
760              discountedTotalSet {
761                shopMoney { amount currencyCode }
762                presentmentMoney { amount currencyCode }
763              }
764              totalDiscountSet {
765                shopMoney { amount currencyCode }
766                presentmentMoney { amount currencyCode }
767              }
768              requiresShipping
769              taxable
770              customAttributes { key value }
771            }
772          }
773        }"#;
774
775    build_export_query("draftOrders", body, filter)
776}
777
778// endregion