diff --git a/docs/auto-tag-products-in-a-manual-collection/script.liquid b/docs/auto-tag-products-in-a-manual-collection/script.liquid index ac8c8184..0ad10223 100644 --- a/docs/auto-tag-products-in-a-manual-collection/script.liquid +++ b/docs/auto-tag-products-in-a-manual-collection/script.liquid @@ -1,116 +1,183 @@ {% assign collection_tag = options.collection_tag__required %} +{% assign collection_id = options.collection_id__number_required %} -{% assign collection_qualifies = false %} +{% assign collection_tag_lower = collection_tag | downcase %} -{% if event.topic contains "shopify/collections/" and collection.id == options.collection_id__number_required %} - {% assign collection_qualifies = true %} +{% if event.topic == "shopify/collections/update" %} + {% comment %} + -- on collection update, exit if this is not the configured collection + {% endcomment %} -{% elsif event.topic == "mechanic/user/trigger" %} - {% assign collection_qualifies = true %} - {% assign collection = shop.collections[options.collection_id__number_required] %} + {% unless collection.id == collection_id or event.preview %} + {% break %} + {% endunless %} {% endif %} -{% if collection_qualifies or event.preview %} - {% assign collection_product_ids = collection.products | map: "admin_graphql_api_id" %} +{% comment %} + -- get all of the product IDs currently in the collection +{% endcomment %} - {% assign cursor = nil %} - {% assign tagged_product_ids = array %} +{% assign cursor = nil %} +{% assign collection_product_ids = array %} - {% for n in (0..100) %} - {% capture query %} - query { +{% for n in (1..100) %} + {% capture query %} + query { + collection(id: {{ collection_id | prepend: "gid://shopify/Collection/" | json }}) { products( first: 250 after: {{ cursor | json }} - query: {{ collection_tag | json | prepend: "tag:" | json }} ) { pageInfo { hasNextPage + endCursor } - edges { - cursor - node { - id - } + nodes { + id } } } - {% endcapture %} - - {% assign result = query | shopify %} + } + {% endcapture %} - {% if event.preview %} - {% assign collection_product_ids = array %} - {% assign collection_product_ids[0] = "gid://shopify/Product/1234567890" %} - {% assign collection_product_ids[1] = "gid://shopify/Product/2345678901" %} + {% assign result = query | shopify %} - {% capture result_json %} - { - "data": { + {% if event.preview %} + {% capture result_json %} + { + "data": { + "collection": { "products": { - "edges": [ + "nodes": [ { - "node": { - "id": "gid://shopify/Product/1234567890" - } - }, - { - "node": { - "id": "gid://shopify/Product/9876543210" - } + "id": "gid://shopify/Product/1234567890" } ] } } } - {% endcapture %} + } + {% endcapture %} - {% assign result = result_json | parse_json %} - {% endif %} + {% assign result = result_json | parse_json %} + {% endif %} + + {% assign collection_product_ids + = result.data.collection.products.nodes + | map: "id" + | concat: collection_product_ids + %} + + {% if result.data.collection.products.pageInfo.hasNextPage %} + {% assign cursor = result.data.collection.products.pageInfo.endCursor %} + {% else %} + {% break %} + {% endif %} +{% endfor %} + +{% comment %} + -- get IDs for all products with collection tag +{% endcomment %} + +{% assign cursor = nil %} +{% assign tagged_product_ids = array %} + +{% for n in (0..100) %} + {% capture query %} + query { + products( + first: 250 + after: {{ cursor | json }} + query: {{ collection_tag | json | prepend: "tag:" | json }} + ) { + pageInfo { + hasNextPage + endCursor + } + nodes { + id + tags + } + } + } + {% endcapture %} + + {% assign result = query | shopify %} + + {% if event.preview %} + {% capture result_json %} + { + "data": { + "products": { + "nodes": [ + { + "id": "gid://shopify/Product/2345678901", + "tags": {{ collection_tag | json }} + } + ] + } + } + } + {% endcapture %} + + {% assign result = result_json | parse_json %} + {% endif %} - {% assign products_result = result.data.products.edges | map: "node" | map: "id" %} - {% assign tagged_product_ids = tagged_product_ids | concat: products_result %} + {% comment %} + -- double check that the collection tag exists on each product to avoid occasional query filter quirkiness + {% endcomment %} - {% if result.data.products.pageInfo.hasNextPage %} - {% assign cursor = result.data.products.edges.last.cursor %} - {% else %} - {% break %} + {% for product in result.data.products.nodes %} + {% assign product_tags_lower = product.tags | json | downcase | parse_json %} + + {% if product_tags_lower contains collection_tag_lower %} + {% assign tagged_product_ids = tagged_product_ids | push: product.id %} {% endif %} {% endfor %} - {% for collection_product_id in collection_product_ids %} - {% unless tagged_product_ids contains collection_product_id %} - {% action "shopify" %} - mutation { - tagsAdd( - id: {{ collection_product_id | json }} - tags: {{ collection_tag | json }} - ) { - userErrors { - field - message - } + {% if result.data.products.pageInfo.hasNextPage %} + {% assign cursor = result.data.products.pageInfo.endCursor %} + {% else %} + {% break %} + {% endif %} +{% endfor %} + +{% comment %} + -- add and remove the collection tag from products +{% endcomment %} + +{% for collection_product_id in collection_product_ids %} + {% unless tagged_product_ids contains collection_product_id %} + {% action "shopify" %} + mutation { + tagsAdd( + id: {{ collection_product_id | json }} + tags: {{ collection_tag | json }} + ) { + userErrors { + field + message } } - {% endaction %} - {% endunless %} - {% endfor %} + } + {% endaction %} + {% endunless %} +{% endfor %} - {% for tagged_product_id in tagged_product_ids %} - {% unless collection_product_ids contains tagged_product_id %} - {% action "shopify" %} - mutation { - tagsRemove( - id: {{ tagged_product_id | json }} - tags: {{ collection_tag | json }} - ) { - userErrors { - field - message - } +{% for tagged_product_id in tagged_product_ids %} + {% unless collection_product_ids contains tagged_product_id %} + {% action "shopify" %} + mutation { + tagsRemove( + id: {{ tagged_product_id | json }} + tags: {{ collection_tag | json }} + ) { + userErrors { + field + message } } - {% endaction %} - {% endunless %} - {% endfor %} -{% endif %} + } + {% endaction %} + {% endunless %} +{% endfor %} diff --git a/docs/delete-the-oldest-x-products-from-a-specific-collection/README.md b/docs/delete-the-oldest-x-products-from-a-specific-collection/README.md index f62ab839..27477215 100644 --- a/docs/delete-the-oldest-x-products-from-a-specific-collection/README.md +++ b/docs/delete-the-oldest-x-products-from-a-specific-collection/README.md @@ -25,7 +25,6 @@ Either triggered manually, or configured to run daily, this task will look for t ```liquid mechanic/user/trigger - {% if options.run_daily__boolean %} mechanic/scheduler/daily {% endif %} @@ -37,7 +36,11 @@ mechanic/user/trigger Either triggered manually, or configured to run daily, this task will look for the oldest products in the collection of your choice, and delete as many of them as you wish. -This task requires a collection ID - [learn how to find yours](https://help.usemechanic.com/en/articles/2946120-how-do-i-find-an-id-for-a-product-collection-order-or-something-else). +**IMPORTANT**: When first configuring this task, run it manually once in "Test mode" to see a list of which products it would delete. Once verfied, be sure to uncheck this option to have the task make the deletions going forward. + +*Notes:* +- The products will be deleted asycnronously by Shopify, which means they may appear in the products list of admin for a bit after a task run. +- This task requires a collection ID - [learn how to locate common resource IDs](https://learn.mechanic.dev/techniques/finding-a-resource-id). ## Installing this task diff --git a/docs/delete-the-oldest-x-products-from-a-specific-collection/script.liquid b/docs/delete-the-oldest-x-products-from-a-specific-collection/script.liquid index b1d28c9b..653cfe03 100644 --- a/docs/delete-the-oldest-x-products-from-a-specific-collection/script.liquid +++ b/docs/delete-the-oldest-x-products-from-a-specific-collection/script.liquid @@ -1,46 +1,93 @@ -{% assign collection = shop.collections[options.collection_id__number_required] %} +{% assign collection_id = options.collection_id__number_required %} +{% assign number_of_products_to_delete_at_once = options.number_of_products_to_delete_at_once__number_required %} +{% assign test_mode = options.test_mode__boolean %} -{% if event.preview != true and collection == nil %} - {"error": {{ options.collection_id__number_required | json | prepend: "Unable to find collection " | json }}} +{% if number_of_products_to_delete_at_once > 250 %} + {% error "This task only supports deleting up to 250 products in a single task run." %} {% endif %} -{% assign products = collection.products %} +{% comment %} + -- get X oldest products that are in the collection +{% endcomment %} + +{% capture query %} + query { + collection(id: {{ collection_id | prepend: "gid://shopify/Collection/" | json }}) { + products( + first: {{ number_of_products_to_delete_at_once }} + sortKey: CREATED + ) { + nodes { + id + title + createdAt + } + } + } + } +{% endcapture %} + +{% assign result = query | shopify %} {% if event.preview %} - {% assign products = array %} - {% assign products[0] = '{"id":12345,"created_at":"2000-01-01"}' | parse_json %} + {% capture result_json %} + { + "data": { + "collection": { + "products": { + "nodes": [ + { + "id": "gid://shopify/Product/1234567890", + "createdAt": "2020-02-02T01:01:00Z" + } + ] + } + } + } + } + {% endcapture %} - {% comment %} - Uncomment me for more testing! - {% assign products[1] = '{"id":67890,"created_at":"2001-01-01"}' | parse_json %} - {% endcomment %} + {% assign result = result_json | parse_json %} {% endif %} -{% assign products_sorted = products | sort: "created_at" %} +{% assign collection = result.data.collection %} +{% assign products = collection.products.nodes %} -{% assign product_ids_to_delete = array %} -{% for product in products_sorted %} - {% if forloop.index > options.number_of_products_to_delete_at_once__number_required %} - {% break %} - {% endif %} +{% if collection == blank %} + {% error "Unable to find a collection by the configured ID in this shop." %} +{% endif %} - {% assign product_ids_to_delete[product_ids_to_delete.size] = product.id %} -{% endfor %} +{% log products_to_delete: products %} -{% if options.test_mode__boolean %} - {% action "echo" %} - {"product_ids_to_delete": {{ product_ids_to_delete | json }}} - {% endaction %} -{% else %} - {% for product_id in product_ids_to_delete %} - {% action "shopify" %} - [ - "delete", - [ - "product", - {{ product_id | json }} - ] - ] - {% endaction %} - {% endfor %} +{% if test_mode %} + {% break %} {% endif %} + +{% comment %} + -- delete all returned products asynchronously in case they have many variants and/or media (which could cause an API timeout) +{% endcomment %} + +{% for product in collection.products.nodes %} + {% action "shopify" %} + mutation { + productDelete( + input: { + id: {{ product.id | json }} + } + synchronous: false + ) { + productDeleteOperation { + id + product { + legacyResourceId + title + } + } + userErrors { + field + message + } + } + } + {% endaction %} +{% endfor %} diff --git a/docs/delete-variants-having-a-metafield-date-that-has-passed/README.md b/docs/delete-variants-having-a-metafield-date-that-has-passed/README.md index cf5adb09..40677e6c 100644 --- a/docs/delete-variants-having-a-metafield-date-that-has-passed/README.md +++ b/docs/delete-variants-having-a-metafield-date-that-has-passed/README.md @@ -24,6 +24,7 @@ Do you sell tickets to recurring events? Do you use a new product variant for ea ```liquid mechanic/user/trigger mechanic/scheduler/daily +mechanic/shopify/bulk_operation ``` [Learn about event subscriptions in Mechanic](https://learn.mechanic.dev/core/tasks/subscriptions) diff --git a/docs/delete-variants-having-a-metafield-date-that-has-passed/script.liquid b/docs/delete-variants-having-a-metafield-date-that-has-passed/script.liquid index 54559ac2..024d56db 100644 --- a/docs/delete-variants-having-a-metafield-date-that-has-passed/script.liquid +++ b/docs/delete-variants-having-a-metafield-date-that-has-passed/script.liquid @@ -1,34 +1,108 @@ -{% if event.preview %} - { - "action": { - "type": "shopify", - "options": [ - "delete", - ["variant", {{ variant.id | json }}] - ] - } - } -{% else %} - {% assign today = "now" | date: "%Y-%m-%d" %} +{% assign date_metafield_namespace = options.date_metafield_namespace__required %} +{% assign date_metafield_key = options.date_metafield_key__required %} - {% for product in shop.products.published %} - {% if product.variants.size == 1 and product.variants[0].title == "Default Title" %} - {% continue %} - {% endif %} +{% assign today = "now" | date: "%Y-%m-%d" %} + +{% if event.topic == "mechanic/user/trigger" or event.topic == "mechanic/scheduler/daily" %} + {% comment %} + -- get all active, published, multi-variant products in the shop + {% endcomment %} - {% for variant in product.variants %} - {% assign expiration_date = variant.metafields[options.date_metafield_namespace__required][options.date_metafield_key__required] %} - {% if expiration_date != blank and expiration_date < today %} - { - "action": { - "type": "shopify", - "options": [ - "delete", - ["variant", {{ variant.id | json }}] - ] + {% capture bulk_operation_query %} + query { + products( + query: "status:active published_status:published has_only_default_variant:false" + ) { + edges { + node { + __typename + id + variants { + edges { + node { + __typename + id + metafield( + namespace: {{ date_metafield_namespace | json }} + key: {{ date_metafield_key | json }} + ) { + value + } + } + } + } } } + } + } + {% endcapture %} + + {% action "shopify" %} + mutation { + bulkOperationRunQuery( + query: {{ bulk_operation_query | json }} + ) { + bulkOperation { + id + status + } + userErrors { + field + message + } + } + } + {% endaction %} + +{% elsif event.topic == "mechanic/shopify/bulk_operation" %} + {% if event.preview %} + {% capture jsonl_string %} + {"__typename":"Product","id":"gid://shopify/Product/1234567890"} + {"__typename":"ProductVariant","id":"gid://shopify/ProductVariant/1234567890","metafield":{"value":"2020-02-02"},"__parentId":"gid://shopify/Product/1234567890"} + {"__typename":"ProductVariant","id":"gid://shopify/ProductVariant/2345678901","__parentId":"gid://shopify/Product/1234567890"} + {% endcapture %} + + {% assign bulkOperation = hash %} + {% assign bulkOperation["objects"] = jsonl_string | parse_jsonl %} + {% endif %} + + {% assign products = bulkOperation.objects | where: "__typename", "Product" %} + {% assign bulk_variants = bulkOperation.objects | where: "__typename", "ProductVariant" %} + + {% comment %} + -- for each product, qualify and delete variants in bulk by metafield value + {% endcomment %} + + {% for product in products %} + {% assign variants = bulk_variants | where: "__parentId", product.id %} + + {% assign variant_ids_to_delete = array %} + + {% for variant in variants %} + {% if variant.metafield != blank and variant.metafield.value < today %} + {% assign variant_ids_to_delete = variant_ids_to_delete | push: variant.id %} {% endif %} {% endfor %} + + {% if variant_ids_to_delete != blank %} + {% action "shopify" %} + mutation { + productVariantsBulkDelete( + productId: {{ product.id | json }} + variantsIds: {{ variant_ids_to_delete | json }} + ) { + product { + id + title + } + userErrors { + code + field + message + } + } + } + {% endaction %} + {% endif %} {% endfor %} {% endif %} diff --git a/docs/manage-collection-templates-by-product-inventory/README.md b/docs/manage-collection-templates-by-product-inventory/README.md index e6988613..8977b26a 100644 --- a/docs/manage-collection-templates-by-product-inventory/README.md +++ b/docs/manage-collection-templates-by-product-inventory/README.md @@ -36,10 +36,12 @@ mechanic/shopify/bulk_operation ## Documentation -Use this task to automatically switch collections over to an appropriate theme template, when every product in the collection has gone completely out of stock, and to switch them back when inventory arrives. - +Use this task to automatically switch collections over to an appropriate theme template, when every product in the collection has gone completely out of stock, and to switch them back when inventory arrives. + This task can be run manually (using the "Run task" button), or be configured to run daily/hourly. If you encounter performance issues when running every time an inventory level is updated, disable this option. +Note: For this task, products which do not track inventory in Shopify will be considered as in stock. + ## Installing this task Find this task [in the library at tasks.mechanic.dev](https://tasks.mechanic.dev/manage-collection-templates-by-product-inventory), and use the "Try this task" button. Or, import [this task's JSON export](../../tasks/manage-collection-templates-by-product-inventory.json) – see [Importing and exporting tasks](https://learn.mechanic.dev/core/tasks/import-and-export) to learn how imports work. diff --git a/docs/manage-collection-templates-by-product-inventory/script.liquid b/docs/manage-collection-templates-by-product-inventory/script.liquid index dca8c151..08a5ba18 100644 --- a/docs/manage-collection-templates-by-product-inventory/script.liquid +++ b/docs/manage-collection-templates-by-product-inventory/script.liquid @@ -1,171 +1,222 @@ +{% assign out_of_stock_collection_template_suffix = options.collection_template_suffix_when_all_products_are_out_of_stock__required %} +{% assign default_collection_template_suffix = options.default_collection_template_suffix %} + {% if event.topic contains "shopify/inventory_levels/" %} - {% assign collections = inventory_level.variant.product.collections %} + {% comment %} + -- get this product's total inventory, and up to 250 collections + {% endcomment %} + + {% capture query %} + query { + inventoryItem(id: {{ inventory_level.inventory_item_id | prepend: "gid://shopify/InventoryItem/" | json }}) { + variant { + product { + id + totalInventory + tracksInventory + collections(first: 250) { + nodes { + id + templateSuffix + } + } + } + } + } + } + {% endcapture %} + + {% assign result = query | shopify %} {% if event.preview %} - {% capture collections_json %} - [ - { - "admin_graphql_api_id": "gid://shopify/Collection/1234567890", - "template_suffix": null, - "products": [ - { - "variants": [ - { - "inventory_quantity": 0 + {% capture result_json %} + { + "data": { + "inventoryItem": { + "variant": { + "product": { + "id": "gid://shopify/Product/1234567890", + "totalInventory": 0, + "tracksInventory": true, + "collections": { + "nodes": [ + { + "id": "gid://shopify/Collection/1234567890", + "templateSuffix": {{ default_collection_template_suffix | json }} + } + ] } - ] + } } - ] + } } - ] + } {% endcapture %} - {% assign collections = collections_json | parse_json %} + {% assign result = result_json | parse_json %} {% endif %} + {% assign product = result.data.inventoryItem.variant.product %} + {% assign collections = product.collections.nodes %} + {% for collection in collections %} - {% assign current_template_suffix = collection.template_suffix %} + {% assign has_default_template = nil %} + {% assign has_out_of_stock_template = nil %} + {% assign has_a_product_in_stock = nil %} + + {% assign current_template_suffix = collection.templateSuffix %} {% comment %} - Shopify stores this as *either* an empty string or as nil. Mechanic auto-nils - empty strings for task options, so we coerce blanks to nils here. + -- Shopify stores this as *either* an empty string or as nil. Mechanic auto-nils empty strings for task options, so we coerce blanks to nils here. {% endcomment %} + {% if current_template_suffix == blank %} {% assign current_template_suffix = nil %} {% endif %} - {% assign total_inventory = 0 %} + {% if current_template_suffix == default_collection_template_suffix %} + {% assign has_default_template = true %} - {% assign set_template_suffix = false %} - {% assign new_template_suffix = nil %} + {% elsif current_template_suffix == out_of_stock_collection_template_suffix %} + {% assign has_out_of_stock_template = true %} + {% endif %} - {% for product in collection.products %} - {% assign product_total_inventory = product.variants | map: "inventory_quantity" | sum %} - {% assign total_inventory = total_inventory | plus: product_total_inventory %} - {% endfor %} + {% comment %} + -- if the product's stock status matches the paired collection template, then stop processing that collection + -- NOTE: another product might cause the collection to change templates, but that should be handled in an inventory level event for that product + {% endcomment %} - {% if total_inventory <= 0 %} - {% if current_template_suffix != options.collection_template_suffix_when_all_products_are_out_of_stock__required %} - {% assign set_template_suffix = true %} - {% assign new_template_suffix = options.collection_template_suffix_when_all_products_are_out_of_stock__required %} - {% endif %} - {% else %} - {% if current_template_suffix != options.default_collection_template_suffix %} - {% assign set_template_suffix = true %} - {% assign new_template_suffix = options.default_collection_template_suffix %} + {% if product.totalInventory <= 0 and product.tracksInventory %} + {% if has_out_of_stock_template %} + {% continue %} {% endif %} + + {% elsif has_default_template %} + {% continue %} {% endif %} - { - "log": { - "collection_id": {{ collection.admin_graphql_api_id | json }}, - "total_inventory": {{ total_inventory | json }}, - "current_template_suffix": {{ current_template_suffix | json }}, - "set_template_suffix": {{ set_template_suffix | json }}, - "new_template_suffix": {{ new_template_suffix | json }} - } - } + {% comment %} + -- query collection for up to 25K products, getting inventory of each + -- break the query loop early when at least one product is in stock and the collection has the out of stock template + {% endcomment %} - {% if set_template_suffix %} - {% action "shopify" %} - mutation { - collectionUpdate( - input: { - id: {{ collection.admin_graphql_api_id | json }} - templateSuffix: {{ new_template_suffix | json }} - } - ) { - userErrors { - field - message + {% assign cursor = nil %} + + {% for n in (1..100) %} + {% capture query %} + query { + collection(id: {{ collection.id | json }}) { + products( + first: 250 + after: {{ cursor | json }} + ) { + pageInfo { + hasNextPage + endCursor + } + nodes { + id + totalInventory + tracksInventory + } } } } - {% endaction %} - {% endif %} - {% endfor %} -{% elsif event.topic == "mechanic/shopify/bulk_operation" %} - {% if event.preview %} - {% assign collection_object = '{"__typename":"Collection","id":"gid://shopify/Collection/1234567890","templateSuffix":null}' | parse_json %} - {% assign product_object = '{"__typename":"Product","id":"gid://shopify/Product/1234567890","__parentId":"gid://shopify/Collection/1234567890","totalInventory":0}' | parse_json %} - - {% assign objects = array %} - {% assign objects[0] = collection_object %} - {% assign objects[1] = product_object %} + {% endcapture %} - {% assign bulkOperation = hash %} - {% assign bulkOperation["objects"] = objects %} - {% endif %} - - {% assign collection_suffixes = hash %} - {% assign collection_total_inventory = hash %} - - {% for object in bulkOperation.objects %} - {% case object.__typename %} - {% when "Collection" %} - {% assign collection_suffixes[object.id] = object.templateSuffix %} - {% when "Product" %} - {% assign collection_total_inventory[object.__parentId] = collection_total_inventory[object.__parentId] | default: 0 | plus: object.totalInventory %} - {% endcase %} - {% endfor %} + {% assign result = query | shopify %} - {% for keyval in collection_total_inventory %} - {% assign collection_id = keyval[0] %} - {% assign total_inventory = keyval[1] %} - {% assign current_template_suffix = collection_suffixes[collection_id] %} + {% if event.preview %} + {% capture result_json %} + { + "data": { + "collection": { + "products": { + "nodes": [ + { + "id": "gid://shopify/Product/1234567890", + "totalInventory": 0, + "tracksInventory": true + } + ] + } + } + } + } + {% endcapture %} - {% comment %} - Shopify stores this as *either* an empty string or as nil. Mechanic auto-nils - empty strings for task options, so we coerce blanks to nils here. - {% endcomment %} - {% if current_template_suffix == blank %} - {% assign current_template_suffix = nil %} - {% endif %} + {% assign result = result_json | parse_json %} + {% endif %} - {% assign set_template_suffix = false %} - {% assign new_template_suffix = nil %} + {% for product in result.data.collection.products.nodes %} + {% if product.totalInventory > 0 or product.tracksInventory == false %} + {% assign has_a_product_in_stock = true %} + {% break %} + {% endif %} + {% endfor %} - {% if total_inventory <= 0 %} - {% if current_template_suffix != options.collection_template_suffix_when_all_products_are_out_of_stock__required %} - {% assign set_template_suffix = true %} - {% assign new_template_suffix = options.collection_template_suffix_when_all_products_are_out_of_stock__required %} + {% if has_a_product_in_stock %} + {% break %} {% endif %} - {% else %} - {% if current_template_suffix != options.default_collection_template_suffix %} - {% assign set_template_suffix = true %} - {% assign new_template_suffix = options.default_collection_template_suffix %} - {% endif %} - {% endif %} - { - "log": { - "collection_id": {{ collection_id | json }}, - "total_inventory": {{ total_inventory | json }}, - "current_template_suffix": {{ current_template_suffix | json }}, - "set_template_suffix": {{ set_template_suffix | json }}, - "new_template_suffix": {{ new_template_suffix | json }} - } - } + {% if result.data.collection.products.pageInfo.hasNextPage %} + {% assign cursor = result.data.collection.products.pageInfo.endCursor %} + {% else %} + {% break %} + {% endif %} + {% endfor %} - {% if set_template_suffix %} - {% action "shopify" %} - mutation { - collectionUpdate( - input: { - id: {{ collection_id | json }} - templateSuffix: {{ new_template_suffix | json }} + {% if has_a_product_in_stock %} + {% unless has_default_template %} + {% action "shopify" %} + mutation { + collectionUpdate( + input: { + id: {{ collection.id | json }} + templateSuffix: {{ default_collection_template_suffix | json }} + } + ) { + collection { + id + title + templateSuffix + } + userErrors { + field + message + } } - ) { - userErrors { - field - message + } + {% endaction %} + {% endunless %} + + {% else %} + {% unless has_out_of_stock_template %} + {% action "shopify" %} + mutation { + collectionUpdate( + input: { + id: {{ collection.id | json }} + templateSuffix: {{ out_of_stock_collection_template_suffix | json }} + } + ) { + collection { + id + title + templateSuffix + } + userErrors { + field + message + } } } - } - {% endaction %} + {% endaction %} + {% endunless %} {% endif %} {% endfor %} -{% else %} + +{% elsif event.topic == "mechanic/user/trigger" or event.topic contains "mechanic/scheduler/" %} {% capture bulk_operation_query %} query { collections { @@ -180,6 +231,7 @@ __typename id totalInventory + tracksInventory } } } @@ -205,4 +257,104 @@ } } {% endaction %} + +{% elsif event.topic == "mechanic/shopify/bulk_operation" %} + {% if event.preview %} + {% capture jsonl_string %} + {"__typename":"Collection","id":"gid://shopify/Collection/1234567890","templateSuffix":{{ default_collection_template_suffix | json }}} + {"__typename":"Product","id":"gid://shopify/Product/1234567890","totalInventory":0,"__parentId":"gid://shopify/Collection/1234567890"} + {"__typename":"Product","id":"gid://shopify/Product/2345678901","totalInventory":0,"__parentId":"gid://shopify/Collection/1234567890"} + {% endcapture %} + + {% assign bulkOperation = hash %} + {% assign bulkOperation["objects"] = jsonl_string | parse_jsonl %} + {% endif %} + + {% assign collections = bulkOperation.objects | where: "__typename", "Collection" %} + {% assign products = bulkOperation.objects | where: "__typename", "Product" %} + + {% for collection in collections %} + {% assign has_default_template = nil %} + {% assign has_out_of_stock_template = nil %} + {% assign has_a_product_in_stock = nil %} + + {% assign current_template_suffix = collection.templateSuffix %} + + {% comment %} + -- Shopify stores this as *either* an empty string or as nil. Mechanic auto-nils empty strings for task options, so we coerce blanks to nils here. + {% endcomment %} + + {% if current_template_suffix == blank %} + {% assign current_template_suffix = nil %} + {% endif %} + + {% if current_template_suffix == default_collection_template_suffix %} + {% assign has_default_template = true %} + + {% elsif current_template_suffix == out_of_stock_collection_template_suffix %} + {% assign has_out_of_stock_template = true %} + {% endif %} + + {% assign collection_products = products | where: "__parentId", collection.id %} + + {% comment %} + -- break the products loop early when at least one product is in stock + {% endcomment %} + + {% for product in collection_products %} + {% if product.totalInventory > 0 or product.tracksInventory == false %} + {% assign has_a_product_in_stock = true %} + {% break %} + {% endif %} + {% endfor %} + + {% if has_a_product_in_stock %} + {% unless has_default_template %} + {% action "shopify" %} + mutation { + collectionUpdate( + input: { + id: {{ collection.id | json }} + templateSuffix: {{ default_collection_template_suffix | json }} + } + ) { + collection { + id + title + templateSuffix + } + userErrors { + field + message + } + } + } + {% endaction %} + {% endunless %} + + {% else %} + {% unless has_out_of_stock_template %} + {% action "shopify" %} + mutation { + collectionUpdate( + input: { + id: {{ collection.id | json }} + templateSuffix: {{ out_of_stock_collection_template_suffix | json }} + } + ) { + collection { + id + title + templateSuffix + } + userErrors { + field + message + } + } + } + {% endaction %} + {% endunless %} + {% endif %} + {% endfor %} {% endif %} diff --git a/tasks/auto-tag-products-in-a-manual-collection.json b/tasks/auto-tag-products-in-a-manual-collection.json index 45f9d60d..17d26af9 100644 --- a/tasks/auto-tag-products-in-a-manual-collection.json +++ b/tasks/auto-tag-products-in-a-manual-collection.json @@ -9,7 +9,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% assign collection_tag = options.collection_tag__required %}\n\n{% assign collection_qualifies = false %}\n\n{% if event.topic contains \"shopify/collections/\" and collection.id == options.collection_id__number_required %}\n {% assign collection_qualifies = true %}\n\n{% elsif event.topic == \"mechanic/user/trigger\" %}\n {% assign collection_qualifies = true %}\n {% assign collection = shop.collections[options.collection_id__number_required] %}\n{% endif %}\n\n{% if collection_qualifies or event.preview %}\n {% assign collection_product_ids = collection.products | map: \"admin_graphql_api_id\" %}\n\n {% assign cursor = nil %}\n {% assign tagged_product_ids = array %}\n\n {% for n in (0..100) %}\n {% capture query %}\n query {\n products(\n first: 250\n after: {{ cursor | json }}\n query: {{ collection_tag | json | prepend: \"tag:\" | json }}\n ) {\n pageInfo {\n hasNextPage\n }\n edges {\n cursor\n node {\n id\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% assign collection_product_ids = array %}\n {% assign collection_product_ids[0] = \"gid://shopify/Product/1234567890\" %}\n {% assign collection_product_ids[1] = \"gid://shopify/Product/2345678901\" %}\n\n {% capture result_json %}\n {\n \"data\": {\n \"products\": {\n \"edges\": [\n {\n \"node\": {\n \"id\": \"gid://shopify/Product/1234567890\"\n }\n },\n {\n \"node\": {\n \"id\": \"gid://shopify/Product/9876543210\"\n }\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign products_result = result.data.products.edges | map: \"node\" | map: \"id\" %}\n {% assign tagged_product_ids = tagged_product_ids | concat: products_result %}\n\n {% if result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.products.edges.last.cursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% for collection_product_id in collection_product_ids %}\n {% unless tagged_product_ids contains collection_product_id %}\n {% action \"shopify\" %}\n mutation {\n tagsAdd(\n id: {{ collection_product_id | json }}\n tags: {{ collection_tag | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endunless %}\n {% endfor %}\n\n {% for tagged_product_id in tagged_product_ids %}\n {% unless collection_product_ids contains tagged_product_id %}\n {% action \"shopify\" %}\n mutation {\n tagsRemove(\n id: {{ tagged_product_id | json }}\n tags: {{ collection_tag | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endunless %}\n {% endfor %}\n{% endif %}", + "script": "{% assign collection_tag = options.collection_tag__required %}\n{% assign collection_id = options.collection_id__number_required %}\n\n{% assign collection_tag_lower = collection_tag | downcase %}\n\n{% if event.topic == \"shopify/collections/update\" %}\n {% comment %}\n -- on collection update, exit if this is not the configured collection\n {% endcomment %}\n\n {% unless collection.id == collection_id or event.preview %}\n {% break %}\n {% endunless %}\n{% endif %}\n\n{% comment %}\n -- get all of the product IDs currently in the collection\n{% endcomment %}\n\n{% assign cursor = nil %}\n{% assign collection_product_ids = array %}\n\n{% for n in (1..100) %}\n {% capture query %}\n query {\n collection(id: {{ collection_id | prepend: \"gid://shopify/Collection/\" | json }}) {\n products(\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"collection\": {\n \"products\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Product/1234567890\"\n }\n ]\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign collection_product_ids\n = result.data.collection.products.nodes\n | map: \"id\"\n | concat: collection_product_ids\n %}\n\n {% if result.data.collection.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.collection.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n{% endfor %}\n\n{% comment %}\n -- get IDs for all products with collection tag\n{% endcomment %}\n\n{% assign cursor = nil %}\n{% assign tagged_product_ids = array %}\n\n{% for n in (0..100) %}\n {% capture query %}\n query {\n products(\n first: 250\n after: {{ cursor | json }}\n query: {{ collection_tag | json | prepend: \"tag:\" | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n tags\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"products\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Product/2345678901\",\n \"tags\": {{ collection_tag | json }}\n }\n ]\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% comment %}\n -- double check that the collection tag exists on each product to avoid occasional query filter quirkiness\n {% endcomment %}\n\n {% for product in result.data.products.nodes %}\n {% assign product_tags_lower = product.tags | json | downcase | parse_json %}\n\n {% if product_tags_lower contains collection_tag_lower %}\n {% assign tagged_product_ids = tagged_product_ids | push: product.id %}\n {% endif %}\n {% endfor %}\n\n {% if result.data.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n{% endfor %}\n\n{% comment %}\n -- add and remove the collection tag from products\n{% endcomment %}\n\n{% for collection_product_id in collection_product_ids %}\n {% unless tagged_product_ids contains collection_product_id %}\n {% action \"shopify\" %}\n mutation {\n tagsAdd(\n id: {{ collection_product_id | json }}\n tags: {{ collection_tag | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endunless %}\n{% endfor %}\n\n{% for tagged_product_id in tagged_product_ids %}\n {% unless collection_product_ids contains tagged_product_id %}\n {% action \"shopify\" %}\n mutation {\n tagsRemove(\n id: {{ tagged_product_id | json }}\n tags: {{ collection_tag | json }}\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endunless %}\n{% endfor %}\n", "subscriptions": [ "mechanic/user/trigger", "shopify/collections/update" diff --git a/tasks/delete-the-oldest-x-products-from-a-specific-collection.json b/tasks/delete-the-oldest-x-products-from-a-specific-collection.json index 0f633830..b9558989 100644 --- a/tasks/delete-the-oldest-x-products-from-a-specific-collection.json +++ b/tasks/delete-the-oldest-x-products-from-a-specific-collection.json @@ -1,5 +1,5 @@ { - "docs": "Either triggered manually, or configured to run daily, this task will look for the oldest products in the collection of your choice, and delete as many of them as you wish.\n\nThis task requires a collection ID - [learn how to find yours](https://help.usemechanic.com/en/articles/2946120-how-do-i-find-an-id-for-a-product-collection-order-or-something-else).", + "docs": "Either triggered manually, or configured to run daily, this task will look for the oldest products in the collection of your choice, and delete as many of them as you wish.\n\n**IMPORTANT**: When first configuring this task, run it manually once in \"Test mode\" to see a list of which products it would delete. Once verfied, be sure to uncheck this option to have the task make the deletions going forward.\n\n*Notes:*\n- The products will be deleted asycnronously by Shopify, which means they may appear in the products list of admin for a bit after a task run.\n- This task requires a collection ID - [learn how to locate common resource IDs](https://learn.mechanic.dev/techniques/finding-a-resource-id).", "halt_action_run_sequence_on_error": false, "name": "Delete the oldest x products from a specific collection", "online_store_javascript": null, @@ -11,11 +11,11 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% assign collection = shop.collections[options.collection_id__number_required] %}\n\n{% if event.preview != true and collection == nil %}\n {\"error\": {{ options.collection_id__number_required | json | prepend: \"Unable to find collection \" | json }}}\n{% endif %}\n\n{% assign products = collection.products %}\n\n{% if event.preview %}\n {% assign products = array %}\n {% assign products[0] = '{\"id\":12345,\"created_at\":\"2000-01-01\"}' | parse_json %}\n\n {% comment %}\n Uncomment me for more testing!\n {% assign products[1] = '{\"id\":67890,\"created_at\":\"2001-01-01\"}' | parse_json %}\n {% endcomment %}\n{% endif %}\n\n{% assign products_sorted = products | sort: \"created_at\" %}\n\n{% assign product_ids_to_delete = array %}\n{% for product in products_sorted %}\n {% if forloop.index > options.number_of_products_to_delete_at_once__number_required %}\n {% break %}\n {% endif %}\n\n {% assign product_ids_to_delete[product_ids_to_delete.size] = product.id %}\n{% endfor %}\n\n{% if options.test_mode__boolean %}\n {% action \"echo\" %}\n {\"product_ids_to_delete\": {{ product_ids_to_delete | json }}}\n {% endaction %}\n{% else %}\n {% for product_id in product_ids_to_delete %}\n {% action \"shopify\" %}\n [\n \"delete\",\n [\n \"product\",\n {{ product_id | json }}\n ]\n ]\n {% endaction %}\n {% endfor %}\n{% endif %}", + "script": "{% assign collection_id = options.collection_id__number_required %}\n{% assign number_of_products_to_delete_at_once = options.number_of_products_to_delete_at_once__number_required %}\n{% assign test_mode = options.test_mode__boolean %}\n\n{% if number_of_products_to_delete_at_once > 250 %}\n {% error \"This task only supports deleting up to 250 products in a single task run.\" %}\n{% endif %}\n\n{% comment %}\n -- get X oldest products that are in the collection\n{% endcomment %}\n\n{% capture query %}\n query {\n collection(id: {{ collection_id | prepend: \"gid://shopify/Collection/\" | json }}) {\n products(\n first: {{ number_of_products_to_delete_at_once }}\n sortKey: CREATED\n ) {\n nodes {\n id\n title\n createdAt\n }\n }\n }\n }\n{% endcapture %}\n\n{% assign result = query | shopify %}\n\n{% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"collection\": {\n \"products\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"createdAt\": \"2020-02-02T01:01:00Z\"\n }\n ]\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n{% endif %}\n\n{% assign collection = result.data.collection %}\n{% assign products = collection.products.nodes %}\n\n{% if collection == blank %}\n {% error \"Unable to find a collection by the configured ID in this shop.\" %}\n{% endif %}\n\n{% log products_to_delete: products %}\n\n{% if test_mode %}\n {% break %}\n{% endif %}\n\n{% comment %}\n -- delete all returned products asynchronously in case they have many variants and/or media (which could cause an API timeout)\n{% endcomment %}\n\n{% for product in collection.products.nodes %}\n {% action \"shopify\" %}\n mutation {\n productDelete(\n input: {\n id: {{ product.id | json }}\n }\n synchronous: false\n ) {\n productDeleteOperation {\n id\n product {\n legacyResourceId\n title\n }\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n{% endfor %}\n", "subscriptions": [ "mechanic/user/trigger" ], - "subscriptions_template": "mechanic/user/trigger\n\n{% if options.run_daily__boolean %}\n mechanic/scheduler/daily\n{% endif %}", + "subscriptions_template": "mechanic/user/trigger\n{% if options.run_daily__boolean %}\n mechanic/scheduler/daily\n{% endif %}", "tags": [ "Collections", "Delete", diff --git a/tasks/delete-variants-having-a-metafield-date-that-has-passed.json b/tasks/delete-variants-having-a-metafield-date-that-has-passed.json index d25db21f..90ba95c1 100644 --- a/tasks/delete-variants-having-a-metafield-date-that-has-passed.json +++ b/tasks/delete-variants-having-a-metafield-date-that-has-passed.json @@ -9,12 +9,13 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% if event.preview %}\n {\n \"action\": {\n \"type\": \"shopify\",\n \"options\": [\n \"delete\",\n [\"variant\", {{ variant.id | json }}]\n ]\n }\n }\n{% else %}\n {% assign today = \"now\" | date: \"%Y-%m-%d\" %}\n\n {% for product in shop.products.published %}\n {% if product.variants.size == 1 and product.variants[0].title == \"Default Title\" %}\n {% continue %}\n {% endif %}\n\n {% for variant in product.variants %}\n {% assign expiration_date = variant.metafields[options.date_metafield_namespace__required][options.date_metafield_key__required] %}\n {% if expiration_date != blank and expiration_date < today %}\n {\n \"action\": {\n \"type\": \"shopify\",\n \"options\": [\n \"delete\",\n [\"variant\", {{ variant.id | json }}]\n ]\n }\n }\n {% endif %}\n {% endfor %}\n {% endfor %}\n{% endif %}", + "script": "{% assign date_metafield_namespace = options.date_metafield_namespace__required %}\n{% assign date_metafield_key = options.date_metafield_key__required %}\n\n{% assign today = \"now\" | date: \"%Y-%m-%d\" %}\n\n{% if event.topic == \"mechanic/user/trigger\" or event.topic == \"mechanic/scheduler/daily\" %}\n {% comment %}\n -- get all active, published, multi-variant products in the shop\n {% endcomment %}\n\n {% capture bulk_operation_query %}\n query {\n products(\n query: \"status:active published_status:published has_only_default_variant:false\"\n ) {\n edges {\n node {\n __typename\n id\n variants {\n edges {\n node {\n __typename\n id\n metafield(\n namespace: {{ date_metafield_namespace | json }}\n key: {{ date_metafield_key | json }}\n ) {\n value\n }\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% if event.preview %}\n {% capture jsonl_string %}\n {\"__typename\":\"Product\",\"id\":\"gid://shopify/Product/1234567890\"}\n {\"__typename\":\"ProductVariant\",\"id\":\"gid://shopify/ProductVariant/1234567890\",\"metafield\":{\"value\":\"2020-02-02\"},\"__parentId\":\"gid://shopify/Product/1234567890\"}\n {\"__typename\":\"ProductVariant\",\"id\":\"gid://shopify/ProductVariant/2345678901\",\"__parentId\":\"gid://shopify/Product/1234567890\"}\n {% endcapture %}\n\n {% assign bulkOperation = hash %}\n {% assign bulkOperation[\"objects\"] = jsonl_string | parse_jsonl %}\n {% endif %}\n\n {% assign products = bulkOperation.objects | where: \"__typename\", \"Product\" %}\n {% assign bulk_variants = bulkOperation.objects | where: \"__typename\", \"ProductVariant\" %}\n\n {% comment %}\n -- for each product, qualify and delete variants in bulk by metafield value\n {% endcomment %}\n\n {% for product in products %}\n {% assign variants = bulk_variants | where: \"__parentId\", product.id %}\n\n {% assign variant_ids_to_delete = array %}\n\n {% for variant in variants %}\n {% if variant.metafield != blank and variant.metafield.value < today %}\n {% assign variant_ids_to_delete = variant_ids_to_delete | push: variant.id %}\n {% endif %}\n {% endfor %}\n\n {% if variant_ids_to_delete != blank %}\n {% action \"shopify\" %}\n mutation {\n productVariantsBulkDelete(\n productId: {{ product.id | json }}\n variantsIds: {{ variant_ids_to_delete | json }}\n ) {\n product {\n id\n title\n }\n userErrors {\n code\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n{% endif %}\n", "subscriptions": [ "mechanic/user/trigger", - "mechanic/scheduler/daily" + "mechanic/scheduler/daily", + "mechanic/shopify/bulk_operation" ], - "subscriptions_template": "mechanic/user/trigger\nmechanic/scheduler/daily", + "subscriptions_template": "mechanic/user/trigger\nmechanic/scheduler/daily\nmechanic/shopify/bulk_operation", "tags": [ "Delete", "Metafields", diff --git a/tasks/manage-collection-templates-by-product-inventory.json b/tasks/manage-collection-templates-by-product-inventory.json index 3f5cbdf2..df09d87f 100644 --- a/tasks/manage-collection-templates-by-product-inventory.json +++ b/tasks/manage-collection-templates-by-product-inventory.json @@ -1,5 +1,5 @@ { - "docs": "Use this task to automatically switch collections over to an appropriate theme template, when every product in the collection has gone completely out of stock, and to switch them back when inventory arrives.\r\n\r\nThis task can be run manually (using the \"Run task\" button), or be configured to run daily/hourly. If you encounter performance issues when running every time an inventory level is updated, disable this option.", + "docs": "Use this task to automatically switch collections over to an appropriate theme template, when every product in the collection has gone completely out of stock, and to switch them back when inventory arrives.\n\nThis task can be run manually (using the \"Run task\" button), or be configured to run daily/hourly. If you encounter performance issues when running every time an inventory level is updated, disable this option.\n\nNote: For this task, products which do not track inventory in Shopify will be considered as in stock.", "halt_action_run_sequence_on_error": false, "name": "Manage collection templates by product inventory", "online_store_javascript": null, @@ -12,7 +12,7 @@ }, "order_status_javascript": null, "perform_action_runs_in_sequence": false, - "script": "{% if event.topic contains \"shopify/inventory_levels/\" %}\n {% assign collections = inventory_level.variant.product.collections %}\n\n {% if event.preview %}\n {% capture collections_json %}\n [\n {\n \"admin_graphql_api_id\": \"gid://shopify/Collection/1234567890\",\n \"template_suffix\": null,\n \"products\": [\n {\n \"variants\": [\n {\n \"inventory_quantity\": 0\n }\n ]\n }\n ]\n }\n ]\n {% endcapture %}\n\n {% assign collections = collections_json | parse_json %}\n {% endif %}\n\n {% for collection in collections %}\n {% assign current_template_suffix = collection.template_suffix %}\n\n {% comment %}\n Shopify stores this as *either* an empty string or as nil. Mechanic auto-nils\n empty strings for task options, so we coerce blanks to nils here.\n {% endcomment %}\n {% if current_template_suffix == blank %}\n {% assign current_template_suffix = nil %}\n {% endif %}\n\n {% assign total_inventory = 0 %}\n\n {% assign set_template_suffix = false %}\n {% assign new_template_suffix = nil %}\n\n {% for product in collection.products %}\n {% assign product_total_inventory = product.variants | map: \"inventory_quantity\" | sum %}\n {% assign total_inventory = total_inventory | plus: product_total_inventory %}\n {% endfor %}\n\n {% if total_inventory <= 0 %}\n {% if current_template_suffix != options.collection_template_suffix_when_all_products_are_out_of_stock__required %}\n {% assign set_template_suffix = true %}\n {% assign new_template_suffix = options.collection_template_suffix_when_all_products_are_out_of_stock__required %}\n {% endif %}\n {% else %}\n {% if current_template_suffix != options.default_collection_template_suffix %}\n {% assign set_template_suffix = true %}\n {% assign new_template_suffix = options.default_collection_template_suffix %}\n {% endif %}\n {% endif %}\n\n {\n \"log\": {\n \"collection_id\": {{ collection.admin_graphql_api_id | json }},\n \"total_inventory\": {{ total_inventory | json }},\n \"current_template_suffix\": {{ current_template_suffix | json }},\n \"set_template_suffix\": {{ set_template_suffix | json }},\n \"new_template_suffix\": {{ new_template_suffix | json }}\n }\n }\n\n {% if set_template_suffix %}\n {% action \"shopify\" %}\n mutation {\n collectionUpdate(\n input: {\n id: {{ collection.admin_graphql_api_id | json }}\n templateSuffix: {{ new_template_suffix | json }}\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% if event.preview %}\n {% assign collection_object = '{\"__typename\":\"Collection\",\"id\":\"gid://shopify/Collection/1234567890\",\"templateSuffix\":null}' | parse_json %}\n {% assign product_object = '{\"__typename\":\"Product\",\"id\":\"gid://shopify/Product/1234567890\",\"__parentId\":\"gid://shopify/Collection/1234567890\",\"totalInventory\":0}' | parse_json %}\n\n {% assign objects = array %}\n {% assign objects[0] = collection_object %}\n {% assign objects[1] = product_object %}\n\n {% assign bulkOperation = hash %}\n {% assign bulkOperation[\"objects\"] = objects %}\n {% endif %}\n\n {% assign collection_suffixes = hash %}\n {% assign collection_total_inventory = hash %}\n\n {% for object in bulkOperation.objects %}\n {% case object.__typename %}\n {% when \"Collection\" %}\n {% assign collection_suffixes[object.id] = object.templateSuffix %}\n {% when \"Product\" %}\n {% assign collection_total_inventory[object.__parentId] = collection_total_inventory[object.__parentId] | default: 0 | plus: object.totalInventory %}\n {% endcase %}\n {% endfor %}\n\n {% for keyval in collection_total_inventory %}\n {% assign collection_id = keyval[0] %}\n {% assign total_inventory = keyval[1] %}\n {% assign current_template_suffix = collection_suffixes[collection_id] %}\n\n {% comment %}\n Shopify stores this as *either* an empty string or as nil. Mechanic auto-nils\n empty strings for task options, so we coerce blanks to nils here.\n {% endcomment %}\n {% if current_template_suffix == blank %}\n {% assign current_template_suffix = nil %}\n {% endif %}\n\n {% assign set_template_suffix = false %}\n {% assign new_template_suffix = nil %}\n\n {% if total_inventory <= 0 %}\n {% if current_template_suffix != options.collection_template_suffix_when_all_products_are_out_of_stock__required %}\n {% assign set_template_suffix = true %}\n {% assign new_template_suffix = options.collection_template_suffix_when_all_products_are_out_of_stock__required %}\n {% endif %}\n {% else %}\n {% if current_template_suffix != options.default_collection_template_suffix %}\n {% assign set_template_suffix = true %}\n {% assign new_template_suffix = options.default_collection_template_suffix %}\n {% endif %}\n {% endif %}\n\n {\n \"log\": {\n \"collection_id\": {{ collection_id | json }},\n \"total_inventory\": {{ total_inventory | json }},\n \"current_template_suffix\": {{ current_template_suffix | json }},\n \"set_template_suffix\": {{ set_template_suffix | json }},\n \"new_template_suffix\": {{ new_template_suffix | json }}\n }\n }\n\n {% if set_template_suffix %}\n {% action \"shopify\" %}\n mutation {\n collectionUpdate(\n input: {\n id: {{ collection_id | json }}\n templateSuffix: {{ new_template_suffix | json }}\n }\n ) {\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endif %}\n {% endfor %}\n{% else %}\n {% capture bulk_operation_query %}\n query {\n collections {\n edges {\n node {\n __typename\n id\n templateSuffix\n products {\n edges {\n node {\n __typename\n id\n totalInventory\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n{% endif %}", + "script": "{% assign out_of_stock_collection_template_suffix = options.collection_template_suffix_when_all_products_are_out_of_stock__required %}\n{% assign default_collection_template_suffix = options.default_collection_template_suffix %}\n\n{% if event.topic contains \"shopify/inventory_levels/\" %}\n {% comment %}\n -- get this product's total inventory, and up to 250 collections\n {% endcomment %}\n\n {% capture query %}\n query {\n inventoryItem(id: {{ inventory_level.inventory_item_id | prepend: \"gid://shopify/InventoryItem/\" | json }}) {\n variant {\n product {\n id\n totalInventory\n tracksInventory\n collections(first: 250) {\n nodes {\n id\n templateSuffix\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"inventoryItem\": {\n \"variant\": {\n \"product\": {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"totalInventory\": 0,\n \"tracksInventory\": true,\n \"collections\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Collection/1234567890\",\n \"templateSuffix\": {{ default_collection_template_suffix | json }}\n }\n ]\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% assign product = result.data.inventoryItem.variant.product %}\n {% assign collections = product.collections.nodes %}\n\n {% for collection in collections %}\n {% assign has_default_template = nil %}\n {% assign has_out_of_stock_template = nil %}\n {% assign has_a_product_in_stock = nil %}\n\n {% assign current_template_suffix = collection.templateSuffix %}\n\n {% comment %}\n -- Shopify stores this as *either* an empty string or as nil. Mechanic auto-nils empty strings for task options, so we coerce blanks to nils here.\n {% endcomment %}\n\n {% if current_template_suffix == blank %}\n {% assign current_template_suffix = nil %}\n {% endif %}\n\n {% if current_template_suffix == default_collection_template_suffix %}\n {% assign has_default_template = true %}\n\n {% elsif current_template_suffix == out_of_stock_collection_template_suffix %}\n {% assign has_out_of_stock_template = true %}\n {% endif %}\n\n {% comment %}\n -- if the product's stock status matches the paired collection template, then stop processing that collection\n -- NOTE: another product might cause the collection to change templates, but that should be handled in an inventory level event for that product\n {% endcomment %}\n\n {% if product.totalInventory <= 0 and product.tracksInventory %}\n {% if has_out_of_stock_template %}\n {% continue %}\n {% endif %}\n\n {% elsif has_default_template %}\n {% continue %}\n {% endif %}\n\n {% comment %}\n -- query collection for up to 25K products, getting inventory of each\n -- break the query loop early when at least one product is in stock and the collection has the out of stock template\n {% endcomment %}\n\n {% assign cursor = nil %}\n\n {% for n in (1..100) %}\n {% capture query %}\n query {\n collection(id: {{ collection.id | json }}) {\n products(\n first: 250\n after: {{ cursor | json }}\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n totalInventory\n tracksInventory\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if event.preview %}\n {% capture result_json %}\n {\n \"data\": {\n \"collection\": {\n \"products\": {\n \"nodes\": [\n {\n \"id\": \"gid://shopify/Product/1234567890\",\n \"totalInventory\": 0,\n \"tracksInventory\": true\n }\n ]\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = result_json | parse_json %}\n {% endif %}\n\n {% for product in result.data.collection.products.nodes %}\n {% if product.totalInventory > 0 or product.tracksInventory == false %}\n {% assign has_a_product_in_stock = true %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% if has_a_product_in_stock %}\n {% break %}\n {% endif %}\n\n {% if result.data.collection.products.pageInfo.hasNextPage %}\n {% assign cursor = result.data.collection.products.pageInfo.endCursor %}\n {% else %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% if has_a_product_in_stock %}\n {% unless has_default_template %}\n {% action \"shopify\" %}\n mutation {\n collectionUpdate(\n input: {\n id: {{ collection.id | json }}\n templateSuffix: {{ default_collection_template_suffix | json }}\n }\n ) {\n collection {\n id\n title\n templateSuffix\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endunless %}\n\n {% else %}\n {% unless has_out_of_stock_template %}\n {% action \"shopify\" %}\n mutation {\n collectionUpdate(\n input: {\n id: {{ collection.id | json }}\n templateSuffix: {{ out_of_stock_collection_template_suffix | json }}\n }\n ) {\n collection {\n id\n title\n templateSuffix\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endunless %}\n {% endif %}\n {% endfor %}\n\n{% elsif event.topic == \"mechanic/user/trigger\" or event.topic contains \"mechanic/scheduler/\" %}\n {% capture bulk_operation_query %}\n query {\n collections {\n edges {\n node {\n __typename\n id\n templateSuffix\n products {\n edges {\n node {\n __typename\n id\n totalInventory\n tracksInventory\n }\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% action \"shopify\" %}\n mutation {\n bulkOperationRunQuery(\n query: {{ bulk_operation_query | json }}\n ) {\n bulkOperation {\n id\n status\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n\n{% elsif event.topic == \"mechanic/shopify/bulk_operation\" %}\n {% if event.preview %}\n {% capture jsonl_string %}\n {\"__typename\":\"Collection\",\"id\":\"gid://shopify/Collection/1234567890\",\"templateSuffix\":{{ default_collection_template_suffix | json }}}\n {\"__typename\":\"Product\",\"id\":\"gid://shopify/Product/1234567890\",\"totalInventory\":0,\"__parentId\":\"gid://shopify/Collection/1234567890\"}\n {\"__typename\":\"Product\",\"id\":\"gid://shopify/Product/2345678901\",\"totalInventory\":0,\"__parentId\":\"gid://shopify/Collection/1234567890\"}\n {% endcapture %}\n\n {% assign bulkOperation = hash %}\n {% assign bulkOperation[\"objects\"] = jsonl_string | parse_jsonl %}\n {% endif %}\n\n {% assign collections = bulkOperation.objects | where: \"__typename\", \"Collection\" %}\n {% assign products = bulkOperation.objects | where: \"__typename\", \"Product\" %}\n\n {% for collection in collections %}\n {% assign has_default_template = nil %}\n {% assign has_out_of_stock_template = nil %}\n {% assign has_a_product_in_stock = nil %}\n\n {% assign current_template_suffix = collection.templateSuffix %}\n\n {% comment %}\n -- Shopify stores this as *either* an empty string or as nil. Mechanic auto-nils empty strings for task options, so we coerce blanks to nils here.\n {% endcomment %}\n\n {% if current_template_suffix == blank %}\n {% assign current_template_suffix = nil %}\n {% endif %}\n\n {% if current_template_suffix == default_collection_template_suffix %}\n {% assign has_default_template = true %}\n\n {% elsif current_template_suffix == out_of_stock_collection_template_suffix %}\n {% assign has_out_of_stock_template = true %}\n {% endif %}\n\n {% assign collection_products = products | where: \"__parentId\", collection.id %}\n\n {% comment %}\n -- break the products loop early when at least one product is in stock\n {% endcomment %}\n\n {% for product in collection_products %}\n {% if product.totalInventory > 0 or product.tracksInventory == false %}\n {% assign has_a_product_in_stock = true %}\n {% break %}\n {% endif %}\n {% endfor %}\n\n {% if has_a_product_in_stock %}\n {% unless has_default_template %}\n {% action \"shopify\" %}\n mutation {\n collectionUpdate(\n input: {\n id: {{ collection.id | json }}\n templateSuffix: {{ default_collection_template_suffix | json }}\n }\n ) {\n collection {\n id\n title\n templateSuffix\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endunless %}\n\n {% else %}\n {% unless has_out_of_stock_template %}\n {% action \"shopify\" %}\n mutation {\n collectionUpdate(\n input: {\n id: {{ collection.id | json }}\n templateSuffix: {{ out_of_stock_collection_template_suffix | json }}\n }\n ) {\n collection {\n id\n title\n templateSuffix\n }\n userErrors {\n field\n message\n }\n }\n }\n {% endaction %}\n {% endunless %}\n {% endif %}\n {% endfor %}\n{% endif %}\n", "subscriptions": [ "shopify/inventory_levels/update", "mechanic/user/trigger",