@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55

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 (475) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/clients/fluent-client.js +13 -6
  3. package/dist/cjs/utils/pagination-helpers.js +38 -2
  4. package/dist/cjs/versori/fluent-versori-client.js +11 -5
  5. package/dist/esm/clients/fluent-client.js +13 -6
  6. package/dist/esm/utils/pagination-helpers.js +38 -2
  7. package/dist/esm/versori/fluent-versori-client.js +11 -5
  8. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  9. package/dist/tsconfig.tsbuildinfo +1 -1
  10. package/dist/tsconfig.types.tsbuildinfo +1 -1
  11. package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
  12. package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
  13. package/docs/00-START-HERE/cli-documentation-index.md +202 -202
  14. package/docs/00-START-HERE/cli-quick-reference.md +252 -252
  15. package/docs/00-START-HERE/decision-tree.md +552 -552
  16. package/docs/00-START-HERE/getting-started.md +1070 -1070
  17. package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
  18. package/docs/00-START-HERE/readme.md +237 -237
  19. package/docs/00-START-HERE/retailerid-configuration.md +404 -404
  20. package/docs/00-START-HERE/sdk-philosophy.md +794 -794
  21. package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
  22. package/docs/01-TEMPLATES/faq.md +686 -686
  23. package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
  24. package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
  25. package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
  26. package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
  27. package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
  28. package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
  29. package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
  30. package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
  31. package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
  32. package/docs/01-TEMPLATES/readme.md +957 -957
  33. package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
  34. package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
  35. package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
  36. package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
  37. package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
  38. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
  39. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
  40. package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
  41. package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
  42. package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
  43. package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
  44. package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
  45. package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
  46. package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
  47. package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
  48. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
  49. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
  50. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
  51. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
  52. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
  53. package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
  54. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
  55. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
  56. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
  57. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
  58. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
  59. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
  60. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
  61. package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
  62. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
  63. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
  64. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
  65. package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
  66. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
  67. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
  68. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
  69. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
  70. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
  71. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
  72. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
  73. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
  74. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
  75. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
  76. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
  77. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
  78. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
  79. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
  80. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
  81. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
  82. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
  83. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
  84. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
  85. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
  86. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
  87. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
  88. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
  89. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
  90. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
  91. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
  92. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
  93. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
  94. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
  95. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
  96. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
  97. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
  98. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
  99. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
  100. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
  101. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
  102. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
  103. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
  104. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
  105. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
  106. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
  107. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
  108. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
  109. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
  110. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
  111. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
  112. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
  113. package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
  114. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
  115. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
  116. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
  117. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
  118. package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
  119. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
  120. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
  121. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
  122. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
  123. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
  124. package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
  125. package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
  126. package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
  127. package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
  128. package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
  129. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
  130. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
  131. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
  132. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
  133. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
  134. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
  135. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
  136. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
  137. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
  138. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
  139. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
  140. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
  141. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
  142. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
  143. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
  144. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
  145. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
  146. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
  147. package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
  148. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
  149. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
  150. package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
  151. package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
  152. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
  153. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
  154. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
  155. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
  156. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
  157. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
  158. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
  159. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
  160. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
  161. package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
  162. package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
  163. package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
  164. package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
  165. package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
  166. package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
  167. package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
  168. package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
  169. package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
  170. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
  171. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
  172. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
  173. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
  174. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
  175. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
  176. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
  177. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
  178. package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
  179. package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
  180. package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
  181. package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
  182. package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
  183. package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
  184. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
  185. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
  186. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
  187. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
  188. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
  189. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
  190. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
  191. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
  192. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
  193. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
  194. package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
  195. package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
  196. package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
  197. package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
  198. package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
  199. package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
  200. package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
  201. package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
  202. package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
  203. package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
  204. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
  205. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
  206. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
  207. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
  208. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
  209. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
  210. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
  211. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
  212. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
  213. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
  214. package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
  215. package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
  216. package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
  217. package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
  218. package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
  219. package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
  220. package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
  221. package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
  222. package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
  223. package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
  224. package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
  225. package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
  226. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
  227. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
  228. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
  229. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
  230. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
  231. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
  232. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
  233. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
  234. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
  235. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
  236. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
  237. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
  238. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
  239. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
  240. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
  241. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
  242. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
  243. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
  244. package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
  245. package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
  246. package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
  247. package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
  248. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
  249. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
  250. package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
  251. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
  252. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
  253. package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
  254. package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
  255. package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
  256. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
  257. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
  258. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
  259. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
  260. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
  261. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
  262. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
  263. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
  264. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
  265. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
  266. package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
  267. package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
  268. package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
  269. package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
  270. package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
  271. package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
  272. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
  273. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
  274. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
  275. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
  276. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
  277. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
  278. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
  279. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
  280. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
  281. package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
  282. package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
  283. package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
  284. package/docs/02-CORE-GUIDES/readme.md +194 -194
  285. package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
  286. package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
  287. package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
  288. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
  289. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
  290. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
  291. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
  292. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
  293. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
  294. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
  295. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
  296. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
  297. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
  298. package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
  299. package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
  300. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
  301. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
  302. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
  303. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
  304. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
  305. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
  306. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
  307. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
  308. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
  309. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
  310. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
  311. package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
  312. package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
  313. package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
  314. package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
  315. package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
  316. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
  317. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
  318. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
  319. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
  320. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
  321. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
  322. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
  323. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
  324. package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
  325. package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
  326. package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
  327. package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
  328. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
  329. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
  330. package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
  331. package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
  332. package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
  333. package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
  334. package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
  335. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
  336. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
  337. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
  338. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
  339. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
  340. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
  341. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
  342. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
  343. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
  344. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
  345. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
  346. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
  347. package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
  348. package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
  349. package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
  350. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
  351. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
  352. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
  353. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
  354. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
  355. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
  356. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
  357. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
  358. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
  359. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
  360. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
  361. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
  362. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
  363. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
  364. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
  365. package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
  366. package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
  367. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
  368. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
  369. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
  370. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
  371. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
  372. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
  373. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
  374. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
  375. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
  376. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
  377. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
  378. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
  379. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
  380. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
  381. package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
  382. package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
  383. package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
  384. package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
  385. package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
  386. package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
  387. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
  388. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
  389. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
  390. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
  391. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
  392. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
  393. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
  394. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
  395. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
  396. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
  397. package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
  398. package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
  399. package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
  400. package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
  401. package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
  402. package/docs/03-PATTERN-GUIDES/readme.md +159 -159
  403. package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
  404. package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
  405. package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
  406. package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
  407. package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
  408. package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
  409. package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
  410. package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
  411. package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
  412. package/docs/04-REFERENCE/architecture/readme.md +279 -279
  413. package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
  414. package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
  415. package/docs/04-REFERENCE/platforms/readme.md +135 -135
  416. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
  417. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
  418. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
  419. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
  420. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
  421. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
  422. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
  423. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
  424. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
  425. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
  426. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
  427. package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
  428. package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
  429. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
  430. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
  431. package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
  432. package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
  433. package/docs/04-REFERENCE/readme.md +148 -148
  434. package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
  435. package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
  436. package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
  437. package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
  438. package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
  439. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
  440. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
  441. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
  442. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
  443. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
  444. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
  445. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
  446. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
  447. package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
  448. package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
  449. package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
  450. package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
  451. package/docs/04-REFERENCE/schema/readme.md +141 -141
  452. package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
  453. package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
  454. package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
  455. package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
  456. package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
  457. package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
  458. package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
  459. package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
  460. package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
  461. package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
  462. package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
  463. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
  464. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
  465. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
  466. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
  467. package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
  468. package/docs/04-REFERENCE/testing/readme.md +86 -86
  469. package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
  470. package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
  471. package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
  472. package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
  473. package/docs/template-loading-matrix.md +242 -242
  474. package/package.json +5 -3
  475. 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)