@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +11 -0
  3. package/dist/cjs/clients/fluent-client.js +13 -6
  4. package/dist/cjs/utils/pagination-helpers.js +38 -2
  5. package/dist/cjs/versori/fluent-versori-client.js +11 -5
  6. package/dist/esm/clients/fluent-client.js +13 -6
  7. package/dist/esm/utils/pagination-helpers.js +38 -2
  8. package/dist/esm/versori/fluent-versori-client.js +11 -5
  9. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  10. package/dist/tsconfig.tsbuildinfo +1 -1
  11. package/dist/tsconfig.types.tsbuildinfo +1 -1
  12. package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
  13. package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
  14. package/docs/00-START-HERE/cli-documentation-index.md +202 -202
  15. package/docs/00-START-HERE/cli-quick-reference.md +252 -252
  16. package/docs/00-START-HERE/decision-tree.md +552 -552
  17. package/docs/00-START-HERE/getting-started.md +1070 -1070
  18. package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
  19. package/docs/00-START-HERE/readme.md +237 -237
  20. package/docs/00-START-HERE/retailerid-configuration.md +404 -404
  21. package/docs/00-START-HERE/sdk-philosophy.md +794 -794
  22. package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
  23. package/docs/01-TEMPLATES/faq.md +686 -686
  24. package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
  25. package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
  26. package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
  27. package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
  28. package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
  29. package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
  30. package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
  31. package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
  32. package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
  33. package/docs/01-TEMPLATES/readme.md +957 -957
  34. package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
  35. package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
  36. package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
  37. package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
  38. package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
  39. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
  40. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
  41. package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
  42. package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
  43. package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
  44. package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
  45. package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
  46. package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
  47. package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
  48. package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
  49. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
  50. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
  51. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
  52. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
  53. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
  54. package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
  55. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
  56. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
  57. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
  58. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
  59. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
  60. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
  61. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
  62. package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
  63. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
  64. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
  65. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
  66. package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
  67. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
  68. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
  69. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
  70. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
  71. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
  72. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
  73. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
  74. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
  75. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
  76. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
  77. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
  78. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
  79. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
  80. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
  81. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
  82. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
  83. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
  84. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
  85. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
  86. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
  87. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
  88. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
  89. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
  90. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
  91. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
  92. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
  93. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
  94. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
  95. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
  96. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
  97. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
  98. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
  99. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
  100. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
  101. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
  102. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
  103. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
  104. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
  105. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
  106. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
  107. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
  108. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
  109. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
  110. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
  111. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
  112. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
  113. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
  114. package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
  115. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
  116. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
  117. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
  118. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
  119. package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
  120. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
  121. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
  122. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
  123. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
  124. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
  125. package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
  126. package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
  127. package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
  128. package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
  129. package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
  130. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
  131. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
  132. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
  133. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
  134. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
  135. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
  136. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
  137. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
  138. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
  139. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
  140. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
  141. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
  142. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
  143. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
  144. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
  145. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
  146. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
  147. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
  148. package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
  149. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
  150. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
  151. package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
  152. package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
  153. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
  154. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
  155. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
  156. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
  157. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
  158. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
  159. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
  160. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
  161. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
  162. package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
  163. package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
  164. package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
  165. package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
  166. package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
  167. package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
  168. package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
  169. package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
  170. package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
  171. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
  172. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
  173. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
  174. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
  175. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
  176. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
  177. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
  178. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
  179. package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
  180. package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
  181. package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
  182. package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
  183. package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
  184. package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
  185. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
  186. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
  187. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
  188. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
  189. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
  190. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
  191. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
  192. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
  193. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
  194. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
  195. package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
  196. package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
  197. package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
  198. package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
  199. package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
  200. package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
  201. package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
  202. package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
  203. package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
  204. package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
  205. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
  206. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
  207. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
  208. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
  209. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
  210. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
  211. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
  212. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
  213. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
  214. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
  215. package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
  216. package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
  217. package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
  218. package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
  219. package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
  220. package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
  221. package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
  222. package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
  223. package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
  224. package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
  225. package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
  226. package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
  227. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
  228. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
  229. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
  230. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
  231. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
  232. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
  233. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
  234. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
  235. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
  236. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
  237. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
  238. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
  239. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
  240. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
  241. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
  242. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
  243. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
  244. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
  245. package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
  246. package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
  247. package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
  248. package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
  249. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
  250. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
  251. package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
  252. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
  253. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
  254. package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
  255. package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
  256. package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
  257. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
  258. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
  259. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
  260. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
  261. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
  262. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
  263. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
  264. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
  265. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
  266. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
  267. package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
  268. package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
  269. package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
  270. package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
  271. package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
  272. package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
  273. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
  274. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
  275. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
  276. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
  277. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
  278. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
  279. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
  280. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
  281. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
  282. package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
  283. package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
  284. package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
  285. package/docs/02-CORE-GUIDES/readme.md +194 -194
  286. package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
  287. package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
  288. package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
  289. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
  290. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
  291. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
  292. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
  293. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
  294. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
  295. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
  296. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
  297. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
  298. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
  299. package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
  300. package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
  301. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
  302. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
  303. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
  304. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
  305. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
  306. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
  307. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
  308. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
  309. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
  310. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
  311. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
  312. package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
  313. package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
  314. package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
  315. package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
  316. package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
  317. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
  318. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
  319. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
  320. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
  321. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
  322. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
  323. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
  324. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
  325. package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
  326. package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
  327. package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
  328. package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
  329. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
  330. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
  331. package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
  332. package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
  333. package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
  334. package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
  335. package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
  336. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
  337. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
  338. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
  339. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
  340. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
  341. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
  342. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
  343. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
  344. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
  345. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
  346. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
  347. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
  348. package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
  349. package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
  350. package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
  351. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
  352. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
  353. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
  354. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
  355. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
  356. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
  357. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
  358. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
  359. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
  360. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
  361. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
  362. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
  363. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
  364. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
  365. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
  366. package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
  367. package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
  368. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
  369. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
  370. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
  371. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
  372. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
  373. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
  374. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
  375. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
  376. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
  377. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
  378. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
  379. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
  380. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
  381. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
  382. package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
  383. package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
  384. package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
  385. package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
  386. package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
  387. package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
  388. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
  389. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
  390. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
  391. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
  392. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
  393. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
  394. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
  395. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
  396. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
  397. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
  398. package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
  399. package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
  400. package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
  401. package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
  402. package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
  403. package/docs/03-PATTERN-GUIDES/readme.md +159 -159
  404. package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
  405. package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
  406. package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
  407. package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
  408. package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
  409. package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
  410. package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
  411. package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
  412. package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
  413. package/docs/04-REFERENCE/architecture/readme.md +279 -279
  414. package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
  415. package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
  416. package/docs/04-REFERENCE/platforms/readme.md +135 -135
  417. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
  418. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
  419. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
  420. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
  421. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
  422. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
  423. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
  424. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
  425. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
  426. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
  427. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
  428. package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
  429. package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
  430. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
  431. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
  432. package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
  433. package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
  434. package/docs/04-REFERENCE/readme.md +148 -148
  435. package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
  436. package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
  437. package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
  438. package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
  439. package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
  440. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
  441. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
  442. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
  443. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
  444. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
  445. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
  446. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
  447. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
  448. package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
  449. package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
  450. package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
  451. package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
  452. package/docs/04-REFERENCE/schema/readme.md +141 -141
  453. package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
  454. package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
  455. package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
  456. package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
  457. package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
  458. package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
  459. package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
  460. package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
  461. package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
  462. package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
  463. package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
  464. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
  465. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
  466. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
  467. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
  468. package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
  469. package/docs/04-REFERENCE/testing/readme.md +86 -86
  470. package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
  471. package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
  472. package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
  473. package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
  474. package/docs/template-loading-matrix.md +242 -242
  475. package/package.json +5 -3
  476. package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
@@ -1,2476 +1,2476 @@
1
- # Module 4: Workflows - HTTP, Webhooks, Scheduled & Internal Functions
2
-
3
- [← Back to Versori Platform Guide](../platforms-versori-readme.md)
4
-
5
- **Module 4 of 8** | **Level**: Intermediate | **Time**: 30 minutes
6
-
7
- ---
8
-
9
- ## Learning Objectives
10
-
11
- By the end of this module, you will:
12
-
13
- - ✅ Master all four workflow types: http(), webhook(), schedule(), fn()
14
- - ✅ Understand when to use each workflow type
15
- - ✅ Implement HTTP workflows with Fluent API calls
16
- - ✅ Build webhook receivers with signature validation
17
- - ✅ Create scheduled tasks with cron patterns
18
- - ✅ Compose multi-step workflows with fn()
19
- - ✅ **Handle non-JSON responses** (XML, HTML, CSV) correctly
20
- - ✅ Implement error handling and retry strategies
21
-
22
- ---
23
-
24
- ## Workflow Types Overview
25
-
26
- Versori provides four fundamental workflow types, each with specific capabilities and use cases:
27
-
28
- | Type | Purpose | External API Access | Context | Use When |
29
- | -------------- | --------------------- | -------------------------------- | ---------------------------- | ------------------------------------- |
30
- | **http()** | External API calls | ✅ Yes (requires connection) | `fetch`, `log`, `activation` | Query/mutate Fluent API |
31
- | **webhook()** | Receive HTTP requests | ❌ No (unless chained with http) | `data`, `request()`, `log` | Receive SFCC orders, Rubix events |
32
- | **schedule()** | Time-based tasks | ✅ Yes (if connection provided) | `fetch`, `log`, `activation` | Daily sync, hourly extraction |
33
- | **fn()** | Internal processing | ❌ No | `openKv`, `log`, `request()` | Data transformation, state management |
34
-
35
- ---
36
-
37
- ## ⚠️ CRITICAL: Accessing Headers & Choosing Workflow Types
38
-
39
- ### Accessing HTTP Headers
40
-
41
- **The `Context<D>` interface does NOT have a `headers` property.** Always use `ctx.request()`:
42
-
43
- ```typescript
44
- // ✅ CORRECT - Access headers via request()
45
- const req = ctx.request();
46
- const apiKey = req?.headers['x-api-key'] as string;
47
- const contentType = req?.headers['content-type'];
48
-
49
- // ❌ WRONG - ctx.headers doesn't exist in @versori/run
50
- const apiKey = ctx.headers?.get?.('x-api-key'); // ERROR!
51
- ```
52
-
53
- ### When to Use http() vs fn()
54
-
55
- **Decision Rule:** Do you need to call external APIs (Fluent Commerce, S3 presigned URLs, etc.)?
56
-
57
- #### Use `http()` When:
58
-
59
- - ✅ Calling Fluent Commerce GraphQL API
60
- - ✅ Need connection credentials and authentication
61
- - ✅ Require `ctx.fetch` with OAuth2 auth
62
- - ✅ Need `connectionVariables` or `baseUrl`
63
-
64
- **What you get in http():**
65
-
66
- ```typescript
67
- http('my-task', { connection: 'fluent_commerce' }, async ctx => {
68
- ctx.fetch; // ✅ Authenticated fetch with connection
69
- ctx.connectionVariables; // ✅ Connection configuration
70
- ctx.baseUrl; // ✅ API base URL from connection
71
- ctx.request(); // ✅ HTTP request object (for headers)
72
- (ctx.log, ctx.data, ctx.activation, ctx.openKv()); // ✅ All standard context
73
-
74
- // Can call external APIs:
75
- const client = await createClient(ctx); // ✅ Works!
76
- });
77
- ```
78
-
79
- #### Use `fn()` When:
80
-
81
- - ✅ Pure data transformation (no external API calls)
82
- - ✅ State management with KV storage only
83
- - ✅ Parsing, mapping, validation logic
84
- - ✅ File tracking and deduplication
85
-
86
- **What you get in fn():**
87
-
88
- ```typescript
89
- fn('my-task', async ctx => {
90
- ctx.data; // ✅ Input data
91
- ctx.log; // ✅ Logger
92
- ctx.activation; // ✅ Variables from activation
93
- ctx.openKv(); // ✅ KV storage access
94
- ctx.request(); // ✅ HTTP request object (for headers)
95
-
96
- // ❌ NO ctx.fetch - cannot make authenticated API calls
97
- // ❌ NO connectionVariables - no connection context
98
- // ❌ CANNOT call createClient() - will fail
99
- });
100
- ```
101
-
102
- ### Example: Adhoc Extraction (Requires http())
103
-
104
- ```typescript
105
- // ✅ CORRECT - Use http() for Fluent API access with connection-based security
106
- export const adhocExtraction = webhook('adhoc-extract', {
107
- connection: 'webhook-auth', // ← Platform validates API key automatically
108
- response: { mode: 'sync' },
109
- }).then(
110
- http('execute-extraction', { connection: 'fluent_commerce' }, async ctx => {
111
- const { log, data } = ctx;
112
-
113
- // ✅ If we're here, authentication already passed via connection
114
- // No manual validation code needed!
115
-
116
- // http() provides connection context for createClient()
117
- const client = await createClient(ctx); // ✅ Works!
118
- const result = await client.graphql({ query: '...' });
119
-
120
- return { success: true, data: result.data };
121
- })
122
- );
123
-
124
- // ❌ WRONG - fn() cannot access Fluent API
125
- export const brokenExtraction = webhook('adhoc-extract', {
126
- connection: 'webhook-auth'
127
- }).then(
128
- fn('execute-extraction', async ctx => {
129
- const client = await createClient(ctx); // ❌ FAILS - no connection context in fn()
130
- })
131
- );
132
- ```
133
-
134
- ### Quick Decision Table
135
-
136
- | Need To | Use | Reason |
137
- | ------------------------------- | -------- | ------------------------------------ |
138
- | Call Fluent GraphQL API | `http()` | Requires connection + authentication |
139
- | Parse CSV to JSON | `fn()` | No external API needed |
140
- | Check if file already processed | `fn()` | Uses KV storage only |
141
- | Send batch to Fluent | `http()` | Requires Fluent API access |
142
- | Transform/map data | `fn()` | Pure data transformation |
143
- | Query external REST API | `http()` | Requires fetch + connection |
144
- | Access HTTP headers | **Both** | Use `ctx.request()` in either |
145
-
146
- ---
147
-
148
- ## ⚠️ Deno Compatibility: Buffer Import
149
-
150
- **CRITICAL for Versori Platform (Deno runtime):** Always import Buffer explicitly when working with binary data, file uploads, or base64 encoding/decoding.
151
-
152
- ### Why Buffer Import is Required
153
-
154
- Versori runs on Deno, not Node.js. In Node.js, `Buffer` is globally available, but in Deno it must be explicitly imported from `node:buffer`.
155
-
156
- ```typescript
157
- // ✅ CORRECT - Always import Buffer in Versori/Deno
158
- import { Buffer } from 'node:buffer';
159
-
160
- await sftp.uploadFile('/path/file.xml', Buffer.from(xmlContent, 'utf-8'));
161
- const decoded = Buffer.from(base64String, 'base64').toString('utf-8');
162
-
163
- // ❌ WRONG - Buffer is not global in Deno
164
- await sftp.uploadFile('/path/file.xml', Buffer.from(xmlContent, 'utf-8')); // ReferenceError!
165
- ```
166
-
167
- ### When to Import Buffer
168
-
169
- Import Buffer when your workflow involves:
170
-
171
- | Operation | Example | Requires Buffer? |
172
- |-----------|---------|------------------|
173
- | **File uploads** (SFTP, S3) | `sftp.uploadFile()` | ✅ Yes |
174
- | **Base64 encoding/decoding** | `Buffer.from(str, 'base64')` | ✅ Yes |
175
- | **Binary data handling** | Working with binary files | ✅ Yes |
176
- | **String encoding** | `Buffer.from(str, 'utf-8')` | ✅ Yes |
177
- | **XML response generation** | Returning XML as string | ❌ No (string only) |
178
- | **JSON operations** | `JSON.parse()`, `JSON.stringify()` | ❌ No |
179
- | **GraphQL queries** | `client.graphql()` | ❌ No |
180
-
181
- ### Pattern: Add Buffer Import at Top of File
182
-
183
- ```typescript
184
- // ✅ CORRECT Pattern - Add at top of file with other imports
185
- import { Buffer } from 'node:buffer'; // Required for Deno
186
- import { schedule, http, fn } from '@versori/run';
187
- import { createClient, SftpDataSource } from '@fluentcommerce/fc-connect-sdk';
188
-
189
- export const sftpUpload = schedule('upload', '0 * * * *')
190
- .then(fn('prepare-file', async (ctx) => {
191
- // Buffer available here
192
- const fileContent = Buffer.from(ctx.data.content, 'utf-8');
193
- return { fileContent };
194
- }));
195
- ```
196
-
197
- **See Also:** All examples in this module that use Buffer include the import statement.
198
-
199
- ---
200
-
201
- ## HTTP Functions - External API Calls
202
-
203
- HTTP workflows enable authenticated calls to external APIs, including Fluent Commerce.
204
-
205
- ### Basic HTTP Workflow
206
-
207
- ```typescript
208
- import { http } from '@versori/run';
209
- import { createClient } from '@fluentcommerce/fc-connect-sdk';
210
-
211
- /**
212
- * Query inventory positions from Fluent Commerce
213
- *
214
- * Endpoint: https://{workspace}.versori.run/query-inventory
215
- * Method: GET
216
- */
217
- export const queryInventory = http(
218
- 'query-inventory',
219
- {
220
- connection: 'fluent_commerce', // CRITICAL: Provides OAuth2 auth
221
- timeout: 30000, // 30 second timeout
222
- retry: {
223
- attempts: 3, // Retry up to 3 times
224
- delay: 1000, // 1 second between retries
225
- },
226
- },
227
- async ctx => {
228
- ctx.log('info', 'Starting inventory query');
229
-
230
- try {
231
- // Create SDK client (auto-configured from connection)
232
- const client = await createClient(ctx);
233
-
234
- // GraphQL query - schema validated against Fluent Commerce API
235
- const result = await client.graphql({
236
- query: `
237
- query GetInventoryPositions($first: Int!) {
238
- inventoryPositions(first: $first) {
239
- edges {
240
- node {
241
- id
242
- ref
243
- productRef
244
- locationRef
245
- onHand # ✅ Correct field for InventoryPosition
246
- status
247
- }
248
- cursor
249
- }
250
- pageInfo {
251
- hasNextPage
252
- endCursor
253
- }
254
- }
255
- }
256
- `,
257
- variables: { first: 100 },
258
- });
259
-
260
- // Check for GraphQL errors
261
- if (result.errors?.length) {
262
- throw new Error(`GraphQL error: ${result.errors[0].message}`);
263
- }
264
-
265
- const positions = result.data.inventoryPositions.edges;
266
- ctx.log('info', `Retrieved ${positions.length} inventory positions`);
267
-
268
- return {
269
- success: true,
270
- count: positions.length,
271
- data: positions.map(edge => edge.node),
272
- pageInfo: result.data.inventoryPositions.pageInfo,
273
- };
274
- } catch (error) {
275
- ctx.log('error', 'Inventory query failed', {
276
- error: error instanceof Error ? error.message : String(error),
277
- });
278
-
279
- return {
280
- success: false,
281
- error: error instanceof Error ? error.message : 'Unknown error',
282
- };
283
- }
284
- }
285
- );
286
- ```
287
-
288
- ### HTTP with Query Parameters
289
-
290
- ```typescript
291
- export const searchOrders = http(
292
- 'search-orders',
293
- {
294
- connection: 'fluent_commerce',
295
- },
296
- async ctx => {
297
- // Extract query parameters
298
- const status = ctx.query?.status || 'OPEN';
299
- const limit = parseInt(ctx.query?.limit || '50');
300
- const customerRef = ctx.query?.customerRef;
301
-
302
- ctx.log('info', 'Searching orders', { status, limit, customerRef });
303
-
304
- const client = await createClient(ctx);
305
-
306
- // Build GraphQL query with filters - schema validated
307
- const result = await client.graphql({
308
- query: `
309
- query SearchOrders($status: String!, $first: Int!, $customerRef: String) {
310
- orders(status: $status, first: $first, customerRef: $customerRef) {
311
- edges {
312
- node {
313
- id
314
- ref
315
- status
316
- totalPrice
317
- customer {
318
- ref
319
- firstName
320
- lastName
321
- }
322
- items {
323
- edges {
324
- node {
325
- productRef
326
- quantity
327
- price
328
- }
329
- }
330
- }
331
- }
332
- }
333
- }
334
- }
335
- `,
336
- variables: { status, first: limit, customerRef },
337
- });
338
-
339
- return {
340
- orders: result.data.orders.edges.map(e => e.node),
341
- count: result.data.orders.edges.length,
342
- };
343
- }
344
- );
345
- ```
346
-
347
- ### HTTP with POST Body (Mutations)
348
-
349
- ```typescript
350
- import { http } from '@versori/run';
351
- import { createClient } from '@fluentcommerce/fc-connect-sdk';
352
-
353
- /**
354
- * Create inventory position
355
- *
356
- * POST https://{workspace}.versori.run/create-inventory
357
- * Body: { productRef, locationRef, qty }
358
- */
359
- export const createInventory = http(
360
- 'create-inventory',
361
- {
362
- connection: 'fluent_commerce',
363
- },
364
- async ctx => {
365
- // Extract POST body
366
- const { productRef, locationRef, qty } = ctx.data || {};
367
-
368
- // Validation
369
- if (!productRef || !locationRef || qty === undefined) {
370
- return {
371
- success: false,
372
- error: 'Missing required fields: productRef, locationRef, qty',
373
- };
374
- }
375
-
376
- const client = await createClient(ctx);
377
-
378
- // GraphQL mutation - schema validated
379
- const result = await client.graphql({
380
- query: `
381
- mutation CreateInventoryPosition($input: CreateInventoryPositionInput!) {
382
- createInventoryPosition(input: $input) {
383
- id
384
- ref
385
- productRef
386
- locationRef
387
- onHand
388
- status
389
- }
390
- }
391
- `,
392
- variables: {
393
- input: {
394
- productRef,
395
- locationRef,
396
- qty,
397
- type: 'AVAILABLE',
398
- },
399
- },
400
- });
401
-
402
- if (result.errors?.length) {
403
- return {
404
- success: false,
405
- error: result.errors[0].message,
406
- };
407
- }
408
-
409
- ctx.log('info', 'Inventory position created', {
410
- id: result.data.createInventoryPosition.id,
411
- });
412
-
413
- return {
414
- success: true,
415
- data: result.data.createInventoryPosition,
416
- };
417
- }
418
- );
419
- ```
420
-
421
- ---
422
-
423
- ## Webhook Functions - Receiving External Requests
424
-
425
- Webhook workflows receive external HTTP requests. They provide `ctx.data` (parsed body) and `ctx.request()` for accessing headers and raw request details.
426
-
427
- ### Basic Webhook Receiver
428
-
429
- ```typescript
430
- import { webhook } from '@versori/run';
431
- import { parseWebhookRequest } from '@fluentcommerce/fc-connect-sdk';
432
-
433
- /**
434
- * Receive SFCC order webhook
435
- *
436
- * POST https://{workspace}.versori.run/receive-order
437
- * Body: XML order payload
438
- */
439
- export const receiveOrder = webhook('receive-order', async ctx => {
440
- const req = ctx.request();
441
- ctx.log('info', 'Received order webhook', {
442
- contentType: req?.headers['content-type'],
443
- bodySize: JSON.stringify(ctx.data).length,
444
- });
445
-
446
- try {
447
- // Parse incoming payload
448
- const payload = parseWebhookRequest(ctx.data);
449
-
450
- if (!payload) {
451
- return {
452
- success: false,
453
- error: 'Invalid payload format',
454
- status: 400,
455
- };
456
- }
457
-
458
- // Process order (use fn() or http() in chain)
459
- ctx.log('info', 'Order parsed successfully', {
460
- orderId: payload.orderId,
461
- });
462
-
463
- return {
464
- success: true,
465
- orderId: payload.orderId,
466
- status: 200,
467
- };
468
- } catch (error) {
469
- ctx.log('error', 'Webhook processing failed', {
470
- error: error instanceof Error ? error.message : String(error),
471
- });
472
-
473
- return {
474
- success: false,
475
- error: error instanceof Error ? error.message : 'Processing failed',
476
- status: 500,
477
- };
478
- }
479
- });
480
- ```
481
-
482
- ### Webhook with Signature Validation (Fluent Rubix)
483
-
484
- **IMPORTANT**: The webhook validation is **ONLY** for webhooks sent from **Fluent Commerce Rubix workflows**.
485
-
486
- ```typescript
487
- import { webhook } from '@versori/run';
488
- import {
489
- WebhookValidationService,
490
- SignatureAlgorithm,
491
- parseWebhookRequest,
492
- validateFluentEvent,
493
- } from '@fluentcommerce/fc-connect-sdk';
494
-
495
- /**
496
- * Receive Fluent Rubix webhook with signature validation
497
- *
498
- * POST https://{workspace}.versori.run/rubix-event
499
- * Headers: fluent-signature (or x-fluent-signature)
500
- */
501
- export const receiveRubixEvent = webhook('rubix-event', async ctx => {
502
- const logger = ctx.log; // Use native Versori logger
503
-
504
- // Get public key from connector variables
505
- const publicKey = ctx.vars?.FLUENT_WEBHOOK_PUBLIC_KEY;
506
- if (!publicKey) {
507
- logger.error('Missing FLUENT_WEBHOOK_PUBLIC_KEY in connector variables');
508
- return { error: 'Configuration error: missing public key', status: 500 };
509
- }
510
-
511
- // IMPORTANT: This validator is ONLY for Fluent Commerce Rubix workflows
512
- const validator = new WebhookValidationService(
513
- {
514
- algorithm: SignatureAlgorithm.SHA512_WITH_RSA,
515
- strictValidation: true,
516
- },
517
- logger
518
- );
519
-
520
- // Extract signature from headers using ctx.request()
521
- const req = ctx.request();
522
- const signature = req?.headers['fluent-signature'] || req?.headers['x-fluent-signature'];
523
-
524
- if (!signature) {
525
- logger.warn('Missing webhook signature in headers');
526
- return { error: 'Missing signature', status: 401 };
527
- }
528
-
529
- // Validate Fluent Commerce Rubix webhook signature
530
- const validationResult = await validator.validateWebhookSignature(
531
- JSON.stringify(ctx.data),
532
- signature,
533
- publicKey,
534
- SignatureAlgorithm.SHA512_WITH_RSA
535
- );
536
-
537
- if (!validationResult.isValid) {
538
- logger.error('Fluent Commerce Rubix webhook validation failed', validationResult);
539
- return {
540
- error: 'Invalid Fluent Rubix webhook signature',
541
- details: validationResult.error,
542
- status: 401,
543
- };
544
- }
545
-
546
- // Parse webhook payload
547
- const payload = parseWebhookRequest(ctx.data, logger, 'receiveRubixEvent');
548
-
549
- if (!payload) {
550
- return { error: 'Invalid payload', status: 400 };
551
- }
552
-
553
- // Validate event structure
554
- const validation = validateFluentEvent(payload, logger, 'receiveRubixEvent');
555
- if (!validation.isValid) {
556
- return {
557
- error: 'Validation failed',
558
- details: validation.warnings,
559
- status: 400,
560
- };
561
- }
562
-
563
- logger.info('Rubix webhook validated successfully', {
564
- eventName: payload.name,
565
- entityType: payload.entityType,
566
- });
567
-
568
- return {
569
- success: true,
570
- eventName: payload.name,
571
- status: 200,
572
- };
573
- });
574
- ```
575
-
576
- ### CRITICAL: Non-JSON Response Handlers (XML, HTML, CSV)
577
-
578
- **Problem**: By default, Versori JSON-encodes all webhook responses. This breaks XML, HTML, CSV, and other non-JSON content.
579
-
580
- **Solution**: Use custom `onSuccess` and `onError` handlers that return `Response` objects.
581
-
582
- #### XML Response Example
583
-
584
- ```typescript
585
- import { webhook, fn } from '@versori/run';
586
-
587
- /**
588
- * Return XML response from webhook
589
- *
590
- * GET/POST https://{workspace}.versori.run/order-status-xml
591
- * Returns: Raw XML (NOT JSON-encoded)
592
- */
593
- export const orderStatusXML = webhook('order-status-xml', {
594
- response: {
595
- mode: 'sync',
596
- onSuccess: ctx => {
597
- // ctx.data contains the final value from .then() chain
598
- return new Response(ctx.data, {
599
- status: 200,
600
- headers: {
601
- 'Content-Type': 'application/xml; charset=utf-8',
602
- 'X-Execution-Id': ctx.executionId,
603
- },
604
- });
605
- },
606
- onError: ctx => {
607
- // ctx.data contains the error from .catch()
608
- const errorXml = `<?xml version="1.0" encoding="UTF-8"?>
609
- <ErrorResponse>
610
- <Error>
611
- <Code>PROCESSING_ERROR</Code>
612
- <Message>${ctx.data?.message || 'Unknown error'}</Message>
613
- <Timestamp>${new Date().toISOString()}</Timestamp>
614
- <ExecutionId>${ctx.executionId}</ExecutionId>
615
- </Error>
616
- </ErrorResponse>`;
617
-
618
- return new Response(errorXml, {
619
- status: 500,
620
- headers: {
621
- 'Content-Type': 'application/xml; charset=utf-8',
622
- 'X-Execution-Id': ctx.executionId,
623
- },
624
- });
625
- },
626
- },
627
- cors: true,
628
- })
629
- .then(
630
- fn('generate-xml', ({ data }) => {
631
- // Return raw XML string - this becomes ctx.data in onSuccess
632
- const orderId = data?.orderId || 'unknown';
633
- return `<?xml version="1.0" encoding="UTF-8"?>
634
- <OrderStatusResponse>
635
- <OrderId>${orderId}</OrderId>
636
- <Status>SHIPPED</Status>
637
- <Timestamp>${new Date().toISOString()}</Timestamp>
638
- </OrderStatusResponse>`;
639
- })
640
- )
641
- .catch(({ data }) => {
642
- // Return error - this becomes ctx.data in onError
643
- return { message: data instanceof Error ? data.message : String(data) };
644
- });
645
- ```
646
-
647
- **Why This Works**:
648
-
649
- - @versori/run's `sendResponse()` function streams `Response` objects directly **without JSON encoding**
650
- - The Content-Type header is preserved exactly as specified
651
- - Works with @versori/run v0.4.4 and all v0.4.x versions
652
-
653
- #### HTML Response Example
654
-
655
- ```typescript
656
- export const statusPage = webhook('status-page', {
657
- response: {
658
- mode: 'sync',
659
- onSuccess: ctx =>
660
- new Response(ctx.data, {
661
- status: 200,
662
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
663
- }),
664
- },
665
- }).then(
666
- fn('generate-html', () => {
667
- return `<!DOCTYPE html>
668
- <html lang="en">
669
- <head>
670
- <meta charset="UTF-8">
671
- <title>Order Status</title>
672
- </head>
673
- <body>
674
- <h1>System Operational</h1>
675
- <p>All services running normally.</p>
676
- </body>
677
- </html>`;
678
- })
679
- );
680
- ```
681
-
682
- #### CSV Response Example
683
-
684
- ```typescript
685
- export const exportCSV = webhook('export-csv', {
686
- response: {
687
- mode: 'sync',
688
- onSuccess: ctx =>
689
- new Response(ctx.data, {
690
- status: 200,
691
- headers: {
692
- 'Content-Type': 'text/csv; charset=utf-8',
693
- 'Content-Disposition': 'attachment; filename="orders.csv"',
694
- },
695
- }),
696
- },
697
- }).then(
698
- fn('generate-csv', () => {
699
- return 'OrderId,Status,Total\n123,SHIPPED,99.99\n456,PENDING,149.99';
700
- })
701
- );
702
- ```
703
-
704
- #### Complex Multi-Step Workflow with XML Response
705
-
706
- ```typescript
707
- import { webhook, fn, http } from '@versori/run';
708
- import { createClient } from '@fluentcommerce/fc-connect-sdk';
709
- import { XMLBuilder } from 'fast-xml-parser';
710
-
711
- /**
712
- * Fetch order from Fluent, return as XML
713
- *
714
- * POST https://{workspace}.versori.run/order-detail-xml
715
- * Body: { orderId }
716
- * Returns: Order details as XML
717
- */
718
- export const orderDetailXML = webhook('order-detail-xml', {
719
- response: {
720
- mode: 'sync',
721
- onSuccess: ctx =>
722
- new Response(ctx.data, {
723
- status: 200,
724
- headers: {
725
- 'Content-Type': 'application/xml; charset=utf-8',
726
- 'X-Execution-Id': ctx.executionId,
727
- 'X-Order-Id': ctx.metadata?.orderId || 'unknown',
728
- },
729
- }),
730
- onError: ctx => {
731
- const errorXml = `<?xml version="1.0" encoding="UTF-8"?>
732
- <ErrorResponse>
733
- <Error>
734
- <Code>PROCESSING_ERROR</Code>
735
- <Message>${ctx.data?.message || 'Unknown error'}</Message>
736
- <Timestamp>${new Date().toISOString()}</Timestamp>
737
- <ExecutionId>${ctx.executionId}</ExecutionId>
738
- </Error>
739
- </ErrorResponse>`;
740
-
741
- return new Response(errorXml, {
742
- status: 500,
743
- headers: {
744
- 'Content-Type': 'application/xml; charset=utf-8',
745
- 'X-Execution-Id': ctx.executionId,
746
- },
747
- });
748
- },
749
- },
750
- cors: true,
751
- })
752
- // Step 1: Parse incoming request
753
- .then(
754
- fn('parse-request', ({ data }) => {
755
- const orderId = data?.orderId;
756
- if (!orderId) {
757
- throw new Error('OrderId missing from request');
758
- }
759
- return { orderId };
760
- })
761
- )
762
-
763
- // Step 2: Fetch from Fluent Commerce
764
- .then(
765
- http(
766
- 'fetch-order',
767
- {
768
- connection: 'fluent_commerce',
769
- },
770
- async ctx => {
771
- const { orderId } = ctx.data;
772
- const client = await createClient(ctx);
773
-
774
- // GraphQL query - schema validated
775
- const result = await client.graphql({
776
- query: `
777
- query GetOrder($ref: String!) {
778
- order(ref: $ref) {
779
- id
780
- ref
781
- status
782
- totalPrice
783
- customer {
784
- ref
785
- firstName
786
- lastName
787
- }
788
- }
789
- }
790
- `,
791
- variables: { ref: orderId },
792
- });
793
-
794
- if (!result.data?.order) {
795
- throw new Error(`Order not found: ${orderId}`);
796
- }
797
-
798
- return {
799
- orderId,
800
- orderData: result.data.order,
801
- };
802
- }
803
- )
804
- )
805
-
806
- // Step 3: Build XML response
807
- .then(
808
- fn('build-xml', ({ data }) => {
809
- const builder = new XMLBuilder({
810
- ignoreAttributes: false,
811
- attributeNamePrefix: '@',
812
- format: true,
813
- });
814
-
815
- const xmlObject = {
816
- '?xml': { '@version': '1.0', '@encoding': 'UTF-8' },
817
- OrderDetailResponse: {
818
- '@orderId': data.orderId,
819
- Order: {
820
- Id: data.orderData.id,
821
- Ref: data.orderData.ref,
822
- Status: data.orderData.status,
823
- TotalPrice: data.orderData.totalPrice,
824
- Customer: {
825
- Ref: data.orderData.customer.ref,
826
- Name: `${data.orderData.customer.firstName} ${data.orderData.customer.lastName}`,
827
- },
828
- },
829
- },
830
- };
831
-
832
- return builder.build(xmlObject);
833
- })
834
- )
835
-
836
- .catch(({ data }) => {
837
- // Return error message as object - onError handler will format as XML
838
- return { message: data instanceof Error ? data.message : String(data) };
839
- });
840
- ```
841
-
842
- **Key Patterns**:
843
-
844
- - `onSuccess` and `onError` **return Response objects**
845
- - Workflow steps **return raw strings** (NOT Response objects)
846
- - Content-Type header matches actual content format
847
- - Error handler uses same Content-Type as success handler
848
-
849
- ---
850
-
851
- ## Scheduled Functions - Time-Based Recurring Tasks
852
-
853
- Scheduled workflows run on a cron schedule. They **MUST** be chained with `.then()` to add processing logic. To access external APIs (like Fluent Commerce), chain with `http()` task.
854
-
855
- ### ⚠️ CRITICAL: schedule() Signature
856
-
857
- **schedule() does NOT accept a handler or options as parameters**. It returns a `Workflow` that must be chained.
858
-
859
- **Signature**:
860
- ```typescript
861
- function schedule(
862
- id: string,
863
- schedule: string,
864
- activationPredicate?: ActivationPredicate
865
- ): Workflow<ScheduleData>
866
- ```
867
-
868
- **activationPredicate**: Optional - `'all'` or custom filter function `(activation?: Activation) => boolean`
869
-
870
- ### Basic Scheduled Workflow
871
-
872
- ```typescript
873
- import { schedule, http } from '@versori/run';
874
- import { createClient } from '@fluentcommerce/fc-connect-sdk';
875
-
876
- /**
877
- * Daily inventory sync - runs at 2 AM UTC daily
878
- *
879
- * Cron: 0 2 * * * (every day at 2:00 AM)
880
- */
881
- export const dailyInventorySync = schedule(
882
- 'daily-inventory-sync',
883
- '0 2 * * *'
884
- )
885
- .then(
886
- http(
887
- 'sync-inventory',
888
- { connection: 'fluent_commerce' },
889
- async ctx => {
890
- ctx.log.info('Starting daily inventory sync');
891
-
892
- const client = await createClient(ctx);
893
-
894
- // Fetch inventory from external source (e.g., S3)
895
- const inventoryData = await fetchInventoryFromS3(ctx);
896
-
897
- // Create batch job
898
- const job = await client.createJob({
899
- name: 'Daily Inventory Sync',
900
- retailerId: ctx.activation.getVariable('FLUENT_RETAILER_ID'),
901
- });
902
-
903
- // Send batch data
904
- const batch = await client.sendBatch(job.id, {
905
- action: 'UPSERT',
906
- entityType: 'INVENTORY',
907
- entities: inventoryData,
908
- });
909
-
910
- ctx.log.info('Daily sync complete', {
911
- jobId: job.id,
912
- batchId: batch.id,
913
- recordCount: inventoryData.length,
914
- });
915
-
916
- return {
917
- success: true,
918
- jobId: job.id,
919
- recordCount: inventoryData.length,
920
- };
921
- }
922
- )
923
- );
924
- ```
925
-
926
- ### Common Cron Patterns
927
-
928
- ```typescript
929
- // Every minute - with http() for API access
930
- export const everyMinute = schedule('every-minute', '* * * * *')
931
- .then(http('task', { connection: 'fluent_commerce' }, async (ctx) => {
932
- // Fluent API access available
933
- const client = await createClient(ctx);
934
- // ... process
935
- }));
936
-
937
- // Every minute - with fn() for KV-only
938
- export const everyMinuteKV = schedule('every-minute-kv', '* * * * *')
939
- .then(fn('task', async (ctx) => {
940
- // KV storage only, no external API
941
- const kv = ctx.openKv(':project:');
942
- // ... process
943
- }));
944
-
945
- // Every hour at minute 0
946
- export const hourly = schedule('hourly', '0 * * * *')
947
- .then(http('sync', { connection: 'fluent_commerce' }, async (ctx) => {
948
- // Hourly inventory sync
949
- }));
950
-
951
- // Every 6 hours - Fluent inventory snapshot
952
- export const every6Hours = schedule('every-6-hours', '0 */6 * * *')
953
- .then(http('snapshot', { connection: 'fluent_commerce' }, async (ctx) => {
954
- // Extract inventory snapshot every 6 hours
955
- }));
956
-
957
- // Daily at 2 AM - Full sync
958
- export const dailyAt2AM = schedule('daily-2am', '0 2 * * *')
959
- .then(http('full-sync', { connection: 'fluent_commerce' }, async (ctx) => {
960
- // Daily full inventory sync
961
- }));
962
-
963
- // Weekdays at 9 AM - Business hours processing
964
- export const weekdaysAt9AM = schedule('weekdays-9am', '0 9 * * 1-5')
965
- .then(http('business-hours', { connection: 'fluent_commerce' }, async (ctx) => {
966
- // Weekday order processing
967
- }));
968
-
969
- // First of every month at midnight - Monthly reports
970
- export const monthlyFirst = schedule('monthly-first', '0 0 1 * *')
971
- .then(http('monthly-report', { connection: 'fluent_commerce' }, async (ctx) => {
972
- // Generate monthly inventory reports
973
- }));
974
-
975
- // Every Sunday at 3 AM - Weekly cleanup
976
- export const weeklySunday = schedule('weekly-sunday', '0 3 * * 0')
977
- .then(http('weekly-cleanup', { connection: 'fluent_commerce' }, async (ctx) => {
978
- // Weekly data cleanup
979
- }));
980
-
981
- // With activation predicate (filter by activation)
982
- export const allActivations = schedule('all-act', '0 * * * *', 'all')
983
- .then(fn('task', async (ctx) => {
984
- // Runs for all activations
985
- }));
986
-
987
- export const customPredicate = schedule('custom', '0 * * * *', (a) => a?.id?.startsWith('prod'))
988
- .then(fn('task', async (ctx) => {
989
- // Runs only for production activations
990
- }));
991
- ```
992
-
993
- ### Scheduled Workflow with State Management
994
-
995
- ```typescript
996
- import { schedule, fn, http } from '@versori/run';
997
- import { createClient, VersoriFileTracker } from '@fluentcommerce/fc-connect-sdk';
998
-
999
- /**
1000
- * Hourly incremental sync with file tracking
1001
- *
1002
- * Cron: 0 * * * * (every hour)
1003
- */
1004
- export const hourlyIncrementalSync = schedule(
1005
- 'hourly-sync',
1006
- '0 * * * *'
1007
- )
1008
- .then(
1009
- fn('check-files', async ctx => {
1010
- const kv = ctx.openKv(':project:');
1011
- const tracker = new VersoriFileTracker(kv, 'hourly-sync');
1012
-
1013
- const lastFile = await tracker.getLastProcessedFile();
1014
- const newFiles = await listFilesAfter(lastFile);
1015
-
1016
- if (newFiles.length === 0) {
1017
- ctx.log.info('No new files to process');
1018
- throw new Error('SKIP'); // Signal to skip processing
1019
- }
1020
-
1021
- return { newFiles, tracker };
1022
- })
1023
- )
1024
- .then(
1025
- http(
1026
- 'process-files',
1027
- { connection: 'fluent_commerce' },
1028
- async ctx => {
1029
- const { newFiles, tracker } = ctx.data;
1030
- const client = await createClient(ctx);
1031
- let processedCount = 0;
1032
-
1033
- for (const file of newFiles) {
1034
- if (await tracker.wasFileProcessed(file.name)) {
1035
- ctx.log.info('Skipping already processed file', { file: file.name });
1036
- continue;
1037
- }
1038
-
1039
- const records = await processFile(file);
1040
-
1041
- const job = await client.createJob({ name: `Hourly Sync - ${file.name}` });
1042
- await client.sendBatch(job.id, {
1043
- action: 'UPSERT',
1044
- entityType: 'INVENTORY',
1045
- entities: records,
1046
- });
1047
-
1048
- await tracker.markFileProcessed(file.name, {
1049
- recordCount: records.length,
1050
- timestamp: Date.now(),
1051
- });
1052
-
1053
- await tracker.setLastProcessedFile(file.name);
1054
- processedCount += records.length;
1055
- }
1056
-
1057
- ctx.log.info('Hourly sync complete', {
1058
- filesProcessed: newFiles.length,
1059
- recordsProcessed: processedCount,
1060
- });
1061
-
1062
- return {
1063
- success: true,
1064
- filesProcessed: newFiles.length,
1065
- recordsProcessed: processedCount,
1066
- };
1067
- }
1068
- )
1069
- )
1070
- .catch(({ data }) => {
1071
- if (data.message === 'SKIP') {
1072
- return { skipped: true, reason: 'No new files' };
1073
- }
1074
- return { success: false, error: data.message };
1075
- });
1076
- ```
1077
-
1078
- ---
1079
-
1080
- ## Internal Functions (fn) - Data Transformation & State
1081
-
1082
- Internal functions (`fn()`) are for processing that **does NOT require external API access**. They have access to KV storage and logging, but **NOT** `ctx.fetch`.
1083
-
1084
- ### File Tracking with fn()
1085
-
1086
- ```typescript
1087
- import { fn } from '@versori/run';
1088
- import { VersoriFileTracker } from '@fluentcommerce/fc-connect-sdk';
1089
-
1090
- /**
1091
- * Track file processing state
1092
- *
1093
- * Used internally by other workflows (NOT exposed as HTTP endpoint)
1094
- */
1095
- export const trackFileProcessing = fn('track-file', async ctx => {
1096
- const kv = ctx.openKv(':project:');
1097
- const tracker = new VersoriFileTracker(kv, 'inventory-ingestion');
1098
-
1099
- const fileName = ctx.data?.fileName;
1100
- if (!fileName) {
1101
- return { error: 'Missing fileName' };
1102
- }
1103
-
1104
- // Check if processed
1105
- if (await tracker.wasFileProcessed(fileName)) {
1106
- return { skipped: true, reason: 'Already processed' };
1107
- }
1108
-
1109
- // Mark as processed
1110
- await tracker.markFileProcessed(fileName, {
1111
- records: ctx.data?.recordCount || 0,
1112
- timestamp: Date.now(),
1113
- });
1114
-
1115
- return { tracked: true, fileName };
1116
- });
1117
- ```
1118
-
1119
- ### Data Transformation with fn()
1120
-
1121
- ```typescript
1122
- import { fn } from '@versori/run';
1123
- import { UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
1124
-
1125
- /**
1126
- * Transform CSV data to Fluent format
1127
- *
1128
- * Used as part of workflow composition
1129
- */
1130
- export const transformInventoryData = fn('transform-inventory', async ctx => {
1131
- const rawData = ctx.data?.records;
1132
- if (!rawData || !Array.isArray(rawData)) {
1133
- return { error: 'Invalid input data' };
1134
- }
1135
-
1136
- // Define field mapping
1137
- const mappingConfig = {
1138
- fields: {
1139
- productRef: { source: 'sku_id', required: true },
1140
- locationRef: { source: 'location_code', required: true },
1141
- qty: { source: 'quantity', resolver: 'sdk.parseInt' },
1142
- status: { source: 'status', resolver: 'sdk.uppercase' },
1143
- },
1144
- };
1145
-
1146
- const mapper = new UniversalMapper(mappingConfig);
1147
-
1148
- // Transform all records
1149
- const transformed = [];
1150
- const errors = [];
1151
-
1152
- for (const record of rawData) {
1153
- const result = await mapper.map(record);
1154
- if (result.success) {
1155
- transformed.push(result.data);
1156
- } else {
1157
- errors.push({ record, errors: result.errors });
1158
- }
1159
- }
1160
-
1161
- ctx.log('info', 'Transformation complete', {
1162
- totalRecords: rawData.length,
1163
- transformed: transformed.length,
1164
- errors: errors.length,
1165
- });
1166
-
1167
- return {
1168
- transformed,
1169
- errors,
1170
- stats: {
1171
- total: rawData.length,
1172
- success: transformed.length,
1173
- failed: errors.length,
1174
- },
1175
- };
1176
- });
1177
- ```
1178
-
1179
- ---
1180
-
1181
- ## Workflow Composition - Multi-Step Patterns
1182
-
1183
- Combine multiple workflow types using `.then()` and `.catch()` chaining.
1184
-
1185
- ### Pattern 1: Webhook → fn() → http()
1186
-
1187
- ```typescript
1188
- import { webhook, fn, http } from '@versori/run';
1189
- import { createClient } from '@fluentcommerce/fc-connect-sdk';
1190
-
1191
- /**
1192
- * Receive order → validate → send to Fluent
1193
- *
1194
- * POST https://{workspace}.versori.run/process-order
1195
- */
1196
- export const processOrder = webhook('process-order')
1197
- // Step 1: Parse and validate (fn - no external API)
1198
- .then(
1199
- fn('validate-order', ({ data }) => {
1200
- if (!data?.orderId || !data?.items) {
1201
- throw new Error('Invalid order data: missing orderId or items');
1202
- }
1203
-
1204
- return {
1205
- orderId: data.orderId,
1206
- items: data.items.map(item => ({
1207
- productRef: item.sku,
1208
- quantity: parseInt(item.qty, 10),
1209
- price: parseFloat(item.price),
1210
- })),
1211
- };
1212
- })
1213
- )
1214
-
1215
- // Step 2: Send to Fluent (http - requires connection)
1216
- .then(
1217
- http(
1218
- 'send-to-fluent',
1219
- {
1220
- connection: 'fluent_commerce',
1221
- },
1222
- async ctx => {
1223
- const { orderId, items } = ctx.data;
1224
- const client = await createClient(ctx);
1225
-
1226
- // GraphQL mutation - schema validated
1227
- const result = await client.graphql({
1228
- query: `
1229
- mutation CreateOrder($input: CreateOrderInput!) {
1230
- createOrder(input: $input) {
1231
- id
1232
- ref
1233
- status
1234
- }
1235
- }
1236
- `,
1237
- variables: {
1238
- input: {
1239
- ref: orderId,
1240
- items: items.map(item => ({
1241
- productRef: item.productRef,
1242
- quantity: item.quantity,
1243
- price: item.price,
1244
- })),
1245
- },
1246
- },
1247
- });
1248
-
1249
- return {
1250
- success: true,
1251
- fluentOrderId: result.data.createOrder.id,
1252
- fluentOrderRef: result.data.createOrder.ref,
1253
- };
1254
- }
1255
- )
1256
- )
1257
-
1258
- // Error handling
1259
- .catch(({ data }) => {
1260
- return {
1261
- success: false,
1262
- error: data instanceof Error ? data.message : 'Processing failed',
1263
- status: 500,
1264
- };
1265
- });
1266
- ```
1267
-
1268
- ### Pattern 2: Scheduled → fn() (File Tracking) → http() (API Call)
1269
-
1270
- ```typescript
1271
- import { schedule, fn, http } from '@versori/run';
1272
- import { createClient, VersoriFileTracker } from '@fluentcommerce/fc-connect-sdk';
1273
-
1274
- /**
1275
- * Daily sync with file tracking
1276
- *
1277
- * Cron: 0 1 * * * (daily at 1 AM)
1278
- */
1279
- export const dailySyncWithTracking = schedule('daily-sync', '0 1 * * *', {
1280
- connection: 'fluent_commerce',
1281
- })
1282
- // Step 1: Check if already processed today (fn - KV access)
1283
- .then(
1284
- fn('check-processed', async ctx => {
1285
- const kv = ctx.openKv(':project:');
1286
- const tracker = new VersoriFileTracker(kv, 'daily-sync');
1287
-
1288
- const today = new Date().toISOString().split('T')[0];
1289
- const fileKey = `inventory-${today}.csv`;
1290
-
1291
- if (await tracker.wasFileProcessed(fileKey)) {
1292
- throw new Error('Already processed today');
1293
- }
1294
-
1295
- return { fileKey, today };
1296
- })
1297
- )
1298
-
1299
- // Step 2: Fetch and process (http - external API)
1300
- .then(
1301
- http(
1302
- 'process-sync',
1303
- {
1304
- connection: 'fluent_commerce',
1305
- },
1306
- async ctx => {
1307
- const { fileKey } = ctx.data;
1308
- const client = await createClient(ctx);
1309
-
1310
- // Fetch data from external source
1311
- const records = await fetchDailyInventory();
1312
-
1313
- // Send to Fluent
1314
- const job = await client.createJob({ name: `Daily Sync ${fileKey}` });
1315
- await client.sendBatch(job.id, {
1316
- action: 'UPSERT',
1317
- entityType: 'INVENTORY',
1318
- entities: records,
1319
- });
1320
-
1321
- return {
1322
- fileKey,
1323
- jobId: job.id,
1324
- recordCount: records.length,
1325
- };
1326
- }
1327
- )
1328
- )
1329
-
1330
- // Step 3: Mark as processed (fn - KV access)
1331
- .then(
1332
- fn('mark-processed', async ctx => {
1333
- const kv = ctx.openKv(':project:');
1334
- const tracker = new VersoriFileTracker(kv, 'daily-sync');
1335
- const { fileKey, recordCount } = ctx.data;
1336
-
1337
- await tracker.markFileProcessed(fileKey, {
1338
- recordCount,
1339
- timestamp: Date.now(),
1340
- });
1341
-
1342
- return {
1343
- success: true,
1344
- fileKey,
1345
- recordCount,
1346
- };
1347
- })
1348
- )
1349
-
1350
- .catch(({ data }) => {
1351
- if (data.message === 'Already processed today') {
1352
- return { skipped: true, reason: 'Already processed today' };
1353
- }
1354
- return { success: false, error: data.message };
1355
- });
1356
- ```
1357
-
1358
- ---
1359
-
1360
- ## Error Handling & Retry Strategies
1361
-
1362
- ### Basic Error Handling
1363
-
1364
- ```typescript
1365
- export const robustWorkflow = http(
1366
- 'robust',
1367
- {
1368
- connection: 'fluent_commerce',
1369
- retry: {
1370
- attempts: 3,
1371
- delay: 1000,
1372
- backoff: 2, // Exponential backoff multiplier
1373
- },
1374
- },
1375
- async ctx => {
1376
- try {
1377
- const client = await createClient(ctx);
1378
- const result = await client.graphql({ query: '...' });
1379
-
1380
- if (result.errors?.length) {
1381
- throw new Error(`GraphQL failed: ${result.errors[0].message}`);
1382
- }
1383
-
1384
- return { success: true, data: result.data };
1385
- } catch (error) {
1386
- ctx.log('error', 'Operation failed', {
1387
- error: error instanceof Error ? error.message : String(error),
1388
- stack: error instanceof Error ? error.stack : undefined,
1389
- });
1390
-
1391
- throw error; // Re-throw for retry mechanism
1392
- }
1393
- }
1394
- );
1395
- ```
1396
-
1397
- ### Advanced Error Handling with Fallback
1398
-
1399
- ```typescript
1400
- export const withFallback = http(
1401
- 'with-fallback',
1402
- {
1403
- connection: 'fluent_commerce',
1404
- },
1405
- async ctx => {
1406
- const client = await createClient(ctx);
1407
-
1408
- try {
1409
- // Primary operation
1410
- const result = await client.graphql({ query: '...' });
1411
- return { source: 'primary', data: result.data };
1412
- } catch (primaryError) {
1413
- ctx.log('warn', 'Primary operation failed, trying fallback', {
1414
- error: primaryError instanceof Error ? primaryError.message : String(primaryError),
1415
- });
1416
-
1417
- try {
1418
- // Fallback operation
1419
- const fallbackResult = await client.graphql({ query: '... (simpler query)' });
1420
- return { source: 'fallback', data: fallbackResult.data };
1421
- } catch (fallbackError) {
1422
- ctx.log('error', 'Both primary and fallback failed', {
1423
- primaryError: primaryError instanceof Error ? primaryError.message : String(primaryError),
1424
- fallbackError:
1425
- fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
1426
- });
1427
-
1428
- throw new Error('All operations failed');
1429
- }
1430
- }
1431
- }
1432
- );
1433
- ```
1434
-
1435
- ---
1436
-
1437
- ## Real-World Fluent Commerce Use Cases
1438
-
1439
- This section provides detailed, production-ready examples for common Fluent Commerce integration patterns.
1440
-
1441
- ### Use Case 1: Inventory Position Ingestion from CSV
1442
-
1443
- **Scenario**: Daily CSV file from warehouse management system → Fluent inventory positions
1444
-
1445
- ```typescript
1446
- import { schedule, fn, http } from '@versori/run';
1447
- import {
1448
- createClient,
1449
- S3DataSource,
1450
- CSVParserService,
1451
- UniversalMapper,
1452
- VersoriFileTracker
1453
- } from '@fluentcommerce/fc-connect-sdk';
1454
-
1455
- /**
1456
- * Daily inventory ingestion from S3 CSV
1457
- *
1458
- * Cron: 0 3 * * * (3 AM daily)
1459
- * Flow: Check file → Parse CSV → Transform → Send to Fluent
1460
- */
1461
- export const dailyInventoryIngestion = schedule('daily-inventory-ingestion', '0 3 * * *')
1462
- // Step 1: Check for new files (fn - KV only)
1463
- .then(fn('check-new-files', async ctx => {
1464
- const kv = ctx.openKv(':project:');
1465
- const tracker = new VersoriFileTracker(kv, 'inventory-ingestion');
1466
-
1467
- const s3Config = {
1468
- bucket: ctx.activation.getVariable<string>('S3_BUCKET'),
1469
- region: ctx.activation.getVariable<string>('AWS_REGION'),
1470
- accessKeyId: ctx.activation.getVariable<string>('AWS_ACCESS_KEY_ID'),
1471
- secretAccessKey: ctx.activation.getVariable<string>('AWS_SECRET_ACCESS_KEY'),
1472
- };
1473
-
1474
- const s3 = new S3DataSource(s3Config, ctx.log);
1475
- const files = await s3.listFiles({ prefix: 'inventory/daily/' });
1476
-
1477
- // Filter unprocessed files
1478
- const newFiles = [];
1479
- for (const file of files) {
1480
- if (!(await tracker.wasFileProcessed(file.key))) {
1481
- newFiles.push(file);
1482
- }
1483
- }
1484
-
1485
- if (newFiles.length === 0) {
1486
- throw new Error('NO_NEW_FILES');
1487
- }
1488
-
1489
- ctx.log.info('Found new inventory files', { count: newFiles.length });
1490
- return { s3Config, files: newFiles, tracker };
1491
- }))
1492
-
1493
- // Step 2: Parse and transform (fn - no API needed)
1494
- .then(fn('parse-transform', async ctx => {
1495
- const { s3Config, files, tracker } = ctx.data;
1496
- const s3 = new S3DataSource(s3Config, ctx.log);
1497
- const parser = new CSVParserService(ctx.log);
1498
-
1499
- // Mapping configuration
1500
- const mappingConfig = {
1501
- fields: {
1502
- // Fluent field: CSV column
1503
- ref: { source: 'sku', required: true },
1504
- productRef: { source: 'sku', required: true },
1505
- locationRef: { source: 'location_code', required: true },
1506
- qty: { source: 'quantity', resolver: 'sdk.parseInt', required: true },
1507
- type: { value: 'AVAILABLE' }, // Static value
1508
- storageArea: { source: 'warehouse_zone' },
1509
- expectedOn: { source: 'delivery_date', resolver: 'sdk.formatDate' },
1510
- },
1511
- };
1512
-
1513
- const mapper = new UniversalMapper(mappingConfig);
1514
- const allRecords = [];
1515
-
1516
- for (const file of files) {
1517
- const content = await s3.readFile(file.key);
1518
- const parsed = await parser.parse(content, {
1519
- headers: true,
1520
- skipEmptyLines: true
1521
- });
1522
-
1523
- // Transform each row
1524
- for (const row of parsed) {
1525
- const result = await mapper.map(row);
1526
- if (result.success) {
1527
- allRecords.push(result.data);
1528
- } else {
1529
- ctx.log.warn('Mapping failed', { row, errors: result.errors });
1530
- }
1531
- }
1532
- }
1533
-
1534
- ctx.log.info('Parsed and transformed records', {
1535
- fileCount: files.length,
1536
- recordCount: allRecords.length
1537
- });
1538
-
1539
- return { records: allRecords, files, tracker };
1540
- }))
1541
-
1542
- // Step 3: Send to Fluent (http - requires API connection)
1543
- .then(http('send-to-fluent', { connection: 'fluent_commerce' }, async ctx => {
1544
- const { records, files, tracker } = ctx.data;
1545
- const client = await createClient(ctx);
1546
-
1547
- // Create batch job
1548
- const job = await client.createJob({
1549
- name: `Daily Inventory Ingestion - ${new Date().toISOString().split('T')[0]}`,
1550
- retailerId: ctx.activation.getVariable<number>('FLUENT_RETAILER_ID'),
1551
- });
1552
-
1553
- // Send in batches of 500
1554
- const batchSize = 500;
1555
- const batches = [];
1556
-
1557
- for (let i = 0; i < records.length; i += batchSize) {
1558
- const chunk = records.slice(i, i + batchSize);
1559
- const batch = await client.sendBatch(job.id, {
1560
- action: 'UPSERT',
1561
- entityType: 'INVENTORY_POSITION',
1562
- entities: chunk,
1563
- meta: {
1564
- preprocessing: 'skip', // Delta file, skip BPP
1565
- },
1566
- });
1567
- batches.push(batch.id);
1568
- }
1569
-
1570
- // Mark files as processed
1571
- for (const file of files) {
1572
- await tracker.markFileProcessed(file.key, {
1573
- jobId: job.id,
1574
- recordCount: records.length,
1575
- timestamp: Date.now(),
1576
- });
1577
- }
1578
-
1579
- ctx.log.info('Inventory ingestion complete', {
1580
- jobId: job.id,
1581
- batches: batches.length,
1582
- totalRecords: records.length,
1583
- });
1584
-
1585
- return {
1586
- success: true,
1587
- jobId: job.id,
1588
- recordCount: records.length,
1589
- filesProcessed: files.length,
1590
- };
1591
- }))
1592
-
1593
- .catch(({ data }) => {
1594
- if (data.message === 'NO_NEW_FILES') {
1595
- return { skipped: true, reason: 'No new files to process' };
1596
- }
1597
- return { success: false, error: data.message || 'Unknown error' };
1598
- });
1599
- ```
1600
-
1601
- ### Use Case 2: Order Creation from SFCC XML Webhook
1602
-
1603
- **Scenario**: SFCC sends order XML → Validate → Create order in Fluent
1604
-
1605
- ```typescript
1606
- import { webhook, fn, http } from '@versori/run';
1607
- import {
1608
- createClient,
1609
- XMLParserService,
1610
- GraphQLMutationMapper
1611
- } from '@fluentcommerce/fc-connect-sdk';
1612
-
1613
- /**
1614
- * Receive SFCC order XML webhook and create order in Fluent
1615
- *
1616
- * POST https://{workspace}.versori.run/sfcc-order-create
1617
- * Content-Type: application/xml
1618
- */
1619
- export const sfccOrderCreate = webhook('sfcc-order-create', {
1620
- connection: 'sfcc-webhook-auth', // Platform validates API key
1621
- response: {
1622
- mode: 'sync',
1623
- onSuccess: ctx => new Response(ctx.data, {
1624
- status: 200,
1625
- headers: { 'Content-Type': 'application/xml; charset=utf-8' },
1626
- }),
1627
- onError: ctx => {
1628
- const errorXml = `<?xml version="1.0"?>
1629
- <ErrorResponse>
1630
- <Error>
1631
- <Code>PROCESSING_ERROR</Code>
1632
- <Message>${ctx.data?.message || 'Order processing failed'}</Message>
1633
- <ExecutionId>${ctx.executionId}</ExecutionId>
1634
- </Error>
1635
- </ErrorResponse>`;
1636
- return new Response(errorXml, {
1637
- status: 500,
1638
- headers: { 'Content-Type': 'application/xml; charset=utf-8' },
1639
- });
1640
- },
1641
- },
1642
- })
1643
- // Step 1: Parse XML (fn - no API needed)
1644
- .then(fn('parse-xml', async ctx => {
1645
- const parser = new XMLParserService(ctx.log);
1646
- const xmlData = await parser.parse(ctx.data);
1647
-
1648
- // Validate required fields
1649
- if (!xmlData.order?.['@id']) {
1650
- throw new Error('Invalid order XML: missing order ID');
1651
- }
1652
-
1653
- ctx.log.info('Parsed SFCC order XML', {
1654
- orderId: xmlData.order['@id'],
1655
- customerEmail: xmlData.order.customer?.email
1656
- });
1657
-
1658
- return { xmlData };
1659
- }))
1660
-
1661
- // Step 2: Transform and create order (http - Fluent API access)
1662
- .then(http('create-fluent-order', { connection: 'fluent_commerce' }, async ctx => {
1663
- const { xmlData } = ctx.data;
1664
- const client = await createClient(ctx);
1665
-
1666
- // GraphQL mutation mapping config
1667
- const mappingConfig = {
1668
- mutationName: 'createOrder',
1669
- sourceFormat: 'xml',
1670
- arguments: {
1671
- input: {
1672
- ref: { source: 'order.@id' },
1673
- type: { value: 'STANDARD' },
1674
- retailer: { source: 'order.@retailerId', resolver: 'sdk.parseInt' },
1675
- customer: {
1676
- firstName: { source: 'order.customer.firstname' },
1677
- lastName: { source: 'order.customer.lastname' },
1678
- email: { source: 'order.customer.email' },
1679
- },
1680
- items: {
1681
- source: 'order.items.item',
1682
- isArray: true,
1683
- mapping: {
1684
- productRef: { source: '@sku' },
1685
- quantity: { source: 'quantity', resolver: 'sdk.parseInt' },
1686
- price: {
1687
- value: { source: 'price', resolver: 'sdk.parseFloat' },
1688
- currency: { source: '@currency', default: 'USD' },
1689
- },
1690
- },
1691
- },
1692
- fulfilment: {
1693
- type: { value: 'STANDARD' },
1694
- deliveryType: { source: 'order.shipping.method' },
1695
- deliveryAddress: {
1696
- name: { source: 'order.shipping.address.name' },
1697
- street: { source: 'order.shipping.address.street' },
1698
- city: { source: 'order.shipping.address.city' },
1699
- state: { source: 'order.shipping.address.state' },
1700
- postcode: { source: 'order.shipping.address.postalcode' },
1701
- country: { source: 'order.shipping.address.country' },
1702
- },
1703
- },
1704
- payments: {
1705
- source: 'order.payments.payment',
1706
- isArray: true,
1707
- mapping: {
1708
- method: { source: '@method' },
1709
- amount: { source: 'amount', resolver: 'sdk.parseFloat' },
1710
- cardType: { source: 'creditcard.type' },
1711
- },
1712
- },
1713
- },
1714
- },
1715
- };
1716
-
1717
- const mapper = new GraphQLMutationMapper(mappingConfig, ctx.log, { fluentClient: client });
1718
- const { mutation, variables } = await mapper.generateMutation(xmlData);
1719
-
1720
- // Execute mutation
1721
- const result = await client.graphql({ query: mutation, variables });
1722
-
1723
- if (result.errors?.length) {
1724
- throw new Error(`Order creation failed: ${result.errors[0].message}`);
1725
- }
1726
-
1727
- const order = result.data.createOrder;
1728
- ctx.log.info('Order created in Fluent', {
1729
- fluentOrderId: order.id,
1730
- sfccOrderId: xmlData.order['@id']
1731
- });
1732
-
1733
- return {
1734
- fluentOrderId: order.id,
1735
- fluentOrderRef: order.ref,
1736
- sfccOrderId: xmlData.order['@id'],
1737
- };
1738
- }))
1739
-
1740
- // Step 3: Generate success XML response (fn)
1741
- .then(fn('generate-response', ctx => {
1742
- const { fluentOrderId, sfccOrderId } = ctx.data;
1743
- return `<?xml version="1.0"?>
1744
- <OrderResponse>
1745
- <Success>true</Success>
1746
- <SFCCOrderId>${sfccOrderId}</SFCCOrderId>
1747
- <FluentOrderId>${fluentOrderId}</FluentOrderId>
1748
- <Timestamp>${new Date().toISOString()}</Timestamp>
1749
- </OrderResponse>`;
1750
- }))
1751
-
1752
- .catch(({ data }) => {
1753
- return { message: data instanceof Error ? data.message : String(data) };
1754
- });
1755
- ```
1756
-
1757
- ### Use Case 3: Virtual Position Extraction to SFTP
1758
-
1759
- **Scenario**: Scheduled extraction of Fluent virtual positions → XML → Upload to SFTP
1760
-
1761
- ```typescript
1762
- import { Buffer } from 'node:buffer'; // ⚠️ Required for Deno compatibility
1763
- import { schedule, http, fn } from '@versori/run';
1764
- import {
1765
- createClient,
1766
- ExtractionOrchestrator,
1767
- SftpDataSource,
1768
- VersoriFileTracker
1769
- } from '@fluentcommerce/fc-connect-sdk';
1770
- import { XMLBuilder } from 'fast-xml-parser';
1771
-
1772
- /**
1773
- * Extract virtual positions and upload to SFTP as XML
1774
- *
1775
- * Cron: 0 */4 * * * (every 4 hours)
1776
- */
1777
- export const virtualPositionExtraction = schedule('virtual-position-extraction', '0 */4 * * *')
1778
- // Step 1: Extract from Fluent (http - API access)
1779
- .then(http('extract-positions', { connection: 'fluent_commerce' }, async ctx => {
1780
- const client = await createClient(ctx);
1781
-
1782
- // Define extraction config
1783
- const extractionConfig = {
1784
- query: `
1785
- query GetVirtualPositions($first: Int!, $after: String) {
1786
- virtualPositions(first: $first, after: $after) {
1787
- edges {
1788
- node {
1789
- id
1790
- ref
1791
- productRef
1792
- groupRef
1793
- onHand
1794
- quantity
1795
- type
1796
- status
1797
- createdOn
1798
- updatedOn
1799
- }
1800
- cursor
1801
- }
1802
- pageInfo {
1803
- hasNextPage
1804
- endCursor
1805
- }
1806
- }
1807
- }
1808
- `,
1809
- variables: { first: 200 },
1810
- pathToData: 'virtualPositions',
1811
- pathToCursor: 'pageInfo.endCursor',
1812
- pathToHasNext: 'pageInfo.hasNextPage',
1813
- };
1814
-
1815
- // Use ExtractionOrchestrator for auto-pagination
1816
- const orchestrator = new ExtractionOrchestrator(client, ctx.log);
1817
- const result = await orchestrator.extractWithPagination(extractionConfig);
1818
-
1819
- if (!result.success) {
1820
- throw new Error(`Extraction failed: ${result.errors?.join(', ')}`);
1821
- }
1822
-
1823
- ctx.log.info('Extracted virtual positions', {
1824
- recordCount: result.data?.length || 0
1825
- });
1826
-
1827
- return { positions: result.data || [] };
1828
- }))
1829
-
1830
- // Step 2: Transform to XML (fn - no API)
1831
- .then(fn('transform-to-xml', ctx => {
1832
- const { positions } = ctx.data;
1833
-
1834
- const xmlBuilder = new XMLBuilder({
1835
- ignoreAttributes: false,
1836
- attributeNamePrefix: '@',
1837
- format: true,
1838
- indentBy: ' ',
1839
- });
1840
-
1841
- const xmlObject = {
1842
- '?xml': { '@version': '1.0', '@encoding': 'UTF-8' },
1843
- VirtualPositionFeed: {
1844
- '@timestamp': new Date().toISOString(),
1845
- '@recordCount': positions.length,
1846
- Positions: {
1847
- Position: positions.map(pos => ({
1848
- '@id': pos.id,
1849
- Ref: pos.ref,
1850
- ProductRef: pos.productRef,
1851
- GroupRef: pos.groupRef,
1852
- OnHand: pos.onHand,
1853
- Quantity: pos.quantity,
1854
- Type: pos.type,
1855
- Status: pos.status,
1856
- CreatedOn: pos.createdOn,
1857
- UpdatedOn: pos.updatedOn,
1858
- })),
1859
- },
1860
- },
1861
- };
1862
-
1863
- const xmlContent = xmlBuilder.build(xmlObject);
1864
-
1865
- const fileName = `virtual-positions-${new Date().toISOString().replace(/:/g, '-')}.xml`;
1866
-
1867
- ctx.log.info('Generated XML feed', { fileName, recordCount: positions.length });
1868
-
1869
- return { xmlContent, fileName, recordCount: positions.length };
1870
- }))
1871
-
1872
- // Step 3: Upload to SFTP (fn - external SFTP, not Fluent API)
1873
- .then(fn('upload-to-sftp', async ctx => {
1874
- const { xmlContent, fileName, recordCount } = ctx.data;
1875
-
1876
- const sftpConfig = {
1877
- host: ctx.activation.getVariable<string>('SFTP_HOST'),
1878
- port: ctx.activation.getVariable<number>('SFTP_PORT') || 22,
1879
- username: ctx.activation.getVariable<string>('SFTP_USERNAME'),
1880
- password: ctx.activation.getVariable<string>('SFTP_PASSWORD'),
1881
- };
1882
-
1883
- const sftp = new SftpDataSource(sftpConfig, ctx.log);
1884
- await sftp.connect();
1885
-
1886
- try {
1887
- const remotePath = `/outbound/${fileName}`;
1888
- await sftp.uploadFile(remotePath, Buffer.from(xmlContent, 'utf-8'));
1889
-
1890
- ctx.log.info('Uploaded to SFTP', { remotePath, recordCount });
1891
-
1892
- // Track in KV
1893
- const kv = ctx.openKv(':project:');
1894
- const tracker = new VersoriFileTracker(kv, 'virtual-position-extraction');
1895
- await tracker.markFileProcessed(fileName, {
1896
- recordCount,
1897
- timestamp: Date.now(),
1898
- remotePath,
1899
- });
1900
-
1901
- return {
1902
- success: true,
1903
- fileName,
1904
- remotePath,
1905
- recordCount,
1906
- };
1907
- } finally {
1908
- await sftp.disconnect();
1909
- }
1910
- }))
1911
-
1912
- .catch(({ data }) => ({
1913
- success: false,
1914
- error: data instanceof Error ? data.message : String(data),
1915
- }));
1916
- ```
1917
-
1918
- ### Use Case 4: Multi-Tenant Order Status Webhook
1919
-
1920
- **Scenario**: Multiple customers share one connector, route to correct Fluent instance
1921
-
1922
- ```typescript
1923
- import { webhook, fn, http } from '@versori/run';
1924
- import { createClient } from '@fluentcommerce/fc-connect-sdk';
1925
-
1926
- /**
1927
- * Multi-tenant order status update
1928
- *
1929
- * POST https://{workspace}.versori.run/order-status-update
1930
- * Body: { customerId, orderId, status }
1931
- */
1932
- export const orderStatusUpdate = webhook('order-status-update', {
1933
- response: { mode: 'sync' },
1934
- })
1935
- // Step 1: Route to correct connection (fn)
1936
- .then(fn('route-connection', ctx => {
1937
- const { customerId, orderId, status } = ctx.data;
1938
-
1939
- if (!customerId || !orderId || !status) {
1940
- throw new Error('Missing required fields: customerId, orderId, status');
1941
- }
1942
-
1943
- // Access all connections via activation.connections
1944
- const connections = ctx.activation.connections;
1945
-
1946
- // Find customer-specific Fluent connection
1947
- const connectionName = `fluent_${customerId.toLowerCase()}`;
1948
- const connection = connections?.find(c => c.name === connectionName);
1949
-
1950
- if (!connection) {
1951
- throw new Error(`No Fluent connection found for customer: ${customerId}`);
1952
- }
1953
-
1954
- ctx.log.info('Routed to customer connection', {
1955
- customerId,
1956
- connectionName,
1957
- connectionId: connection.id
1958
- });
1959
-
1960
- return { connectionName, orderId, status, customerId };
1961
- }))
1962
-
1963
- // Step 2: Update order status (http - with dynamic connection)
1964
- .then(http('update-order', (ctx) => {
1965
- // Return dynamic connection name
1966
- return { connection: ctx.data.connectionName };
1967
- }, async ctx => {
1968
- const { orderId, status, customerId } = ctx.data;
1969
- const client = await createClient(ctx);
1970
-
1971
- // Update order status
1972
- const result = await client.graphql({
1973
- query: `
1974
- mutation UpdateOrder($ref: String!, $status: String!) {
1975
- updateOrder(input: { ref: $ref, status: $status }) {
1976
- id
1977
- ref
1978
- status
1979
- }
1980
- }
1981
- `,
1982
- variables: { ref: orderId, status },
1983
- });
1984
-
1985
- if (result.errors?.length) {
1986
- throw new Error(`Failed to update order: ${result.errors[0].message}`);
1987
- }
1988
-
1989
- ctx.log.info('Order status updated', {
1990
- customerId,
1991
- orderId,
1992
- newStatus: status
1993
- });
1994
-
1995
- return {
1996
- success: true,
1997
- customerId,
1998
- orderId,
1999
- status: result.data.updateOrder.status,
2000
- };
2001
- }))
2002
-
2003
- .catch(({ data }) => ({
2004
- success: false,
2005
- error: data instanceof Error ? data.message : String(data),
2006
- status: 500,
2007
- }));
2008
- ```
2009
-
2010
- ### Use Case 5: Product Catalog Sync with Enrichment
2011
-
2012
- **Scenario**: Fetch products from PIM system → Enrich → Update Fluent catalog
2013
-
2014
- ```typescript
2015
- import { schedule, fn, http } from '@versori/run';
2016
- import {
2017
- createClient,
2018
- UniversalMapper,
2019
- VersoriFileTracker
2020
- } from '@fluentcommerce/fc-connect-sdk';
2021
-
2022
- /**
2023
- * Daily product catalog sync
2024
- *
2025
- * Cron: 0 4 * * * (4 AM daily)
2026
- */
2027
- export const productCatalogSync = schedule('product-catalog-sync', '0 4 * * *')
2028
- // Step 1: Fetch from external PIM (http - external API)
2029
- .then(http('fetch-from-pim', { connection: 'pim_system' }, async ctx => {
2030
- // Fetch products from PIM API
2031
- const response = await ctx.fetch('/api/v1/products?updated_since=yesterday');
2032
- const products = await response.json();
2033
-
2034
- ctx.log.info('Fetched products from PIM', { count: products.length });
2035
- return { pimProducts: products };
2036
- }))
2037
-
2038
- // Step 2: Enrich and transform (fn - data processing)
2039
- .then(fn('enrich-transform', async ctx => {
2040
- const { pimProducts } = ctx.data;
2041
-
2042
- const mappingConfig = {
2043
- fields: {
2044
- ref: { source: 'sku', required: true },
2045
- type: { value: 'STANDARD' },
2046
- name: { source: 'name', required: true },
2047
- summary: { source: 'short_description' },
2048
- gtin: { source: 'barcode' },
2049
- prices: {
2050
- source: 'pricing',
2051
- isArray: true,
2052
- mapping: {
2053
- type: { source: 'price_type' },
2054
- value: { source: 'amount', resolver: 'sdk.parseFloat' },
2055
- currency: { source: 'currency', default: 'USD' },
2056
- },
2057
- },
2058
- attributes: {
2059
- source: 'custom_attributes',
2060
- isArray: true,
2061
- mapping: {
2062
- name: { source: 'attr_name' },
2063
- value: { source: 'attr_value', resolver: 'sdk.toString' },
2064
- type: { value: 'STRING' },
2065
- },
2066
- },
2067
- taxType: {
2068
- type: { source: 'tax.type' },
2069
- group: { source: 'tax.group' },
2070
- tariff: { source: 'tax.tariff_code' },
2071
- },
2072
- },
2073
- };
2074
-
2075
- const mapper = new UniversalMapper(mappingConfig);
2076
- const enrichedProducts = [];
2077
-
2078
- for (const product of pimProducts) {
2079
- const result = await mapper.map(product);
2080
- if (result.success) {
2081
- enrichedProducts.push(result.data);
2082
- } else {
2083
- ctx.log.warn('Product mapping failed', {
2084
- sku: product.sku,
2085
- errors: result.errors
2086
- });
2087
- }
2088
- }
2089
-
2090
- ctx.log.info('Products enriched', { count: enrichedProducts.length });
2091
- return { products: enrichedProducts };
2092
- }))
2093
-
2094
- // Step 3: Send to Fluent (http - Fluent API)
2095
- .then(http('sync-to-fluent', { connection: 'fluent_commerce' }, async ctx => {
2096
- const { products } = ctx.data;
2097
- const client = await createClient(ctx);
2098
-
2099
- const job = await client.createJob({
2100
- name: `Product Catalog Sync - ${new Date().toISOString().split('T')[0]}`,
2101
- retailerId: ctx.activation.getVariable<number>('FLUENT_RETAILER_ID'),
2102
- });
2103
-
2104
- // Send in chunks
2105
- const batchSize = 100;
2106
- const batches = [];
2107
-
2108
- for (let i = 0; i < products.length; i += batchSize) {
2109
- const chunk = products.slice(i, i + batchSize);
2110
- const batch = await client.sendBatch(job.id, {
2111
- action: 'UPSERT',
2112
- entityType: 'PRODUCT',
2113
- entities: chunk,
2114
- meta: {
2115
- preprocessing: 'apply', // Full snapshot, use BPP
2116
- },
2117
- });
2118
- batches.push(batch.id);
2119
- }
2120
-
2121
- ctx.log.info('Product sync complete', {
2122
- jobId: job.id,
2123
- productCount: products.length,
2124
- batchCount: batches.length,
2125
- });
2126
-
2127
- return {
2128
- success: true,
2129
- jobId: job.id,
2130
- productCount: products.length,
2131
- };
2132
- }))
2133
-
2134
- .catch(({ data }) => ({
2135
- success: false,
2136
- error: data instanceof Error ? data.message : String(data),
2137
- }));
2138
- ```
2139
-
2140
- ---
2141
-
2142
- ## ⚠️ CRITICAL: `.parallel()` Error Behavior
2143
-
2144
- **BEFORE USING `.parallel()` - READ THIS:**
2145
-
2146
- The `.parallel()` method has **all-or-nothing error behavior** that makes it **UNSUITABLE for most batch processing use cases**.
2147
-
2148
- ### What Happens When One Task Fails
2149
-
2150
- ```typescript
2151
- // ❌ PROBLEM: If ANY file fails, you lose ALL results
2152
- schedule('process-files', '0 2 * * *')
2153
- .then(fn('fetch', () => ['file1.xml', 'file2.xml', 'file3.xml']))
2154
- .unpack()
2155
- .parallel(fn('process', async (ctx) => {
2156
- return await processFile(ctx.data);
2157
- }));
2158
- ```
2159
-
2160
- **If `file2.xml` fails:**
2161
- - ❌ `file1.xml` might have processed successfully but **result is lost**
2162
- - ❌ `file3.xml` never runs (or is cancelled mid-execution)
2163
- - ❌ **Entire workflow fails**
2164
- - ❌ **NO partial results returned**
2165
- - ❌ You can't retry only failed items
2166
-
2167
- ### Implementation Detail
2168
-
2169
- The `.parallel()` method uses RxJS `mergeMap` + `toArray()`:
2170
- - Any error in `mergeMap` propagates to the stream
2171
- - `toArray()` never completes if the stream errors
2172
- - **Result: one failure stops everything**
2173
-
2174
- ### Comparison: `.parallel()` vs `Promise.allSettled`
2175
-
2176
- | Feature | `.parallel()` | `Promise.allSettled` |
2177
- |---------|--------------|---------------------|
2178
- | Fault tolerance | ❌ Fails on first error | ✅ Continues on errors |
2179
- | Partial results | ❌ None if any fail | ✅ All results |
2180
- | Error isolation | ❌ No isolation | ✅ Complete isolation |
2181
- | Retry failures | ❌ Must retry all | ✅ Retry only failures |
2182
-
2183
- ### When to Use `.parallel()`
2184
-
2185
- **ONLY use `.parallel()` when:**
2186
- 1. ✅ **All tasks MUST succeed** (transactional requirement)
2187
- 2. ✅ **Small number of items** (3-10 max)
2188
- 3. ✅ **Fast, reliable operations** (< 10 seconds each)
2189
- 4. ✅ **Partial success is worse than complete failure**
2190
-
2191
- **Examples where `.parallel()` is appropriate:**
2192
- - Fetching 3-5 required entity types (all needed for next step)
2193
- - Validating multiple webhook signatures (all must be valid)
2194
- - Small, predictable, low-failure-rate operations
2195
-
2196
- ### When to Use `Promise.allSettled` (SDK Pattern)
2197
-
2198
- **PREFER `Promise.allSettled` for:**
2199
- 1. ✅ **Batch processing** (files, records, entities)
2200
- 2. ✅ **Large datasets** (10+ items)
2201
- 3. ✅ **Failures are expected** (network issues, bad data)
2202
- 4. ✅ **Partial success is valuable** (process 990/1000 items)
2203
- 5. ✅ **Need retry logic** (retry only failures)
2204
-
2205
- **Example using SDK pattern:**
2206
- ```typescript
2207
- export const batchProcess = schedule('batch', '0 */6 * * *')
2208
- .then(
2209
- http('process', { connection: 'fluent_commerce' }, async (ctx) => {
2210
- const items = await fetchItems();
2211
-
2212
- // ✅ Use Promise.allSettled for fault tolerance
2213
- const results = await Promise.allSettled(
2214
- items.map(item =>
2215
- processItem(item)
2216
- .then(result => ({ success: true, item, result }))
2217
- .catch(error => ({ success: false, item, error: error.message }))
2218
- )
2219
- );
2220
-
2221
- const successes = results
2222
- .filter(r => r.status === 'fulfilled')
2223
- .map(r => r.value);
2224
-
2225
- const failures = results
2226
- .filter(r => r.status === 'rejected' || !r.value.success);
2227
-
2228
- ctx.log.info(`Processed ${successes.length} items, ${failures.length} failed`);
2229
-
2230
- // ✅ Handle failures gracefully
2231
- if (failures.length > 0) {
2232
- // Store failures for retry
2233
- const kv = ctx.openKv(':project:');
2234
- await kv.set('failed-items', failures);
2235
- }
2236
-
2237
- return { successes, failures };
2238
- })
2239
- );
2240
- ```
2241
-
2242
- ---
2243
-
2244
- ## Parallel Processing with `.unpack()` and `.parallel()`
2245
-
2246
- Versori supports parallel processing via `.unpack()` and `.parallel()` methods, but **they have critical limitations** that affect fault tolerance and error handling (see warning above).
2247
-
2248
- ### Available Methods
2249
-
2250
- ```typescript
2251
- import { schedule, fn } from '@versori/run';
2252
-
2253
- schedule('process-files', '0 2 * * *')
2254
- .then(fn('fetch-files', (ctx) => {
2255
- return ['file1.xml', 'file2.xml', 'file3.xml']; // Returns array
2256
- }))
2257
- .unpack() // ✅ Converts array to ArrayTask
2258
- .parallel( // ✅ Processes each item in parallel
2259
- fn('process-file', async (ctx) => {
2260
- // ctx.data is now a single item from the array
2261
- return await processFile(ctx.data);
2262
- })
2263
- );
2264
- ```
2265
-
2266
- ### ⚠️ CRITICAL LIMITATIONS
2267
-
2268
- #### 1. **NOT Fault-Tolerant: One Failure Stops Everything**
2269
-
2270
- **Problem**: If any parallel task fails, the **entire workflow fails** and **no results are returned**.
2271
-
2272
- ```typescript
2273
- // ❌ RISKY: If file2.xml fails, entire workflow fails
2274
- schedule('process-files', '0 2 * * *')
2275
- .then(fn('fetch-files', (ctx) => ['file1.xml', 'file2.xml', 'file3.xml']))
2276
- .unpack()
2277
- .parallel(fn('process-file', async (ctx) => {
2278
- // If ANY file fails here, ALL results lost
2279
- return await processFile(ctx.data);
2280
- }));
2281
-
2282
- // Result:
2283
- // ✅ file1.xml might complete
2284
- // ❌ file2.xml fails → ENTIRE WORKFLOW FAILS
2285
- // ❌ file3.xml NEVER RUNS (or gets cancelled)
2286
- // ❌ No partial results returned
2287
- ```
2288
-
2289
- **Why**: Uses RxJS `mergeMap` + `toArray()` which stops on first error.
2290
-
2291
- #### 2. **Shared Timeout Across All Tasks**
2292
-
2293
- **Problem**: All parallel tasks share the **same workflow timeout**.
2294
-
2295
- | Trigger Type | Timeout | Impact |
2296
- |-------------|---------|--------|
2297
- | **HTTP/Webhook** | 30 seconds | All parallel tasks must complete in 30s total |
2298
- | **Schedule** | 5 minutes | All parallel tasks must complete in 5min total |
2299
-
2300
- ```typescript
2301
- // ⚠️ If processing 10 files in parallel:
2302
- schedule('process-files', '0 2 * * *')
2303
- .then(fn('fetch-files', (ctx) => Array(10).fill('file.xml')))
2304
- .unpack()
2305
- .parallel(fn('process-file', async (ctx) => {
2306
- // If ANY file takes > 5 minutes, ENTIRE workflow fails
2307
- return await processFile(ctx.data);
2308
- }));
2309
- ```
2310
-
2311
- #### 3. **NOT ACID Compliant**
2312
-
2313
- **Problem**: No atomicity guarantees. If one task fails:
2314
- - ✅ Successful tasks may have completed their work
2315
- - ❌ But you won't know which ones succeeded
2316
- - ❌ No rollback mechanism
2317
- - ❌ No transaction support
2318
-
2319
- ### ✅ Recommended: Use `Promise.allSettled()` Instead
2320
-
2321
- For **fault-tolerant** parallel processing, use `Promise.allSettled()` which continues even when some tasks fail:
2322
-
2323
- ```typescript
2324
- import { schedule, fn } from '@versori/run';
2325
-
2326
- schedule('process-files', '0 2 * * *')
2327
- .then(fn('process-all-files', async (ctx) => {
2328
- const files = ['file1.xml', 'file2.xml', 'file3.xml'];
2329
-
2330
- // ✅ Fault-tolerant: Continues even if some fail
2331
- const results = await Promise.allSettled(
2332
- files.map(file => processFile(file))
2333
- );
2334
-
2335
- // Process results
2336
- const successes = results
2337
- .filter(r => r.status === 'fulfilled')
2338
- .map(r => r.value);
2339
-
2340
- const failures = results
2341
- .filter(r => r.status === 'rejected')
2342
- .map(r => ({ file: r.reason.file, error: r.reason.message }));
2343
-
2344
- ctx.log.info(`Processed ${successes.length} files, ${failures.length} failed`);
2345
-
2346
- return { successes, failures };
2347
- }));
2348
- ```
2349
-
2350
- **Comparison**:
2351
-
2352
- | Feature | `.unpack().parallel()` | `Promise.allSettled()` |
2353
- |---------|------------------------|------------------------|
2354
- | Fault tolerance | ❌ Fails on first error | ✅ Continues on errors |
2355
- | Partial results | ❌ No results if any fail | ✅ Returns all results |
2356
- | Error isolation | ❌ No isolation | ✅ Complete isolation |
2357
- | Time limits | ⚠️ Shared timeout | ⚠️ Shared timeout |
2358
- | Best for | All-or-nothing workflows | Real-world with failures |
2359
-
2360
- ### When to Use `.unpack().parallel()`
2361
-
2362
- **Only use if**:
2363
- - ✅ All tasks **must** succeed (transactional)
2364
- - ✅ You can wrap each task with error handling that returns error objects instead of throwing
2365
- - ✅ You're okay with losing all results on any failure
2366
-
2367
- **Example with error handling**:
2368
-
2369
- ```typescript
2370
- schedule('process-files', '0 2 * * *')
2371
- .then(fn('fetch-files', (ctx) => ['file1.xml', 'file2.xml', 'file3.xml']))
2372
- .unpack()
2373
- .parallel(fn('process-file', async (ctx) => {
2374
- try {
2375
- const result = await processFile(ctx.data);
2376
- return { success: true, file: ctx.data, result };
2377
- } catch (error) {
2378
- // ✅ Return error object instead of throwing
2379
- return {
2380
- success: false,
2381
- file: ctx.data,
2382
- error: error.message
2383
- };
2384
- }
2385
- }))
2386
- .then(fn('process-results', (ctx) => {
2387
- // ✅ Now you have all results (success + failures)
2388
- const results = ctx.data;
2389
- const successes = results.filter(r => r.success);
2390
- const failures = results.filter(r => !r.success);
2391
-
2392
- ctx.log.info(`Processed ${successes.length} files, ${failures.length} failed`);
2393
- return { successes, failures };
2394
- }));
2395
- ```
2396
-
2397
- ### When to Use `Promise.allSettled()` (Recommended)
2398
-
2399
- **Use `Promise.allSettled()` for**:
2400
- - ✅ Batch API submissions
2401
- - ✅ File processing
2402
- - ✅ Any scenario where failures are expected
2403
- - ✅ When you need partial results
2404
-
2405
- **Already implemented in your code**:
2406
-
2407
- ```typescript
2408
- // ✅ Your current BatchProcessorService uses this pattern
2409
- const batchResults = await Promise.allSettled(
2410
- batchChunk.map((chunk, index) =>
2411
- this.client.sendBatch(jobId, {
2412
- action: 'UPSERT',
2413
- entityType: 'INVENTORY',
2414
- entities: chunk,
2415
- }).then(batch => ({ success: true, batch }))
2416
- .catch(error => ({ success: false, error }))
2417
- )
2418
- );
2419
- ```
2420
-
2421
- ---
2422
-
2423
- ## Key Takeaways
2424
-
2425
- - 🎯 **http()** for external API calls - requires connection, provides `ctx.fetch`
2426
- - 🎯 **webhook()** for receiving requests - provides `ctx.data`, `ctx.request()` for headers
2427
- - 🎯 **schedule()** for time-based tasks - supports cron patterns, can have connection
2428
- - 🎯 **fn()** for internal processing - NO external API access, has `ctx.openKv()`
2429
- - 🎯 **Non-JSON responses** require custom `onSuccess`/`onError` handlers returning Response objects
2430
- - 🎯 **Workflow composition** with `.then()` and `.catch()` enables multi-step patterns
2431
- - 🎯 **Error handling** with try/catch and retry configuration ensures robustness
2432
- - ⚠️ **`.unpack().parallel()`** exists but is NOT fault-tolerant - use `Promise.allSettled()` for resilience
2433
-
2434
- ---
2435
-
2436
- ## Practice Exercise
2437
-
2438
- Create a scheduled workflow that:
2439
-
2440
- 1. Runs hourly (cron: `0 * * * *`)
2441
- 2. Fetches inventory from an S3 CSV file
2442
- 3. Checks KV storage to avoid duplicate processing
2443
- 4. Transforms data using UniversalMapper
2444
- 5. Sends to Fluent via Batch API
2445
- 6. Marks file as processed in KV storage
2446
- 7. Returns XML response on error
2447
-
2448
- **Hints**:
2449
-
2450
- - Use `schedule()` with connection
2451
- - Chain with `fn()` for KV checks
2452
- - Use `http()` context for Fluent API calls
2453
- - Implement custom error handler for XML response
2454
-
2455
- **Solution** available in [Module 8: Best Practices](./platforms-versori-08-best-practices.md#practice-solutions)
2456
-
2457
- ---
2458
-
2459
- ## Next Steps
2460
-
2461
- Now that you've mastered all workflow types, let's explore connection management in detail.
2462
-
2463
- Continue to [Module 5: Connections →](./platforms-versori-05-connections.md) to learn about OAuth2 connection types, management, and validation.
2464
-
2465
- ---
2466
-
2467
- ## Related Documentation
2468
-
2469
- - [Module 3: Authentication](./platforms-versori-03-authentication.md) - OAuth2 basics
2470
- - [Module 5: Connections](./platforms-versori-05-connections.md) - Connection management
2471
- - [Module 6: KV Storage](./platforms-versori-06-kv-storage.md) - State management
2472
- - [Webhook Response Patterns](.././modules/platforms-versori-04-workflows.md#critical-non-json-response-handlers-xml-html-csv) - Complete non-JSON response guide
2473
-
2474
- ---
2475
-
2476
- [← Previous: Module 3](./platforms-versori-03-authentication.md) | [Back to Guide](../platforms-versori-readme.md) | [Next: Module 5: Connections →](./platforms-versori-05-connections.md)
1
+ # Module 4: Workflows - HTTP, Webhooks, Scheduled & Internal Functions
2
+
3
+ [← Back to Versori Platform Guide](../platforms-versori-readme.md)
4
+
5
+ **Module 4 of 8** | **Level**: Intermediate | **Time**: 30 minutes
6
+
7
+ ---
8
+
9
+ ## Learning Objectives
10
+
11
+ By the end of this module, you will:
12
+
13
+ - ✅ Master all four workflow types: http(), webhook(), schedule(), fn()
14
+ - ✅ Understand when to use each workflow type
15
+ - ✅ Implement HTTP workflows with Fluent API calls
16
+ - ✅ Build webhook receivers with signature validation
17
+ - ✅ Create scheduled tasks with cron patterns
18
+ - ✅ Compose multi-step workflows with fn()
19
+ - ✅ **Handle non-JSON responses** (XML, HTML, CSV) correctly
20
+ - ✅ Implement error handling and retry strategies
21
+
22
+ ---
23
+
24
+ ## Workflow Types Overview
25
+
26
+ Versori provides four fundamental workflow types, each with specific capabilities and use cases:
27
+
28
+ | Type | Purpose | External API Access | Context | Use When |
29
+ | -------------- | --------------------- | -------------------------------- | ---------------------------- | ------------------------------------- |
30
+ | **http()** | External API calls | ✅ Yes (requires connection) | `fetch`, `log`, `activation` | Query/mutate Fluent API |
31
+ | **webhook()** | Receive HTTP requests | ❌ No (unless chained with http) | `data`, `request()`, `log` | Receive SFCC orders, Rubix events |
32
+ | **schedule()** | Time-based tasks | ✅ Yes (if connection provided) | `fetch`, `log`, `activation` | Daily sync, hourly extraction |
33
+ | **fn()** | Internal processing | ❌ No | `openKv`, `log`, `request()` | Data transformation, state management |
34
+
35
+ ---
36
+
37
+ ## ⚠️ CRITICAL: Accessing Headers & Choosing Workflow Types
38
+
39
+ ### Accessing HTTP Headers
40
+
41
+ **The `Context<D>` interface does NOT have a `headers` property.** Always use `ctx.request()`:
42
+
43
+ ```typescript
44
+ // ✅ CORRECT - Access headers via request()
45
+ const req = ctx.request();
46
+ const apiKey = req?.headers['x-api-key'] as string;
47
+ const contentType = req?.headers['content-type'];
48
+
49
+ // ❌ WRONG - ctx.headers doesn't exist in @versori/run
50
+ const apiKey = ctx.headers?.get?.('x-api-key'); // ERROR!
51
+ ```
52
+
53
+ ### When to Use http() vs fn()
54
+
55
+ **Decision Rule:** Do you need to call external APIs (Fluent Commerce, S3 presigned URLs, etc.)?
56
+
57
+ #### Use `http()` When:
58
+
59
+ - ✅ Calling Fluent Commerce GraphQL API
60
+ - ✅ Need connection credentials and authentication
61
+ - ✅ Require `ctx.fetch` with OAuth2 auth
62
+ - ✅ Need `connectionVariables` or `baseUrl`
63
+
64
+ **What you get in http():**
65
+
66
+ ```typescript
67
+ http('my-task', { connection: 'fluent_commerce' }, async ctx => {
68
+ ctx.fetch; // ✅ Authenticated fetch with connection
69
+ ctx.connectionVariables; // ✅ Connection configuration
70
+ ctx.baseUrl; // ✅ API base URL from connection
71
+ ctx.request(); // ✅ HTTP request object (for headers)
72
+ (ctx.log, ctx.data, ctx.activation, ctx.openKv()); // ✅ All standard context
73
+
74
+ // Can call external APIs:
75
+ const client = await createClient(ctx); // ✅ Works!
76
+ });
77
+ ```
78
+
79
+ #### Use `fn()` When:
80
+
81
+ - ✅ Pure data transformation (no external API calls)
82
+ - ✅ State management with KV storage only
83
+ - ✅ Parsing, mapping, validation logic
84
+ - ✅ File tracking and deduplication
85
+
86
+ **What you get in fn():**
87
+
88
+ ```typescript
89
+ fn('my-task', async ctx => {
90
+ ctx.data; // ✅ Input data
91
+ ctx.log; // ✅ Logger
92
+ ctx.activation; // ✅ Variables from activation
93
+ ctx.openKv(); // ✅ KV storage access
94
+ ctx.request(); // ✅ HTTP request object (for headers)
95
+
96
+ // ❌ NO ctx.fetch - cannot make authenticated API calls
97
+ // ❌ NO connectionVariables - no connection context
98
+ // ❌ CANNOT call createClient() - will fail
99
+ });
100
+ ```
101
+
102
+ ### Example: Adhoc Extraction (Requires http())
103
+
104
+ ```typescript
105
+ // ✅ CORRECT - Use http() for Fluent API access with connection-based security
106
+ export const adhocExtraction = webhook('adhoc-extract', {
107
+ connection: 'webhook-auth', // ← Platform validates API key automatically
108
+ response: { mode: 'sync' },
109
+ }).then(
110
+ http('execute-extraction', { connection: 'fluent_commerce' }, async ctx => {
111
+ const { log, data } = ctx;
112
+
113
+ // ✅ If we're here, authentication already passed via connection
114
+ // No manual validation code needed!
115
+
116
+ // http() provides connection context for createClient()
117
+ const client = await createClient(ctx); // ✅ Works!
118
+ const result = await client.graphql({ query: '...' });
119
+
120
+ return { success: true, data: result.data };
121
+ })
122
+ );
123
+
124
+ // ❌ WRONG - fn() cannot access Fluent API
125
+ export const brokenExtraction = webhook('adhoc-extract', {
126
+ connection: 'webhook-auth'
127
+ }).then(
128
+ fn('execute-extraction', async ctx => {
129
+ const client = await createClient(ctx); // ❌ FAILS - no connection context in fn()
130
+ })
131
+ );
132
+ ```
133
+
134
+ ### Quick Decision Table
135
+
136
+ | Need To | Use | Reason |
137
+ | ------------------------------- | -------- | ------------------------------------ |
138
+ | Call Fluent GraphQL API | `http()` | Requires connection + authentication |
139
+ | Parse CSV to JSON | `fn()` | No external API needed |
140
+ | Check if file already processed | `fn()` | Uses KV storage only |
141
+ | Send batch to Fluent | `http()` | Requires Fluent API access |
142
+ | Transform/map data | `fn()` | Pure data transformation |
143
+ | Query external REST API | `http()` | Requires fetch + connection |
144
+ | Access HTTP headers | **Both** | Use `ctx.request()` in either |
145
+
146
+ ---
147
+
148
+ ## ⚠️ Deno Compatibility: Buffer Import
149
+
150
+ **CRITICAL for Versori Platform (Deno runtime):** Always import Buffer explicitly when working with binary data, file uploads, or base64 encoding/decoding.
151
+
152
+ ### Why Buffer Import is Required
153
+
154
+ Versori runs on Deno, not Node.js. In Node.js, `Buffer` is globally available, but in Deno it must be explicitly imported from `node:buffer`.
155
+
156
+ ```typescript
157
+ // ✅ CORRECT - Always import Buffer in Versori/Deno
158
+ import { Buffer } from 'node:buffer';
159
+
160
+ await sftp.uploadFile('/path/file.xml', Buffer.from(xmlContent, 'utf-8'));
161
+ const decoded = Buffer.from(base64String, 'base64').toString('utf-8');
162
+
163
+ // ❌ WRONG - Buffer is not global in Deno
164
+ await sftp.uploadFile('/path/file.xml', Buffer.from(xmlContent, 'utf-8')); // ReferenceError!
165
+ ```
166
+
167
+ ### When to Import Buffer
168
+
169
+ Import Buffer when your workflow involves:
170
+
171
+ | Operation | Example | Requires Buffer? |
172
+ |-----------|---------|------------------|
173
+ | **File uploads** (SFTP, S3) | `sftp.uploadFile()` | ✅ Yes |
174
+ | **Base64 encoding/decoding** | `Buffer.from(str, 'base64')` | ✅ Yes |
175
+ | **Binary data handling** | Working with binary files | ✅ Yes |
176
+ | **String encoding** | `Buffer.from(str, 'utf-8')` | ✅ Yes |
177
+ | **XML response generation** | Returning XML as string | ❌ No (string only) |
178
+ | **JSON operations** | `JSON.parse()`, `JSON.stringify()` | ❌ No |
179
+ | **GraphQL queries** | `client.graphql()` | ❌ No |
180
+
181
+ ### Pattern: Add Buffer Import at Top of File
182
+
183
+ ```typescript
184
+ // ✅ CORRECT Pattern - Add at top of file with other imports
185
+ import { Buffer } from 'node:buffer'; // Required for Deno
186
+ import { schedule, http, fn } from '@versori/run';
187
+ import { createClient, SftpDataSource } from '@fluentcommerce/fc-connect-sdk';
188
+
189
+ export const sftpUpload = schedule('upload', '0 * * * *')
190
+ .then(fn('prepare-file', async (ctx) => {
191
+ // Buffer available here
192
+ const fileContent = Buffer.from(ctx.data.content, 'utf-8');
193
+ return { fileContent };
194
+ }));
195
+ ```
196
+
197
+ **See Also:** All examples in this module that use Buffer include the import statement.
198
+
199
+ ---
200
+
201
+ ## HTTP Functions - External API Calls
202
+
203
+ HTTP workflows enable authenticated calls to external APIs, including Fluent Commerce.
204
+
205
+ ### Basic HTTP Workflow
206
+
207
+ ```typescript
208
+ import { http } from '@versori/run';
209
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
210
+
211
+ /**
212
+ * Query inventory positions from Fluent Commerce
213
+ *
214
+ * Endpoint: https://{workspace}.versori.run/query-inventory
215
+ * Method: GET
216
+ */
217
+ export const queryInventory = http(
218
+ 'query-inventory',
219
+ {
220
+ connection: 'fluent_commerce', // CRITICAL: Provides OAuth2 auth
221
+ timeout: 30000, // 30 second timeout
222
+ retry: {
223
+ attempts: 3, // Retry up to 3 times
224
+ delay: 1000, // 1 second between retries
225
+ },
226
+ },
227
+ async ctx => {
228
+ ctx.log('info', 'Starting inventory query');
229
+
230
+ try {
231
+ // Create SDK client (auto-configured from connection)
232
+ const client = await createClient(ctx);
233
+
234
+ // GraphQL query - schema validated against Fluent Commerce API
235
+ const result = await client.graphql({
236
+ query: `
237
+ query GetInventoryPositions($first: Int!) {
238
+ inventoryPositions(first: $first) {
239
+ edges {
240
+ node {
241
+ id
242
+ ref
243
+ productRef
244
+ locationRef
245
+ onHand # ✅ Correct field for InventoryPosition
246
+ status
247
+ }
248
+ cursor
249
+ }
250
+ pageInfo {
251
+ hasNextPage
252
+ endCursor
253
+ }
254
+ }
255
+ }
256
+ `,
257
+ variables: { first: 100 },
258
+ });
259
+
260
+ // Check for GraphQL errors
261
+ if (result.errors?.length) {
262
+ throw new Error(`GraphQL error: ${result.errors[0].message}`);
263
+ }
264
+
265
+ const positions = result.data.inventoryPositions.edges;
266
+ ctx.log('info', `Retrieved ${positions.length} inventory positions`);
267
+
268
+ return {
269
+ success: true,
270
+ count: positions.length,
271
+ data: positions.map(edge => edge.node),
272
+ pageInfo: result.data.inventoryPositions.pageInfo,
273
+ };
274
+ } catch (error) {
275
+ ctx.log('error', 'Inventory query failed', {
276
+ error: error instanceof Error ? error.message : String(error),
277
+ });
278
+
279
+ return {
280
+ success: false,
281
+ error: error instanceof Error ? error.message : 'Unknown error',
282
+ };
283
+ }
284
+ }
285
+ );
286
+ ```
287
+
288
+ ### HTTP with Query Parameters
289
+
290
+ ```typescript
291
+ export const searchOrders = http(
292
+ 'search-orders',
293
+ {
294
+ connection: 'fluent_commerce',
295
+ },
296
+ async ctx => {
297
+ // Extract query parameters
298
+ const status = ctx.query?.status || 'OPEN';
299
+ const limit = parseInt(ctx.query?.limit || '50');
300
+ const customerRef = ctx.query?.customerRef;
301
+
302
+ ctx.log('info', 'Searching orders', { status, limit, customerRef });
303
+
304
+ const client = await createClient(ctx);
305
+
306
+ // Build GraphQL query with filters - schema validated
307
+ const result = await client.graphql({
308
+ query: `
309
+ query SearchOrders($status: String!, $first: Int!, $customerRef: String) {
310
+ orders(status: $status, first: $first, customerRef: $customerRef) {
311
+ edges {
312
+ node {
313
+ id
314
+ ref
315
+ status
316
+ totalPrice
317
+ customer {
318
+ ref
319
+ firstName
320
+ lastName
321
+ }
322
+ items {
323
+ edges {
324
+ node {
325
+ productRef
326
+ quantity
327
+ price
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+ `,
336
+ variables: { status, first: limit, customerRef },
337
+ });
338
+
339
+ return {
340
+ orders: result.data.orders.edges.map(e => e.node),
341
+ count: result.data.orders.edges.length,
342
+ };
343
+ }
344
+ );
345
+ ```
346
+
347
+ ### HTTP with POST Body (Mutations)
348
+
349
+ ```typescript
350
+ import { http } from '@versori/run';
351
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
352
+
353
+ /**
354
+ * Create inventory position
355
+ *
356
+ * POST https://{workspace}.versori.run/create-inventory
357
+ * Body: { productRef, locationRef, qty }
358
+ */
359
+ export const createInventory = http(
360
+ 'create-inventory',
361
+ {
362
+ connection: 'fluent_commerce',
363
+ },
364
+ async ctx => {
365
+ // Extract POST body
366
+ const { productRef, locationRef, qty } = ctx.data || {};
367
+
368
+ // Validation
369
+ if (!productRef || !locationRef || qty === undefined) {
370
+ return {
371
+ success: false,
372
+ error: 'Missing required fields: productRef, locationRef, qty',
373
+ };
374
+ }
375
+
376
+ const client = await createClient(ctx);
377
+
378
+ // GraphQL mutation - schema validated
379
+ const result = await client.graphql({
380
+ query: `
381
+ mutation CreateInventoryPosition($input: CreateInventoryPositionInput!) {
382
+ createInventoryPosition(input: $input) {
383
+ id
384
+ ref
385
+ productRef
386
+ locationRef
387
+ onHand
388
+ status
389
+ }
390
+ }
391
+ `,
392
+ variables: {
393
+ input: {
394
+ productRef,
395
+ locationRef,
396
+ qty,
397
+ type: 'AVAILABLE',
398
+ },
399
+ },
400
+ });
401
+
402
+ if (result.errors?.length) {
403
+ return {
404
+ success: false,
405
+ error: result.errors[0].message,
406
+ };
407
+ }
408
+
409
+ ctx.log('info', 'Inventory position created', {
410
+ id: result.data.createInventoryPosition.id,
411
+ });
412
+
413
+ return {
414
+ success: true,
415
+ data: result.data.createInventoryPosition,
416
+ };
417
+ }
418
+ );
419
+ ```
420
+
421
+ ---
422
+
423
+ ## Webhook Functions - Receiving External Requests
424
+
425
+ Webhook workflows receive external HTTP requests. They provide `ctx.data` (parsed body) and `ctx.request()` for accessing headers and raw request details.
426
+
427
+ ### Basic Webhook Receiver
428
+
429
+ ```typescript
430
+ import { webhook } from '@versori/run';
431
+ import { parseWebhookRequest } from '@fluentcommerce/fc-connect-sdk';
432
+
433
+ /**
434
+ * Receive SFCC order webhook
435
+ *
436
+ * POST https://{workspace}.versori.run/receive-order
437
+ * Body: XML order payload
438
+ */
439
+ export const receiveOrder = webhook('receive-order', async ctx => {
440
+ const req = ctx.request();
441
+ ctx.log('info', 'Received order webhook', {
442
+ contentType: req?.headers['content-type'],
443
+ bodySize: JSON.stringify(ctx.data).length,
444
+ });
445
+
446
+ try {
447
+ // Parse incoming payload
448
+ const payload = parseWebhookRequest(ctx.data);
449
+
450
+ if (!payload) {
451
+ return {
452
+ success: false,
453
+ error: 'Invalid payload format',
454
+ status: 400,
455
+ };
456
+ }
457
+
458
+ // Process order (use fn() or http() in chain)
459
+ ctx.log('info', 'Order parsed successfully', {
460
+ orderId: payload.orderId,
461
+ });
462
+
463
+ return {
464
+ success: true,
465
+ orderId: payload.orderId,
466
+ status: 200,
467
+ };
468
+ } catch (error) {
469
+ ctx.log('error', 'Webhook processing failed', {
470
+ error: error instanceof Error ? error.message : String(error),
471
+ });
472
+
473
+ return {
474
+ success: false,
475
+ error: error instanceof Error ? error.message : 'Processing failed',
476
+ status: 500,
477
+ };
478
+ }
479
+ });
480
+ ```
481
+
482
+ ### Webhook with Signature Validation (Fluent Rubix)
483
+
484
+ **IMPORTANT**: The webhook validation is **ONLY** for webhooks sent from **Fluent Commerce Rubix workflows**.
485
+
486
+ ```typescript
487
+ import { webhook } from '@versori/run';
488
+ import {
489
+ WebhookValidationService,
490
+ SignatureAlgorithm,
491
+ parseWebhookRequest,
492
+ validateFluentEvent,
493
+ } from '@fluentcommerce/fc-connect-sdk';
494
+
495
+ /**
496
+ * Receive Fluent Rubix webhook with signature validation
497
+ *
498
+ * POST https://{workspace}.versori.run/rubix-event
499
+ * Headers: fluent-signature (or x-fluent-signature)
500
+ */
501
+ export const receiveRubixEvent = webhook('rubix-event', async ctx => {
502
+ const logger = ctx.log; // Use native Versori logger
503
+
504
+ // Get public key from connector variables
505
+ const publicKey = ctx.vars?.FLUENT_WEBHOOK_PUBLIC_KEY;
506
+ if (!publicKey) {
507
+ logger.error('Missing FLUENT_WEBHOOK_PUBLIC_KEY in connector variables');
508
+ return { error: 'Configuration error: missing public key', status: 500 };
509
+ }
510
+
511
+ // IMPORTANT: This validator is ONLY for Fluent Commerce Rubix workflows
512
+ const validator = new WebhookValidationService(
513
+ {
514
+ algorithm: SignatureAlgorithm.SHA512_WITH_RSA,
515
+ strictValidation: true,
516
+ },
517
+ logger
518
+ );
519
+
520
+ // Extract signature from headers using ctx.request()
521
+ const req = ctx.request();
522
+ const signature = req?.headers['fluent-signature'] || req?.headers['x-fluent-signature'];
523
+
524
+ if (!signature) {
525
+ logger.warn('Missing webhook signature in headers');
526
+ return { error: 'Missing signature', status: 401 };
527
+ }
528
+
529
+ // Validate Fluent Commerce Rubix webhook signature
530
+ const validationResult = await validator.validateWebhookSignature(
531
+ JSON.stringify(ctx.data),
532
+ signature,
533
+ publicKey,
534
+ SignatureAlgorithm.SHA512_WITH_RSA
535
+ );
536
+
537
+ if (!validationResult.isValid) {
538
+ logger.error('Fluent Commerce Rubix webhook validation failed', validationResult);
539
+ return {
540
+ error: 'Invalid Fluent Rubix webhook signature',
541
+ details: validationResult.error,
542
+ status: 401,
543
+ };
544
+ }
545
+
546
+ // Parse webhook payload
547
+ const payload = parseWebhookRequest(ctx.data, logger, 'receiveRubixEvent');
548
+
549
+ if (!payload) {
550
+ return { error: 'Invalid payload', status: 400 };
551
+ }
552
+
553
+ // Validate event structure
554
+ const validation = validateFluentEvent(payload, logger, 'receiveRubixEvent');
555
+ if (!validation.isValid) {
556
+ return {
557
+ error: 'Validation failed',
558
+ details: validation.warnings,
559
+ status: 400,
560
+ };
561
+ }
562
+
563
+ logger.info('Rubix webhook validated successfully', {
564
+ eventName: payload.name,
565
+ entityType: payload.entityType,
566
+ });
567
+
568
+ return {
569
+ success: true,
570
+ eventName: payload.name,
571
+ status: 200,
572
+ };
573
+ });
574
+ ```
575
+
576
+ ### CRITICAL: Non-JSON Response Handlers (XML, HTML, CSV)
577
+
578
+ **Problem**: By default, Versori JSON-encodes all webhook responses. This breaks XML, HTML, CSV, and other non-JSON content.
579
+
580
+ **Solution**: Use custom `onSuccess` and `onError` handlers that return `Response` objects.
581
+
582
+ #### XML Response Example
583
+
584
+ ```typescript
585
+ import { webhook, fn } from '@versori/run';
586
+
587
+ /**
588
+ * Return XML response from webhook
589
+ *
590
+ * GET/POST https://{workspace}.versori.run/order-status-xml
591
+ * Returns: Raw XML (NOT JSON-encoded)
592
+ */
593
+ export const orderStatusXML = webhook('order-status-xml', {
594
+ response: {
595
+ mode: 'sync',
596
+ onSuccess: ctx => {
597
+ // ctx.data contains the final value from .then() chain
598
+ return new Response(ctx.data, {
599
+ status: 200,
600
+ headers: {
601
+ 'Content-Type': 'application/xml; charset=utf-8',
602
+ 'X-Execution-Id': ctx.executionId,
603
+ },
604
+ });
605
+ },
606
+ onError: ctx => {
607
+ // ctx.data contains the error from .catch()
608
+ const errorXml = `<?xml version="1.0" encoding="UTF-8"?>
609
+ <ErrorResponse>
610
+ <Error>
611
+ <Code>PROCESSING_ERROR</Code>
612
+ <Message>${ctx.data?.message || 'Unknown error'}</Message>
613
+ <Timestamp>${new Date().toISOString()}</Timestamp>
614
+ <ExecutionId>${ctx.executionId}</ExecutionId>
615
+ </Error>
616
+ </ErrorResponse>`;
617
+
618
+ return new Response(errorXml, {
619
+ status: 500,
620
+ headers: {
621
+ 'Content-Type': 'application/xml; charset=utf-8',
622
+ 'X-Execution-Id': ctx.executionId,
623
+ },
624
+ });
625
+ },
626
+ },
627
+ cors: true,
628
+ })
629
+ .then(
630
+ fn('generate-xml', ({ data }) => {
631
+ // Return raw XML string - this becomes ctx.data in onSuccess
632
+ const orderId = data?.orderId || 'unknown';
633
+ return `<?xml version="1.0" encoding="UTF-8"?>
634
+ <OrderStatusResponse>
635
+ <OrderId>${orderId}</OrderId>
636
+ <Status>SHIPPED</Status>
637
+ <Timestamp>${new Date().toISOString()}</Timestamp>
638
+ </OrderStatusResponse>`;
639
+ })
640
+ )
641
+ .catch(({ data }) => {
642
+ // Return error - this becomes ctx.data in onError
643
+ return { message: data instanceof Error ? data.message : String(data) };
644
+ });
645
+ ```
646
+
647
+ **Why This Works**:
648
+
649
+ - @versori/run's `sendResponse()` function streams `Response` objects directly **without JSON encoding**
650
+ - The Content-Type header is preserved exactly as specified
651
+ - Works with @versori/run v0.4.4 and all v0.4.x versions
652
+
653
+ #### HTML Response Example
654
+
655
+ ```typescript
656
+ export const statusPage = webhook('status-page', {
657
+ response: {
658
+ mode: 'sync',
659
+ onSuccess: ctx =>
660
+ new Response(ctx.data, {
661
+ status: 200,
662
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
663
+ }),
664
+ },
665
+ }).then(
666
+ fn('generate-html', () => {
667
+ return `<!DOCTYPE html>
668
+ <html lang="en">
669
+ <head>
670
+ <meta charset="UTF-8">
671
+ <title>Order Status</title>
672
+ </head>
673
+ <body>
674
+ <h1>System Operational</h1>
675
+ <p>All services running normally.</p>
676
+ </body>
677
+ </html>`;
678
+ })
679
+ );
680
+ ```
681
+
682
+ #### CSV Response Example
683
+
684
+ ```typescript
685
+ export const exportCSV = webhook('export-csv', {
686
+ response: {
687
+ mode: 'sync',
688
+ onSuccess: ctx =>
689
+ new Response(ctx.data, {
690
+ status: 200,
691
+ headers: {
692
+ 'Content-Type': 'text/csv; charset=utf-8',
693
+ 'Content-Disposition': 'attachment; filename="orders.csv"',
694
+ },
695
+ }),
696
+ },
697
+ }).then(
698
+ fn('generate-csv', () => {
699
+ return 'OrderId,Status,Total\n123,SHIPPED,99.99\n456,PENDING,149.99';
700
+ })
701
+ );
702
+ ```
703
+
704
+ #### Complex Multi-Step Workflow with XML Response
705
+
706
+ ```typescript
707
+ import { webhook, fn, http } from '@versori/run';
708
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
709
+ import { XMLBuilder } from 'fast-xml-parser';
710
+
711
+ /**
712
+ * Fetch order from Fluent, return as XML
713
+ *
714
+ * POST https://{workspace}.versori.run/order-detail-xml
715
+ * Body: { orderId }
716
+ * Returns: Order details as XML
717
+ */
718
+ export const orderDetailXML = webhook('order-detail-xml', {
719
+ response: {
720
+ mode: 'sync',
721
+ onSuccess: ctx =>
722
+ new Response(ctx.data, {
723
+ status: 200,
724
+ headers: {
725
+ 'Content-Type': 'application/xml; charset=utf-8',
726
+ 'X-Execution-Id': ctx.executionId,
727
+ 'X-Order-Id': ctx.metadata?.orderId || 'unknown',
728
+ },
729
+ }),
730
+ onError: ctx => {
731
+ const errorXml = `<?xml version="1.0" encoding="UTF-8"?>
732
+ <ErrorResponse>
733
+ <Error>
734
+ <Code>PROCESSING_ERROR</Code>
735
+ <Message>${ctx.data?.message || 'Unknown error'}</Message>
736
+ <Timestamp>${new Date().toISOString()}</Timestamp>
737
+ <ExecutionId>${ctx.executionId}</ExecutionId>
738
+ </Error>
739
+ </ErrorResponse>`;
740
+
741
+ return new Response(errorXml, {
742
+ status: 500,
743
+ headers: {
744
+ 'Content-Type': 'application/xml; charset=utf-8',
745
+ 'X-Execution-Id': ctx.executionId,
746
+ },
747
+ });
748
+ },
749
+ },
750
+ cors: true,
751
+ })
752
+ // Step 1: Parse incoming request
753
+ .then(
754
+ fn('parse-request', ({ data }) => {
755
+ const orderId = data?.orderId;
756
+ if (!orderId) {
757
+ throw new Error('OrderId missing from request');
758
+ }
759
+ return { orderId };
760
+ })
761
+ )
762
+
763
+ // Step 2: Fetch from Fluent Commerce
764
+ .then(
765
+ http(
766
+ 'fetch-order',
767
+ {
768
+ connection: 'fluent_commerce',
769
+ },
770
+ async ctx => {
771
+ const { orderId } = ctx.data;
772
+ const client = await createClient(ctx);
773
+
774
+ // GraphQL query - schema validated
775
+ const result = await client.graphql({
776
+ query: `
777
+ query GetOrder($ref: String!) {
778
+ order(ref: $ref) {
779
+ id
780
+ ref
781
+ status
782
+ totalPrice
783
+ customer {
784
+ ref
785
+ firstName
786
+ lastName
787
+ }
788
+ }
789
+ }
790
+ `,
791
+ variables: { ref: orderId },
792
+ });
793
+
794
+ if (!result.data?.order) {
795
+ throw new Error(`Order not found: ${orderId}`);
796
+ }
797
+
798
+ return {
799
+ orderId,
800
+ orderData: result.data.order,
801
+ };
802
+ }
803
+ )
804
+ )
805
+
806
+ // Step 3: Build XML response
807
+ .then(
808
+ fn('build-xml', ({ data }) => {
809
+ const builder = new XMLBuilder({
810
+ ignoreAttributes: false,
811
+ attributeNamePrefix: '@',
812
+ format: true,
813
+ });
814
+
815
+ const xmlObject = {
816
+ '?xml': { '@version': '1.0', '@encoding': 'UTF-8' },
817
+ OrderDetailResponse: {
818
+ '@orderId': data.orderId,
819
+ Order: {
820
+ Id: data.orderData.id,
821
+ Ref: data.orderData.ref,
822
+ Status: data.orderData.status,
823
+ TotalPrice: data.orderData.totalPrice,
824
+ Customer: {
825
+ Ref: data.orderData.customer.ref,
826
+ Name: `${data.orderData.customer.firstName} ${data.orderData.customer.lastName}`,
827
+ },
828
+ },
829
+ },
830
+ };
831
+
832
+ return builder.build(xmlObject);
833
+ })
834
+ )
835
+
836
+ .catch(({ data }) => {
837
+ // Return error message as object - onError handler will format as XML
838
+ return { message: data instanceof Error ? data.message : String(data) };
839
+ });
840
+ ```
841
+
842
+ **Key Patterns**:
843
+
844
+ - `onSuccess` and `onError` **return Response objects**
845
+ - Workflow steps **return raw strings** (NOT Response objects)
846
+ - Content-Type header matches actual content format
847
+ - Error handler uses same Content-Type as success handler
848
+
849
+ ---
850
+
851
+ ## Scheduled Functions - Time-Based Recurring Tasks
852
+
853
+ Scheduled workflows run on a cron schedule. They **MUST** be chained with `.then()` to add processing logic. To access external APIs (like Fluent Commerce), chain with `http()` task.
854
+
855
+ ### ⚠️ CRITICAL: schedule() Signature
856
+
857
+ **schedule() does NOT accept a handler or options as parameters**. It returns a `Workflow` that must be chained.
858
+
859
+ **Signature**:
860
+ ```typescript
861
+ function schedule(
862
+ id: string,
863
+ schedule: string,
864
+ activationPredicate?: ActivationPredicate
865
+ ): Workflow<ScheduleData>
866
+ ```
867
+
868
+ **activationPredicate**: Optional - `'all'` or custom filter function `(activation?: Activation) => boolean`
869
+
870
+ ### Basic Scheduled Workflow
871
+
872
+ ```typescript
873
+ import { schedule, http } from '@versori/run';
874
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
875
+
876
+ /**
877
+ * Daily inventory sync - runs at 2 AM UTC daily
878
+ *
879
+ * Cron: 0 2 * * * (every day at 2:00 AM)
880
+ */
881
+ export const dailyInventorySync = schedule(
882
+ 'daily-inventory-sync',
883
+ '0 2 * * *'
884
+ )
885
+ .then(
886
+ http(
887
+ 'sync-inventory',
888
+ { connection: 'fluent_commerce' },
889
+ async ctx => {
890
+ ctx.log.info('Starting daily inventory sync');
891
+
892
+ const client = await createClient(ctx);
893
+
894
+ // Fetch inventory from external source (e.g., S3)
895
+ const inventoryData = await fetchInventoryFromS3(ctx);
896
+
897
+ // Create batch job
898
+ const job = await client.createJob({
899
+ name: 'Daily Inventory Sync',
900
+ retailerId: ctx.activation.getVariable('FLUENT_RETAILER_ID'),
901
+ });
902
+
903
+ // Send batch data
904
+ const batch = await client.sendBatch(job.id, {
905
+ action: 'UPSERT',
906
+ entityType: 'INVENTORY',
907
+ entities: inventoryData,
908
+ });
909
+
910
+ ctx.log.info('Daily sync complete', {
911
+ jobId: job.id,
912
+ batchId: batch.id,
913
+ recordCount: inventoryData.length,
914
+ });
915
+
916
+ return {
917
+ success: true,
918
+ jobId: job.id,
919
+ recordCount: inventoryData.length,
920
+ };
921
+ }
922
+ )
923
+ );
924
+ ```
925
+
926
+ ### Common Cron Patterns
927
+
928
+ ```typescript
929
+ // Every minute - with http() for API access
930
+ export const everyMinute = schedule('every-minute', '* * * * *')
931
+ .then(http('task', { connection: 'fluent_commerce' }, async (ctx) => {
932
+ // Fluent API access available
933
+ const client = await createClient(ctx);
934
+ // ... process
935
+ }));
936
+
937
+ // Every minute - with fn() for KV-only
938
+ export const everyMinuteKV = schedule('every-minute-kv', '* * * * *')
939
+ .then(fn('task', async (ctx) => {
940
+ // KV storage only, no external API
941
+ const kv = ctx.openKv(':project:');
942
+ // ... process
943
+ }));
944
+
945
+ // Every hour at minute 0
946
+ export const hourly = schedule('hourly', '0 * * * *')
947
+ .then(http('sync', { connection: 'fluent_commerce' }, async (ctx) => {
948
+ // Hourly inventory sync
949
+ }));
950
+
951
+ // Every 6 hours - Fluent inventory snapshot
952
+ export const every6Hours = schedule('every-6-hours', '0 */6 * * *')
953
+ .then(http('snapshot', { connection: 'fluent_commerce' }, async (ctx) => {
954
+ // Extract inventory snapshot every 6 hours
955
+ }));
956
+
957
+ // Daily at 2 AM - Full sync
958
+ export const dailyAt2AM = schedule('daily-2am', '0 2 * * *')
959
+ .then(http('full-sync', { connection: 'fluent_commerce' }, async (ctx) => {
960
+ // Daily full inventory sync
961
+ }));
962
+
963
+ // Weekdays at 9 AM - Business hours processing
964
+ export const weekdaysAt9AM = schedule('weekdays-9am', '0 9 * * 1-5')
965
+ .then(http('business-hours', { connection: 'fluent_commerce' }, async (ctx) => {
966
+ // Weekday order processing
967
+ }));
968
+
969
+ // First of every month at midnight - Monthly reports
970
+ export const monthlyFirst = schedule('monthly-first', '0 0 1 * *')
971
+ .then(http('monthly-report', { connection: 'fluent_commerce' }, async (ctx) => {
972
+ // Generate monthly inventory reports
973
+ }));
974
+
975
+ // Every Sunday at 3 AM - Weekly cleanup
976
+ export const weeklySunday = schedule('weekly-sunday', '0 3 * * 0')
977
+ .then(http('weekly-cleanup', { connection: 'fluent_commerce' }, async (ctx) => {
978
+ // Weekly data cleanup
979
+ }));
980
+
981
+ // With activation predicate (filter by activation)
982
+ export const allActivations = schedule('all-act', '0 * * * *', 'all')
983
+ .then(fn('task', async (ctx) => {
984
+ // Runs for all activations
985
+ }));
986
+
987
+ export const customPredicate = schedule('custom', '0 * * * *', (a) => a?.id?.startsWith('prod'))
988
+ .then(fn('task', async (ctx) => {
989
+ // Runs only for production activations
990
+ }));
991
+ ```
992
+
993
+ ### Scheduled Workflow with State Management
994
+
995
+ ```typescript
996
+ import { schedule, fn, http } from '@versori/run';
997
+ import { createClient, VersoriFileTracker } from '@fluentcommerce/fc-connect-sdk';
998
+
999
+ /**
1000
+ * Hourly incremental sync with file tracking
1001
+ *
1002
+ * Cron: 0 * * * * (every hour)
1003
+ */
1004
+ export const hourlyIncrementalSync = schedule(
1005
+ 'hourly-sync',
1006
+ '0 * * * *'
1007
+ )
1008
+ .then(
1009
+ fn('check-files', async ctx => {
1010
+ const kv = ctx.openKv(':project:');
1011
+ const tracker = new VersoriFileTracker(kv, 'hourly-sync');
1012
+
1013
+ const lastFile = await tracker.getLastProcessedFile();
1014
+ const newFiles = await listFilesAfter(lastFile);
1015
+
1016
+ if (newFiles.length === 0) {
1017
+ ctx.log.info('No new files to process');
1018
+ throw new Error('SKIP'); // Signal to skip processing
1019
+ }
1020
+
1021
+ return { newFiles, tracker };
1022
+ })
1023
+ )
1024
+ .then(
1025
+ http(
1026
+ 'process-files',
1027
+ { connection: 'fluent_commerce' },
1028
+ async ctx => {
1029
+ const { newFiles, tracker } = ctx.data;
1030
+ const client = await createClient(ctx);
1031
+ let processedCount = 0;
1032
+
1033
+ for (const file of newFiles) {
1034
+ if (await tracker.wasFileProcessed(file.name)) {
1035
+ ctx.log.info('Skipping already processed file', { file: file.name });
1036
+ continue;
1037
+ }
1038
+
1039
+ const records = await processFile(file);
1040
+
1041
+ const job = await client.createJob({ name: `Hourly Sync - ${file.name}` });
1042
+ await client.sendBatch(job.id, {
1043
+ action: 'UPSERT',
1044
+ entityType: 'INVENTORY',
1045
+ entities: records,
1046
+ });
1047
+
1048
+ await tracker.markFileProcessed(file.name, {
1049
+ recordCount: records.length,
1050
+ timestamp: Date.now(),
1051
+ });
1052
+
1053
+ await tracker.setLastProcessedFile(file.name);
1054
+ processedCount += records.length;
1055
+ }
1056
+
1057
+ ctx.log.info('Hourly sync complete', {
1058
+ filesProcessed: newFiles.length,
1059
+ recordsProcessed: processedCount,
1060
+ });
1061
+
1062
+ return {
1063
+ success: true,
1064
+ filesProcessed: newFiles.length,
1065
+ recordsProcessed: processedCount,
1066
+ };
1067
+ }
1068
+ )
1069
+ )
1070
+ .catch(({ data }) => {
1071
+ if (data.message === 'SKIP') {
1072
+ return { skipped: true, reason: 'No new files' };
1073
+ }
1074
+ return { success: false, error: data.message };
1075
+ });
1076
+ ```
1077
+
1078
+ ---
1079
+
1080
+ ## Internal Functions (fn) - Data Transformation & State
1081
+
1082
+ Internal functions (`fn()`) are for processing that **does NOT require external API access**. They have access to KV storage and logging, but **NOT** `ctx.fetch`.
1083
+
1084
+ ### File Tracking with fn()
1085
+
1086
+ ```typescript
1087
+ import { fn } from '@versori/run';
1088
+ import { VersoriFileTracker } from '@fluentcommerce/fc-connect-sdk';
1089
+
1090
+ /**
1091
+ * Track file processing state
1092
+ *
1093
+ * Used internally by other workflows (NOT exposed as HTTP endpoint)
1094
+ */
1095
+ export const trackFileProcessing = fn('track-file', async ctx => {
1096
+ const kv = ctx.openKv(':project:');
1097
+ const tracker = new VersoriFileTracker(kv, 'inventory-ingestion');
1098
+
1099
+ const fileName = ctx.data?.fileName;
1100
+ if (!fileName) {
1101
+ return { error: 'Missing fileName' };
1102
+ }
1103
+
1104
+ // Check if processed
1105
+ if (await tracker.wasFileProcessed(fileName)) {
1106
+ return { skipped: true, reason: 'Already processed' };
1107
+ }
1108
+
1109
+ // Mark as processed
1110
+ await tracker.markFileProcessed(fileName, {
1111
+ records: ctx.data?.recordCount || 0,
1112
+ timestamp: Date.now(),
1113
+ });
1114
+
1115
+ return { tracked: true, fileName };
1116
+ });
1117
+ ```
1118
+
1119
+ ### Data Transformation with fn()
1120
+
1121
+ ```typescript
1122
+ import { fn } from '@versori/run';
1123
+ import { UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
1124
+
1125
+ /**
1126
+ * Transform CSV data to Fluent format
1127
+ *
1128
+ * Used as part of workflow composition
1129
+ */
1130
+ export const transformInventoryData = fn('transform-inventory', async ctx => {
1131
+ const rawData = ctx.data?.records;
1132
+ if (!rawData || !Array.isArray(rawData)) {
1133
+ return { error: 'Invalid input data' };
1134
+ }
1135
+
1136
+ // Define field mapping
1137
+ const mappingConfig = {
1138
+ fields: {
1139
+ productRef: { source: 'sku_id', required: true },
1140
+ locationRef: { source: 'location_code', required: true },
1141
+ qty: { source: 'quantity', resolver: 'sdk.parseInt' },
1142
+ status: { source: 'status', resolver: 'sdk.uppercase' },
1143
+ },
1144
+ };
1145
+
1146
+ const mapper = new UniversalMapper(mappingConfig);
1147
+
1148
+ // Transform all records
1149
+ const transformed = [];
1150
+ const errors = [];
1151
+
1152
+ for (const record of rawData) {
1153
+ const result = await mapper.map(record);
1154
+ if (result.success) {
1155
+ transformed.push(result.data);
1156
+ } else {
1157
+ errors.push({ record, errors: result.errors });
1158
+ }
1159
+ }
1160
+
1161
+ ctx.log('info', 'Transformation complete', {
1162
+ totalRecords: rawData.length,
1163
+ transformed: transformed.length,
1164
+ errors: errors.length,
1165
+ });
1166
+
1167
+ return {
1168
+ transformed,
1169
+ errors,
1170
+ stats: {
1171
+ total: rawData.length,
1172
+ success: transformed.length,
1173
+ failed: errors.length,
1174
+ },
1175
+ };
1176
+ });
1177
+ ```
1178
+
1179
+ ---
1180
+
1181
+ ## Workflow Composition - Multi-Step Patterns
1182
+
1183
+ Combine multiple workflow types using `.then()` and `.catch()` chaining.
1184
+
1185
+ ### Pattern 1: Webhook → fn() → http()
1186
+
1187
+ ```typescript
1188
+ import { webhook, fn, http } from '@versori/run';
1189
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
1190
+
1191
+ /**
1192
+ * Receive order → validate → send to Fluent
1193
+ *
1194
+ * POST https://{workspace}.versori.run/process-order
1195
+ */
1196
+ export const processOrder = webhook('process-order')
1197
+ // Step 1: Parse and validate (fn - no external API)
1198
+ .then(
1199
+ fn('validate-order', ({ data }) => {
1200
+ if (!data?.orderId || !data?.items) {
1201
+ throw new Error('Invalid order data: missing orderId or items');
1202
+ }
1203
+
1204
+ return {
1205
+ orderId: data.orderId,
1206
+ items: data.items.map(item => ({
1207
+ productRef: item.sku,
1208
+ quantity: parseInt(item.qty, 10),
1209
+ price: parseFloat(item.price),
1210
+ })),
1211
+ };
1212
+ })
1213
+ )
1214
+
1215
+ // Step 2: Send to Fluent (http - requires connection)
1216
+ .then(
1217
+ http(
1218
+ 'send-to-fluent',
1219
+ {
1220
+ connection: 'fluent_commerce',
1221
+ },
1222
+ async ctx => {
1223
+ const { orderId, items } = ctx.data;
1224
+ const client = await createClient(ctx);
1225
+
1226
+ // GraphQL mutation - schema validated
1227
+ const result = await client.graphql({
1228
+ query: `
1229
+ mutation CreateOrder($input: CreateOrderInput!) {
1230
+ createOrder(input: $input) {
1231
+ id
1232
+ ref
1233
+ status
1234
+ }
1235
+ }
1236
+ `,
1237
+ variables: {
1238
+ input: {
1239
+ ref: orderId,
1240
+ items: items.map(item => ({
1241
+ productRef: item.productRef,
1242
+ quantity: item.quantity,
1243
+ price: item.price,
1244
+ })),
1245
+ },
1246
+ },
1247
+ });
1248
+
1249
+ return {
1250
+ success: true,
1251
+ fluentOrderId: result.data.createOrder.id,
1252
+ fluentOrderRef: result.data.createOrder.ref,
1253
+ };
1254
+ }
1255
+ )
1256
+ )
1257
+
1258
+ // Error handling
1259
+ .catch(({ data }) => {
1260
+ return {
1261
+ success: false,
1262
+ error: data instanceof Error ? data.message : 'Processing failed',
1263
+ status: 500,
1264
+ };
1265
+ });
1266
+ ```
1267
+
1268
+ ### Pattern 2: Scheduled → fn() (File Tracking) → http() (API Call)
1269
+
1270
+ ```typescript
1271
+ import { schedule, fn, http } from '@versori/run';
1272
+ import { createClient, VersoriFileTracker } from '@fluentcommerce/fc-connect-sdk';
1273
+
1274
+ /**
1275
+ * Daily sync with file tracking
1276
+ *
1277
+ * Cron: 0 1 * * * (daily at 1 AM)
1278
+ */
1279
+ export const dailySyncWithTracking = schedule('daily-sync', '0 1 * * *', {
1280
+ connection: 'fluent_commerce',
1281
+ })
1282
+ // Step 1: Check if already processed today (fn - KV access)
1283
+ .then(
1284
+ fn('check-processed', async ctx => {
1285
+ const kv = ctx.openKv(':project:');
1286
+ const tracker = new VersoriFileTracker(kv, 'daily-sync');
1287
+
1288
+ const today = new Date().toISOString().split('T')[0];
1289
+ const fileKey = `inventory-${today}.csv`;
1290
+
1291
+ if (await tracker.wasFileProcessed(fileKey)) {
1292
+ throw new Error('Already processed today');
1293
+ }
1294
+
1295
+ return { fileKey, today };
1296
+ })
1297
+ )
1298
+
1299
+ // Step 2: Fetch and process (http - external API)
1300
+ .then(
1301
+ http(
1302
+ 'process-sync',
1303
+ {
1304
+ connection: 'fluent_commerce',
1305
+ },
1306
+ async ctx => {
1307
+ const { fileKey } = ctx.data;
1308
+ const client = await createClient(ctx);
1309
+
1310
+ // Fetch data from external source
1311
+ const records = await fetchDailyInventory();
1312
+
1313
+ // Send to Fluent
1314
+ const job = await client.createJob({ name: `Daily Sync ${fileKey}` });
1315
+ await client.sendBatch(job.id, {
1316
+ action: 'UPSERT',
1317
+ entityType: 'INVENTORY',
1318
+ entities: records,
1319
+ });
1320
+
1321
+ return {
1322
+ fileKey,
1323
+ jobId: job.id,
1324
+ recordCount: records.length,
1325
+ };
1326
+ }
1327
+ )
1328
+ )
1329
+
1330
+ // Step 3: Mark as processed (fn - KV access)
1331
+ .then(
1332
+ fn('mark-processed', async ctx => {
1333
+ const kv = ctx.openKv(':project:');
1334
+ const tracker = new VersoriFileTracker(kv, 'daily-sync');
1335
+ const { fileKey, recordCount } = ctx.data;
1336
+
1337
+ await tracker.markFileProcessed(fileKey, {
1338
+ recordCount,
1339
+ timestamp: Date.now(),
1340
+ });
1341
+
1342
+ return {
1343
+ success: true,
1344
+ fileKey,
1345
+ recordCount,
1346
+ };
1347
+ })
1348
+ )
1349
+
1350
+ .catch(({ data }) => {
1351
+ if (data.message === 'Already processed today') {
1352
+ return { skipped: true, reason: 'Already processed today' };
1353
+ }
1354
+ return { success: false, error: data.message };
1355
+ });
1356
+ ```
1357
+
1358
+ ---
1359
+
1360
+ ## Error Handling & Retry Strategies
1361
+
1362
+ ### Basic Error Handling
1363
+
1364
+ ```typescript
1365
+ export const robustWorkflow = http(
1366
+ 'robust',
1367
+ {
1368
+ connection: 'fluent_commerce',
1369
+ retry: {
1370
+ attempts: 3,
1371
+ delay: 1000,
1372
+ backoff: 2, // Exponential backoff multiplier
1373
+ },
1374
+ },
1375
+ async ctx => {
1376
+ try {
1377
+ const client = await createClient(ctx);
1378
+ const result = await client.graphql({ query: '...' });
1379
+
1380
+ if (result.errors?.length) {
1381
+ throw new Error(`GraphQL failed: ${result.errors[0].message}`);
1382
+ }
1383
+
1384
+ return { success: true, data: result.data };
1385
+ } catch (error) {
1386
+ ctx.log('error', 'Operation failed', {
1387
+ error: error instanceof Error ? error.message : String(error),
1388
+ stack: error instanceof Error ? error.stack : undefined,
1389
+ });
1390
+
1391
+ throw error; // Re-throw for retry mechanism
1392
+ }
1393
+ }
1394
+ );
1395
+ ```
1396
+
1397
+ ### Advanced Error Handling with Fallback
1398
+
1399
+ ```typescript
1400
+ export const withFallback = http(
1401
+ 'with-fallback',
1402
+ {
1403
+ connection: 'fluent_commerce',
1404
+ },
1405
+ async ctx => {
1406
+ const client = await createClient(ctx);
1407
+
1408
+ try {
1409
+ // Primary operation
1410
+ const result = await client.graphql({ query: '...' });
1411
+ return { source: 'primary', data: result.data };
1412
+ } catch (primaryError) {
1413
+ ctx.log('warn', 'Primary operation failed, trying fallback', {
1414
+ error: primaryError instanceof Error ? primaryError.message : String(primaryError),
1415
+ });
1416
+
1417
+ try {
1418
+ // Fallback operation
1419
+ const fallbackResult = await client.graphql({ query: '... (simpler query)' });
1420
+ return { source: 'fallback', data: fallbackResult.data };
1421
+ } catch (fallbackError) {
1422
+ ctx.log('error', 'Both primary and fallback failed', {
1423
+ primaryError: primaryError instanceof Error ? primaryError.message : String(primaryError),
1424
+ fallbackError:
1425
+ fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
1426
+ });
1427
+
1428
+ throw new Error('All operations failed');
1429
+ }
1430
+ }
1431
+ }
1432
+ );
1433
+ ```
1434
+
1435
+ ---
1436
+
1437
+ ## Real-World Fluent Commerce Use Cases
1438
+
1439
+ This section provides detailed, production-ready examples for common Fluent Commerce integration patterns.
1440
+
1441
+ ### Use Case 1: Inventory Position Ingestion from CSV
1442
+
1443
+ **Scenario**: Daily CSV file from warehouse management system → Fluent inventory positions
1444
+
1445
+ ```typescript
1446
+ import { schedule, fn, http } from '@versori/run';
1447
+ import {
1448
+ createClient,
1449
+ S3DataSource,
1450
+ CSVParserService,
1451
+ UniversalMapper,
1452
+ VersoriFileTracker
1453
+ } from '@fluentcommerce/fc-connect-sdk';
1454
+
1455
+ /**
1456
+ * Daily inventory ingestion from S3 CSV
1457
+ *
1458
+ * Cron: 0 3 * * * (3 AM daily)
1459
+ * Flow: Check file → Parse CSV → Transform → Send to Fluent
1460
+ */
1461
+ export const dailyInventoryIngestion = schedule('daily-inventory-ingestion', '0 3 * * *')
1462
+ // Step 1: Check for new files (fn - KV only)
1463
+ .then(fn('check-new-files', async ctx => {
1464
+ const kv = ctx.openKv(':project:');
1465
+ const tracker = new VersoriFileTracker(kv, 'inventory-ingestion');
1466
+
1467
+ const s3Config = {
1468
+ bucket: ctx.activation.getVariable<string>('S3_BUCKET'),
1469
+ region: ctx.activation.getVariable<string>('AWS_REGION'),
1470
+ accessKeyId: ctx.activation.getVariable<string>('AWS_ACCESS_KEY_ID'),
1471
+ secretAccessKey: ctx.activation.getVariable<string>('AWS_SECRET_ACCESS_KEY'),
1472
+ };
1473
+
1474
+ const s3 = new S3DataSource(s3Config, ctx.log);
1475
+ const files = await s3.listFiles({ prefix: 'inventory/daily/' });
1476
+
1477
+ // Filter unprocessed files
1478
+ const newFiles = [];
1479
+ for (const file of files) {
1480
+ if (!(await tracker.wasFileProcessed(file.key))) {
1481
+ newFiles.push(file);
1482
+ }
1483
+ }
1484
+
1485
+ if (newFiles.length === 0) {
1486
+ throw new Error('NO_NEW_FILES');
1487
+ }
1488
+
1489
+ ctx.log.info('Found new inventory files', { count: newFiles.length });
1490
+ return { s3Config, files: newFiles, tracker };
1491
+ }))
1492
+
1493
+ // Step 2: Parse and transform (fn - no API needed)
1494
+ .then(fn('parse-transform', async ctx => {
1495
+ const { s3Config, files, tracker } = ctx.data;
1496
+ const s3 = new S3DataSource(s3Config, ctx.log);
1497
+ const parser = new CSVParserService(ctx.log);
1498
+
1499
+ // Mapping configuration
1500
+ const mappingConfig = {
1501
+ fields: {
1502
+ // Fluent field: CSV column
1503
+ ref: { source: 'sku', required: true },
1504
+ productRef: { source: 'sku', required: true },
1505
+ locationRef: { source: 'location_code', required: true },
1506
+ qty: { source: 'quantity', resolver: 'sdk.parseInt', required: true },
1507
+ type: { value: 'AVAILABLE' }, // Static value
1508
+ storageArea: { source: 'warehouse_zone' },
1509
+ expectedOn: { source: 'delivery_date', resolver: 'sdk.formatDate' },
1510
+ },
1511
+ };
1512
+
1513
+ const mapper = new UniversalMapper(mappingConfig);
1514
+ const allRecords = [];
1515
+
1516
+ for (const file of files) {
1517
+ const content = await s3.readFile(file.key);
1518
+ const parsed = await parser.parse(content, {
1519
+ headers: true,
1520
+ skipEmptyLines: true
1521
+ });
1522
+
1523
+ // Transform each row
1524
+ for (const row of parsed) {
1525
+ const result = await mapper.map(row);
1526
+ if (result.success) {
1527
+ allRecords.push(result.data);
1528
+ } else {
1529
+ ctx.log.warn('Mapping failed', { row, errors: result.errors });
1530
+ }
1531
+ }
1532
+ }
1533
+
1534
+ ctx.log.info('Parsed and transformed records', {
1535
+ fileCount: files.length,
1536
+ recordCount: allRecords.length
1537
+ });
1538
+
1539
+ return { records: allRecords, files, tracker };
1540
+ }))
1541
+
1542
+ // Step 3: Send to Fluent (http - requires API connection)
1543
+ .then(http('send-to-fluent', { connection: 'fluent_commerce' }, async ctx => {
1544
+ const { records, files, tracker } = ctx.data;
1545
+ const client = await createClient(ctx);
1546
+
1547
+ // Create batch job
1548
+ const job = await client.createJob({
1549
+ name: `Daily Inventory Ingestion - ${new Date().toISOString().split('T')[0]}`,
1550
+ retailerId: ctx.activation.getVariable<number>('FLUENT_RETAILER_ID'),
1551
+ });
1552
+
1553
+ // Send in batches of 500
1554
+ const batchSize = 500;
1555
+ const batches = [];
1556
+
1557
+ for (let i = 0; i < records.length; i += batchSize) {
1558
+ const chunk = records.slice(i, i + batchSize);
1559
+ const batch = await client.sendBatch(job.id, {
1560
+ action: 'UPSERT',
1561
+ entityType: 'INVENTORY_POSITION',
1562
+ entities: chunk,
1563
+ meta: {
1564
+ preprocessing: 'skip', // Delta file, skip BPP
1565
+ },
1566
+ });
1567
+ batches.push(batch.id);
1568
+ }
1569
+
1570
+ // Mark files as processed
1571
+ for (const file of files) {
1572
+ await tracker.markFileProcessed(file.key, {
1573
+ jobId: job.id,
1574
+ recordCount: records.length,
1575
+ timestamp: Date.now(),
1576
+ });
1577
+ }
1578
+
1579
+ ctx.log.info('Inventory ingestion complete', {
1580
+ jobId: job.id,
1581
+ batches: batches.length,
1582
+ totalRecords: records.length,
1583
+ });
1584
+
1585
+ return {
1586
+ success: true,
1587
+ jobId: job.id,
1588
+ recordCount: records.length,
1589
+ filesProcessed: files.length,
1590
+ };
1591
+ }))
1592
+
1593
+ .catch(({ data }) => {
1594
+ if (data.message === 'NO_NEW_FILES') {
1595
+ return { skipped: true, reason: 'No new files to process' };
1596
+ }
1597
+ return { success: false, error: data.message || 'Unknown error' };
1598
+ });
1599
+ ```
1600
+
1601
+ ### Use Case 2: Order Creation from SFCC XML Webhook
1602
+
1603
+ **Scenario**: SFCC sends order XML → Validate → Create order in Fluent
1604
+
1605
+ ```typescript
1606
+ import { webhook, fn, http } from '@versori/run';
1607
+ import {
1608
+ createClient,
1609
+ XMLParserService,
1610
+ GraphQLMutationMapper
1611
+ } from '@fluentcommerce/fc-connect-sdk';
1612
+
1613
+ /**
1614
+ * Receive SFCC order XML webhook and create order in Fluent
1615
+ *
1616
+ * POST https://{workspace}.versori.run/sfcc-order-create
1617
+ * Content-Type: application/xml
1618
+ */
1619
+ export const sfccOrderCreate = webhook('sfcc-order-create', {
1620
+ connection: 'sfcc-webhook-auth', // Platform validates API key
1621
+ response: {
1622
+ mode: 'sync',
1623
+ onSuccess: ctx => new Response(ctx.data, {
1624
+ status: 200,
1625
+ headers: { 'Content-Type': 'application/xml; charset=utf-8' },
1626
+ }),
1627
+ onError: ctx => {
1628
+ const errorXml = `<?xml version="1.0"?>
1629
+ <ErrorResponse>
1630
+ <Error>
1631
+ <Code>PROCESSING_ERROR</Code>
1632
+ <Message>${ctx.data?.message || 'Order processing failed'}</Message>
1633
+ <ExecutionId>${ctx.executionId}</ExecutionId>
1634
+ </Error>
1635
+ </ErrorResponse>`;
1636
+ return new Response(errorXml, {
1637
+ status: 500,
1638
+ headers: { 'Content-Type': 'application/xml; charset=utf-8' },
1639
+ });
1640
+ },
1641
+ },
1642
+ })
1643
+ // Step 1: Parse XML (fn - no API needed)
1644
+ .then(fn('parse-xml', async ctx => {
1645
+ const parser = new XMLParserService(ctx.log);
1646
+ const xmlData = await parser.parse(ctx.data);
1647
+
1648
+ // Validate required fields
1649
+ if (!xmlData.order?.['@id']) {
1650
+ throw new Error('Invalid order XML: missing order ID');
1651
+ }
1652
+
1653
+ ctx.log.info('Parsed SFCC order XML', {
1654
+ orderId: xmlData.order['@id'],
1655
+ customerEmail: xmlData.order.customer?.email
1656
+ });
1657
+
1658
+ return { xmlData };
1659
+ }))
1660
+
1661
+ // Step 2: Transform and create order (http - Fluent API access)
1662
+ .then(http('create-fluent-order', { connection: 'fluent_commerce' }, async ctx => {
1663
+ const { xmlData } = ctx.data;
1664
+ const client = await createClient(ctx);
1665
+
1666
+ // GraphQL mutation mapping config
1667
+ const mappingConfig = {
1668
+ mutationName: 'createOrder',
1669
+ sourceFormat: 'xml',
1670
+ arguments: {
1671
+ input: {
1672
+ ref: { source: 'order.@id' },
1673
+ type: { value: 'STANDARD' },
1674
+ retailer: { source: 'order.@retailerId', resolver: 'sdk.parseInt' },
1675
+ customer: {
1676
+ firstName: { source: 'order.customer.firstname' },
1677
+ lastName: { source: 'order.customer.lastname' },
1678
+ email: { source: 'order.customer.email' },
1679
+ },
1680
+ items: {
1681
+ source: 'order.items.item',
1682
+ isArray: true,
1683
+ mapping: {
1684
+ productRef: { source: '@sku' },
1685
+ quantity: { source: 'quantity', resolver: 'sdk.parseInt' },
1686
+ price: {
1687
+ value: { source: 'price', resolver: 'sdk.parseFloat' },
1688
+ currency: { source: '@currency', default: 'USD' },
1689
+ },
1690
+ },
1691
+ },
1692
+ fulfilment: {
1693
+ type: { value: 'STANDARD' },
1694
+ deliveryType: { source: 'order.shipping.method' },
1695
+ deliveryAddress: {
1696
+ name: { source: 'order.shipping.address.name' },
1697
+ street: { source: 'order.shipping.address.street' },
1698
+ city: { source: 'order.shipping.address.city' },
1699
+ state: { source: 'order.shipping.address.state' },
1700
+ postcode: { source: 'order.shipping.address.postalcode' },
1701
+ country: { source: 'order.shipping.address.country' },
1702
+ },
1703
+ },
1704
+ payments: {
1705
+ source: 'order.payments.payment',
1706
+ isArray: true,
1707
+ mapping: {
1708
+ method: { source: '@method' },
1709
+ amount: { source: 'amount', resolver: 'sdk.parseFloat' },
1710
+ cardType: { source: 'creditcard.type' },
1711
+ },
1712
+ },
1713
+ },
1714
+ },
1715
+ };
1716
+
1717
+ const mapper = new GraphQLMutationMapper(mappingConfig, ctx.log, { fluentClient: client });
1718
+ const { mutation, variables } = await mapper.generateMutation(xmlData);
1719
+
1720
+ // Execute mutation
1721
+ const result = await client.graphql({ query: mutation, variables });
1722
+
1723
+ if (result.errors?.length) {
1724
+ throw new Error(`Order creation failed: ${result.errors[0].message}`);
1725
+ }
1726
+
1727
+ const order = result.data.createOrder;
1728
+ ctx.log.info('Order created in Fluent', {
1729
+ fluentOrderId: order.id,
1730
+ sfccOrderId: xmlData.order['@id']
1731
+ });
1732
+
1733
+ return {
1734
+ fluentOrderId: order.id,
1735
+ fluentOrderRef: order.ref,
1736
+ sfccOrderId: xmlData.order['@id'],
1737
+ };
1738
+ }))
1739
+
1740
+ // Step 3: Generate success XML response (fn)
1741
+ .then(fn('generate-response', ctx => {
1742
+ const { fluentOrderId, sfccOrderId } = ctx.data;
1743
+ return `<?xml version="1.0"?>
1744
+ <OrderResponse>
1745
+ <Success>true</Success>
1746
+ <SFCCOrderId>${sfccOrderId}</SFCCOrderId>
1747
+ <FluentOrderId>${fluentOrderId}</FluentOrderId>
1748
+ <Timestamp>${new Date().toISOString()}</Timestamp>
1749
+ </OrderResponse>`;
1750
+ }))
1751
+
1752
+ .catch(({ data }) => {
1753
+ return { message: data instanceof Error ? data.message : String(data) };
1754
+ });
1755
+ ```
1756
+
1757
+ ### Use Case 3: Virtual Position Extraction to SFTP
1758
+
1759
+ **Scenario**: Scheduled extraction of Fluent virtual positions → XML → Upload to SFTP
1760
+
1761
+ ```typescript
1762
+ import { Buffer } from 'node:buffer'; // ⚠️ Required for Deno compatibility
1763
+ import { schedule, http, fn } from '@versori/run';
1764
+ import {
1765
+ createClient,
1766
+ ExtractionOrchestrator,
1767
+ SftpDataSource,
1768
+ VersoriFileTracker
1769
+ } from '@fluentcommerce/fc-connect-sdk';
1770
+ import { XMLBuilder } from 'fast-xml-parser';
1771
+
1772
+ /**
1773
+ * Extract virtual positions and upload to SFTP as XML
1774
+ *
1775
+ * Cron: 0 */4 * * * (every 4 hours)
1776
+ */
1777
+ export const virtualPositionExtraction = schedule('virtual-position-extraction', '0 */4 * * *')
1778
+ // Step 1: Extract from Fluent (http - API access)
1779
+ .then(http('extract-positions', { connection: 'fluent_commerce' }, async ctx => {
1780
+ const client = await createClient(ctx);
1781
+
1782
+ // Define extraction config
1783
+ const extractionConfig = {
1784
+ query: `
1785
+ query GetVirtualPositions($first: Int!, $after: String) {
1786
+ virtualPositions(first: $first, after: $after) {
1787
+ edges {
1788
+ node {
1789
+ id
1790
+ ref
1791
+ productRef
1792
+ groupRef
1793
+ onHand
1794
+ quantity
1795
+ type
1796
+ status
1797
+ createdOn
1798
+ updatedOn
1799
+ }
1800
+ cursor
1801
+ }
1802
+ pageInfo {
1803
+ hasNextPage
1804
+ endCursor
1805
+ }
1806
+ }
1807
+ }
1808
+ `,
1809
+ variables: { first: 200 },
1810
+ pathToData: 'virtualPositions',
1811
+ pathToCursor: 'pageInfo.endCursor',
1812
+ pathToHasNext: 'pageInfo.hasNextPage',
1813
+ };
1814
+
1815
+ // Use ExtractionOrchestrator for auto-pagination
1816
+ const orchestrator = new ExtractionOrchestrator(client, ctx.log);
1817
+ const result = await orchestrator.extractWithPagination(extractionConfig);
1818
+
1819
+ if (!result.success) {
1820
+ throw new Error(`Extraction failed: ${result.errors?.join(', ')}`);
1821
+ }
1822
+
1823
+ ctx.log.info('Extracted virtual positions', {
1824
+ recordCount: result.data?.length || 0
1825
+ });
1826
+
1827
+ return { positions: result.data || [] };
1828
+ }))
1829
+
1830
+ // Step 2: Transform to XML (fn - no API)
1831
+ .then(fn('transform-to-xml', ctx => {
1832
+ const { positions } = ctx.data;
1833
+
1834
+ const xmlBuilder = new XMLBuilder({
1835
+ ignoreAttributes: false,
1836
+ attributeNamePrefix: '@',
1837
+ format: true,
1838
+ indentBy: ' ',
1839
+ });
1840
+
1841
+ const xmlObject = {
1842
+ '?xml': { '@version': '1.0', '@encoding': 'UTF-8' },
1843
+ VirtualPositionFeed: {
1844
+ '@timestamp': new Date().toISOString(),
1845
+ '@recordCount': positions.length,
1846
+ Positions: {
1847
+ Position: positions.map(pos => ({
1848
+ '@id': pos.id,
1849
+ Ref: pos.ref,
1850
+ ProductRef: pos.productRef,
1851
+ GroupRef: pos.groupRef,
1852
+ OnHand: pos.onHand,
1853
+ Quantity: pos.quantity,
1854
+ Type: pos.type,
1855
+ Status: pos.status,
1856
+ CreatedOn: pos.createdOn,
1857
+ UpdatedOn: pos.updatedOn,
1858
+ })),
1859
+ },
1860
+ },
1861
+ };
1862
+
1863
+ const xmlContent = xmlBuilder.build(xmlObject);
1864
+
1865
+ const fileName = `virtual-positions-${new Date().toISOString().replace(/:/g, '-')}.xml`;
1866
+
1867
+ ctx.log.info('Generated XML feed', { fileName, recordCount: positions.length });
1868
+
1869
+ return { xmlContent, fileName, recordCount: positions.length };
1870
+ }))
1871
+
1872
+ // Step 3: Upload to SFTP (fn - external SFTP, not Fluent API)
1873
+ .then(fn('upload-to-sftp', async ctx => {
1874
+ const { xmlContent, fileName, recordCount } = ctx.data;
1875
+
1876
+ const sftpConfig = {
1877
+ host: ctx.activation.getVariable<string>('SFTP_HOST'),
1878
+ port: ctx.activation.getVariable<number>('SFTP_PORT') || 22,
1879
+ username: ctx.activation.getVariable<string>('SFTP_USERNAME'),
1880
+ password: ctx.activation.getVariable<string>('SFTP_PASSWORD'),
1881
+ };
1882
+
1883
+ const sftp = new SftpDataSource(sftpConfig, ctx.log);
1884
+ await sftp.connect();
1885
+
1886
+ try {
1887
+ const remotePath = `/outbound/${fileName}`;
1888
+ await sftp.uploadFile(remotePath, Buffer.from(xmlContent, 'utf-8'));
1889
+
1890
+ ctx.log.info('Uploaded to SFTP', { remotePath, recordCount });
1891
+
1892
+ // Track in KV
1893
+ const kv = ctx.openKv(':project:');
1894
+ const tracker = new VersoriFileTracker(kv, 'virtual-position-extraction');
1895
+ await tracker.markFileProcessed(fileName, {
1896
+ recordCount,
1897
+ timestamp: Date.now(),
1898
+ remotePath,
1899
+ });
1900
+
1901
+ return {
1902
+ success: true,
1903
+ fileName,
1904
+ remotePath,
1905
+ recordCount,
1906
+ };
1907
+ } finally {
1908
+ await sftp.disconnect();
1909
+ }
1910
+ }))
1911
+
1912
+ .catch(({ data }) => ({
1913
+ success: false,
1914
+ error: data instanceof Error ? data.message : String(data),
1915
+ }));
1916
+ ```
1917
+
1918
+ ### Use Case 4: Multi-Tenant Order Status Webhook
1919
+
1920
+ **Scenario**: Multiple customers share one connector, route to correct Fluent instance
1921
+
1922
+ ```typescript
1923
+ import { webhook, fn, http } from '@versori/run';
1924
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
1925
+
1926
+ /**
1927
+ * Multi-tenant order status update
1928
+ *
1929
+ * POST https://{workspace}.versori.run/order-status-update
1930
+ * Body: { customerId, orderId, status }
1931
+ */
1932
+ export const orderStatusUpdate = webhook('order-status-update', {
1933
+ response: { mode: 'sync' },
1934
+ })
1935
+ // Step 1: Route to correct connection (fn)
1936
+ .then(fn('route-connection', ctx => {
1937
+ const { customerId, orderId, status } = ctx.data;
1938
+
1939
+ if (!customerId || !orderId || !status) {
1940
+ throw new Error('Missing required fields: customerId, orderId, status');
1941
+ }
1942
+
1943
+ // Access all connections via activation.connections
1944
+ const connections = ctx.activation.connections;
1945
+
1946
+ // Find customer-specific Fluent connection
1947
+ const connectionName = `fluent_${customerId.toLowerCase()}`;
1948
+ const connection = connections?.find(c => c.name === connectionName);
1949
+
1950
+ if (!connection) {
1951
+ throw new Error(`No Fluent connection found for customer: ${customerId}`);
1952
+ }
1953
+
1954
+ ctx.log.info('Routed to customer connection', {
1955
+ customerId,
1956
+ connectionName,
1957
+ connectionId: connection.id
1958
+ });
1959
+
1960
+ return { connectionName, orderId, status, customerId };
1961
+ }))
1962
+
1963
+ // Step 2: Update order status (http - with dynamic connection)
1964
+ .then(http('update-order', (ctx) => {
1965
+ // Return dynamic connection name
1966
+ return { connection: ctx.data.connectionName };
1967
+ }, async ctx => {
1968
+ const { orderId, status, customerId } = ctx.data;
1969
+ const client = await createClient(ctx);
1970
+
1971
+ // Update order status
1972
+ const result = await client.graphql({
1973
+ query: `
1974
+ mutation UpdateOrder($ref: String!, $status: String!) {
1975
+ updateOrder(input: { ref: $ref, status: $status }) {
1976
+ id
1977
+ ref
1978
+ status
1979
+ }
1980
+ }
1981
+ `,
1982
+ variables: { ref: orderId, status },
1983
+ });
1984
+
1985
+ if (result.errors?.length) {
1986
+ throw new Error(`Failed to update order: ${result.errors[0].message}`);
1987
+ }
1988
+
1989
+ ctx.log.info('Order status updated', {
1990
+ customerId,
1991
+ orderId,
1992
+ newStatus: status
1993
+ });
1994
+
1995
+ return {
1996
+ success: true,
1997
+ customerId,
1998
+ orderId,
1999
+ status: result.data.updateOrder.status,
2000
+ };
2001
+ }))
2002
+
2003
+ .catch(({ data }) => ({
2004
+ success: false,
2005
+ error: data instanceof Error ? data.message : String(data),
2006
+ status: 500,
2007
+ }));
2008
+ ```
2009
+
2010
+ ### Use Case 5: Product Catalog Sync with Enrichment
2011
+
2012
+ **Scenario**: Fetch products from PIM system → Enrich → Update Fluent catalog
2013
+
2014
+ ```typescript
2015
+ import { schedule, fn, http } from '@versori/run';
2016
+ import {
2017
+ createClient,
2018
+ UniversalMapper,
2019
+ VersoriFileTracker
2020
+ } from '@fluentcommerce/fc-connect-sdk';
2021
+
2022
+ /**
2023
+ * Daily product catalog sync
2024
+ *
2025
+ * Cron: 0 4 * * * (4 AM daily)
2026
+ */
2027
+ export const productCatalogSync = schedule('product-catalog-sync', '0 4 * * *')
2028
+ // Step 1: Fetch from external PIM (http - external API)
2029
+ .then(http('fetch-from-pim', { connection: 'pim_system' }, async ctx => {
2030
+ // Fetch products from PIM API
2031
+ const response = await ctx.fetch('/api/v1/products?updated_since=yesterday');
2032
+ const products = await response.json();
2033
+
2034
+ ctx.log.info('Fetched products from PIM', { count: products.length });
2035
+ return { pimProducts: products };
2036
+ }))
2037
+
2038
+ // Step 2: Enrich and transform (fn - data processing)
2039
+ .then(fn('enrich-transform', async ctx => {
2040
+ const { pimProducts } = ctx.data;
2041
+
2042
+ const mappingConfig = {
2043
+ fields: {
2044
+ ref: { source: 'sku', required: true },
2045
+ type: { value: 'STANDARD' },
2046
+ name: { source: 'name', required: true },
2047
+ summary: { source: 'short_description' },
2048
+ gtin: { source: 'barcode' },
2049
+ prices: {
2050
+ source: 'pricing',
2051
+ isArray: true,
2052
+ mapping: {
2053
+ type: { source: 'price_type' },
2054
+ value: { source: 'amount', resolver: 'sdk.parseFloat' },
2055
+ currency: { source: 'currency', default: 'USD' },
2056
+ },
2057
+ },
2058
+ attributes: {
2059
+ source: 'custom_attributes',
2060
+ isArray: true,
2061
+ mapping: {
2062
+ name: { source: 'attr_name' },
2063
+ value: { source: 'attr_value', resolver: 'sdk.toString' },
2064
+ type: { value: 'STRING' },
2065
+ },
2066
+ },
2067
+ taxType: {
2068
+ type: { source: 'tax.type' },
2069
+ group: { source: 'tax.group' },
2070
+ tariff: { source: 'tax.tariff_code' },
2071
+ },
2072
+ },
2073
+ };
2074
+
2075
+ const mapper = new UniversalMapper(mappingConfig);
2076
+ const enrichedProducts = [];
2077
+
2078
+ for (const product of pimProducts) {
2079
+ const result = await mapper.map(product);
2080
+ if (result.success) {
2081
+ enrichedProducts.push(result.data);
2082
+ } else {
2083
+ ctx.log.warn('Product mapping failed', {
2084
+ sku: product.sku,
2085
+ errors: result.errors
2086
+ });
2087
+ }
2088
+ }
2089
+
2090
+ ctx.log.info('Products enriched', { count: enrichedProducts.length });
2091
+ return { products: enrichedProducts };
2092
+ }))
2093
+
2094
+ // Step 3: Send to Fluent (http - Fluent API)
2095
+ .then(http('sync-to-fluent', { connection: 'fluent_commerce' }, async ctx => {
2096
+ const { products } = ctx.data;
2097
+ const client = await createClient(ctx);
2098
+
2099
+ const job = await client.createJob({
2100
+ name: `Product Catalog Sync - ${new Date().toISOString().split('T')[0]}`,
2101
+ retailerId: ctx.activation.getVariable<number>('FLUENT_RETAILER_ID'),
2102
+ });
2103
+
2104
+ // Send in chunks
2105
+ const batchSize = 100;
2106
+ const batches = [];
2107
+
2108
+ for (let i = 0; i < products.length; i += batchSize) {
2109
+ const chunk = products.slice(i, i + batchSize);
2110
+ const batch = await client.sendBatch(job.id, {
2111
+ action: 'UPSERT',
2112
+ entityType: 'PRODUCT',
2113
+ entities: chunk,
2114
+ meta: {
2115
+ preprocessing: 'apply', // Full snapshot, use BPP
2116
+ },
2117
+ });
2118
+ batches.push(batch.id);
2119
+ }
2120
+
2121
+ ctx.log.info('Product sync complete', {
2122
+ jobId: job.id,
2123
+ productCount: products.length,
2124
+ batchCount: batches.length,
2125
+ });
2126
+
2127
+ return {
2128
+ success: true,
2129
+ jobId: job.id,
2130
+ productCount: products.length,
2131
+ };
2132
+ }))
2133
+
2134
+ .catch(({ data }) => ({
2135
+ success: false,
2136
+ error: data instanceof Error ? data.message : String(data),
2137
+ }));
2138
+ ```
2139
+
2140
+ ---
2141
+
2142
+ ## ⚠️ CRITICAL: `.parallel()` Error Behavior
2143
+
2144
+ **BEFORE USING `.parallel()` - READ THIS:**
2145
+
2146
+ The `.parallel()` method has **all-or-nothing error behavior** that makes it **UNSUITABLE for most batch processing use cases**.
2147
+
2148
+ ### What Happens When One Task Fails
2149
+
2150
+ ```typescript
2151
+ // ❌ PROBLEM: If ANY file fails, you lose ALL results
2152
+ schedule('process-files', '0 2 * * *')
2153
+ .then(fn('fetch', () => ['file1.xml', 'file2.xml', 'file3.xml']))
2154
+ .unpack()
2155
+ .parallel(fn('process', async (ctx) => {
2156
+ return await processFile(ctx.data);
2157
+ }));
2158
+ ```
2159
+
2160
+ **If `file2.xml` fails:**
2161
+ - ❌ `file1.xml` might have processed successfully but **result is lost**
2162
+ - ❌ `file3.xml` never runs (or is cancelled mid-execution)
2163
+ - ❌ **Entire workflow fails**
2164
+ - ❌ **NO partial results returned**
2165
+ - ❌ You can't retry only failed items
2166
+
2167
+ ### Implementation Detail
2168
+
2169
+ The `.parallel()` method uses RxJS `mergeMap` + `toArray()`:
2170
+ - Any error in `mergeMap` propagates to the stream
2171
+ - `toArray()` never completes if the stream errors
2172
+ - **Result: one failure stops everything**
2173
+
2174
+ ### Comparison: `.parallel()` vs `Promise.allSettled`
2175
+
2176
+ | Feature | `.parallel()` | `Promise.allSettled` |
2177
+ |---------|--------------|---------------------|
2178
+ | Fault tolerance | ❌ Fails on first error | ✅ Continues on errors |
2179
+ | Partial results | ❌ None if any fail | ✅ All results |
2180
+ | Error isolation | ❌ No isolation | ✅ Complete isolation |
2181
+ | Retry failures | ❌ Must retry all | ✅ Retry only failures |
2182
+
2183
+ ### When to Use `.parallel()`
2184
+
2185
+ **ONLY use `.parallel()` when:**
2186
+ 1. ✅ **All tasks MUST succeed** (transactional requirement)
2187
+ 2. ✅ **Small number of items** (3-10 max)
2188
+ 3. ✅ **Fast, reliable operations** (< 10 seconds each)
2189
+ 4. ✅ **Partial success is worse than complete failure**
2190
+
2191
+ **Examples where `.parallel()` is appropriate:**
2192
+ - Fetching 3-5 required entity types (all needed for next step)
2193
+ - Validating multiple webhook signatures (all must be valid)
2194
+ - Small, predictable, low-failure-rate operations
2195
+
2196
+ ### When to Use `Promise.allSettled` (SDK Pattern)
2197
+
2198
+ **PREFER `Promise.allSettled` for:**
2199
+ 1. ✅ **Batch processing** (files, records, entities)
2200
+ 2. ✅ **Large datasets** (10+ items)
2201
+ 3. ✅ **Failures are expected** (network issues, bad data)
2202
+ 4. ✅ **Partial success is valuable** (process 990/1000 items)
2203
+ 5. ✅ **Need retry logic** (retry only failures)
2204
+
2205
+ **Example using SDK pattern:**
2206
+ ```typescript
2207
+ export const batchProcess = schedule('batch', '0 */6 * * *')
2208
+ .then(
2209
+ http('process', { connection: 'fluent_commerce' }, async (ctx) => {
2210
+ const items = await fetchItems();
2211
+
2212
+ // ✅ Use Promise.allSettled for fault tolerance
2213
+ const results = await Promise.allSettled(
2214
+ items.map(item =>
2215
+ processItem(item)
2216
+ .then(result => ({ success: true, item, result }))
2217
+ .catch(error => ({ success: false, item, error: error.message }))
2218
+ )
2219
+ );
2220
+
2221
+ const successes = results
2222
+ .filter(r => r.status === 'fulfilled')
2223
+ .map(r => r.value);
2224
+
2225
+ const failures = results
2226
+ .filter(r => r.status === 'rejected' || !r.value.success);
2227
+
2228
+ ctx.log.info(`Processed ${successes.length} items, ${failures.length} failed`);
2229
+
2230
+ // ✅ Handle failures gracefully
2231
+ if (failures.length > 0) {
2232
+ // Store failures for retry
2233
+ const kv = ctx.openKv(':project:');
2234
+ await kv.set('failed-items', failures);
2235
+ }
2236
+
2237
+ return { successes, failures };
2238
+ })
2239
+ );
2240
+ ```
2241
+
2242
+ ---
2243
+
2244
+ ## Parallel Processing with `.unpack()` and `.parallel()`
2245
+
2246
+ Versori supports parallel processing via `.unpack()` and `.parallel()` methods, but **they have critical limitations** that affect fault tolerance and error handling (see warning above).
2247
+
2248
+ ### Available Methods
2249
+
2250
+ ```typescript
2251
+ import { schedule, fn } from '@versori/run';
2252
+
2253
+ schedule('process-files', '0 2 * * *')
2254
+ .then(fn('fetch-files', (ctx) => {
2255
+ return ['file1.xml', 'file2.xml', 'file3.xml']; // Returns array
2256
+ }))
2257
+ .unpack() // ✅ Converts array to ArrayTask
2258
+ .parallel( // ✅ Processes each item in parallel
2259
+ fn('process-file', async (ctx) => {
2260
+ // ctx.data is now a single item from the array
2261
+ return await processFile(ctx.data);
2262
+ })
2263
+ );
2264
+ ```
2265
+
2266
+ ### ⚠️ CRITICAL LIMITATIONS
2267
+
2268
+ #### 1. **NOT Fault-Tolerant: One Failure Stops Everything**
2269
+
2270
+ **Problem**: If any parallel task fails, the **entire workflow fails** and **no results are returned**.
2271
+
2272
+ ```typescript
2273
+ // ❌ RISKY: If file2.xml fails, entire workflow fails
2274
+ schedule('process-files', '0 2 * * *')
2275
+ .then(fn('fetch-files', (ctx) => ['file1.xml', 'file2.xml', 'file3.xml']))
2276
+ .unpack()
2277
+ .parallel(fn('process-file', async (ctx) => {
2278
+ // If ANY file fails here, ALL results lost
2279
+ return await processFile(ctx.data);
2280
+ }));
2281
+
2282
+ // Result:
2283
+ // ✅ file1.xml might complete
2284
+ // ❌ file2.xml fails → ENTIRE WORKFLOW FAILS
2285
+ // ❌ file3.xml NEVER RUNS (or gets cancelled)
2286
+ // ❌ No partial results returned
2287
+ ```
2288
+
2289
+ **Why**: Uses RxJS `mergeMap` + `toArray()` which stops on first error.
2290
+
2291
+ #### 2. **Shared Timeout Across All Tasks**
2292
+
2293
+ **Problem**: All parallel tasks share the **same workflow timeout**.
2294
+
2295
+ | Trigger Type | Timeout | Impact |
2296
+ |-------------|---------|--------|
2297
+ | **HTTP/Webhook** | 30 seconds | All parallel tasks must complete in 30s total |
2298
+ | **Schedule** | 5 minutes | All parallel tasks must complete in 5min total |
2299
+
2300
+ ```typescript
2301
+ // ⚠️ If processing 10 files in parallel:
2302
+ schedule('process-files', '0 2 * * *')
2303
+ .then(fn('fetch-files', (ctx) => Array(10).fill('file.xml')))
2304
+ .unpack()
2305
+ .parallel(fn('process-file', async (ctx) => {
2306
+ // If ANY file takes > 5 minutes, ENTIRE workflow fails
2307
+ return await processFile(ctx.data);
2308
+ }));
2309
+ ```
2310
+
2311
+ #### 3. **NOT ACID Compliant**
2312
+
2313
+ **Problem**: No atomicity guarantees. If one task fails:
2314
+ - ✅ Successful tasks may have completed their work
2315
+ - ❌ But you won't know which ones succeeded
2316
+ - ❌ No rollback mechanism
2317
+ - ❌ No transaction support
2318
+
2319
+ ### ✅ Recommended: Use `Promise.allSettled()` Instead
2320
+
2321
+ For **fault-tolerant** parallel processing, use `Promise.allSettled()` which continues even when some tasks fail:
2322
+
2323
+ ```typescript
2324
+ import { schedule, fn } from '@versori/run';
2325
+
2326
+ schedule('process-files', '0 2 * * *')
2327
+ .then(fn('process-all-files', async (ctx) => {
2328
+ const files = ['file1.xml', 'file2.xml', 'file3.xml'];
2329
+
2330
+ // ✅ Fault-tolerant: Continues even if some fail
2331
+ const results = await Promise.allSettled(
2332
+ files.map(file => processFile(file))
2333
+ );
2334
+
2335
+ // Process results
2336
+ const successes = results
2337
+ .filter(r => r.status === 'fulfilled')
2338
+ .map(r => r.value);
2339
+
2340
+ const failures = results
2341
+ .filter(r => r.status === 'rejected')
2342
+ .map(r => ({ file: r.reason.file, error: r.reason.message }));
2343
+
2344
+ ctx.log.info(`Processed ${successes.length} files, ${failures.length} failed`);
2345
+
2346
+ return { successes, failures };
2347
+ }));
2348
+ ```
2349
+
2350
+ **Comparison**:
2351
+
2352
+ | Feature | `.unpack().parallel()` | `Promise.allSettled()` |
2353
+ |---------|------------------------|------------------------|
2354
+ | Fault tolerance | ❌ Fails on first error | ✅ Continues on errors |
2355
+ | Partial results | ❌ No results if any fail | ✅ Returns all results |
2356
+ | Error isolation | ❌ No isolation | ✅ Complete isolation |
2357
+ | Time limits | ⚠️ Shared timeout | ⚠️ Shared timeout |
2358
+ | Best for | All-or-nothing workflows | Real-world with failures |
2359
+
2360
+ ### When to Use `.unpack().parallel()`
2361
+
2362
+ **Only use if**:
2363
+ - ✅ All tasks **must** succeed (transactional)
2364
+ - ✅ You can wrap each task with error handling that returns error objects instead of throwing
2365
+ - ✅ You're okay with losing all results on any failure
2366
+
2367
+ **Example with error handling**:
2368
+
2369
+ ```typescript
2370
+ schedule('process-files', '0 2 * * *')
2371
+ .then(fn('fetch-files', (ctx) => ['file1.xml', 'file2.xml', 'file3.xml']))
2372
+ .unpack()
2373
+ .parallel(fn('process-file', async (ctx) => {
2374
+ try {
2375
+ const result = await processFile(ctx.data);
2376
+ return { success: true, file: ctx.data, result };
2377
+ } catch (error) {
2378
+ // ✅ Return error object instead of throwing
2379
+ return {
2380
+ success: false,
2381
+ file: ctx.data,
2382
+ error: error.message
2383
+ };
2384
+ }
2385
+ }))
2386
+ .then(fn('process-results', (ctx) => {
2387
+ // ✅ Now you have all results (success + failures)
2388
+ const results = ctx.data;
2389
+ const successes = results.filter(r => r.success);
2390
+ const failures = results.filter(r => !r.success);
2391
+
2392
+ ctx.log.info(`Processed ${successes.length} files, ${failures.length} failed`);
2393
+ return { successes, failures };
2394
+ }));
2395
+ ```
2396
+
2397
+ ### When to Use `Promise.allSettled()` (Recommended)
2398
+
2399
+ **Use `Promise.allSettled()` for**:
2400
+ - ✅ Batch API submissions
2401
+ - ✅ File processing
2402
+ - ✅ Any scenario where failures are expected
2403
+ - ✅ When you need partial results
2404
+
2405
+ **Already implemented in your code**:
2406
+
2407
+ ```typescript
2408
+ // ✅ Your current BatchProcessorService uses this pattern
2409
+ const batchResults = await Promise.allSettled(
2410
+ batchChunk.map((chunk, index) =>
2411
+ this.client.sendBatch(jobId, {
2412
+ action: 'UPSERT',
2413
+ entityType: 'INVENTORY',
2414
+ entities: chunk,
2415
+ }).then(batch => ({ success: true, batch }))
2416
+ .catch(error => ({ success: false, error }))
2417
+ )
2418
+ );
2419
+ ```
2420
+
2421
+ ---
2422
+
2423
+ ## Key Takeaways
2424
+
2425
+ - 🎯 **http()** for external API calls - requires connection, provides `ctx.fetch`
2426
+ - 🎯 **webhook()** for receiving requests - provides `ctx.data`, `ctx.request()` for headers
2427
+ - 🎯 **schedule()** for time-based tasks - supports cron patterns, can have connection
2428
+ - 🎯 **fn()** for internal processing - NO external API access, has `ctx.openKv()`
2429
+ - 🎯 **Non-JSON responses** require custom `onSuccess`/`onError` handlers returning Response objects
2430
+ - 🎯 **Workflow composition** with `.then()` and `.catch()` enables multi-step patterns
2431
+ - 🎯 **Error handling** with try/catch and retry configuration ensures robustness
2432
+ - ⚠️ **`.unpack().parallel()`** exists but is NOT fault-tolerant - use `Promise.allSettled()` for resilience
2433
+
2434
+ ---
2435
+
2436
+ ## Practice Exercise
2437
+
2438
+ Create a scheduled workflow that:
2439
+
2440
+ 1. Runs hourly (cron: `0 * * * *`)
2441
+ 2. Fetches inventory from an S3 CSV file
2442
+ 3. Checks KV storage to avoid duplicate processing
2443
+ 4. Transforms data using UniversalMapper
2444
+ 5. Sends to Fluent via Batch API
2445
+ 6. Marks file as processed in KV storage
2446
+ 7. Returns XML response on error
2447
+
2448
+ **Hints**:
2449
+
2450
+ - Use `schedule()` with connection
2451
+ - Chain with `fn()` for KV checks
2452
+ - Use `http()` context for Fluent API calls
2453
+ - Implement custom error handler for XML response
2454
+
2455
+ **Solution** available in [Module 8: Best Practices](./platforms-versori-08-best-practices.md#practice-solutions)
2456
+
2457
+ ---
2458
+
2459
+ ## Next Steps
2460
+
2461
+ Now that you've mastered all workflow types, let's explore connection management in detail.
2462
+
2463
+ Continue to [Module 5: Connections →](./platforms-versori-05-connections.md) to learn about OAuth2 connection types, management, and validation.
2464
+
2465
+ ---
2466
+
2467
+ ## Related Documentation
2468
+
2469
+ - [Module 3: Authentication](./platforms-versori-03-authentication.md) - OAuth2 basics
2470
+ - [Module 5: Connections](./platforms-versori-05-connections.md) - Connection management
2471
+ - [Module 6: KV Storage](./platforms-versori-06-kv-storage.md) - State management
2472
+ - [Webhook Response Patterns](.././modules/platforms-versori-04-workflows.md#critical-non-json-response-handlers-xml-html-csv) - Complete non-JSON response guide
2473
+
2474
+ ---
2475
+
2476
+ [← Previous: Module 3](./platforms-versori-03-authentication.md) | [Back to Guide](../platforms-versori-readme.md) | [Next: Module 5: Connections →](./platforms-versori-05-connections.md)