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