Skip to content

Commit fb8d8f0

Browse files
authored
new demo task: paginated external api (#466)
1 parent 41e6f37 commit fb8d8f0

File tree

4 files changed

+255
-0
lines changed

4 files changed

+255
-0
lines changed

docs/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ This directory is built automatically. Each task's documentation is generated fr
194194
* [Demonstration: Generating a file and uploading it to Shopify](./demonstration-generate-a-file-and-upload-to-shopify)
195195
* [Demonstration: Order editing](./demonstration-order-editing)
196196
* [Demonstration: Performing action runs in sequence](./demonstration-performing-action-runs-in-sequence)
197+
* [Demonstration: Query external paginated API](./demonstration-query-external-paginated-api)
197198
* [Demonstration: Shopify Flow integration](./demonstration-shopify-flow-integration)
198199
* [Demonstration: Trigger a custom event for specific product or variant changes](./demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes)
199200
* [Demonstration: Upload files to Google Drive](./demonstration-upload-files-to-google-drive)
@@ -766,6 +767,7 @@ This directory is built automatically. Each task's documentation is generated fr
766767
* [Demonstration: Generating a file and uploading it to Shopify](./demonstration-generate-a-file-and-upload-to-shopify)
767768
* [Demonstration: Order editing](./demonstration-order-editing)
768769
* [Demonstration: Performing action runs in sequence](./demonstration-performing-action-runs-in-sequence)
770+
* [Demonstration: Query external paginated API](./demonstration-query-external-paginated-api)
769771
* [Demonstration: Shopify Flow integration](./demonstration-shopify-flow-integration)
770772
* [Demonstration: Trigger a custom event for specific product or variant changes](./demonstration-trigger-a-custom-event-for-specific-product-or-variant-changes)
771773
* [Demonstration: Upload files to Google Drive](./demonstration-upload-files-to-google-drive)
@@ -887,6 +889,7 @@ This directory is built automatically. Each task's documentation is generated fr
887889

888890
* [Add new customers to GetResponse](./add-new-customers-to-getresponse)
889891
* [Demonstration: Fetch an external configuration file](./demonstration-fetch-an-external-configuration-file)
892+
* [Demonstration: Query external paginated API](./demonstration-query-external-paginated-api)
890893
* [Import PayPal transactions as Shopify orders](./import-paypal-transactions-as-shopify-orders)
891894
* [Report Toaster: Pirate Ship Integration](./report-toaster-pirateship-integration)
892895
* [Report Toaster: ShipStation Integration](./report-toaster-shipstation-integration)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Demonstration: Query external paginated API
2+
3+
Tags: Demonstration, External API
4+
5+
This task shows how to call an external API using the HTTP action type to first get a limited page of products, and then paginate through the responses until all of the products are fetched.
6+
7+
* View in the task library: [tasks.mechanic.dev/demonstration-query-external-paginated-api](https://tasks.mechanic.dev/demonstration-query-external-paginated-api)
8+
* Task JSON, for direct import: [task.json](../../tasks/demonstration-query-external-paginated-api.json)
9+
* Preview task code: [script.liquid](./script.liquid)
10+
11+
## Subscriptions
12+
13+
```liquid
14+
mechanic/user/trigger
15+
mechanic/actions/perform
16+
user/demo_paginated_api/fetched_products
17+
```
18+
19+
[Learn about event subscriptions in Mechanic](https://learn.mechanic.dev/core/tasks/subscriptions)
20+
21+
## Documentation
22+
23+
This task shows how to call an external API using the HTTP action type to first get a limited page of products, and then paginate through the responses until all of the products are fetched.
24+
25+
For this demonstration, the free [DummyJSON](https://dummyjson.com/) products resource will be used, which has a pagination method similar to many public REST APIs.
26+
27+
The ```mechanic/actions/perform``` event contains the results from the API. A decision is made based on the pagination data in the results on whether to make an additional API call. For each additional call, an array of fetched products is passed as [meta](https://learn.mechanic.dev/core/tasks/code/action-objects#meta) so they may all be processed at the end of the paginated calls.
28+
29+
**NOTE: Reading the developer docs for an API is a must to understanding how its specific pagination works.**
30+
31+
## Installing this task
32+
33+
Find this task [in the library at tasks.mechanic.dev](https://tasks.mechanic.dev/demonstration-query-external-paginated-api), and use the "Try this task" button. Or, import [this task's JSON export](../../tasks/demonstration-query-external-paginated-api.json) – see [Importing and exporting tasks](https://learn.mechanic.dev/core/tasks/import-and-export) to learn how imports work.
34+
35+
## Contributions
36+
37+
Found a bug? Got an improvement to add? Start here: [../../CONTRIBUTING.md](../../CONTRIBUTING.md).
38+
39+
## Task requests
40+
41+
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 numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
{% comment %}
2+
-- using the DummyJSON service for this demonstration, with dummy data for 194 products
3+
-- using a page limit of 50 should result in 4 calls to the API to get all of the products
4+
{% endcomment %}
5+
6+
{% assign api_endpoint = "https://dummyjson.com/products" %}
7+
{% assign limit = 50 %}
8+
9+
{% if event.topic == "mechanic/user/trigger" %}
10+
{% comment %}
11+
-- use query parameters to limit the results and only return the id (default), sku, and stock
12+
{% endcomment %}
13+
14+
{%- capture api_endpoint_with_parameters -%}
15+
{{ api_endpoint }}?select=sku,stock&limit={{ limit }}
16+
{%- endcapture -%}
17+
18+
{% comment %}
19+
-- make the first http get call to the external api
20+
-- note: on this first call we can use the standard http action format since we don't yet need to pass meta values (e.g. products)
21+
{% endcomment %}
22+
23+
{% action "http" %}
24+
{
25+
"method": "get",
26+
"url": {{ api_endpoint_with_parameters | json }},
27+
"verify": "true",
28+
"error_on_5xx": "true"
29+
}
30+
{% endaction %}
31+
32+
{% elsif event.topic == "mechanic/actions/perform" %}
33+
{% comment %}
34+
-- create preview stub data to simulate results that indicate there is more data to query
35+
{% endcomment %}
36+
37+
{% if event.preview %}
38+
{% capture action_json %}
39+
{
40+
"type": "http",
41+
"meta": null,
42+
"run": {
43+
"ok": true,
44+
"result": {
45+
"status": 200,
46+
"body": {
47+
"products": [
48+
{
49+
"id": 1,
50+
"sku": "RCH45Q1A",
51+
"stock": 5
52+
}
53+
],
54+
"total": 194,
55+
"skip": 0,
56+
"limit": 50
57+
}
58+
}
59+
}
60+
}
61+
{% endcapture %}
62+
63+
{% assign action = action_json | parse_json %}
64+
{% endif %}
65+
66+
{% comment %}
67+
-- for demonstration purposes, only process http actions that ran successfully
68+
-- typically, some error checking would be done here on the status code to see if a different action should be taken (e.g. 429 - Too many requests)
69+
{% endcomment %}
70+
71+
{% unless action.type == "http" and action.run.ok and action.run.result.status == 200 %}
72+
{% break %}
73+
{% endunless %}
74+
75+
{% comment %}
76+
-- assign api results to variables
77+
{% endcomment %}
78+
79+
{% assign result_products = action.run.result.body.products %}
80+
{% assign result_total = action.run.result.body.total %}
81+
{% assign result_skip = action.run.result.body.skip %}
82+
{% assign result_limit = action.run.result.body.limit %}
83+
84+
{% comment %}
85+
-- get products passed in meta if this is not the first call, otherwise default to an empty array
86+
{% endcomment %}
87+
88+
{% assign fetched_products = action.meta.fetched_products | default: array %}
89+
90+
{% comment %}
91+
-- concatenate the new results to the fetched products array
92+
{% endcomment %}
93+
94+
{% assign fetched_products = fetched_products | concat: result_products %}
95+
96+
{% comment %}
97+
-- check to see if another API call should be made
98+
-- the DummyJSON API uses limit based pagination with a skip parameter, and the limit returned will indicate the current size
99+
-- this means the total objects retrieved so far should be the sum of the returned skip and limit values
100+
{% endcomment %}
101+
102+
{% assign count_products_retrieved = result_skip | plus: result_limit %}
103+
104+
{% if count_products_retrieved < result_total %}
105+
{% comment %}
106+
-- we do not yet have the full set of products; log out how many have been retrieved and make the next api call
107+
-- note: when accessing an api with low rate limiting, then you may want to use a delay strategy [see tutorial for this demo task for more detail]
108+
{% endcomment %}
109+
110+
{% log
111+
count_products_retrieved: count_products_retrieved,
112+
total_products_expected: result_total
113+
%}
114+
115+
{%- capture api_endpoint_with_parameters -%}
116+
{{ api_endpoint }}?select=sku,stock&limit={{ limit }}&skip={{ count_products_retrieved }}
117+
{%- endcapture -%}
118+
119+
{% comment %}
120+
-- in order to pass the array of products retrieved so far, we need to use the http action format that supports passing meta values
121+
{% endcomment %}
122+
123+
{% action %}
124+
{
125+
"type": "http",
126+
"options": {
127+
"method": "get",
128+
"url": {{ api_endpoint_with_parameters | json }},
129+
"verify": "true",
130+
"error_on_5xx": "true"
131+
},
132+
"meta": {
133+
"fetched_products": {{ fetched_products | json }}
134+
}
135+
}
136+
{% endaction %}
137+
138+
{% break %}
139+
{% endif %}
140+
141+
{% comment %}
142+
-- we've reached the end of the api results, pass the fetched products array to a custom event for additional processing
143+
-- note: the products could be processed here, but using a custom event is a useful indicator in the event logs that the task has finished fetching products
144+
{% endcomment %}
145+
146+
{% action "event" %}
147+
{
148+
"topic": "user/demo_paginated_api/fetched_products",
149+
"task_id": {{ task.id | json }},
150+
"data": {
151+
"fetched_products":{{ fetched_products | json }}
152+
}
153+
}
154+
{% endaction %}
155+
156+
{% elsif event.topic == "user/demo_paginated_api/fetched_products" %}
157+
{% comment %}
158+
-- for this demo, just log out the fetched products
159+
{% endcomment %}
160+
161+
{% assign fetched_products = event.data.fetched_products %}
162+
163+
{% log %}
164+
"Fetched {{ fetched_products.size }} products from the external API."
165+
{% endlog %}
166+
167+
{% log fetched_products: fetched_products %}
168+
169+
{% log "Fetched products would be processed here..." %}
170+
{% endif %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"docs": "This task shows how to call an external API using the HTTP action type to first get a limited page of products, and then paginate through the responses until all of the products are fetched.\n\nFor this demonstration, the free [DummyJSON](https://dummyjson.com/) products resource will be used, which has a pagination method similar to many public REST APIs.\n\nThe ```mechanic/actions/perform``` event contains the results from the API. A decision is made based on the pagination data in the results on whether to make an additional API call. For each additional call, an array of fetched products is passed as [meta](https://learn.mechanic.dev/core/tasks/code/action-objects#meta) so they may all be processed at the end of the paginated calls.\n\n**NOTE: Reading the developer docs for an API is a must to understanding how its specific pagination works.**",
3+
"halt_action_run_sequence_on_error": false,
4+
"name": "Demonstration: Query external paginated API",
5+
"online_store_javascript": null,
6+
"options": {},
7+
"perform_action_runs_in_sequence": false,
8+
"preview_event_definitions": [
9+
{
10+
"description": "All fetched products",
11+
"event_attributes": {
12+
"topic": "user/demo_paginated_api/fetched_products",
13+
"data": {
14+
"fetched_products": [
15+
{
16+
"id": 1,
17+
"sku": "RCH45Q1A",
18+
"stock": 5
19+
},
20+
{
21+
"id": 2,
22+
"sku": "MVCFH27F",
23+
"stock": 44
24+
}
25+
]
26+
}
27+
}
28+
}
29+
],
30+
"script": "{% comment %}\n -- using the DummyJSON service for this demonstration, with dummy data for 194 products\n -- using a page limit of 50 should result in 4 calls to the API to get all of the products\n{% endcomment %}\n\n{% assign api_endpoint = \"https://dummyjson.com/products\" %}\n{% assign limit = 50 %}\n\n{% if event.topic == \"mechanic/user/trigger\" %}\n {% comment %}\n -- use query parameters to limit the results and only return the id (default), sku, and stock\n {% endcomment %}\n\n {%- capture api_endpoint_with_parameters -%}\n {{ api_endpoint }}?select=sku,stock&limit={{ limit }}\n {%- endcapture -%}\n\n {% comment %}\n -- make the first http get call to the external api\n -- note: on this first call we can use the standard http action format since we don't yet need to pass meta values (e.g. products)\n {% endcomment %}\n\n {% action \"http\" %}\n {\n \"method\": \"get\",\n \"url\": {{ api_endpoint_with_parameters | json }},\n \"verify\": \"true\",\n \"error_on_5xx\": \"true\"\n }\n {% endaction %}\n\n{% elsif event.topic == \"mechanic/actions/perform\" %}\n {% comment %}\n -- create preview stub data to simulate results that indicate there is more data to query\n {% endcomment %}\n\n {% if event.preview %}\n {% capture action_json %}\n {\n \"type\": \"http\",\n \"meta\": null,\n \"run\": {\n \"ok\": true,\n \"result\": {\n \"status\": 200,\n \"body\": {\n \"products\": [\n {\n \"id\": 1,\n \"sku\": \"RCH45Q1A\",\n \"stock\": 5\n }\n ],\n \"total\": 194,\n \"skip\": 0,\n \"limit\": 50\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign action = action_json | parse_json %}\n {% endif %}\n\n {% comment %}\n -- for demonstration purposes, only process http actions that ran successfully\n -- typically, some error checking would be done here on the status code to see if a different action should be taken (e.g. 429 - Too many requests)\n {% endcomment %}\n\n {% unless action.type == \"http\" and action.run.ok and action.run.result.status == 200 %}\n {% break %}\n {% endunless %}\n\n {% comment %}\n -- assign api results to variables\n {% endcomment %}\n\n {% assign result_products = action.run.result.body.products %}\n {% assign result_total = action.run.result.body.total %}\n {% assign result_skip = action.run.result.body.skip %}\n {% assign result_limit = action.run.result.body.limit %}\n\n {% comment %}\n -- get products passed in meta if this is not the first call, otherwise default to an empty array\n {% endcomment %}\n\n {% assign fetched_products = action.meta.fetched_products | default: array %}\n\n {% comment %}\n -- concatenate the new results to the fetched products array\n {% endcomment %}\n\n {% assign fetched_products = fetched_products | concat: result_products %}\n\n {% comment %}\n -- check to see if another API call should be made\n -- the DummyJSON API uses limit based pagination with a skip parameter, and the limit returned will indicate the current size\n -- this means the total objects retrieved so far should be the sum of the returned skip and limit values\n {% endcomment %}\n\n {% assign count_products_retrieved = result_skip | plus: result_limit %}\n\n {% if count_products_retrieved < result_total %}\n {% comment %}\n -- we do not yet have the full set of products; log out how many have been retrieved and make the next api call\n -- note: when accessing an api with low rate limiting, then you may want to use a delay strategy [see tutorial for this demo task for more detail]\n {% endcomment %}\n\n {% log\n count_products_retrieved: count_products_retrieved,\n total_products_expected: result_total\n %}\n\n {%- capture api_endpoint_with_parameters -%}\n {{ api_endpoint }}?select=sku,stock&limit={{ limit }}&skip={{ count_products_retrieved }}\n {%- endcapture -%}\n\n {% comment %}\n -- in order to pass the array of products retrieved so far, we need to use the http action format that supports passing meta values\n {% endcomment %}\n\n {% action %}\n {\n \"type\": \"http\",\n \"options\": {\n \"method\": \"get\",\n \"url\": {{ api_endpoint_with_parameters | json }},\n \"verify\": \"true\",\n \"error_on_5xx\": \"true\"\n },\n \"meta\": {\n \"fetched_products\": {{ fetched_products | json }}\n }\n }\n {% endaction %}\n\n {% break %}\n {% endif %}\n\n {% comment %}\n -- we've reached the end of the api results, pass the fetched products array to a custom event for additional processing\n -- note: the products could be processed here, but using a custom event is a useful indicator in the event logs that the task has finished fetching products\n {% endcomment %}\n\n {% action \"event\" %}\n {\n \"topic\": \"user/demo_paginated_api/fetched_products\",\n \"task_id\": {{ task.id | json }},\n \"data\": {\n \"fetched_products\":{{ fetched_products | json }}\n }\n }\n {% endaction %}\n\n{% elsif event.topic == \"user/demo_paginated_api/fetched_products\" %}\n {% comment %}\n -- for this demo, just log out the fetched products\n {% endcomment %}\n\n {% assign fetched_products = event.data.fetched_products %}\n\n {% log %}\n \"Fetched {{ fetched_products.size }} products from the external API.\"\n {% endlog %}\n\n {% log fetched_products: fetched_products %}\n\n {% log \"Fetched products would be processed here...\" %}\n{% endif %}\n",
31+
"subscriptions": [
32+
"mechanic/user/trigger",
33+
"mechanic/actions/perform",
34+
"user/demo_paginated_api/fetched_products"
35+
],
36+
"subscriptions_template": "mechanic/user/trigger\nmechanic/actions/perform\nuser/demo_paginated_api/fetched_products",
37+
"tags": [
38+
"Demonstration",
39+
"External API"
40+
]
41+
}

0 commit comments

Comments
 (0)