Skip to content

Commit

Permalink
new task: fire custom event or product/variant change (#300)
Browse files Browse the repository at this point in the history
  • Loading branch information
tekhaus authored Nov 21, 2023
1 parent a31b919 commit 86beaba
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ This directory is built automatically. Each task's documentation is generated fr
* [Demonstration: Order editing](./demonstration-order-editing)
* [Demonstration: Performing action runs in sequence](./demonstration-performing-action-runs-in-sequence)
* [Demonstration: Shopify Flow integration](./demonstration-shopify-flow-integration)
* [Demonstration: Trigger a custom event for specific product or variant changes](./demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes)
* [Demonstration: Web fonts in PDFs](./demonstration-web-fonts-in-pdfs)
* [Demonstration: Write to a customer metafield](./demonstration-write-to-a-customer-metafield)
* [Download and email a file to a customer, when purchased](./download-and-email-a-file-to-a-customer-when-purchased)
Expand Down Expand Up @@ -745,6 +746,7 @@ This directory is built automatically. Each task's documentation is generated fr
* [Demonstration: Order editing](./demonstration-order-editing)
* [Demonstration: Performing action runs in sequence](./demonstration-performing-action-runs-in-sequence)
* [Demonstration: Shopify Flow integration](./demonstration-shopify-flow-integration)
* [Demonstration: Trigger a custom event for specific product or variant changes](./demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes)
* [Demonstration: Web fonts in PDFs](./demonstration-web-fonts-in-pdfs)
* [Demonstration: Write to a customer metafield](./demonstration-write-to-a-customer-metafield)

Expand Down Expand Up @@ -1342,6 +1344,7 @@ This directory is built automatically. Each task's documentation is generated fr
* [Delete non-purchased Neon Sign Customiser app generated products older than X days](./delete-non-purchased-neon-sign-customiser-generated-products-older-than-x-days)
* [Delete product or product variant metafields in bulk](./delete-product-or-product-variant-metafields-in-bulk)
* [Delete the oldest x products from a specific collection](./delete-the-oldest-x-products-from-a-specific-collection)
* [Demonstration: Trigger a custom event for specific product or variant changes](./demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes)
* [Email a CSV export of products](./email-a-csv-export-of-products)
* [Email a summary of all products and quantities ordered](./email-a-summary-of-all-products-and-quantities-ordered)
* [Email customers after purchasing a certain product](./product-order-email)
Expand Down Expand Up @@ -1731,6 +1734,7 @@ This directory is built automatically. Each task's documentation is generated fr
* [Add Option Name as a Variant Metafield for In Stock Variants](./add-option-names-as-variant-metafields-for-in-stock-variants)
* [Auto-associate variants with a delivery profile, by metafield value](./auto-associate-variants-with-a-delivery-profile-by-metafield-value)
* [Auto-tag products by largest available size](./auto-tag-products-by-largest-available-size)
* [Demonstration: Trigger a custom event for specific product or variant changes](./demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes)
* [Keep variant inventory in sync across locations](./keep-variant-inventory-in-sync-across-locations)
* [Maintain discount percentage filters in variant metafields](./maintain-discount-percentage-filters-in-variant-metafields)
* [Send an email when a product's price goes below its cost](./send-an-email-when-a-products-price-goes-below-its-cost)
Expand Down Expand Up @@ -1763,6 +1767,7 @@ This directory is built automatically. Each task's documentation is generated fr
* [Auto-tag orders when another tag is added](./auto-tag-orders-when-another-tag-is-added)
* [Auto-tag products when another tag is added](./auto-tag-products-when-another-tag-is-added)
* [Auto-update inventory policy based on a "preorder" tag](./auto-update-inventory-policy-based-on-a-preorder-tag)
* [Demonstration: Trigger a custom event for specific product or variant changes](./demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes)
* [Email customers when tagged](./email-customers-when-tagged)
* [Email customers when their order is tagged](./email-customer-when-order-tagged)
* [Email vendors when an order is tagged](./email-vendors-when-an-order-is-tagged)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Demonstration: Trigger a custom event for specific product or variant changes

Tags: Demonstration, Products, Variants, Watch

This demonstration task responds to product updates, by firing a custom event whenever it finds that any of the configured product and variant attributes have changed from a cached snapshot. This is useful for creating custom tasks without having to continually build the scaffolding of a specific attribute monitor in each of them.

* View in the task library: [tasks.mechanic.dev/demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes](https://tasks.mechanic.dev/demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes)
* Task JSON, for direct import: [task.json](../../tasks/demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes.json)
* Preview task code: [script.liquid](./script.liquid)

## Default options

```json
{
"custom_event_topic__required": "user/product/update_monitor",
"product_attributes_to_monitor__array": [
"title"
],
"variant_attributes_to_monitor__array": [
"price",
"sku"
]
}
```

[Learn about task options in Mechanic](https://learn.mechanic.dev/core/tasks/options)

## Subscriptions

```liquid
shopify/products/update
mechanic/user/trigger
shopify/products/create
```

[Learn about event subscriptions in Mechanic](https://learn.mechanic.dev/core/tasks/subscriptions)

## Documentation

This demonstration task responds to product updates, by firing a custom event whenever it finds that any of the configured product and variant attributes have changed from a cached snapshot. This is useful for creating custom tasks without having to continually build the scaffolding of a specific attribute monitor in each of them.

Configure it with your custom event topic and a combination of product and/or variant attributes as desired. This task uses the GraphQL representation of the [Product](https://shopify.dev/docs/api/admin-graphql/latest/objects/Product) and [Variant](https://shopify.dev/docs/api/admin-graphql/latest/objects/ProductVariant) resources, and will only be able to interpet fields that return a single value at those two resource levels. This means you will not be able to enter connections to other resources (e.g. metafields) or fields that have subfields (e.g. featuredImage).

This demonstration task has been preconfigured with a custom event topic of **user/product/update_monitor**, **title** as the product attribute to monitor, and **price** and **sku** as the variant attributes to monitor.

To build custom tasks to respond to the custom events generated by this task, make sure to subscribe to the custom event topic that has been configured in this task. When firing the custom event, the *current* product resource will be sent along as the event data. This will include all of the configured attributes to monitor, but no other fields besides the IDs.

**Important Notes:**
- This task will **not** send along the previous product resource values, as the cached snapshot is hashed for comparison, without explicitly storing the field values themselves.
- The GraphQL resource field names entered in as attributes to monitor must match the Shopify documentation exactly (case-sensitive).
- After changing any of the attributes to monitor in the config of this task, run the task manually to have it cache the snapshot value for all products in the shop.
- The task has an arbitraty limit of 25k products that it will paginate through. If your shop has more than that, or a very large amount of variants (and you are tracking changes to them), then consider converting this task to use [bulk operations](https://learn.mechanic.dev/core/shopify/read/bulk-operations) for the manual runs instead.

## Installing this task

Find this task [in the library at tasks.mechanic.dev](https://tasks.mechanic.dev/demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes), and use the "Try this task" button. Or, import [this task's JSON export](../../tasks/demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes.json) – see [Importing and exporting tasks](https://learn.mechanic.dev/core/tasks/import-and-export) to learn how imports work.

## Contributions

Found a bug? Got an improvement to add? Start here: [../../CONTRIBUTING.md](../../CONTRIBUTING.md).

## Task requests

Submit your [task requests](https://mechanic.canny.io/task-requests) for consideration by the Mechanic community, and they may be chosen for development and inclusion in the [task library](https://tasks.mechanic.dev/)!
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
{% assign custom_event_topic = options.custom_event_topic__required %}
{% assign product_attributes = options.product_attributes_to_monitor__array %}
{% assign variant_attributes = options.variant_attributes_to_monitor__array %}

{% comment %}
-- chck to see if the custom event topic is in the right form, and that at least one attribute is configured to be monitored
{% endcomment %}

{% assign custom_event_topic_parts = custom_event_topic | split: "/" %}

{% if custom_event_topic_parts[0] != "user" %}
{% error "The custom event topic must start with 'user/'" %}
{% endif %}

{% if custom_event_topic_parts.size != 3 %}
{% error "The custom event topic should be in the form of 'user/[subject]/[verb]'" %}
{% endif %}

{% if product_attributes == blank and variant_attributes == blank %}
{% error "Enter at least one product attribute or variant attribute to monitor" %}
{% endif %}

{% comment %}
-- sort the configured attributes, join them together in a string, and then hash them to use as part of the cache key
{% endcomment %}

{% assign sorted_product_attributes = product_attributes | sort %}
{% assign sorted_variant_attributes = variant_attributes | sort %}
{% assign cache_snapshot_id
= sorted_product_attributes
| concat: sorted_variant_attributes
| join: ""
| sha256
%}

{% log
config: "for this task run",
custom_event_topic: custom_event_topic,
product_attributes_to_monitor: sorted_product_attributes,
variant_attributes_to_monitor: sorted_variant_attributes,
cache_snapshot_id: cache_snapshot_id
%}

{% assign products = array %}

{% if event.topic == "shopify/products/update" or event.topic == "shopify/products/create" %}
{% comment %}
-- get product data from GraphLQ instead of the product webhook/REST
{% endcomment %}

{% capture query %}
query {
product(id: {{ product.admin_graphql_api_id | json }}) {
id
legacyResourceId
{{ product_attributes | join: newline }}
{% if variant_attributes != blank -%}
variants(first: 100) {
nodes {
id
{{ variant_attributes | join: newline }}
}
}
{%- endif %}
}
}
{% endcapture %}

{% assign result = query | shopify %}

{% comment %}
-- remove the variants / nodes structure from the product if it exists
{% endcomment %}

{% assign products[0]
= result.data.product
| default: hash
| except: "variants"
%}

{% comment %}
-- if variant attributes are being monitored, then reattach the array at the product level (i.e. no nodes)
{% endcomment %}

{% if variant_attributes != blank %}
{% assign products[0]["variants"] = result.data.product.variants.nodes | default: array %}
{% endif %}

{% elsif event.topic == "mechanic/user/trigger" %}
{% comment %}
-- get all products in the shop first; if being monitored, variants are queried by product later to avoid exceeding query cost limit
{% endcomment %}

{% assign cursor = nil %}

{% for n in (1..100) %}
{% capture query %}
query {
products(
first: 250
after: {{ cursor | json }}
) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
legacyResourceId
{{ product_attributes | join: newline }}
}
}
}
{% endcapture %}

{% assign result = query | shopify %}

{% assign products_batch = result.data.products.nodes %}

{% if variant_attributes == blank %}
{% comment %}
-- if there are no variant attribute lookups configured, then just concatenate the products from this result
{% endcomment %}

{% assign products = products | concat: products_batch %}

{% else %}
{% comment %}
-- loop through the products in this batch and query the variant attributes for each
{% endcomment %}

{% for product in products_batch %}
{% capture variants_query %}
query {
product(id: {{ product.id | json }}) {
variants(first: 100) {
nodes {
id
{{ variant_attributes | join: newline }}
}
}
}
}
{% endcapture %}

{% assign variants_result = variants_query | shopify %}

{% assign product_data = product %}
{% assign product_data["variants"] = variants_result.data.product.variants.nodes %}

{% assign products = products | push: product_data %}
{% endfor %}
{% endif %}

{% if result.data.products.pageInfo.hasNextPage %}
{% assign cursor = result.data.products.pageInfo.endCursor %}
{% else %}
{% break %}
{% endif %}
{% endfor %}
{% endif %}

{% comment %}
-- use a simple product for event previews, since we don't know what attributes will be configured
{% endcomment %}

{% if event.preview %}
{% assign products[0] = hash %}
{% assign products[0]["legacyResourceId"] = "1234567890" %}
{% endif %}

{% comment %}
-- loop through the products(s) queried above, to check for updates to the configured attributes
{% endcomment %}

{% for product in products %}
{% comment %}
-- generate the product specific cache key in combination with the configured attributes
{% endcomment %}

{% assign cache_key
= "product_"
| append: product.legacyResourceId
| append: "_snapshot_"
| append: cache_snapshot_id
%}

{% assign cache_value = cache[cache_key] %}
{% assign expected_cache_value = product | json | sha256 %}

{% comment %}
-- check if a cache value exists and is the same as the expected value; if so, move to next product
{% endcomment %}

{% if cache_value == expected_cache_value %}
{% log
message: "Product has no changes to the configured attributes.",
product_id: product.id
%}
{% continue %}
{% endif %}

{% comment %}
-- fire the configured custom event if a different cache value exists
{% endcomment %}

{% if cache_value != blank or event.preview %}
{% action "event" %}
{
"topic": {{ custom_event_topic | json }},
"data": {{ product | json }}
}
{% endaction %}

{% else %}
{% log
message: "Product does not have a cached snapshot for this combination of product and/or variant attributes; setting it now, but will not trigger the custom event on this task run.",
product: product
%}
{% endif %}

{% comment %}
-- save a hash of the product data in the cache with default expiration of 60 days
{% endcomment %}

{% action "cache", "set", cache_key, expected_cache_value %}
{% endfor %}
Loading

0 comments on commit 86beaba

Please sign in to comment.