@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/clients/fluent-client.js +13 -6
  3. package/dist/cjs/utils/pagination-helpers.js +38 -2
  4. package/dist/cjs/versori/fluent-versori-client.js +11 -5
  5. package/dist/esm/clients/fluent-client.js +13 -6
  6. package/dist/esm/utils/pagination-helpers.js +38 -2
  7. package/dist/esm/versori/fluent-versori-client.js +11 -5
  8. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  9. package/dist/tsconfig.tsbuildinfo +1 -1
  10. package/dist/tsconfig.types.tsbuildinfo +1 -1
  11. package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
  12. package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
  13. package/docs/00-START-HERE/cli-documentation-index.md +202 -202
  14. package/docs/00-START-HERE/cli-quick-reference.md +252 -252
  15. package/docs/00-START-HERE/decision-tree.md +552 -552
  16. package/docs/00-START-HERE/getting-started.md +1070 -1070
  17. package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
  18. package/docs/00-START-HERE/readme.md +237 -237
  19. package/docs/00-START-HERE/retailerid-configuration.md +404 -404
  20. package/docs/00-START-HERE/sdk-philosophy.md +794 -794
  21. package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
  22. package/docs/01-TEMPLATES/faq.md +686 -686
  23. package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
  24. package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
  25. package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
  26. package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
  27. package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
  28. package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
  29. package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
  30. package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
  31. package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
  32. package/docs/01-TEMPLATES/readme.md +957 -957
  33. package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
  34. package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
  35. package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
  36. package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
  37. package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
  38. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
  39. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
  40. package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
  41. package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
  42. package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
  43. package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
  44. package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
  45. package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
  46. package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
  47. package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
  48. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
  49. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
  50. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
  51. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
  52. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
  53. package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
  54. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
  55. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
  56. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
  57. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
  58. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
  59. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
  60. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
  61. package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
  62. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
  63. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
  64. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
  65. package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
  66. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
  67. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
  68. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
  69. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
  70. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
  71. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
  72. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
  73. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
  74. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
  75. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
  76. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
  77. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
  78. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
  79. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
  80. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
  81. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
  82. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
  83. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
  84. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
  85. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
  86. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
  87. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
  88. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
  89. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
  90. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
  91. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
  92. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
  93. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
  94. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
  95. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
  96. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
  97. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
  98. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
  99. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
  100. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
  101. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
  102. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
  103. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
  104. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
  105. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
  106. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
  107. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
  108. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
  109. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
  110. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
  111. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
  112. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
  113. package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
  114. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
  115. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
  116. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
  117. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
  118. package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
  119. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
  120. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
  121. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
  122. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
  123. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
  124. package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
  125. package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
  126. package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
  127. package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
  128. package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
  129. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
  130. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
  131. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
  132. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
  133. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
  134. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
  135. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
  136. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
  137. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
  138. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
  139. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
  140. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
  141. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
  142. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
  143. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
  144. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
  145. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
  146. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
  147. package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
  148. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
  149. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
  150. package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
  151. package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
  152. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
  153. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
  154. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
  155. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
  156. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
  157. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
  158. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
  159. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
  160. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
  161. package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
  162. package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
  163. package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
  164. package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
  165. package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
  166. package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
  167. package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
  168. package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
  169. package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
  170. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
  171. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
  172. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
  173. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
  174. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
  175. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
  176. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
  177. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
  178. package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
  179. package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
  180. package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
  181. package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
  182. package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
  183. package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
  184. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
  185. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
  186. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
  187. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
  188. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
  189. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
  190. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
  191. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
  192. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
  193. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
  194. package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
  195. package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
  196. package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
  197. package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
  198. package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
  199. package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
  200. package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
  201. package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
  202. package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
  203. package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
  204. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
  205. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
  206. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
  207. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
  208. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
  209. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
  210. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
  211. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
  212. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
  213. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
  214. package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
  215. package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
  216. package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
  217. package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
  218. package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
  219. package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
  220. package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
  221. package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
  222. package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
  223. package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
  224. package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
  225. package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
  226. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
  227. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
  228. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
  229. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
  230. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
  231. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
  232. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
  233. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
  234. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
  235. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
  236. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
  237. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
  238. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
  239. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
  240. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
  241. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
  242. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
  243. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
  244. package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
  245. package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
  246. package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
  247. package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
  248. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
  249. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
  250. package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
  251. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
  252. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
  253. package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
  254. package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
  255. package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
  256. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
  257. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
  258. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
  259. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
  260. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
  261. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
  262. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
  263. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
  264. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
  265. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
  266. package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
  267. package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
  268. package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
  269. package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
  270. package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
  271. package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
  272. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
  273. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
  274. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
  275. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
  276. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
  277. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
  278. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
  279. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
  280. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
  281. package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
  282. package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
  283. package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
  284. package/docs/02-CORE-GUIDES/readme.md +194 -194
  285. package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
  286. package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
  287. package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
  288. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
  289. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
  290. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
  291. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
  292. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
  293. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
  294. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
  295. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
  296. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
  297. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
  298. package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
  299. package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
  300. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
  301. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
  302. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
  303. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
  304. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
  305. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
  306. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
  307. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
  308. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
  309. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
  310. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
  311. package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
  312. package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
  313. package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
  314. package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
  315. package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
  316. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
  317. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
  318. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
  319. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
  320. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
  321. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
  322. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
  323. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
  324. package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
  325. package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
  326. package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
  327. package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
  328. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
  329. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
  330. package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
  331. package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
  332. package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
  333. package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
  334. package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
  335. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
  336. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
  337. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
  338. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
  339. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
  340. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
  341. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
  342. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
  343. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
  344. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
  345. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
  346. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
  347. package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
  348. package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
  349. package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
  350. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
  351. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
  352. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
  353. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
  354. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
  355. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
  356. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
  357. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
  358. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
  359. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
  360. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
  361. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
  362. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
  363. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
  364. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
  365. package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
  366. package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
  367. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
  368. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
  369. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
  370. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
  371. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
  372. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
  373. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
  374. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
  375. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
  376. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
  377. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
  378. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
  379. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
  380. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
  381. package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
  382. package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
  383. package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
  384. package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
  385. package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
  386. package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
  387. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
  388. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
  389. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
  390. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
  391. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
  392. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
  393. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
  394. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
  395. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
  396. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
  397. package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
  398. package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
  399. package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
  400. package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
  401. package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
  402. package/docs/03-PATTERN-GUIDES/readme.md +159 -159
  403. package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
  404. package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
  405. package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
  406. package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
  407. package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
  408. package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
  409. package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
  410. package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
  411. package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
  412. package/docs/04-REFERENCE/architecture/readme.md +279 -279
  413. package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
  414. package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
  415. package/docs/04-REFERENCE/platforms/readme.md +135 -135
  416. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
  417. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
  418. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
  419. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
  420. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
  421. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
  422. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
  423. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
  424. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
  425. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
  426. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
  427. package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
  428. package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
  429. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
  430. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
  431. package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
  432. package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
  433. package/docs/04-REFERENCE/readme.md +148 -148
  434. package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
  435. package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
  436. package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
  437. package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
  438. package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
  439. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
  440. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
  441. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
  442. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
  443. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
  444. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
  445. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
  446. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
  447. package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
  448. package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
  449. package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
  450. package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
  451. package/docs/04-REFERENCE/schema/readme.md +141 -141
  452. package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
  453. package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
  454. package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
  455. package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
  456. package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
  457. package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
  458. package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
  459. package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
  460. package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
  461. package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
  462. package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
  463. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
  464. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
  465. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
  466. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
  467. package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
  468. package/docs/04-REFERENCE/testing/readme.md +86 -86
  469. package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
  470. package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
  471. package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
  472. package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
  473. package/docs/template-loading-matrix.md +242 -242
  474. package/package.json +5 -3
  475. package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
@@ -1,1952 +1,1952 @@
1
- ---
2
- template_id: tpl-ingest-s3-json-inventory-batch
3
- canonical_filename: template-ingestion-s3-json-inventory-batch.md
4
- version: 2.0.0
5
- sdk_version: ^0.1.39
6
- runtime: versori
7
- direction: ingestion
8
- source: s3-json
9
- destination: fluent-batch-api
10
- entity: inventory
11
- format: json
12
- logging: versori
13
- status: stable
14
- features:
15
- - batch-api-integration
16
- - memory-management
17
- - enhanced-logging
18
- - attribute-transformation
19
- ---
20
-
21
- # Template: Ingestion - S3 JSON Inventory to Batch API
22
-
23
- **FC Connect SDK Use Case Guide**
24
-
25
- > **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
26
- > **Version**: @fluentcommerce/fc-connect-sdk@^0.1.39
27
-
28
- **🆕 Production Code Enhancements (Applied):**
29
- 1. ✅ Batch API with retry logic and BPP change detection
30
- 2. ✅ Memory management (clearing large arrays after batch processing)
31
- 3. ✅ Enhanced logging with emoji progress tracking (📦 batch creation, 📤 batch sending, ✅ completion)
32
- 4. ✅ Attribute transformation with nested field support
33
-
34
- **Template Version:** 2.0.0
35
- **Last Updated:** 2025-01-24
36
-
37
- **Context**: Versori scheduled workflow that reads inventory JSON files from S3, transforms data with UniversalMapper, and sends bulk inventory updates to Fluent Commerce Batch API with BPP change detection
38
-
39
- **Complexity**: Medium
40
-
41
- **Runtime**: Versori Platform
42
-
43
- **Estimated Lines**: ~520 lines
44
-
45
- ---
46
-
47
- ## STEP 1: Understand This Template
48
-
49
- **What This Template Does:**
50
-
51
- - Scheduled Versori workflow for bulk inventory ingestion from S3 JSON files
52
- - Reads JSON files from S3 with retry logic and streaming support
53
- - Parses JSON with format detection (standard JSON vs JSON Lines)
54
- - Transforms data using UniversalMapper with direct field access (no special notation)
55
- - Sends bulk updates to Fluent Commerce Batch API with chunking
56
- - Uses BPP (Batch Pre-Processing) for change detection
57
- - Tracks processing state with VersoriFileTracker to prevent duplicates
58
- - Archives processed files and handles errors gracefully
59
-
60
- **Key SDK Components:**
61
-
62
- - `createClient()` - Universal client factory (auto-detects Versori context)
63
- - `S3DataSource` - S3 operations with streaming (NEW: enhanced retry logic)
64
- - `JSONParserService` - JSON parsing with format auto-detection (JSON vs JSON Lines)
65
- - `UniversalMapper` - Field transformation with SDK resolvers
66
- - `VersoriFileTracker` - State management (prevent duplicate processing)
67
- - `JobTracker` - Job lifecycle tracking
68
- - Native Versori `log` - Use `log` from context
69
-
70
- **Entity Type:**
71
-
72
- - **InventoryQuantity** - Fluent entity for inventory positions and quantities
73
- - **EntityType: 'INVENTORY'** - Used in Batch API `sendBatch()` call
74
- - **Batch API Method** - Uses `createJob()` and `sendBatch()` (not Event API)
75
-
76
- **Critical Patterns:**
77
-
78
- - **JSON Format Support**: Standard JSON objects and JSON Lines (one record per line)
79
- - **Direct Field Access**: Use simple paths like `"locationRef"` (no `@` prefix needed for JSON)
80
- - **Safe S3 Paths**: Use absolute paths in S3DataSource config
81
- - **Always Dispose**: Call `s3.dispose()` in `finally` block to release connections
82
- - **BPP Configuration**: Enabled by default for full snapshots, skip for delta feeds
83
- - **Progress Logging**: Enhanced logging with context (sample SKUs, locations)
84
- - **Detailed Logging Toggle**: Optional detailed payload logging for debugging (default: disabled)
85
- - **File Tracking Toggle**: Optional file tracking to prevent duplicates (default: enabled)
86
- - **Retry Logic**: Enhanced S3 operations with exponential backoff
87
-
88
- **When to Use This Template:**
89
-
90
- - ✅ Daily/hourly full inventory snapshots from S3 JSON files
91
- - ✅ Bulk inventory updates with BPP change detection
92
- - ✅ JSON or JSON Lines format data
93
- - ✅ Need state management to prevent duplicate processing
94
- - ✅ Files should be archived after processing
95
-
96
- **When NOT to Use:**
97
-
98
- - ❌ Single inventory updates (use GraphQL mutation instead)
99
- - ❌ Products, Locations, Customers (use Event API templates)
100
- - ❌ Real-time inventory events (use Event API)
101
- - ❌ XML/CSV/Parquet formats (use appropriate format template)
102
-
103
- ---
104
-
105
- ## STEP 2: AI Prompt
106
-
107
- **Copy this prompt to generate the complete implementation:**
108
-
109
- ```
110
- Create a Versori scheduled workflow for S3 JSON inventory ingestion to Fluent Commerce Batch API.
111
-
112
- REQUIREMENTS:
113
- 1. Runtime: Versori Platform (scheduled workflow)
114
- 2. Source: S3 JSON files from s3://bucket/inventory/
115
- 3. Destination: Fluent Commerce Batch API (InventoryQuantity entity)
116
- 4. Format: JSON (standard JSON objects and JSON Lines support)
117
- 5. Entity: InventoryQuantity (EntityType: 'INVENTORY')
118
-
119
- KEY FEATURES:
120
- - S3 JSON file discovery with VersoriFileTracker for state management
121
- - JSON parsing with format auto-detection (JSON vs JSON Lines)
122
- - Direct field mapping using UniversalMapper (no special notation needed)
123
- - Batch API with chunking (1000 records per batch) and BPP change detection
124
- - Safe S3 paths with absolute path requirements
125
- - S3 dispose() in finally block
126
- - Job lifecycle tracking with JobTracker
127
- - File archival on S3 after successful processing
128
- - Error handling with exponential backoff retry
129
-
130
- CRITICAL REQUIREMENTS:
131
- 1. JSON Parser: JSONParserService with format auto-detection
132
- 2. Array Extraction: Handle both standard JSON arrays and JSON Lines
133
- 3. Mapping Config: Use direct paths like "locationRef" (no @ prefix)
134
- 4. Entity Type: 'INVENTORY' (for InventoryQuantity)
135
- 5. BPP: Enabled by default (full snapshots), skip for delta feeds
136
- 6. S3 Config: Include proper AWS credentials configuration
137
- 7. S3 Dispose: Always call s3.dispose() in finally block
138
- 8. Native Logging: Use log from context
139
- 9. Retry Logic: Enhanced S3 operations with exponential backoff
140
-
141
- SDK METHODS TO USE:
142
- - createClient(ctx) - Pass entire Versori context, auto-detects platform
143
- - client.setRetailerId(retailerId) - REQUIRED after createClient for Batch API operations
144
- - new S3DataSource(config, log) - Config includes AWS credentials
145
- - await s3.listFiles({ prefix, maxKeys })
146
- - await s3.downloadFile(key, { encoding: 'utf8' })
147
- - await s3.moveFile(sourceKey, destKey)
148
- - await s3.dispose() - MUST call in finally block
149
- - new JSONParserService()
150
- - await parser.parse(jsonContent, { format: 'json' | 'jsonl' })
151
- - new UniversalMapper(mappingConfig)
152
- - await mapper.map(records)
153
- - new VersoriFileTracker(ctx.openKv(':project:'), 'prefix')
154
- - await fileTracker.wasFileProcessed(fileName)
155
- - await fileTracker.markFileProcessed(fileName, metadata)
156
- - new JobTracker(ctx.openKv(':project:'), log)
157
- - await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' })
158
- - await tracker.updateJob(jobId, { status: 'processing' })
159
- - await tracker.markCompleted(jobId, details)
160
- - await tracker.markFailed(jobId, error)
161
- - await client.createJob({ name, meta: { preprocessing: 'skip' } })
162
- - await client.sendBatch(jobId, { action: 'UPSERT', entityType: 'INVENTORY', entities })
163
- - Fire-and-forget batch submission (no polling)
164
-
165
- FORBIDDEN PATTERNS:
166
- - ❌ LoggingService (removed - use native log on Versori)
167
- - ❌ Don't use @ prefix for JSON fields (that's for XML attributes only)
168
- - ❌ Don't forget to wrap records in proper structure for mapping
169
- - ❌ Don't use Event API (use Batch API)
170
- - ❌ Don't forget to call s3.dispose() in finally block
171
- - ❌ Don't use relative S3 paths (use absolute paths)
172
-
173
- MAPPING CONFIGURATION FILE: config/inventory.batch.json
174
- Structure:
175
- {
176
- "name": "inventory.batch.json",
177
- "version": "1.0.0",
178
- "description": "JSON inventory to Fluent Commerce Batch API mapping",
179
- "fields": {
180
- "locationRef": { "source": "locationRef", "required": true, "resolver": "sdk.trim" },
181
- "skuRef": { "source": "skuRef", "required": true, "resolver": "sdk.trim" },
182
- "qty": { "source": "qty", "required": true, "resolver": "sdk.parseInt" },
183
- "type": { "source": "type", "required": true, "resolver": "sdk.uppercase" },
184
- "status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
185
- "expectedOn": { "source": "expectedOn", "required": false, "resolver": "sdk.formatDate" },
186
- "attributes.expiryDate": { "source": "attributes.expiryDate", "required": false, "resolver": "sdk.formatDate" },
187
- "attributes.batchNumber": { "source": "attributes.batchNumber", "required": false },
188
- "attributes.condition": { "source": "attributes.condition", "required": false, "defaultValue": "NEW", "resolver": "sdk.uppercase" },
189
- "attributes.storageZone": { "source": "attributes.storageZone", "required": false }
190
- }
191
- }
192
-
193
- GENERATE:
194
- 1. package.json with dependencies
195
- 2. index.ts (workflow entry point with scheduled trigger)
196
- 3. src/workflows/scheduled/daily-inventory-sync.ts (scheduled workflow)
197
- 4. src/workflows/webhook/adhoc-inventory-sync.ts (webhook workflow)
198
- 5. src/workflows/webhook/job-status-check.ts (status webhook)
199
- 6. src/services/inventory-sync.service.ts (shared orchestration logic)
200
- 7. src/types/inventory.types.ts (TypeScript type definitions)
201
- 8. config/inventory.batch.json (mapping configuration - external JSON file)
202
- 9. .env.example (environment variables)
203
-
204
- NOTE: Use external JSON files for mapping configuration (not TypeScript .config files)
205
-
206
- Ensure all code is production-ready with proper error handling, S3 dispose() in finally block. Polling is intentionally omitted (fire-and-forget).
207
- ```
208
-
209
- ---
210
-
211
- ## What You'll Build
212
-
213
- ### Project Structure
214
-
215
- ```
216
- s3-json-inventory-batch-sync/
217
- ├── package.json
218
- ├── index.ts # Workflow entry point
219
- └── src/
220
- ├── workflows/
221
- │ ├── scheduled/
222
- │ │ └── daily-inventory-sync.ts # Scheduled: Daily inventory sync
223
- │ │
224
- │ └── webhook/
225
- │ ├── adhoc-inventory-sync.ts # Webhook: Manual trigger
226
- │ └── job-status-check.ts # Webhook: Status query
227
-
228
- ├── services/
229
- │ └── inventory-sync.service.ts # Shared orchestration logic (reusable)
230
-
231
- ├── config/
232
- │ └── inventory.batch.json # Mapping configuration (external JSON)
233
-
234
- └── types/
235
- └── inventory.types.ts # TypeScript interfaces
236
- ```
237
-
238
- ### Features
239
-
240
- - ✅ Scheduled Versori workflow (daily/hourly inventory sync)
241
- - ✅ S3 file download with enhanced retry logic
242
- - ✅ JSON parsing with format auto-detection (standard JSON vs JSON Lines)
243
- - ✅ Direct field mapping (no special notation needed for JSON)
244
- - ✅ Field mapping with UniversalMapper and external JSON config
245
- - ✅ Batch API job creation and chunked processing (1000 records/batch)
246
- - ✅ BPP (Batch Pre-Processing) change detection
247
- - ✅ Fire-and-forget batch submission with audit logging
248
- - ✅ State management (VersoriFileTracker + JobTracker prevent duplicates)
249
- - ✅ File archival on S3 (success → processed/, failure → errors/)
250
- - ✅ Comprehensive error handling with exponential backoff retry
251
- - ✅ Modular architecture (reusable services, easy to test)
252
-
253
- ---
254
-
255
- ## Versori Workflows Structure
256
-
257
- **Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
258
-
259
- **Trigger Types:**
260
- - **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
261
- - **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
262
- - **`workflow()`** → Durable workflows (advanced, rarely used)
263
-
264
- **Execution Steps (chained to triggers):**
265
- - **`http()`** → External API calls (chained from schedule/webhook)
266
- - **`fn()`** → Internal processing (chained from schedule/webhook)
267
-
268
- ### Recommended Project Structure
269
-
270
- ```
271
- s3-json-inventory-batch-sync/
272
- ├── index.ts # Entry point - exports all workflows
273
- └── src/
274
- ├── workflows/
275
- │ ├── scheduled/
276
- │ │ └── daily-inventory-sync.ts # Scheduled: Daily inventory sync
277
- │ │
278
- │ └── webhook/
279
- │ ├── adhoc-inventory-sync.ts # Webhook: Manual trigger
280
- │ └── job-status-check.ts # Webhook: Status query
281
-
282
- ├── services/
283
- │ └── inventory-sync.service.ts # Shared orchestration logic (reusable)
284
-
285
- └── types/
286
- └── inventory.types.ts # Shared type definitions
287
- ```
288
-
289
- **Benefits:**
290
- - ✅ Clear trigger separation (`scheduled/` vs `webhook/`)
291
- - ✅ Descriptive file names (easy to browse and understand)
292
- - ✅ Scalable (add new workflows without cluttering)
293
- - ✅ Reusable code in `services/` (DRY principle)
294
- - ✅ Easy to modify individual workflows without affecting others
295
-
296
- ---
297
-
298
- ## Workflow Files
299
-
300
- ### 1. Scheduled Workflows (`src/workflows/scheduled/`)
301
-
302
- All time-based triggers that run automatically on cron schedules.
303
-
304
- #### `src/workflows/scheduled/daily-inventory-sync.ts`
305
-
306
- **Purpose**: Automatic daily inventory sync
307
- **Trigger**: Cron schedule (`0 2 * * *`)
308
- **Exposed as Endpoint**: ❌ NO - Runs automatically
309
-
310
- ```typescript
311
- import { schedule, http } from '@versori/run';
312
- import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
313
- import { runIngestion } from '../../services/inventory-sync.service';
314
-
315
- /**
316
- * Scheduled Workflow: Daily Inventory Sync
317
- *
318
- * Runs automatically daily at 2 AM UTC
319
- * NOT exposed as HTTP endpoint - Versori executes on schedule
320
- *
321
- * Uses shared service: inventory-sync.service.ts
322
- */
323
- export const dailyInventorySync = schedule(
324
- 'inventory-batch-scheduled',
325
- '0 2 * * *' // Daily at 2 AM UTC
326
- ).then(
327
- http('run-inventory-batch', { connection: 'fluent_commerce' }, async (ctx: any) => {
328
- const { log, openKv } = ctx;
329
- const executionStartTime = Date.now();
330
- const jobId = `inventory-batch-${Date.now()}`;
331
- const tracker = new JobTracker(openKv(':project:'), log);
332
-
333
- log.info('═══════════════════════════════════════════════════════════════');
334
- log.info('🚀 [WORKFLOW] Starting scheduled inventory sync', { jobId });
335
- log.info('═══════════════════════════════════════════════════════════════');
336
-
337
- await tracker.createJob(jobId, {
338
- triggeredBy: 'schedule',
339
- stage: 'initialization',
340
- startTime: executionStartTime,
341
- });
342
-
343
- await tracker.updateJob(jobId, { status: 'processing' });
344
-
345
- try {
346
- // Reuse shared orchestration logic
347
- const result = await runIngestion(ctx, jobId, tracker);
348
-
349
- if (result.success) {
350
- const duration = Date.now() - executionStartTime;
351
- await tracker.markCompleted(jobId, { ...result, duration });
352
- log.info('✅ [WORKFLOW] Inventory sync completed successfully', {
353
- jobId,
354
- filesProcessed: result.filesProcessed,
355
- duration: `${duration}ms (${(duration / 1000).toFixed(2)}s)`,
356
- });
357
- } else {
358
- await tracker.markFailed(jobId, result.error || 'Unknown error');
359
- log.error('❌ [WORKFLOW] Inventory sync failed', {
360
- jobId,
361
- error: result.error,
362
- });
363
- }
364
-
365
- log.info('═══════════════════════════════════════════════════════════════');
366
- log.info('🏁 [WORKFLOW] Execution complete', {
367
- jobId,
368
- success: result.success,
369
- duration: `${Date.now() - executionStartTime}ms`,
370
- });
371
- log.info('═══════════════════════════════════════════════════════════════');
372
-
373
- return { success: true, jobId, ...result };
374
- } catch (e: any) {
375
- const errorMessage = e instanceof Error ? e.message : String(e);
376
- await tracker.markFailed(jobId, errorMessage);
377
- log.error('❌ [WORKFLOW] Inventory sync failed with exception', {
378
- jobId,
379
- error: errorMessage,
380
- stack: e instanceof Error ? e.stack : undefined,
381
- recommendation: 'Check SFTP credentials, network connectivity, and file permissions',
382
- });
383
- return { success: false, jobId, error: errorMessage };
384
- }
385
- })
386
- );
387
- ```
388
-
389
- ---
390
-
391
- ### 2. Webhook Workflows (`src/workflows/webhook/`)
392
-
393
- All HTTP-based triggers that create webhook endpoints.
394
-
395
- #### `src/workflows/webhook/adhoc-inventory-sync.ts`
396
-
397
- **Purpose**: Manual inventory sync trigger (on-demand)
398
- **Trigger**: HTTP POST
399
- **Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-adhoc`
400
- **Use Cases**: Testing, priority processing, ad-hoc runs
401
-
402
- ```typescript
403
- import { webhook, http } from '@versori/run';
404
- import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
405
- import { runIngestion } from '../../services/inventory-sync.service';
406
-
407
- /**
408
- * Webhook: Manual Inventory Sync Trigger
409
- *
410
- * Endpoint: POST https://{workspace}.versori.run/inventory-batch-adhoc
411
- * Request body (optional): { filePattern: "urgent_*.json", maxFiles: 5 }
412
- *
413
- * Pattern: Sync mode + fire-and-forget
414
- * - Returns jobId immediately
415
- * - Background processing continues without blocking response
416
- * - ✅ Works because Versori keeps execution context alive for unawaited promises
417
- *
418
- * Uses shared service: inventory-sync.service.ts
419
- */
420
- export const adhocInventorySync = webhook('inventory-batch-adhoc', {
421
- response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
422
- connection: 'inventory-batch-adhoc', // Versori validates API key
423
- }).then(
424
- http('run-inventory-batch-adhoc', { connection: 'fluent_commerce' }, async (ctx: any) => {
425
- const { log, openKv, data } = ctx;
426
- const executionStartTime = Date.now();
427
- const jobId = `inventory-batch-adhoc-${Date.now()}`;
428
- const tracker = new JobTracker(openKv(':project:'), log);
429
-
430
- const filePattern = data?.filePattern as string || '*.json';
431
- const maxFiles = data?.maxFiles as number;
432
-
433
- log.info('🚀 [WEBHOOK] Adhoc inventory sync triggered', {
434
- jobId,
435
- filePattern,
436
- maxFiles,
437
- requestData: data,
438
- });
439
-
440
- // Create job entry FIRST (awaited to ensure job exists in KV)
441
- await tracker.createJob(jobId, {
442
- triggeredBy: 'manual',
443
- stage: 'initialization',
444
- status: 'queued',
445
- startTime: executionStartTime,
446
- options: { filePattern, maxFiles },
447
- createdAt: new Date().toISOString(),
448
- });
449
-
450
- // ✅ Fire-and-forget: Start background processing WITHOUT await
451
- // The promise continues execution after we return the response
452
- runIngestion(ctx, jobId, tracker)
453
- .then((result) => {
454
- const duration = Date.now() - executionStartTime;
455
- if (result.success) {
456
- log.info('✅ [BACKGROUND] Inventory sync completed successfully', {
457
- jobId,
458
- filesProcessed: result.filesProcessed,
459
- filesFailed: result.filesFailed,
460
- recordsProcessed: result.recordsProcessed,
461
- duration: `${duration}ms (${(duration / 1000).toFixed(2)}s)`,
462
- });
463
- return tracker.markCompleted(jobId, { ...result, duration });
464
- } else {
465
- log.error('❌ [BACKGROUND] Inventory sync failed', {
466
- jobId,
467
- error: result.error,
468
- });
469
- return tracker.markFailed(jobId, result.error || 'Unknown error');
470
- }
471
- })
472
- .catch((error: unknown) => {
473
- const errorMessage = error instanceof Error ? error.message : String(error);
474
- const errorStack = error instanceof Error ? error.stack : undefined;
475
-
476
- log.error('❌ [BACKGROUND] Inventory sync failed with exception', {
477
- jobId,
478
- error: errorMessage,
479
- stack: errorStack,
480
- errorType: error instanceof Error ? error.constructor.name : typeof error,
481
- recommendation: 'Check S3 credentials, bucket permissions, and network connectivity',
482
- });
483
-
484
- return tracker.markFailed(jobId, errorMessage);
485
- });
486
-
487
- // Return immediately with jobId (response sent with this return value)
488
- return {
489
- success: true,
490
- jobId,
491
- message: 'Inventory sync started in background',
492
- statusEndpoint: `https://{workspace}.versori.run/inventory-batch-job-status`,
493
- note: 'Poll the status endpoint with jobId to check progress',
494
- };
495
- })
496
- );
497
- ```
498
-
499
- ---
500
-
501
- #### `src/workflows/webhook/job-status-check.ts`
502
-
503
- **Purpose**: Query job status and progress
504
- **Trigger**: HTTP POST/GET
505
- **Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-job-status`
506
- **Request Body**: `{ "jobId": "inventory-batch-1234567890" }`
507
-
508
- ```typescript
509
- import { webhook, fn } from '@versori/run';
510
- import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
511
-
512
- /**
513
- * Webhook: Job Status Check
514
- *
515
- * Endpoint: POST https://{workspace}.versori.run/inventory-batch-job-status
516
- * Request body: { "jobId": "inventory-batch-1234567890" }
517
- *
518
- * Pattern: webhook().then(fn()) - no external API needed, only KV storage
519
- * Lightweight: Only queries KV store, no Fluent API calls
520
- */
521
- export const jobStatusCheck = webhook('inventory-batch-job-status', {
522
- response: { mode: 'sync' },
523
- connection: 'inventory-batch-job-status',
524
- }).then(
525
- fn('status', async ctx => {
526
- const { data, log, openKv } = ctx;
527
- const jobId = data?.jobId as string;
528
-
529
- if (!jobId) {
530
- return { success: false, error: 'jobId required' };
531
- }
532
-
533
- const tracker = new JobTracker(openKv(':project:'), log);
534
- const status = await tracker.getJob(jobId);
535
-
536
- return status
537
- ? { success: true, jobId, ...status }
538
- : { success: false, error: 'Job not found', jobId };
539
- })
540
- );
541
- ```
542
-
543
- ---
544
-
545
- ### 3. Entry Point (`index.ts`)
546
-
547
- **Purpose**: Register all workflows with Versori platform
548
-
549
- ```typescript
550
- /**
551
- * Entry Point - Registers all workflows with Versori platform
552
- *
553
- * Versori automatically discovers and registers exported workflows
554
- *
555
- * File Structure:
556
- * - src/workflows/scheduled/ → Time-based triggers (cron)
557
- * - src/workflows/webhook/ → HTTP-based triggers (webhooks)
558
- */
559
-
560
- // Scheduled workflows
561
- export { dailyInventorySync } from './src/workflows/scheduled/daily-inventory-sync';
562
-
563
- // Webhook workflows
564
- export { adhocInventorySync } from './src/workflows/webhook/adhoc-inventory-sync';
565
- export { jobStatusCheck } from './src/workflows/webhook/job-status-check';
566
- ```
567
-
568
- **What Gets Exposed:**
569
- - ✅ `adhocInventorySync` → `https://{workspace}.versori.run/inventory-batch-adhoc`
570
- - ✅ `jobStatusCheck` → `https://{workspace}.versori.run/inventory-batch-job-status`
571
- - ❌ `dailyInventorySync` → NOT exposed (runs automatically on cron)
572
-
573
- ## When to Use Batch API vs Event API
574
-
575
- ### ✅ Use Batch API (`createJob`/`sendBatch`) For:
576
-
577
- | Entity Type | Use Case | Why Batch API |
578
- | --------------------- | ---------------------------------- | ----------------------------------------------- |
579
- | **Inventory** | Bulk inventory updates, daily sync | Optimized for high-volume, BPP change detection |
580
- | **InventoryQuantity** | Inventory positions and quantities | Native Batch API entity type |
581
-
582
- ### ❌ Use Event API Instead For:
583
-
584
- | Entity Type | Use Case | Why Event API / GraphQL |
585
- | ------------------- | ------------------------------------- | --------------------------------------------- |
586
- | **Products** | Product catalog sync, variant updates | Triggers workflows, validates business rules |
587
- | **Locations** | Store/warehouse setup | Requires workflow orchestration |
588
- | **Customers** | Customer registration/profile updates | Prefer GraphQL mutations (no Rubix support) |
589
- | **Orders** | Updates/events (not creation) | Events fine for updates; creation via GraphQL |
590
- | **Custom Entities** | Any entity needing workflow triggers | Full Rubix workflow support |
591
-
592
- ### 🔍 Use GraphQL Mutations For:
593
-
594
- | Scenario | Why GraphQL |
595
- | --------------------- | -------------------------------------- |
596
- | **Single operations** | Create one record, update one record |
597
- | **Complex queries** | Fetch data with relationships |
598
- | **Testing/debugging** | Direct API control, immediate feedback |
599
-
600
- Note:
601
-
602
- - Orders: Use `createOrder` GraphQL mutation for order creation (this triggers the Order CREATED event in Rubix). Order updates/events are fine via Event API.
603
- - Customers: Use GraphQL mutations (no Rubix workflow support for customers).
604
-
605
- ---
606
-
607
- ## Understanding BPP (Batch Pre-Processing)
608
-
609
- **BPP** is Fluent's change detection system that filters out unchanged records before workflow processing.
610
-
611
- Note on defaults and control:
612
-
613
- - BPP is a Fluent platform feature. The default on/off behavior is controlled at your Fluent account level, not by the SDK.
614
- - If you omit `meta.preprocessing` in `createJob()`, the account-level default applies.
615
- - To force behavior per job, set `meta.preprocessing` explicitly (`'skip'` to disable for delta feeds).
616
-
617
- ### When to Use BPP (Default - Enabled)
618
-
619
- **✅ Full Inventory Snapshots:**
620
-
621
- - Daily complete inventory dumps
622
- - Entire warehouse stock files
623
- - Records may be identical to previous run
624
-
625
- **Example:**
626
-
627
- ```typescript
628
- // BPP enabled by default - filters unchanged records
629
- const job = await client.createJob({
630
- name: 'Daily Full Inventory',
631
- retailerId: 'my-retailer',
632
- // BPP automatically enabled - no meta needed
633
- });
634
- ```
635
-
636
- **What BPP does:**
637
-
638
- - Compares incoming records with existing records in Fluent
639
- - Filters out records with no changes
640
- - Only passes changed/new records to workflows
641
- - Significantly reduces workflow processing load
642
-
643
- ### When to Skip BPP
644
-
645
- **✅ Delta Feeds (Pre-Filtered Data):**
646
-
647
- - Hourly change files (only updates since last run)
648
- - Event-driven inventory changes
649
- - Pre-filtered data from source system
650
- - All records are guaranteed to be changes
651
-
652
- **Example:**
653
-
654
- ```typescript
655
- // Skip BPP for delta feeds - all records are changes
656
- const job = await client.createJob({
657
- name: 'Hourly Delta Inventory',
658
- retailerId: 'my-retailer',
659
- meta: {
660
- preprocessing: 'skip', // All records already filtered
661
- },
662
- });
663
- ```
664
-
665
- **Performance Impact:**
666
-
667
- - **Full snapshot + BPP**: 10,000 records → 500 changes → Fast
668
- - **Full snapshot, no BPP**: 10,000 records → 10,000 processed → Slow
669
- - **Delta feed + BPP**: 500 records → 500 changes → Fast (but BPP overhead wasted)
670
- - **Delta feed, no BPP**: 500 records → 500 processed → Fastest
671
-
672
- ### Decision Guide
673
-
674
- | Source Data Type | BPP Setting | Reason |
675
- | ---------------------- | ----------------------- | ----------------------------------------------- |
676
- | **Daily full dump** | Enabled (default) | Most records unchanged, BPP filters efficiently |
677
- | **Hourly delta feed** | `preprocessing: 'skip'` | All records are changes, BPP overhead wasted |
678
- | **Initial load** | Enabled (default) | No previous data, but good practice |
679
- | **Manual corrections** | Enabled (default) | Unknown change ratio, let BPP filter |
680
- | **Real-time events** | `preprocessing: 'skip'` | Each record is a change event |
681
-
682
- ---
683
-
684
- ## Processing Modes
685
-
686
- **⚠️ IMPORTANT:** Choose ONE processing mode per connector. These are alternative patterns, not features to use together.
687
-
688
- The SDK supports three processing modes for handling multiple files. **Select the mode that best fits your use case** and configure your connector accordingly:
689
-
690
- ### Mode 1: Per-File Processing (Recommended Default) ✅ IMPLEMENTED
691
-
692
- **When to use:**
693
-
694
- - Multiple large files that shouldn't be in memory together
695
- - Need file-level consistency (file 3 fails → files 1-2 already archived)
696
- - Memory constraints
697
- - Fault isolation (one file failure doesn't affect others)
698
-
699
- **Flow:**
700
-
701
- ```
702
- FOR EACH FILE:
703
- 1. Download file
704
- 2. Parse JSON
705
- 3. Map records
706
- 4. Create Batch API job
707
- 5. Send batches
708
- 6. Write log
709
- 7. Archive file
710
- 8. Mark file processed
711
-
712
- IF FILE FAILS:
713
- - Move to /errors/
714
- - Continue to next file
715
- - Other files unaffected
716
- ```
717
-
718
- **Configuration:**
719
-
720
- ```json
721
- {
722
- "processingMode": "per-file",
723
- "maxFilesPerRun": 10
724
- }
725
- ```
726
-
727
- **Benefits:**
728
-
729
- - ✅ File-level atomicity (each file processed independently)
730
- - ✅ Low memory footprint (1 file at a time)
731
- - ✅ Clear error isolation (failed file doesn't block others)
732
- - ✅ Incremental progress (files archived as completed)
733
-
734
- **Example implementation (see code section below)**
735
-
736
- ### Mode 2: Batch Processing (All Files at Once) ⚠️ OPTIONAL - Choose if needed
737
-
738
- **When to use:**
739
-
740
- - Small files (all fit in memory comfortably)
741
- - Need one Batch API job for all files
742
- - Atomic processing (all files or none)
743
-
744
- **Flow:**
745
-
746
- ```
747
- 1. Download ALL files
748
- 2. Parse ALL files
749
- 3. Map ALL records
750
- 4. Create ONE Batch API job
751
- 5. Send ALL batches
752
- 6. Write logs
753
- 7. Archive ALL files
754
- 8. Mark ALL processed
755
- ```
756
-
757
- **Configuration:**
758
-
759
- ```json
760
- {
761
- "processingMode": "batch",
762
- "maxFilesPerRun": 10
763
- }
764
- ```
765
-
766
- **Benefits:**
767
-
768
- - ✅ Single job for all files (simplifies tracking)
769
- - ✅ Faster for many small files (parallel parsing possible)
770
-
771
- **Drawbacks:**
772
-
773
- - ❌ High memory usage (all files in memory)
774
- - ❌ All-or-nothing (one failure affects all)
775
- - ❌ No incremental progress
776
-
777
- ### Mode 3: Chunked Per-File Processing (Balanced) ⚠️ OPTIONAL - Choose if needed
778
-
779
- **When to use:**
780
-
781
- - Many files (e.g., 100+ files)
782
- - Process N files at a time
783
- - Balance between memory and parallelism
784
-
785
- **Flow:**
786
-
787
- ```
788
- CHUNK files into groups of N (e.g., 5 files per chunk):
789
-
790
- FOR EACH CHUNK:
791
- FOR EACH FILE in chunk:
792
- 1. Download file
793
- 2. Parse JSON
794
- 3. Map records
795
- 4. Create Batch API job
796
- 5. Send batches
797
- 6. Archive file
798
-
799
- Wait for chunk to complete before next chunk
800
- ```
801
-
802
- **Configuration:**
803
-
804
- ```json
805
- {
806
- "processingMode": "chunked",
807
- "fileChunkSize": 5,
808
- "maxFilesPerRun": 100
809
- }
810
- ```
811
-
812
- **Benefits:**
813
-
814
- - ✅ Bounded memory usage (N files at a time)
815
- - ✅ Better throughput than per-file (parallel processing within chunk)
816
- - ✅ Incremental progress (per-chunk)
817
-
818
- **Use cases:**
819
-
820
- - High-volume file ingestion (100+ files per run)
821
- - Rate limiting (control API load)
822
- - Resource-constrained environments
823
-
824
- ### Comparison Matrix
825
-
826
- | Aspect | Per-File | Batch | Chunked |
827
- | ------------------- | ---------------------------------- | ----------------------------- | --------------------------------------- |
828
- | **Memory Usage** | Low (1 file at a time) | High (all files) | Medium (N files) |
829
- | **Consistency** | Per-file atomic | All-or-nothing | Per-chunk atomic |
830
- | **Fault Isolation** | ✅ Best (1 file fails → others OK) | ❌ Worst (1 fails → all fail) | ✅ Good (chunk fails → other chunks OK) |
831
- | **Performance** | Slower (sequential) | Fastest (parallel parsing) | Balanced |
832
- | **Batch API Jobs** | N jobs (1 per file) | 1 job (all files) | N jobs (1 per file) |
833
- | **Use Case** | Large files, strict consistency | Small files, fast processing | Many files, balanced approach |
834
- | **Recommended For** | Production (safest) | Testing, small datasets | High-volume production |
835
-
836
- > **📋 This Template Implementation:**
837
- >
838
- > **✅ This template implements ALL THREE modes** and selects based on `PROCESSING_MODE` variable.
839
- >
840
- > **Choose ONE mode per connector:**
841
- > - **Default:** `PROCESSING_MODE=per-file` (recommended for production)
842
- > - **Alternative:** `PROCESSING_MODE=batch` (for small files, atomic processing)
843
- > - **Alternative:** `PROCESSING_MODE=chunked` (for high-volume scenarios)
844
- >
845
- > **Important:** Do NOT use multiple modes in the same connector. Each connector should use ONE consistent pattern.
846
-
847
- ### Decision Guide
848
-
849
- | Source Data | File Count | File Size | Recommended Mode | Reason |
850
- | -------------- | ---------- | ----------- | ---------------- | --------------------------------------- |
851
- | Daily snapshot | 1-10 | >10MB each | **Per-File** | Memory efficient, fault isolation |
852
- | Hourly deltas | 10-50 | <1MB each | **Batch** | Fast processing, small memory footprint |
853
- | Real-time feed | 100+ | <100KB each | **Chunked** | Balanced throughput + memory |
854
- | Initial load | 1 | >100MB | **Per-File** | Memory safety |
855
- | Testing | 1-5 | Any | **Batch** | Simplicity |
856
-
857
- ---
858
-
859
- ## JSON File Format
860
-
861
- ### Sample: inventory.json (Standard JSON)
862
-
863
- ```json
864
- {
865
- "inventory": [
866
- {
867
- "locationRef": "LOC001",
868
- "skuRef": "SKU-12345",
869
- "qty": 100,
870
- "type": "LAST_ON_HAND",
871
- "status": "ACTIVE",
872
- "expectedOn": "2025-01-25",
873
- "attributes": {
874
- "expiryDate": "2026-12-31",
875
- "batchNumber": "BATCH-A001",
876
- "condition": "NEW",
877
- "storageZone": "ZONE-A"
878
- }
879
- },
880
- {
881
- "locationRef": "LOC001",
882
- "skuRef": "SKU-67890",
883
- "qty": 50,
884
- "type": "LAST_ON_HAND",
885
- "status": "ACTIVE",
886
- "expectedOn": "2025-01-25",
887
- "attributes": {
888
- "expiryDate": "2026-06-30",
889
- "batchNumber": "BATCH-A002",
890
- "condition": "NEW",
891
- "storageZone": "ZONE-B"
892
- }
893
- }
894
- ]
895
- }
896
- ```
897
-
898
- ### Sample: inventory.jsonl (JSON Lines)
899
-
900
- ```jsonl
901
- {"locationRef":"LOC001","skuRef":"SKU-12345","qty":100,"type":"LAST_ON_HAND","status":"ACTIVE","expectedOn":"2025-01-25","attributes":{"expiryDate":"2026-12-31","batchNumber":"BATCH-A001","condition":"NEW","storageZone":"ZONE-A"}}
902
- {"locationRef":"LOC001","skuRef":"SKU-67890","qty":50,"type":"LAST_ON_HAND","status":"ACTIVE","expectedOn":"2025-01-25","attributes":{"expiryDate":"2026-06-30","batchNumber":"BATCH-A002","condition":"NEW","storageZone":"ZONE-B"}}
903
- ```
904
-
905
- **JSON Structure:**
906
-
907
- - Standard JSON: Root object with nested arrays (`{ "inventory": [...] }`)
908
- - JSON Lines: One JSON object per line (streaming-friendly for large files)
909
- - JSONParserService auto-detects format based on content
910
- - Both formats support nested objects with dot notation
911
-
912
- **Field Descriptions:**
913
-
914
- - `locationRef`: Fluent location reference
915
- - `skuRef`: Fluent SKU/product reference
916
- - `qty`: Inventory quantity (integer)
917
- - `type`: Inventory type (LAST_ON_HAND for full snapshots, DELTA for incremental changes)
918
- - `status`: Record status (ACTIVE, INACTIVE)
919
- - `expectedOn`: Expected date (ISO 8601 or parseable format)
920
- - `attributes.expiryDate`: Product expiry date (optional)
921
- - `attributes.batchNumber`: Manufacturing batch/lot number (optional)
922
- - `attributes.condition`: Product condition (NEW, USED, DAMAGED)
923
- - `attributes.storageZone`: Warehouse zone identifier (optional)
924
-
925
- ### JSON Field Mapping
926
-
927
- **IMPORTANT**: JSON uses direct field access (no special prefix needed):
928
-
929
- ```json
930
- {
931
- "fields": {
932
- "locationRef": { "source": "locationRef", "required": true },
933
- "skuRef": { "source": "skuRef", "required": true }
934
- }
935
- }
936
- ```
937
-
938
- **Why**: Unlike XML attributes (which need `@` prefix), JSON fields are accessed directly by name.
939
-
940
- ### Array Handling (Standard JSON vs JSON Lines)
941
-
942
- **JSONParserService behavior:**
943
-
944
- - **Standard JSON**: Returns object or array based on structure
945
- - **JSON Lines**: Returns array of objects (one per line)
946
-
947
- **Solution**: Always normalize to array after parsing:
948
-
949
- ```typescript
950
- const parsed = await parser.parse(jsonContent, { format: 'json' | 'jsonl' });
951
-
952
- // Extract inventory array - handle different JSON structures
953
- let records: any[] = [];
954
- if (format === 'jsonl') {
955
- records = Array.isArray(parsed) ? parsed : [parsed];
956
- } else {
957
- if (Array.isArray(parsed)) {
958
- records = parsed; // Root level array
959
- } else if (parsed?.inventory && Array.isArray(parsed.inventory)) {
960
- records = parsed.inventory; // { "inventory": [...] }
961
- } else if (parsed?.records && Array.isArray(parsed.records)) {
962
- records = parsed.records; // { "records": [...] }
963
- } else {
964
- records = [parsed]; // Single object
965
- }
966
- }
967
- ```
968
-
969
- ---
970
-
971
- ## Service Implementation
972
-
973
- ### File: `src/services/inventory-sync.service.ts`
974
-
975
- **Complete implementation showing all SDK patterns:**
976
-
977
- ```typescript
978
- import { Buffer } from 'node:buffer'; // ✅ Required for Versori/Deno runtime
979
- import {
980
- createClient,
981
- S3DataSource,
982
- JSONParserService,
983
- UniversalMapper,
984
- VersoriFileTracker,
985
- JobTracker,
986
- extractFileName,
987
- } from '@fluentcommerce/fc-connect-sdk';
988
- import type { FileMetadata } from '@fluentcommerce/fc-connect-sdk';
989
- import inventoryMapping from '../config/inventory.batch.json' with { type: 'json' }; // ✅ External JSON import
990
-
991
- /**
992
- * Shared inventory ingestion orchestration logic
993
- * Used by both scheduled and webhook workflows
994
- *
995
- * @param ctx - Versori HTTP context
996
- * @param jobId - Unique job identifier
997
- * @param tracker - JobTracker instance
998
- */
999
- export async function runIngestion(
1000
- ctx: any,
1001
- jobId: string,
1002
- tracker: JobTracker
1003
- ) {
1004
- const { log, activation, openKv } = ctx;
1005
- const startTime = Date.now();
1006
- let s3: S3DataSource | null = null;
1007
-
1008
- // Optional feature toggles
1009
- const enableFileTracking = activation.getVariable('enableFileTracking') !== 'false';
1010
- const detailedLogging = activation.getVariable('detailedLogging') === 'true';
1011
-
1012
- try {
1013
- // ========================================
1014
- // CLIENT INITIALIZATION (with retailerId and validation)
1015
- // ========================================
1016
- log.info('🔧 [InventorySync] Initializing Fluent Commerce client...');
1017
-
1018
- const client = await createClient({
1019
- ...ctx,
1020
- validateConnection: true, // ✅ Validate connection on creation
1021
- });
1022
-
1023
- // ✅ CRITICAL: Set retailerId for Batch API
1024
- const retailerId = activation.getVariable('fluentRetailerId');
1025
- if (!retailerId) {
1026
- log.error('❌ [InventorySync] Missing required fluentRetailerId activation variable');
1027
- throw new Error('fluentRetailerId activation variable is required for Batch API');
1028
- }
1029
- client.setRetailerId(retailerId);
1030
-
1031
- log.info('✅ [InventorySync] Client initialized and validated', { retailerId });
1032
-
1033
- // ========================================
1034
- // S3 DATA SOURCE INITIALIZATION
1035
- // ========================================
1036
- log.info('🔧 [InventorySync] Initializing S3 data source...');
1037
-
1038
- const s3Config = {
1039
- bucket: activation.getVariable('s3BucketName'),
1040
- region: activation.getVariable('awsRegion') || 'us-east-1',
1041
- accessKeyId: activation.getVariable('awsAccessKeyId'),
1042
- secretAccessKey: activation.getVariable('awsSecretAccessKey'),
1043
- prefix: activation.getVariable('s3Prefix') || 'inventory/',
1044
- };
1045
-
1046
- s3 = new S3DataSource(
1047
- {
1048
- type: 'S3_JSON',
1049
- connectionId: 's3-inventory-sync',
1050
- name: 'inventory-sync',
1051
- s3Config,
1052
- },
1053
- log
1054
- );
1055
-
1056
- log.info('✅ [InventorySync] S3 data source initialized', {
1057
- bucket: s3Config.bucket,
1058
- region: s3Config.region,
1059
- prefix: s3Config.prefix,
1060
- });
1061
-
1062
- // ========================================
1063
- // SERVICE INITIALIZATION
1064
- // ========================================
1065
- const parser = new JSONParserService();
1066
- const mapper = new UniversalMapper(inventoryMapping);
1067
- const kv = openKv(':project:');
1068
- const fileTracker = enableFileTracking
1069
- ? new VersoriFileTracker(kv, 's3-json-inventory-sync')
1070
- : null;
1071
- const bppEnabled = activation.getVariable('bppEnabled') !== 'false';
1072
-
1073
- if (detailedLogging) {
1074
- log.info('🔍 [InventorySync] Configuration loaded', {
1075
- bppEnabled,
1076
- enableFileTracking,
1077
- detailedLogging,
1078
- });
1079
- }
1080
-
1081
- // ========================================
1082
- // FILE DISCOVERY
1083
- // ========================================
1084
- log.info('🔍 [InventorySync] Discovering files on S3...');
1085
-
1086
- const files = await s3.listFiles({ prefix: s3Config.prefix });
1087
- log.info('📄 [InventorySync] File discovery complete', { count: files.length });
1088
-
1089
- if (files.length === 0) {
1090
- log.info('⚠️ [InventorySync] No files found to process');
1091
- return { success: true, message: 'No files to process', filesProcessed: 0 };
1092
- }
1093
-
1094
- // ========================================
1095
- // FILE PROCESSING
1096
- // ========================================
1097
- const results = [];
1098
- let fileIndex = 0;
1099
-
1100
- for (const file of files) {
1101
- fileIndex++;
1102
- const fileStartTime = Date.now();
1103
- const fileName = extractFileName(file.path);
1104
-
1105
- log.info(`📄 [FILE ${fileIndex}/${files.length}] Processing: ${fileName}`);
1106
-
1107
- try {
1108
- // Check if already processed
1109
- if (fileTracker && (await fileTracker.wasFileProcessed(file.name))) {
1110
- log.info(`⏭️ [FILE ${fileIndex}/${files.length}] Skipping already processed: ${fileName}`);
1111
- results.push({ fileName, success: true, skipped: true });
1112
- continue;
1113
- }
1114
-
1115
- // Download and parse
1116
- if (detailedLogging) {
1117
- log.info(`🔍 [FILE ${fileIndex}/${files.length}] Downloading: ${fileName}`);
1118
- }
1119
- const jsonContent = (await s3.downloadFile(file.path, { encoding: 'utf8' })) as string;
1120
- const parsed = await parser.parse(jsonContent);
1121
-
1122
- // Extract inventory array
1123
- const records = Array.isArray(parsed) ? parsed :
1124
- parsed?.inventory ? parsed.inventory :
1125
- [parsed];
1126
-
1127
- if (records.length === 0) {
1128
- log.warn(`⚠️ [FILE ${fileIndex}/${files.length}] No records found in: ${fileName}`);
1129
- continue;
1130
- }
1131
-
1132
- log.info(`🔄 [FILE ${fileIndex}/${files.length}] Transforming ${records.length} records`);
1133
-
1134
- // Transform records
1135
- const mappedRecords = [];
1136
- const aggregatedSkippedFields: string[] = [];
1137
- for (const rec of records) {
1138
- const sourceDataWithContext = { ...rec, $context: { retailerId } };
1139
- const mappingResult = await mapper.map(sourceDataWithContext);
1140
- if (mappingResult.success) {
1141
- mappedRecords.push(mappingResult.data);
1142
- }
1143
- // Aggregate skipped fields
1144
- if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
1145
- for (const fieldName of mappingResult.skippedFields) {
1146
- if (!aggregatedSkippedFields.includes(fieldName)) {
1147
- aggregatedSkippedFields.push(fieldName);
1148
- }
1149
- }
1150
- }
1151
- }
1152
-
1153
- if (aggregatedSkippedFields.length > 0) {
1154
- log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
1155
- file: fileName,
1156
- skippedFields: aggregatedSkippedFields,
1157
- note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
1158
- });
1159
- }
1160
-
1161
- if (mappedRecords.length === 0) {
1162
- log.warn(`⚠️ [FILE ${fileIndex}/${files.length}] No valid records after mapping: ${fileName}`, {
1163
- recommendation: 'Check mapping configuration and required fields',
1164
- });
1165
- continue;
1166
- }
1167
-
1168
- // Create job and send batches
1169
- log.info(`📦 [FILE ${fileIndex}/${files.length}] Creating Batch API job for ${mappedRecords.length} records`);
1170
-
1171
- const job = await client.createJob({
1172
- name: `s3-json-${fileName}-${Date.now()}`,
1173
- meta: bppEnabled ? undefined : { preprocessing: 'skip' },
1174
- });
1175
-
1176
- await tracker.updateJob(jobId, {
1177
- status: 'processing',
1178
- details: { fileName, recordCount: mappedRecords.length },
1179
- });
1180
-
1181
- // ? Enhanced: Extract context for progress logging
1182
- const uniqueLocations = [...new Set(mappedRecords.map((r: any) => r.locationRef))];
1183
- const sampleSKUs = mappedRecords.slice(0, 5).map((r: any) => r.skuRef);
1184
-
1185
- // ? Enhanced: Start logging with context
1186
- log.info(`📤 [BatchProcessor] Sending batch for file "${fileName}"`, {
1187
- totalRecords: mappedRecords.length,
1188
- locations: uniqueLocations.join(', '),
1189
- sampleSKUs: sampleSKUs.join(', '),
1190
- jobId: job.id
1191
- });
1192
-
1193
- // Send batches (fire-and-forget)
1194
- await client.sendBatch(job.id, {
1195
- action: 'UPSERT',
1196
- entityType: 'INVENTORY',
1197
- entities: mappedRecords,
1198
- });
1199
-
1200
- // ? Enhanced: Completion logging
1201
- log.info(`✅ [BatchProcessor] Batch sent successfully for file "${fileName}"`, {
1202
- totalRecords: mappedRecords.length,
1203
- jobId: job.id,
1204
- duration: Date.now() - fileStartTime
1205
- });
1206
-
1207
- // ? Enhanced: Clear large arrays after processing to free memory
1208
- mappedRecords.length = 0;
1209
-
1210
- // Mark as processed
1211
- if (fileTracker) {
1212
- await fileTracker.markFileProcessed(file.name, {
1213
- recordCount: mappedRecords.length,
1214
- });
1215
- }
1216
-
1217
- const fileDuration = Date.now() - fileStartTime;
1218
- results.push({
1219
- fileName,
1220
- success: true,
1221
- recordCount: mappedRecords.length,
1222
- duration: fileDuration,
1223
- });
1224
-
1225
- log.info(`✅ [FILE ${fileIndex}/${files.length}] Successfully processed: ${fileName}`, {
1226
- records: mappedRecords.length,
1227
- jobId: job.id,
1228
- duration: `${fileDuration}ms (${(fileDuration / 1000).toFixed(2)}s)`,
1229
- });
1230
-
1231
- } catch (error: any) {
1232
- const fileDuration = Date.now() - fileStartTime;
1233
- log.error(`❌ [FILE ${fileIndex}/${files.length}] Processing failed: ${fileName}`, {
1234
- error: error?.message,
1235
- stack: error?.stack,
1236
- duration: `${fileDuration}ms`,
1237
- recommendation: 'Check S3 permissions, file format, and mapping configuration',
1238
- });
1239
- results.push({ fileName, success: false, error: error?.message, duration: fileDuration });
1240
- }
1241
- }
1242
-
1243
- const totalDuration = Date.now() - startTime;
1244
- const successCount = results.filter((r) => r.success && !r.skipped).length;
1245
-
1246
- log.info('✅ [InventorySync] Batch processing complete', {
1247
- total: files.length,
1248
- processed: successCount,
1249
- skipped: results.filter((r) => r.skipped).length,
1250
- failed: results.filter((r) => !r.success).length,
1251
- duration: `${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)`,
1252
- });
1253
-
1254
- return {
1255
- success: true,
1256
- results,
1257
- filesProcessed: successCount,
1258
- duration: totalDuration,
1259
- };
1260
-
1261
- } catch (error: any) {
1262
- const totalDuration = Date.now() - startTime;
1263
- log.error('❌ [InventorySync] Fatal error during ingestion', {
1264
- message: error?.message,
1265
- stack: error?.stack,
1266
- duration: `${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)`,
1267
- recommendation: 'Check activation variables, S3 credentials, and Fluent API connectivity',
1268
- });
1269
- throw error; // Re-throw for workflow error handling
1270
- } finally {
1271
- // ✅ CRITICAL: Always dispose S3 connection
1272
- if (s3) {
1273
- try {
1274
- await s3.dispose();
1275
- log.info('✅ [InventorySync] S3 connection disposed successfully');
1276
- } catch (disposeError: any) {
1277
- log.error('⚠️ [InventorySync] Failed to dispose S3 connection', {
1278
- error: disposeError?.message,
1279
- });
1280
- }
1281
- }
1282
- }
1283
- }
1284
- ```
1285
-
1286
- **Key Patterns Demonstrated:**
1287
- - ✅ Buffer import for Versori/Deno compatibility
1288
- - ✅ External JSON mapping import with `{ type: 'json' }`
1289
- - ✅ `validateConnection: true` for client initialization
1290
- - ✅ `setRetailerId()` call after client creation (REQUIRED for Batch API)
1291
- - ✅ Emoji-based logging (🚀 ✅ ❌ ⚠️ 🔍 📄) for visual log scanning
1292
- - ✅ Execution boundaries with ═══ separators
1293
- - ✅ `extractFileName()` usage for clean file names
1294
- - ✅ Optional feature toggles (enableFileTracking, detailedLogging)
1295
- - ✅ Duration tracking at file and workflow levels (ms and seconds)
1296
- - ✅ Recommendations in error logs for actionable debugging
1297
- - ✅ S3 `dispose()` in finally block with error handling
1298
- - ✅ VersoriFileTracker for state management (optional via toggle)
1299
- - ✅ JobTracker for job lifecycle tracking
1300
-
1301
- ---
1302
-
1303
- ## Code Flow Explanation
1304
-
1305
- ### Initialization Phase
1306
-
1307
- **Logger Setup:**
1308
-
1309
- ```typescript
1310
- // ✅ CORRECT: Use native Versori log from context
1311
- const { log } = ctx;
1312
- log.info('Starting workflow');
1313
-
1314
- // ❌ WRONG: LoggingService (removed - use native log on Versori)
1315
- // import { LoggingService } from '@fluentcommerce/fc-connect-sdk';
1316
- // const logging = new LoggingService();
1317
- // const log = logging.createLogger({ logLevel: 'info' });
1318
- ```
1319
-
1320
- - Versori provides `log` in the context - use it directly
1321
-
1322
- **Client Creation:**
1323
-
1324
- ```typescript
1325
- // Pass entire Versori context object
1326
- const client = await createClient(ctx);
1327
- ```
1328
-
1329
- - `createClient(ctx)` accepts entire Versori context (fetch, connections, log, activation)
1330
- - Auto-detects Versori platform from context
1331
- - Returns `FluentClient` configured with OAuth2 from connections.fluent_commerce
1332
- - OAuth2 authentication handled automatically
1333
- - This is the **CORRECT** pattern for Versori workflows
1334
-
1335
- **S3 Data Source:**
1336
-
1337
- ```typescript
1338
- const s3 = new S3DataSource(
1339
- {
1340
- type: 'S3_JSON', // Type indicates JSON files (metadata only)
1341
- connectionId: 's3-inventory-sync', // Unique identifier (required)
1342
- name: 'inventory-sync', // Human-readable name (required)
1343
- s3Config: {
1344
- bucket: 'my-inventory-bucket',
1345
- region: 'us-east-1',
1346
- accessKeyId: 'AKIAXXXX',
1347
- secretAccessKey: 'xxxxxxxx',
1348
- },
1349
- },
1350
- log
1351
- );
1352
- ```
1353
-
1354
- - **Constructor params:** `(config: S3DataSourceConfig, log)` - Versori native log, no explicit typing needed
1355
- - `connectionId` and `name` are required in config
1356
- - `type` must be `'S3_JSON'` for JSON files
1357
- - Enhanced retry logic with exponential backoff
1358
-
1359
- ### File Discovery Phase
1360
-
1361
- **List Files:**
1362
-
1363
- ```typescript
1364
- const files = await s3.listFiles({
1365
- prefix: config.s3.prefix, // Override config (optional)
1366
- maxKeys: 1000, // Maximum files to retrieve
1367
- });
1368
- ```
1369
-
1370
- - Returns `FileMetadata[]` with: `name`, `lastModified`, `size`, `path`, `source`
1371
- - Files sorted by modification time (newest first)
1372
- - Directories excluded automatically
1373
-
1374
- **State Check:**
1375
-
1376
- ```typescript
1377
- const alreadyProcessed = await fileTracker.wasFileProcessed(file.name);
1378
- if (alreadyProcessed) {
1379
- continue; // Skip already processed files
1380
- }
1381
- ```
1382
-
1383
- - Uses Versori KV store (`openKv()`) for distributed state
1384
- - Prevents duplicate processing across workflow runs
1385
- - State persists between executions
1386
-
1387
- ### File Processing Phase
1388
-
1389
- **Download File:**
1390
-
1391
- ```typescript
1392
- const jsonContent = (await s3.downloadFile(file.path, {
1393
- encoding: 'utf8',
1394
- })) as string;
1395
- ```
1396
-
1397
- - **Important:** Cast to `string` when `encoding` is specified
1398
- - Without encoding, returns `Buffer`
1399
- - Supports streaming for large files
1400
-
1401
- **Parse JSON:**
1402
-
1403
- ```typescript
1404
- const jsonFormat = activation.getVariable('jsonFormat') || 'json';
1405
- const parsed = await parser.parse(jsonContent, { format: jsonFormat });
1406
-
1407
- // Extract inventory array - handle different JSON structures
1408
- let records: any[] = [];
1409
- if (jsonFormat === 'jsonl') {
1410
- records = Array.isArray(parsed) ? parsed : [parsed];
1411
- } else {
1412
- if (Array.isArray(parsed)) {
1413
- records = parsed; // Root level array
1414
- } else if (parsed?.inventory && Array.isArray(parsed.inventory)) {
1415
- records = parsed.inventory; // { "inventory": [...] }
1416
- } else {
1417
- records = [parsed]; // Single object
1418
- }
1419
- }
1420
- ```
1421
-
1422
- - Parses JSON into JavaScript object or array
1423
- - **Format detection**: Auto-detects standard JSON vs JSON Lines
1424
- - Always normalize to array for consistent processing
1425
- - Throws `ParsingError` on invalid JSON
1426
-
1427
- **JSON Field Handling:**
1428
-
1429
- - Direct field access by name (no special prefix needed)
1430
- - Example: `{ "locationRef": "LOC001" }` → `{ locationRef: 'LOC001' }`
1431
- - Use simple paths like `"locationRef"` in mapping config
1432
-
1433
- **Transform Data:**
1434
-
1435
- ```typescript
1436
- const mapper = new UniversalMapper(inventoryMapping);
1437
-
1438
- // Map each record directly (no wrapper needed for JSON)
1439
- const mappingResult = await mapper.map(rec);
1440
-
1441
- if (!mappingResult.success) {
1442
- // Mapping validation failed for required fields
1443
- log.warn('Mapping failed:', mappingResult.errors);
1444
- continue; // Skip invalid records
1445
- }
1446
-
1447
- mappedRecords.push(mappingResult.data);
1448
- ```
1449
-
1450
- - Direct mapping (no need to wrap in object like XML)
1451
- - `MappingResult`: `{ success: boolean, data: any, errors?: string[] }`
1452
- - `success: false` only if required fields fail
1453
- - Optional field errors reported but don't fail mapping
1454
-
1455
- ### Batch API Phase
1456
-
1457
- **Create Job:**
1458
-
1459
- ```typescript
1460
- // Full snapshot (BPP enabled by default)
1461
- const job = await client.createJob({
1462
- name: 'Daily Full Inventory',
1463
- retailerId: 'my-retailer',
1464
- // BPP automatically enabled - filters unchanged records
1465
- });
1466
-
1467
- // Delta feed (skip BPP)
1468
- const job = await client.createJob({
1469
- name: 'Hourly Deltas',
1470
- retailerId: 'my-retailer',
1471
- meta: {
1472
- preprocessing: 'skip', // All records are changes
1473
- },
1474
- });
1475
- ```
1476
-
1477
- - Returns job object with `id` and metadata
1478
- - `retailerId` required for tenant isolation
1479
- - BPP enabled by default, skip with `meta: { preprocessing: 'skip' }`
1480
-
1481
- **Send Batches:**
1482
-
1483
- ```typescript
1484
- const batch = await client.sendBatch(jobId, {
1485
- action: 'UPSERT', // Most common: create or update
1486
- entityType: 'INVENTORY', // Entity type for inventory
1487
- source: 'S3_JSON', // Data source identifier (optional)
1488
- event: 'InventoryChanged', // Valid Rubix event (optional, defaults to InventoryChanged)
1489
- entities: chunk, // Array of inventory records
1490
- });
1491
- ```
1492
-
1493
- - `action`: `'UPSERT'` (currently the only action supported by Fluent Batch API)
1494
- - `entityType`: `'INVENTORY'` for inventory records
1495
- - `entities`: Array of transformed inventory objects
1496
- - Returns batch object with `id` for tracking
1497
-
1498
- **Fire-and-Forget Pattern:**
1499
-
1500
- ```typescript
1501
- const batch = await client.sendBatch(jobId, { ... });
1502
- log.info('Batch sent (fire-and-forget)', { batchId: batch.id });
1503
-
1504
- // Record batch details for audit trail
1505
- result.batches.push({
1506
- batchId: batch.id,
1507
- recordCount: chunk.length,
1508
- timestamp: new Date().toISOString(),
1509
- status: 'SENT',
1510
- });
1511
- ```
1512
-
1513
- - No polling - batches sent and tracked immediately
1514
- - Batch details recorded for audit logging
1515
- - Log files written to S3 for tracking (optional, configurable)
1516
-
1517
- ### Cleanup Phase
1518
-
1519
- **Archive File:**
1520
-
1521
- ```typescript
1522
- await s3.moveFile(
1523
- file.path,
1524
- `${config.s3.archivePrefix}${file.name}`
1525
- );
1526
- ```
1527
-
1528
- - Moves file on S3 (atomic operation)
1529
- - Creates destination prefix if needed
1530
- - Original file removed after successful move
1531
-
1532
- **Mark Processed:**
1533
-
1534
- ```typescript
1535
- await fileTracker.markFileProcessed(file.name, {
1536
- recordCount: batchResults.totalSent,
1537
- batchCount: batchResults.batchCount,
1538
- jobId: job.id,
1539
- });
1540
- ```
1541
-
1542
- - Stores metadata in Versori KV store
1543
- - Metadata optional but useful for audit trails
1544
- - Prevents reprocessing in future runs
1545
-
1546
- **Dispose Resources:**
1547
-
1548
- ```typescript
1549
- await s3.dispose();
1550
- ```
1551
-
1552
- - Releases S3 connection pool
1553
- - Should be called at end of workflow in `finally` block
1554
- - Ensures proper cleanup
1555
-
1556
- ---
1557
-
1558
- ## Versori Activation Variables
1559
-
1560
- Configure in Versori platform settings:
1561
-
1562
- ```bash
1563
- # S3 Configuration
1564
- S3_BUCKET_NAME=my-inventory-bucket
1565
- AWS_REGION=us-east-1
1566
- AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXX
1567
- AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1568
-
1569
- # S3 Paths
1570
- S3_PREFIX=inventory/
1571
- ARCHIVE_PREFIX=processed/
1572
- ERROR_PREFIX=errors/
1573
- LOG_PREFIX=logs/
1574
-
1575
- # File Processing
1576
- FILE_PATTERN=.json
1577
- JSON_FORMAT=json # or 'jsonl' for JSON Lines
1578
- MAX_FILES=10
1579
-
1580
- # Processing Mode Configuration
1581
- # ⚠️ IMPORTANT: Choose ONE mode per connector (these are alternatives, not used together)
1582
- # Options: "per-file" (default, recommended), "batch", "chunked"
1583
- PROCESSING_MODE=per-file
1584
- # For chunked mode only: number of files to process per chunk (default: 5)
1585
- FILE_CHUNK_SIZE=5
1586
-
1587
- # Batch Log Configuration
1588
- LOG_ENABLED=true # Enable/disable batch log writing (default: true)
1589
- LOG_FORMAT=json # Log format: json or text (default: json)
1590
-
1591
- # Fluent Configuration (via Versori connection)
1592
- # Connection: fluent_commerce (OAuth2)
1593
- FLUENT_RETAILER_ID=my-retailer
1594
-
1595
- # Batch Processing
1596
- BATCH_SIZE=1000
1597
-
1598
- # BPP Configuration
1599
- # true = Full snapshot (BPP filters unchanged records - DEFAULT)
1600
- # false = Delta feed (skip BPP, all records are changes)
1601
- BPP_ENABLED=true
1602
-
1603
- # Optional Feature Toggles (NEW)
1604
- ENABLE_FILE_TRACKING=true # Track processed files to prevent duplicates (default: true)
1605
- DETAILED_LOGGING=false # Enable verbose logging for debugging (default: false)
1606
- ```
1607
-
1608
- ---
1609
-
1610
- ## Batch API Payload Example
1611
-
1612
- What gets sent to Fluent Commerce Batch API:
1613
-
1614
- ```json
1615
- {
1616
- "action": "UPSERT",
1617
- "entityType": "INVENTORY",
1618
- "source": "S3_JSON",
1619
- "event": "InventoryChanged",
1620
- "entities": [
1621
- {
1622
- "retailerId": 1,
1623
- "locationRef": "LOC001",
1624
- "skuRef": "SKU-12345",
1625
- "qty": 100,
1626
- "type": "LAST_ON_HAND",
1627
- "status": "ACTIVE",
1628
- "expectedOn": "2025-01-25T00:00:00.000Z",
1629
- "attributes": {
1630
- "expiryDate": "2026-12-31T00:00:00.000Z",
1631
- "batchNumber": "BATCH-A001",
1632
- "condition": "NEW",
1633
- "storageZone": "ZONE-A"
1634
- }
1635
- },
1636
- {
1637
- "retailerId": 1,
1638
- "locationRef": "LOC001",
1639
- "skuRef": "SKU-67890",
1640
- "qty": 50,
1641
- "type": "LAST_ON_HAND",
1642
- "status": "ACTIVE",
1643
- "expectedOn": "2025-01-25T00:00:00.000Z",
1644
- "attributes": {
1645
- "expiryDate": "2026-06-30T00:00:00.000Z",
1646
- "batchNumber": "BATCH-A002",
1647
- "condition": "NEW",
1648
- "storageZone": "ZONE-B"
1649
- }
1650
- }
1651
- ]
1652
- }
1653
- ```
1654
-
1655
- > **⚠️ CRITICAL:** Each entity MUST include `retailerId` - it's required in the entity payload, not just in createJob()
1656
-
1657
- ---
1658
-
1659
- ## Versori Deployment
1660
-
1661
- Use the Versori CLI for deploy and operations:
1662
-
1663
- ```bash
1664
- # Install dependencies
1665
- npm install
1666
-
1667
- # Deploy to Versori
1668
- versori deploy
1669
-
1670
- # View logs
1671
- versori logs s3-json-inventory-batch-sync
1672
-
1673
- # Trigger manual run (if defined)
1674
- versori run ingest-now
1675
- ```
1676
-
1677
- ---
1678
-
1679
- ## Testing
1680
-
1681
- ### Test Scheduled Batch
1682
-
1683
- Upload a test JSON file to S3 incoming prefix and wait for the scheduled run.
1684
-
1685
- **Check logs:**
1686
-
1687
- ```
1688
- [STEP 1/8] Initializing job tracking
1689
- [STEP 2/8] Initializing Fluent Commerce client and S3
1690
- [STEP 3/8] Discovering files on S3
1691
- [FILE 1/1] Processing file: inventory_20250124.json
1692
- [STEP 4/8] Downloading and parsing: inventory_20250124.json
1693
- [STEP 5/8] Transforming 5000 inventory records from inventory_20250124.json
1694
- [STEP 6/8] Creating batch job and sending 5 batches to Fluent Commerce
1695
- [STEP 7/8] Archiving file: inventory_20250124.json
1696
- [STEP 8/8] Completing job and calculating totals
1697
- ```
1698
-
1699
- ### Test Ad hoc Batch
1700
-
1701
- ```bash
1702
- # Process all pending files
1703
- curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
1704
- -H "Content-Type: application/json" \
1705
- -d '{}'
1706
-
1707
- # Process specific pattern
1708
- curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
1709
- -H "Content-Type: application/json" \
1710
- -d '{
1711
- "filePattern": "urgent_*.json"
1712
- }'
1713
-
1714
- # Force reprocess
1715
- curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
1716
- -H "Content-Type: application/json" \
1717
- -d '{
1718
- "forceReprocess": true,
1719
- "filePattern": "inventory_20250124.json"
1720
- }'
1721
- ```
1722
-
1723
- ### Test Job Status Query
1724
-
1725
- ```bash
1726
- curl -X POST https://api.versori.com/webhooks/inventory-batch-job-status \
1727
- -H "Content-Type: application/json" \
1728
- -d '{
1729
- "jobId": "ADHOC_INV_20251024_183045_abc123"
1730
- }'
1731
- ```
1732
-
1733
- ### Verify Batch Job in Fluent
1734
-
1735
- After processing, check the Batch job status in Fluent Commerce:
1736
-
1737
- ```bash
1738
- # Upload test file to S3
1739
- aws s3 cp inventory-test.json s3://my-bucket/inventory/
1740
-
1741
- # Query job status via GraphQL
1742
- curl -X POST https://your-fluent-instance.com/graphql \
1743
- -H "Authorization: Bearer YOUR_TOKEN" \
1744
- -H "Content-Type: application/json" \
1745
- -d '{
1746
- "query": "query { job(id: \"job-123456\") { id status recordCount processedCount } }"
1747
- }'
1748
- ```
1749
-
1750
- ---
1751
-
1752
- ## Monitoring
1753
-
1754
- ### Success Response
1755
-
1756
- ```json
1757
- {
1758
- "success": true,
1759
- "filesProcessed": 1,
1760
- "filesSkipped": 0,
1761
- "filesFailed": 0,
1762
- "results": [
1763
- {
1764
- "file": "inventory_2025-01-22.json",
1765
- "success": true,
1766
- "recordCount": 5000,
1767
- "batchCount": 5,
1768
- "jobId": "job-123456",
1769
- "duration": 12345
1770
- }
1771
- ],
1772
- "duration": 13456
1773
- }
1774
- ```
1775
-
1776
- ### Error Response
1777
-
1778
- ```json
1779
- {
1780
- "success": false,
1781
- "filesProcessed": 0,
1782
- "filesFailed": 1,
1783
- "results": [
1784
- {
1785
- "file": "inventory_2025-01-22.json",
1786
- "success": false,
1787
- "error": "No valid records after mapping"
1788
- }
1789
- ],
1790
- "duration": 876
1791
- }
1792
- ```
1793
-
1794
- ---
1795
-
1796
- ## Common Pitfalls and Solutions
1797
-
1798
- ### 1. JSON Format Detection
1799
-
1800
- ❌ **Wrong:**
1801
-
1802
- ```typescript
1803
- // Assuming format without checking
1804
- const records = parsed.inventory;
1805
- ```
1806
-
1807
- ✅ **Correct:**
1808
-
1809
- ```typescript
1810
- // Handle both standard JSON and JSON Lines
1811
- let records: any[] = [];
1812
- if (format === 'jsonl') {
1813
- records = Array.isArray(parsed) ? parsed : [parsed];
1814
- } else {
1815
- if (Array.isArray(parsed)) records = parsed;
1816
- else if (parsed?.inventory) records = parsed.inventory;
1817
- else records = [parsed];
1818
- }
1819
- ```
1820
-
1821
- **Why:** JSON files can have different structures; always normalize.
1822
-
1823
- ### 2. JSON Field Mapping
1824
-
1825
- ❌ **Wrong:**
1826
-
1827
- ```json
1828
- {
1829
- "fields": {
1830
- "locationRef": { "source": "@locationRef" }
1831
- }
1832
- }
1833
- ```
1834
-
1835
- ✅ **Correct:**
1836
-
1837
- ```json
1838
- {
1839
- "fields": {
1840
- "locationRef": { "source": "locationRef" }
1841
- }
1842
- }
1843
- ```
1844
-
1845
- **Why:** JSON uses direct field access (no `@` prefix like XML attributes).
1846
-
1847
- ### 3. Mapping Data Structure
1848
-
1849
- ❌ **Wrong:**
1850
-
1851
- ```typescript
1852
- // Wrapping JSON record unnecessarily
1853
- const mappingResult = await mapper.map({ inventory: rec });
1854
- ```
1855
-
1856
- ✅ **Correct:**
1857
-
1858
- ```typescript
1859
- // Map JSON record directly
1860
- const mappingResult = await mapper.map(rec);
1861
- ```
1862
-
1863
- **Why:** Unlike XML, JSON records don't need wrapper objects for mapping.
1864
-
1865
- ### 4. BPP Configuration
1866
-
1867
- ❌ **Wrong:**
1868
-
1869
- ```typescript
1870
- // Using BPP for delta feeds (wastes processing time)
1871
- const job = await client.createJob({
1872
- name: 'Hourly Deltas',
1873
- retailerId: 'my-retailer',
1874
- // BPP enabled by default - unnecessary for delta feeds
1875
- });
1876
- ```
1877
-
1878
- ✅ **Correct:**
1879
-
1880
- ```typescript
1881
- // Skip BPP for delta feeds (all records are changes)
1882
- const job = await client.createJob({
1883
- name: 'Hourly Deltas',
1884
- retailerId: 'my-retailer',
1885
- meta: {
1886
- preprocessing: 'skip', // Skip BPP - all records are changes
1887
- },
1888
- });
1889
- ```
1890
-
1891
- **Why:** Delta feeds are pre-filtered by source system, BPP overhead is wasted.
1892
-
1893
- ### 5. Large JSON Files
1894
-
1895
- ❌ **Wrong:**
1896
-
1897
- ```typescript
1898
- // Loading entire 500MB JSON into memory
1899
- const parsed = await parser.parse(largeJsonContent);
1900
- ```
1901
-
1902
- ✅ **Correct:**
1903
-
1904
- ```typescript
1905
- // Use JSON Lines format for streaming
1906
- const format = 'jsonl';
1907
- const parsed = await parser.parse(largeJsonContent, { format });
1908
- ```
1909
-
1910
- **Why:** JSON Lines processes one record at a time, preventing memory issues.
1911
-
1912
- ---
1913
-
1914
- ## Key Takeaways
1915
-
1916
- - 🎯 **Use Batch API for inventory** - Not Event API (Event API is for products/orders/etc.)
1917
- - 🎯 **Processing mode selection** - Per-file (default) for safety, batch for speed, chunked for scale
1918
- - 🎯 **TRUE modular architecture** - Separate service files with clear responsibilities (see Modular Structure section)
1919
- - 🎯 **BPP by default** - Enabled for full snapshots, skip for delta feeds
1920
- - 🎯 **Format detection** - Auto-detects standard JSON vs JSON Lines
1921
- - 🎯 **Direct field access** - Use simple paths like `"locationRef"` (no special notation)
1922
- - 🎯 **Map records directly** - No need to wrap in object (unlike XML)
1923
- - 🎯 **Chunk records** - Default 1000 per batch, tune based on record size
1924
- - 🎯 **EntityType: INVENTORY** - Correct entity type for inventory records
1925
- - 🎯 **State management** - VersoriFileTracker + JobTracker prevent duplicates
1926
- - 🎯 **Enhanced retry logic** - S3 operations with exponential backoff
1927
- - 🎯 **Always dispose** - Release S3 resources with `dispose()` in finally block
1928
- - 🎯 **Error handling** - Move failed files to error folder, don't fail entire workflow
1929
- - 🎯 **Native logging** - Use `log` from context on Versori platform
1930
- - 🎯 **Streaming for large files** - Use JSON Lines format for files >100MB
1931
- - 🎯 **Emoji logging** - Use 🚀 ✅ ❌ ⚠️ 🔍 📄 for visual log scanning
1932
- - 🎯 **Execution boundaries** - Use ═══ separators for workflow start/end
1933
- - 🎯 **validateConnection** - Enable connection validation on client creation
1934
- - 🎯 **Optional toggles** - Support enableFileTracking and detailedLogging flags
1935
- - 🎯 **Duration tracking** - Track execution time at file and workflow levels
1936
- - 🎯 **Error recommendations** - Include actionable suggestions in error logs
1937
-
1938
- ---
1939
-
1940
- ## Related Documentation
1941
-
1942
- - [Batch API vs Event API Decision Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
1943
- - [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
1944
- - [S3 Data Source](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md#s3-data-source)
1945
- - [JSON Parser](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md#json-parser-service)
1946
- - [State Management](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md)
1947
- - [Job Tracker](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md)
1948
- - [BPP Documentation](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md#batch-pre-processing-bpp-change-detection)
1949
-
1950
- ---
1951
-
1952
- [→ Back to Versori Scheduled Workflows](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) | [Versori Platform Guide →](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
1
+ ---
2
+ template_id: tpl-ingest-s3-json-inventory-batch
3
+ canonical_filename: template-ingestion-s3-json-inventory-batch.md
4
+ version: 2.0.0
5
+ sdk_version: ^0.1.39
6
+ runtime: versori
7
+ direction: ingestion
8
+ source: s3-json
9
+ destination: fluent-batch-api
10
+ entity: inventory
11
+ format: json
12
+ logging: versori
13
+ status: stable
14
+ features:
15
+ - batch-api-integration
16
+ - memory-management
17
+ - enhanced-logging
18
+ - attribute-transformation
19
+ ---
20
+
21
+ # Template: Ingestion - S3 JSON Inventory to Batch API
22
+
23
+ **FC Connect SDK Use Case Guide**
24
+
25
+ > **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
26
+ > **Version**: @fluentcommerce/fc-connect-sdk@^0.1.39
27
+
28
+ **🆕 Production Code Enhancements (Applied):**
29
+ 1. ✅ Batch API with retry logic and BPP change detection
30
+ 2. ✅ Memory management (clearing large arrays after batch processing)
31
+ 3. ✅ Enhanced logging with emoji progress tracking (📦 batch creation, 📤 batch sending, ✅ completion)
32
+ 4. ✅ Attribute transformation with nested field support
33
+
34
+ **Template Version:** 2.0.0
35
+ **Last Updated:** 2025-01-24
36
+
37
+ **Context**: Versori scheduled workflow that reads inventory JSON files from S3, transforms data with UniversalMapper, and sends bulk inventory updates to Fluent Commerce Batch API with BPP change detection
38
+
39
+ **Complexity**: Medium
40
+
41
+ **Runtime**: Versori Platform
42
+
43
+ **Estimated Lines**: ~520 lines
44
+
45
+ ---
46
+
47
+ ## STEP 1: Understand This Template
48
+
49
+ **What This Template Does:**
50
+
51
+ - Scheduled Versori workflow for bulk inventory ingestion from S3 JSON files
52
+ - Reads JSON files from S3 with retry logic and streaming support
53
+ - Parses JSON with format detection (standard JSON vs JSON Lines)
54
+ - Transforms data using UniversalMapper with direct field access (no special notation)
55
+ - Sends bulk updates to Fluent Commerce Batch API with chunking
56
+ - Uses BPP (Batch Pre-Processing) for change detection
57
+ - Tracks processing state with VersoriFileTracker to prevent duplicates
58
+ - Archives processed files and handles errors gracefully
59
+
60
+ **Key SDK Components:**
61
+
62
+ - `createClient()` - Universal client factory (auto-detects Versori context)
63
+ - `S3DataSource` - S3 operations with streaming (NEW: enhanced retry logic)
64
+ - `JSONParserService` - JSON parsing with format auto-detection (JSON vs JSON Lines)
65
+ - `UniversalMapper` - Field transformation with SDK resolvers
66
+ - `VersoriFileTracker` - State management (prevent duplicate processing)
67
+ - `JobTracker` - Job lifecycle tracking
68
+ - Native Versori `log` - Use `log` from context
69
+
70
+ **Entity Type:**
71
+
72
+ - **InventoryQuantity** - Fluent entity for inventory positions and quantities
73
+ - **EntityType: 'INVENTORY'** - Used in Batch API `sendBatch()` call
74
+ - **Batch API Method** - Uses `createJob()` and `sendBatch()` (not Event API)
75
+
76
+ **Critical Patterns:**
77
+
78
+ - **JSON Format Support**: Standard JSON objects and JSON Lines (one record per line)
79
+ - **Direct Field Access**: Use simple paths like `"locationRef"` (no `@` prefix needed for JSON)
80
+ - **Safe S3 Paths**: Use absolute paths in S3DataSource config
81
+ - **Always Dispose**: Call `s3.dispose()` in `finally` block to release connections
82
+ - **BPP Configuration**: Enabled by default for full snapshots, skip for delta feeds
83
+ - **Progress Logging**: Enhanced logging with context (sample SKUs, locations)
84
+ - **Detailed Logging Toggle**: Optional detailed payload logging for debugging (default: disabled)
85
+ - **File Tracking Toggle**: Optional file tracking to prevent duplicates (default: enabled)
86
+ - **Retry Logic**: Enhanced S3 operations with exponential backoff
87
+
88
+ **When to Use This Template:**
89
+
90
+ - ✅ Daily/hourly full inventory snapshots from S3 JSON files
91
+ - ✅ Bulk inventory updates with BPP change detection
92
+ - ✅ JSON or JSON Lines format data
93
+ - ✅ Need state management to prevent duplicate processing
94
+ - ✅ Files should be archived after processing
95
+
96
+ **When NOT to Use:**
97
+
98
+ - ❌ Single inventory updates (use GraphQL mutation instead)
99
+ - ❌ Products, Locations, Customers (use Event API templates)
100
+ - ❌ Real-time inventory events (use Event API)
101
+ - ❌ XML/CSV/Parquet formats (use appropriate format template)
102
+
103
+ ---
104
+
105
+ ## STEP 2: AI Prompt
106
+
107
+ **Copy this prompt to generate the complete implementation:**
108
+
109
+ ```
110
+ Create a Versori scheduled workflow for S3 JSON inventory ingestion to Fluent Commerce Batch API.
111
+
112
+ REQUIREMENTS:
113
+ 1. Runtime: Versori Platform (scheduled workflow)
114
+ 2. Source: S3 JSON files from s3://bucket/inventory/
115
+ 3. Destination: Fluent Commerce Batch API (InventoryQuantity entity)
116
+ 4. Format: JSON (standard JSON objects and JSON Lines support)
117
+ 5. Entity: InventoryQuantity (EntityType: 'INVENTORY')
118
+
119
+ KEY FEATURES:
120
+ - S3 JSON file discovery with VersoriFileTracker for state management
121
+ - JSON parsing with format auto-detection (JSON vs JSON Lines)
122
+ - Direct field mapping using UniversalMapper (no special notation needed)
123
+ - Batch API with chunking (1000 records per batch) and BPP change detection
124
+ - Safe S3 paths with absolute path requirements
125
+ - S3 dispose() in finally block
126
+ - Job lifecycle tracking with JobTracker
127
+ - File archival on S3 after successful processing
128
+ - Error handling with exponential backoff retry
129
+
130
+ CRITICAL REQUIREMENTS:
131
+ 1. JSON Parser: JSONParserService with format auto-detection
132
+ 2. Array Extraction: Handle both standard JSON arrays and JSON Lines
133
+ 3. Mapping Config: Use direct paths like "locationRef" (no @ prefix)
134
+ 4. Entity Type: 'INVENTORY' (for InventoryQuantity)
135
+ 5. BPP: Enabled by default (full snapshots), skip for delta feeds
136
+ 6. S3 Config: Include proper AWS credentials configuration
137
+ 7. S3 Dispose: Always call s3.dispose() in finally block
138
+ 8. Native Logging: Use log from context
139
+ 9. Retry Logic: Enhanced S3 operations with exponential backoff
140
+
141
+ SDK METHODS TO USE:
142
+ - createClient(ctx) - Pass entire Versori context, auto-detects platform
143
+ - client.setRetailerId(retailerId) - REQUIRED after createClient for Batch API operations
144
+ - new S3DataSource(config, log) - Config includes AWS credentials
145
+ - await s3.listFiles({ prefix, maxKeys })
146
+ - await s3.downloadFile(key, { encoding: 'utf8' })
147
+ - await s3.moveFile(sourceKey, destKey)
148
+ - await s3.dispose() - MUST call in finally block
149
+ - new JSONParserService()
150
+ - await parser.parse(jsonContent, { format: 'json' | 'jsonl' })
151
+ - new UniversalMapper(mappingConfig)
152
+ - await mapper.map(records)
153
+ - new VersoriFileTracker(ctx.openKv(':project:'), 'prefix')
154
+ - await fileTracker.wasFileProcessed(fileName)
155
+ - await fileTracker.markFileProcessed(fileName, metadata)
156
+ - new JobTracker(ctx.openKv(':project:'), log)
157
+ - await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' })
158
+ - await tracker.updateJob(jobId, { status: 'processing' })
159
+ - await tracker.markCompleted(jobId, details)
160
+ - await tracker.markFailed(jobId, error)
161
+ - await client.createJob({ name, meta: { preprocessing: 'skip' } })
162
+ - await client.sendBatch(jobId, { action: 'UPSERT', entityType: 'INVENTORY', entities })
163
+ - Fire-and-forget batch submission (no polling)
164
+
165
+ FORBIDDEN PATTERNS:
166
+ - ❌ LoggingService (removed - use native log on Versori)
167
+ - ❌ Don't use @ prefix for JSON fields (that's for XML attributes only)
168
+ - ❌ Don't forget to wrap records in proper structure for mapping
169
+ - ❌ Don't use Event API (use Batch API)
170
+ - ❌ Don't forget to call s3.dispose() in finally block
171
+ - ❌ Don't use relative S3 paths (use absolute paths)
172
+
173
+ MAPPING CONFIGURATION FILE: config/inventory.batch.json
174
+ Structure:
175
+ {
176
+ "name": "inventory.batch.json",
177
+ "version": "1.0.0",
178
+ "description": "JSON inventory to Fluent Commerce Batch API mapping",
179
+ "fields": {
180
+ "locationRef": { "source": "locationRef", "required": true, "resolver": "sdk.trim" },
181
+ "skuRef": { "source": "skuRef", "required": true, "resolver": "sdk.trim" },
182
+ "qty": { "source": "qty", "required": true, "resolver": "sdk.parseInt" },
183
+ "type": { "source": "type", "required": true, "resolver": "sdk.uppercase" },
184
+ "status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
185
+ "expectedOn": { "source": "expectedOn", "required": false, "resolver": "sdk.formatDate" },
186
+ "attributes.expiryDate": { "source": "attributes.expiryDate", "required": false, "resolver": "sdk.formatDate" },
187
+ "attributes.batchNumber": { "source": "attributes.batchNumber", "required": false },
188
+ "attributes.condition": { "source": "attributes.condition", "required": false, "defaultValue": "NEW", "resolver": "sdk.uppercase" },
189
+ "attributes.storageZone": { "source": "attributes.storageZone", "required": false }
190
+ }
191
+ }
192
+
193
+ GENERATE:
194
+ 1. package.json with dependencies
195
+ 2. index.ts (workflow entry point with scheduled trigger)
196
+ 3. src/workflows/scheduled/daily-inventory-sync.ts (scheduled workflow)
197
+ 4. src/workflows/webhook/adhoc-inventory-sync.ts (webhook workflow)
198
+ 5. src/workflows/webhook/job-status-check.ts (status webhook)
199
+ 6. src/services/inventory-sync.service.ts (shared orchestration logic)
200
+ 7. src/types/inventory.types.ts (TypeScript type definitions)
201
+ 8. config/inventory.batch.json (mapping configuration - external JSON file)
202
+ 9. .env.example (environment variables)
203
+
204
+ NOTE: Use external JSON files for mapping configuration (not TypeScript .config files)
205
+
206
+ Ensure all code is production-ready with proper error handling, S3 dispose() in finally block. Polling is intentionally omitted (fire-and-forget).
207
+ ```
208
+
209
+ ---
210
+
211
+ ## What You'll Build
212
+
213
+ ### Project Structure
214
+
215
+ ```
216
+ s3-json-inventory-batch-sync/
217
+ ├── package.json
218
+ ├── index.ts # Workflow entry point
219
+ └── src/
220
+ ├── workflows/
221
+ │ ├── scheduled/
222
+ │ │ └── daily-inventory-sync.ts # Scheduled: Daily inventory sync
223
+ │ │
224
+ │ └── webhook/
225
+ │ ├── adhoc-inventory-sync.ts # Webhook: Manual trigger
226
+ │ └── job-status-check.ts # Webhook: Status query
227
+
228
+ ├── services/
229
+ │ └── inventory-sync.service.ts # Shared orchestration logic (reusable)
230
+
231
+ ├── config/
232
+ │ └── inventory.batch.json # Mapping configuration (external JSON)
233
+
234
+ └── types/
235
+ └── inventory.types.ts # TypeScript interfaces
236
+ ```
237
+
238
+ ### Features
239
+
240
+ - ✅ Scheduled Versori workflow (daily/hourly inventory sync)
241
+ - ✅ S3 file download with enhanced retry logic
242
+ - ✅ JSON parsing with format auto-detection (standard JSON vs JSON Lines)
243
+ - ✅ Direct field mapping (no special notation needed for JSON)
244
+ - ✅ Field mapping with UniversalMapper and external JSON config
245
+ - ✅ Batch API job creation and chunked processing (1000 records/batch)
246
+ - ✅ BPP (Batch Pre-Processing) change detection
247
+ - ✅ Fire-and-forget batch submission with audit logging
248
+ - ✅ State management (VersoriFileTracker + JobTracker prevent duplicates)
249
+ - ✅ File archival on S3 (success → processed/, failure → errors/)
250
+ - ✅ Comprehensive error handling with exponential backoff retry
251
+ - ✅ Modular architecture (reusable services, easy to test)
252
+
253
+ ---
254
+
255
+ ## Versori Workflows Structure
256
+
257
+ **Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
258
+
259
+ **Trigger Types:**
260
+ - **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
261
+ - **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
262
+ - **`workflow()`** → Durable workflows (advanced, rarely used)
263
+
264
+ **Execution Steps (chained to triggers):**
265
+ - **`http()`** → External API calls (chained from schedule/webhook)
266
+ - **`fn()`** → Internal processing (chained from schedule/webhook)
267
+
268
+ ### Recommended Project Structure
269
+
270
+ ```
271
+ s3-json-inventory-batch-sync/
272
+ ├── index.ts # Entry point - exports all workflows
273
+ └── src/
274
+ ├── workflows/
275
+ │ ├── scheduled/
276
+ │ │ └── daily-inventory-sync.ts # Scheduled: Daily inventory sync
277
+ │ │
278
+ │ └── webhook/
279
+ │ ├── adhoc-inventory-sync.ts # Webhook: Manual trigger
280
+ │ └── job-status-check.ts # Webhook: Status query
281
+
282
+ ├── services/
283
+ │ └── inventory-sync.service.ts # Shared orchestration logic (reusable)
284
+
285
+ └── types/
286
+ └── inventory.types.ts # Shared type definitions
287
+ ```
288
+
289
+ **Benefits:**
290
+ - ✅ Clear trigger separation (`scheduled/` vs `webhook/`)
291
+ - ✅ Descriptive file names (easy to browse and understand)
292
+ - ✅ Scalable (add new workflows without cluttering)
293
+ - ✅ Reusable code in `services/` (DRY principle)
294
+ - ✅ Easy to modify individual workflows without affecting others
295
+
296
+ ---
297
+
298
+ ## Workflow Files
299
+
300
+ ### 1. Scheduled Workflows (`src/workflows/scheduled/`)
301
+
302
+ All time-based triggers that run automatically on cron schedules.
303
+
304
+ #### `src/workflows/scheduled/daily-inventory-sync.ts`
305
+
306
+ **Purpose**: Automatic daily inventory sync
307
+ **Trigger**: Cron schedule (`0 2 * * *`)
308
+ **Exposed as Endpoint**: ❌ NO - Runs automatically
309
+
310
+ ```typescript
311
+ import { schedule, http } from '@versori/run';
312
+ import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
313
+ import { runIngestion } from '../../services/inventory-sync.service';
314
+
315
+ /**
316
+ * Scheduled Workflow: Daily Inventory Sync
317
+ *
318
+ * Runs automatically daily at 2 AM UTC
319
+ * NOT exposed as HTTP endpoint - Versori executes on schedule
320
+ *
321
+ * Uses shared service: inventory-sync.service.ts
322
+ */
323
+ export const dailyInventorySync = schedule(
324
+ 'inventory-batch-scheduled',
325
+ '0 2 * * *' // Daily at 2 AM UTC
326
+ ).then(
327
+ http('run-inventory-batch', { connection: 'fluent_commerce' }, async (ctx: any) => {
328
+ const { log, openKv } = ctx;
329
+ const executionStartTime = Date.now();
330
+ const jobId = `inventory-batch-${Date.now()}`;
331
+ const tracker = new JobTracker(openKv(':project:'), log);
332
+
333
+ log.info('═══════════════════════════════════════════════════════════════');
334
+ log.info('🚀 [WORKFLOW] Starting scheduled inventory sync', { jobId });
335
+ log.info('═══════════════════════════════════════════════════════════════');
336
+
337
+ await tracker.createJob(jobId, {
338
+ triggeredBy: 'schedule',
339
+ stage: 'initialization',
340
+ startTime: executionStartTime,
341
+ });
342
+
343
+ await tracker.updateJob(jobId, { status: 'processing' });
344
+
345
+ try {
346
+ // Reuse shared orchestration logic
347
+ const result = await runIngestion(ctx, jobId, tracker);
348
+
349
+ if (result.success) {
350
+ const duration = Date.now() - executionStartTime;
351
+ await tracker.markCompleted(jobId, { ...result, duration });
352
+ log.info('✅ [WORKFLOW] Inventory sync completed successfully', {
353
+ jobId,
354
+ filesProcessed: result.filesProcessed,
355
+ duration: `${duration}ms (${(duration / 1000).toFixed(2)}s)`,
356
+ });
357
+ } else {
358
+ await tracker.markFailed(jobId, result.error || 'Unknown error');
359
+ log.error('❌ [WORKFLOW] Inventory sync failed', {
360
+ jobId,
361
+ error: result.error,
362
+ });
363
+ }
364
+
365
+ log.info('═══════════════════════════════════════════════════════════════');
366
+ log.info('🏁 [WORKFLOW] Execution complete', {
367
+ jobId,
368
+ success: result.success,
369
+ duration: `${Date.now() - executionStartTime}ms`,
370
+ });
371
+ log.info('═══════════════════════════════════════════════════════════════');
372
+
373
+ return { success: true, jobId, ...result };
374
+ } catch (e: any) {
375
+ const errorMessage = e instanceof Error ? e.message : String(e);
376
+ await tracker.markFailed(jobId, errorMessage);
377
+ log.error('❌ [WORKFLOW] Inventory sync failed with exception', {
378
+ jobId,
379
+ error: errorMessage,
380
+ stack: e instanceof Error ? e.stack : undefined,
381
+ recommendation: 'Check SFTP credentials, network connectivity, and file permissions',
382
+ });
383
+ return { success: false, jobId, error: errorMessage };
384
+ }
385
+ })
386
+ );
387
+ ```
388
+
389
+ ---
390
+
391
+ ### 2. Webhook Workflows (`src/workflows/webhook/`)
392
+
393
+ All HTTP-based triggers that create webhook endpoints.
394
+
395
+ #### `src/workflows/webhook/adhoc-inventory-sync.ts`
396
+
397
+ **Purpose**: Manual inventory sync trigger (on-demand)
398
+ **Trigger**: HTTP POST
399
+ **Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-adhoc`
400
+ **Use Cases**: Testing, priority processing, ad-hoc runs
401
+
402
+ ```typescript
403
+ import { webhook, http } from '@versori/run';
404
+ import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
405
+ import { runIngestion } from '../../services/inventory-sync.service';
406
+
407
+ /**
408
+ * Webhook: Manual Inventory Sync Trigger
409
+ *
410
+ * Endpoint: POST https://{workspace}.versori.run/inventory-batch-adhoc
411
+ * Request body (optional): { filePattern: "urgent_*.json", maxFiles: 5 }
412
+ *
413
+ * Pattern: Sync mode + fire-and-forget
414
+ * - Returns jobId immediately
415
+ * - Background processing continues without blocking response
416
+ * - ✅ Works because Versori keeps execution context alive for unawaited promises
417
+ *
418
+ * Uses shared service: inventory-sync.service.ts
419
+ */
420
+ export const adhocInventorySync = webhook('inventory-batch-adhoc', {
421
+ response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
422
+ connection: 'inventory-batch-adhoc', // Versori validates API key
423
+ }).then(
424
+ http('run-inventory-batch-adhoc', { connection: 'fluent_commerce' }, async (ctx: any) => {
425
+ const { log, openKv, data } = ctx;
426
+ const executionStartTime = Date.now();
427
+ const jobId = `inventory-batch-adhoc-${Date.now()}`;
428
+ const tracker = new JobTracker(openKv(':project:'), log);
429
+
430
+ const filePattern = data?.filePattern as string || '*.json';
431
+ const maxFiles = data?.maxFiles as number;
432
+
433
+ log.info('🚀 [WEBHOOK] Adhoc inventory sync triggered', {
434
+ jobId,
435
+ filePattern,
436
+ maxFiles,
437
+ requestData: data,
438
+ });
439
+
440
+ // Create job entry FIRST (awaited to ensure job exists in KV)
441
+ await tracker.createJob(jobId, {
442
+ triggeredBy: 'manual',
443
+ stage: 'initialization',
444
+ status: 'queued',
445
+ startTime: executionStartTime,
446
+ options: { filePattern, maxFiles },
447
+ createdAt: new Date().toISOString(),
448
+ });
449
+
450
+ // ✅ Fire-and-forget: Start background processing WITHOUT await
451
+ // The promise continues execution after we return the response
452
+ runIngestion(ctx, jobId, tracker)
453
+ .then((result) => {
454
+ const duration = Date.now() - executionStartTime;
455
+ if (result.success) {
456
+ log.info('✅ [BACKGROUND] Inventory sync completed successfully', {
457
+ jobId,
458
+ filesProcessed: result.filesProcessed,
459
+ filesFailed: result.filesFailed,
460
+ recordsProcessed: result.recordsProcessed,
461
+ duration: `${duration}ms (${(duration / 1000).toFixed(2)}s)`,
462
+ });
463
+ return tracker.markCompleted(jobId, { ...result, duration });
464
+ } else {
465
+ log.error('❌ [BACKGROUND] Inventory sync failed', {
466
+ jobId,
467
+ error: result.error,
468
+ });
469
+ return tracker.markFailed(jobId, result.error || 'Unknown error');
470
+ }
471
+ })
472
+ .catch((error: unknown) => {
473
+ const errorMessage = error instanceof Error ? error.message : String(error);
474
+ const errorStack = error instanceof Error ? error.stack : undefined;
475
+
476
+ log.error('❌ [BACKGROUND] Inventory sync failed with exception', {
477
+ jobId,
478
+ error: errorMessage,
479
+ stack: errorStack,
480
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
481
+ recommendation: 'Check S3 credentials, bucket permissions, and network connectivity',
482
+ });
483
+
484
+ return tracker.markFailed(jobId, errorMessage);
485
+ });
486
+
487
+ // Return immediately with jobId (response sent with this return value)
488
+ return {
489
+ success: true,
490
+ jobId,
491
+ message: 'Inventory sync started in background',
492
+ statusEndpoint: `https://{workspace}.versori.run/inventory-batch-job-status`,
493
+ note: 'Poll the status endpoint with jobId to check progress',
494
+ };
495
+ })
496
+ );
497
+ ```
498
+
499
+ ---
500
+
501
+ #### `src/workflows/webhook/job-status-check.ts`
502
+
503
+ **Purpose**: Query job status and progress
504
+ **Trigger**: HTTP POST/GET
505
+ **Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-job-status`
506
+ **Request Body**: `{ "jobId": "inventory-batch-1234567890" }`
507
+
508
+ ```typescript
509
+ import { webhook, fn } from '@versori/run';
510
+ import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
511
+
512
+ /**
513
+ * Webhook: Job Status Check
514
+ *
515
+ * Endpoint: POST https://{workspace}.versori.run/inventory-batch-job-status
516
+ * Request body: { "jobId": "inventory-batch-1234567890" }
517
+ *
518
+ * Pattern: webhook().then(fn()) - no external API needed, only KV storage
519
+ * Lightweight: Only queries KV store, no Fluent API calls
520
+ */
521
+ export const jobStatusCheck = webhook('inventory-batch-job-status', {
522
+ response: { mode: 'sync' },
523
+ connection: 'inventory-batch-job-status',
524
+ }).then(
525
+ fn('status', async ctx => {
526
+ const { data, log, openKv } = ctx;
527
+ const jobId = data?.jobId as string;
528
+
529
+ if (!jobId) {
530
+ return { success: false, error: 'jobId required' };
531
+ }
532
+
533
+ const tracker = new JobTracker(openKv(':project:'), log);
534
+ const status = await tracker.getJob(jobId);
535
+
536
+ return status
537
+ ? { success: true, jobId, ...status }
538
+ : { success: false, error: 'Job not found', jobId };
539
+ })
540
+ );
541
+ ```
542
+
543
+ ---
544
+
545
+ ### 3. Entry Point (`index.ts`)
546
+
547
+ **Purpose**: Register all workflows with Versori platform
548
+
549
+ ```typescript
550
+ /**
551
+ * Entry Point - Registers all workflows with Versori platform
552
+ *
553
+ * Versori automatically discovers and registers exported workflows
554
+ *
555
+ * File Structure:
556
+ * - src/workflows/scheduled/ → Time-based triggers (cron)
557
+ * - src/workflows/webhook/ → HTTP-based triggers (webhooks)
558
+ */
559
+
560
+ // Scheduled workflows
561
+ export { dailyInventorySync } from './src/workflows/scheduled/daily-inventory-sync';
562
+
563
+ // Webhook workflows
564
+ export { adhocInventorySync } from './src/workflows/webhook/adhoc-inventory-sync';
565
+ export { jobStatusCheck } from './src/workflows/webhook/job-status-check';
566
+ ```
567
+
568
+ **What Gets Exposed:**
569
+ - ✅ `adhocInventorySync` → `https://{workspace}.versori.run/inventory-batch-adhoc`
570
+ - ✅ `jobStatusCheck` → `https://{workspace}.versori.run/inventory-batch-job-status`
571
+ - ❌ `dailyInventorySync` → NOT exposed (runs automatically on cron)
572
+
573
+ ## When to Use Batch API vs Event API
574
+
575
+ ### ✅ Use Batch API (`createJob`/`sendBatch`) For:
576
+
577
+ | Entity Type | Use Case | Why Batch API |
578
+ | --------------------- | ---------------------------------- | ----------------------------------------------- |
579
+ | **Inventory** | Bulk inventory updates, daily sync | Optimized for high-volume, BPP change detection |
580
+ | **InventoryQuantity** | Inventory positions and quantities | Native Batch API entity type |
581
+
582
+ ### ❌ Use Event API Instead For:
583
+
584
+ | Entity Type | Use Case | Why Event API / GraphQL |
585
+ | ------------------- | ------------------------------------- | --------------------------------------------- |
586
+ | **Products** | Product catalog sync, variant updates | Triggers workflows, validates business rules |
587
+ | **Locations** | Store/warehouse setup | Requires workflow orchestration |
588
+ | **Customers** | Customer registration/profile updates | Prefer GraphQL mutations (no Rubix support) |
589
+ | **Orders** | Updates/events (not creation) | Events fine for updates; creation via GraphQL |
590
+ | **Custom Entities** | Any entity needing workflow triggers | Full Rubix workflow support |
591
+
592
+ ### 🔍 Use GraphQL Mutations For:
593
+
594
+ | Scenario | Why GraphQL |
595
+ | --------------------- | -------------------------------------- |
596
+ | **Single operations** | Create one record, update one record |
597
+ | **Complex queries** | Fetch data with relationships |
598
+ | **Testing/debugging** | Direct API control, immediate feedback |
599
+
600
+ Note:
601
+
602
+ - Orders: Use `createOrder` GraphQL mutation for order creation (this triggers the Order CREATED event in Rubix). Order updates/events are fine via Event API.
603
+ - Customers: Use GraphQL mutations (no Rubix workflow support for customers).
604
+
605
+ ---
606
+
607
+ ## Understanding BPP (Batch Pre-Processing)
608
+
609
+ **BPP** is Fluent's change detection system that filters out unchanged records before workflow processing.
610
+
611
+ Note on defaults and control:
612
+
613
+ - BPP is a Fluent platform feature. The default on/off behavior is controlled at your Fluent account level, not by the SDK.
614
+ - If you omit `meta.preprocessing` in `createJob()`, the account-level default applies.
615
+ - To force behavior per job, set `meta.preprocessing` explicitly (`'skip'` to disable for delta feeds).
616
+
617
+ ### When to Use BPP (Default - Enabled)
618
+
619
+ **✅ Full Inventory Snapshots:**
620
+
621
+ - Daily complete inventory dumps
622
+ - Entire warehouse stock files
623
+ - Records may be identical to previous run
624
+
625
+ **Example:**
626
+
627
+ ```typescript
628
+ // BPP enabled by default - filters unchanged records
629
+ const job = await client.createJob({
630
+ name: 'Daily Full Inventory',
631
+ retailerId: 'my-retailer',
632
+ // BPP automatically enabled - no meta needed
633
+ });
634
+ ```
635
+
636
+ **What BPP does:**
637
+
638
+ - Compares incoming records with existing records in Fluent
639
+ - Filters out records with no changes
640
+ - Only passes changed/new records to workflows
641
+ - Significantly reduces workflow processing load
642
+
643
+ ### When to Skip BPP
644
+
645
+ **✅ Delta Feeds (Pre-Filtered Data):**
646
+
647
+ - Hourly change files (only updates since last run)
648
+ - Event-driven inventory changes
649
+ - Pre-filtered data from source system
650
+ - All records are guaranteed to be changes
651
+
652
+ **Example:**
653
+
654
+ ```typescript
655
+ // Skip BPP for delta feeds - all records are changes
656
+ const job = await client.createJob({
657
+ name: 'Hourly Delta Inventory',
658
+ retailerId: 'my-retailer',
659
+ meta: {
660
+ preprocessing: 'skip', // All records already filtered
661
+ },
662
+ });
663
+ ```
664
+
665
+ **Performance Impact:**
666
+
667
+ - **Full snapshot + BPP**: 10,000 records → 500 changes → Fast
668
+ - **Full snapshot, no BPP**: 10,000 records → 10,000 processed → Slow
669
+ - **Delta feed + BPP**: 500 records → 500 changes → Fast (but BPP overhead wasted)
670
+ - **Delta feed, no BPP**: 500 records → 500 processed → Fastest
671
+
672
+ ### Decision Guide
673
+
674
+ | Source Data Type | BPP Setting | Reason |
675
+ | ---------------------- | ----------------------- | ----------------------------------------------- |
676
+ | **Daily full dump** | Enabled (default) | Most records unchanged, BPP filters efficiently |
677
+ | **Hourly delta feed** | `preprocessing: 'skip'` | All records are changes, BPP overhead wasted |
678
+ | **Initial load** | Enabled (default) | No previous data, but good practice |
679
+ | **Manual corrections** | Enabled (default) | Unknown change ratio, let BPP filter |
680
+ | **Real-time events** | `preprocessing: 'skip'` | Each record is a change event |
681
+
682
+ ---
683
+
684
+ ## Processing Modes
685
+
686
+ **⚠️ IMPORTANT:** Choose ONE processing mode per connector. These are alternative patterns, not features to use together.
687
+
688
+ The SDK supports three processing modes for handling multiple files. **Select the mode that best fits your use case** and configure your connector accordingly:
689
+
690
+ ### Mode 1: Per-File Processing (Recommended Default) ✅ IMPLEMENTED
691
+
692
+ **When to use:**
693
+
694
+ - Multiple large files that shouldn't be in memory together
695
+ - Need file-level consistency (file 3 fails → files 1-2 already archived)
696
+ - Memory constraints
697
+ - Fault isolation (one file failure doesn't affect others)
698
+
699
+ **Flow:**
700
+
701
+ ```
702
+ FOR EACH FILE:
703
+ 1. Download file
704
+ 2. Parse JSON
705
+ 3. Map records
706
+ 4. Create Batch API job
707
+ 5. Send batches
708
+ 6. Write log
709
+ 7. Archive file
710
+ 8. Mark file processed
711
+
712
+ IF FILE FAILS:
713
+ - Move to /errors/
714
+ - Continue to next file
715
+ - Other files unaffected
716
+ ```
717
+
718
+ **Configuration:**
719
+
720
+ ```json
721
+ {
722
+ "processingMode": "per-file",
723
+ "maxFilesPerRun": 10
724
+ }
725
+ ```
726
+
727
+ **Benefits:**
728
+
729
+ - ✅ File-level atomicity (each file processed independently)
730
+ - ✅ Low memory footprint (1 file at a time)
731
+ - ✅ Clear error isolation (failed file doesn't block others)
732
+ - ✅ Incremental progress (files archived as completed)
733
+
734
+ **Example implementation (see code section below)**
735
+
736
+ ### Mode 2: Batch Processing (All Files at Once) ⚠️ OPTIONAL - Choose if needed
737
+
738
+ **When to use:**
739
+
740
+ - Small files (all fit in memory comfortably)
741
+ - Need one Batch API job for all files
742
+ - Atomic processing (all files or none)
743
+
744
+ **Flow:**
745
+
746
+ ```
747
+ 1. Download ALL files
748
+ 2. Parse ALL files
749
+ 3. Map ALL records
750
+ 4. Create ONE Batch API job
751
+ 5. Send ALL batches
752
+ 6. Write logs
753
+ 7. Archive ALL files
754
+ 8. Mark ALL processed
755
+ ```
756
+
757
+ **Configuration:**
758
+
759
+ ```json
760
+ {
761
+ "processingMode": "batch",
762
+ "maxFilesPerRun": 10
763
+ }
764
+ ```
765
+
766
+ **Benefits:**
767
+
768
+ - ✅ Single job for all files (simplifies tracking)
769
+ - ✅ Faster for many small files (parallel parsing possible)
770
+
771
+ **Drawbacks:**
772
+
773
+ - ❌ High memory usage (all files in memory)
774
+ - ❌ All-or-nothing (one failure affects all)
775
+ - ❌ No incremental progress
776
+
777
+ ### Mode 3: Chunked Per-File Processing (Balanced) ⚠️ OPTIONAL - Choose if needed
778
+
779
+ **When to use:**
780
+
781
+ - Many files (e.g., 100+ files)
782
+ - Process N files at a time
783
+ - Balance between memory and parallelism
784
+
785
+ **Flow:**
786
+
787
+ ```
788
+ CHUNK files into groups of N (e.g., 5 files per chunk):
789
+
790
+ FOR EACH CHUNK:
791
+ FOR EACH FILE in chunk:
792
+ 1. Download file
793
+ 2. Parse JSON
794
+ 3. Map records
795
+ 4. Create Batch API job
796
+ 5. Send batches
797
+ 6. Archive file
798
+
799
+ Wait for chunk to complete before next chunk
800
+ ```
801
+
802
+ **Configuration:**
803
+
804
+ ```json
805
+ {
806
+ "processingMode": "chunked",
807
+ "fileChunkSize": 5,
808
+ "maxFilesPerRun": 100
809
+ }
810
+ ```
811
+
812
+ **Benefits:**
813
+
814
+ - ✅ Bounded memory usage (N files at a time)
815
+ - ✅ Better throughput than per-file (parallel processing within chunk)
816
+ - ✅ Incremental progress (per-chunk)
817
+
818
+ **Use cases:**
819
+
820
+ - High-volume file ingestion (100+ files per run)
821
+ - Rate limiting (control API load)
822
+ - Resource-constrained environments
823
+
824
+ ### Comparison Matrix
825
+
826
+ | Aspect | Per-File | Batch | Chunked |
827
+ | ------------------- | ---------------------------------- | ----------------------------- | --------------------------------------- |
828
+ | **Memory Usage** | Low (1 file at a time) | High (all files) | Medium (N files) |
829
+ | **Consistency** | Per-file atomic | All-or-nothing | Per-chunk atomic |
830
+ | **Fault Isolation** | ✅ Best (1 file fails → others OK) | ❌ Worst (1 fails → all fail) | ✅ Good (chunk fails → other chunks OK) |
831
+ | **Performance** | Slower (sequential) | Fastest (parallel parsing) | Balanced |
832
+ | **Batch API Jobs** | N jobs (1 per file) | 1 job (all files) | N jobs (1 per file) |
833
+ | **Use Case** | Large files, strict consistency | Small files, fast processing | Many files, balanced approach |
834
+ | **Recommended For** | Production (safest) | Testing, small datasets | High-volume production |
835
+
836
+ > **📋 This Template Implementation:**
837
+ >
838
+ > **✅ This template implements ALL THREE modes** and selects based on `PROCESSING_MODE` variable.
839
+ >
840
+ > **Choose ONE mode per connector:**
841
+ > - **Default:** `PROCESSING_MODE=per-file` (recommended for production)
842
+ > - **Alternative:** `PROCESSING_MODE=batch` (for small files, atomic processing)
843
+ > - **Alternative:** `PROCESSING_MODE=chunked` (for high-volume scenarios)
844
+ >
845
+ > **Important:** Do NOT use multiple modes in the same connector. Each connector should use ONE consistent pattern.
846
+
847
+ ### Decision Guide
848
+
849
+ | Source Data | File Count | File Size | Recommended Mode | Reason |
850
+ | -------------- | ---------- | ----------- | ---------------- | --------------------------------------- |
851
+ | Daily snapshot | 1-10 | >10MB each | **Per-File** | Memory efficient, fault isolation |
852
+ | Hourly deltas | 10-50 | <1MB each | **Batch** | Fast processing, small memory footprint |
853
+ | Real-time feed | 100+ | <100KB each | **Chunked** | Balanced throughput + memory |
854
+ | Initial load | 1 | >100MB | **Per-File** | Memory safety |
855
+ | Testing | 1-5 | Any | **Batch** | Simplicity |
856
+
857
+ ---
858
+
859
+ ## JSON File Format
860
+
861
+ ### Sample: inventory.json (Standard JSON)
862
+
863
+ ```json
864
+ {
865
+ "inventory": [
866
+ {
867
+ "locationRef": "LOC001",
868
+ "skuRef": "SKU-12345",
869
+ "qty": 100,
870
+ "type": "LAST_ON_HAND",
871
+ "status": "ACTIVE",
872
+ "expectedOn": "2025-01-25",
873
+ "attributes": {
874
+ "expiryDate": "2026-12-31",
875
+ "batchNumber": "BATCH-A001",
876
+ "condition": "NEW",
877
+ "storageZone": "ZONE-A"
878
+ }
879
+ },
880
+ {
881
+ "locationRef": "LOC001",
882
+ "skuRef": "SKU-67890",
883
+ "qty": 50,
884
+ "type": "LAST_ON_HAND",
885
+ "status": "ACTIVE",
886
+ "expectedOn": "2025-01-25",
887
+ "attributes": {
888
+ "expiryDate": "2026-06-30",
889
+ "batchNumber": "BATCH-A002",
890
+ "condition": "NEW",
891
+ "storageZone": "ZONE-B"
892
+ }
893
+ }
894
+ ]
895
+ }
896
+ ```
897
+
898
+ ### Sample: inventory.jsonl (JSON Lines)
899
+
900
+ ```jsonl
901
+ {"locationRef":"LOC001","skuRef":"SKU-12345","qty":100,"type":"LAST_ON_HAND","status":"ACTIVE","expectedOn":"2025-01-25","attributes":{"expiryDate":"2026-12-31","batchNumber":"BATCH-A001","condition":"NEW","storageZone":"ZONE-A"}}
902
+ {"locationRef":"LOC001","skuRef":"SKU-67890","qty":50,"type":"LAST_ON_HAND","status":"ACTIVE","expectedOn":"2025-01-25","attributes":{"expiryDate":"2026-06-30","batchNumber":"BATCH-A002","condition":"NEW","storageZone":"ZONE-B"}}
903
+ ```
904
+
905
+ **JSON Structure:**
906
+
907
+ - Standard JSON: Root object with nested arrays (`{ "inventory": [...] }`)
908
+ - JSON Lines: One JSON object per line (streaming-friendly for large files)
909
+ - JSONParserService auto-detects format based on content
910
+ - Both formats support nested objects with dot notation
911
+
912
+ **Field Descriptions:**
913
+
914
+ - `locationRef`: Fluent location reference
915
+ - `skuRef`: Fluent SKU/product reference
916
+ - `qty`: Inventory quantity (integer)
917
+ - `type`: Inventory type (LAST_ON_HAND for full snapshots, DELTA for incremental changes)
918
+ - `status`: Record status (ACTIVE, INACTIVE)
919
+ - `expectedOn`: Expected date (ISO 8601 or parseable format)
920
+ - `attributes.expiryDate`: Product expiry date (optional)
921
+ - `attributes.batchNumber`: Manufacturing batch/lot number (optional)
922
+ - `attributes.condition`: Product condition (NEW, USED, DAMAGED)
923
+ - `attributes.storageZone`: Warehouse zone identifier (optional)
924
+
925
+ ### JSON Field Mapping
926
+
927
+ **IMPORTANT**: JSON uses direct field access (no special prefix needed):
928
+
929
+ ```json
930
+ {
931
+ "fields": {
932
+ "locationRef": { "source": "locationRef", "required": true },
933
+ "skuRef": { "source": "skuRef", "required": true }
934
+ }
935
+ }
936
+ ```
937
+
938
+ **Why**: Unlike XML attributes (which need `@` prefix), JSON fields are accessed directly by name.
939
+
940
+ ### Array Handling (Standard JSON vs JSON Lines)
941
+
942
+ **JSONParserService behavior:**
943
+
944
+ - **Standard JSON**: Returns object or array based on structure
945
+ - **JSON Lines**: Returns array of objects (one per line)
946
+
947
+ **Solution**: Always normalize to array after parsing:
948
+
949
+ ```typescript
950
+ const parsed = await parser.parse(jsonContent, { format: 'json' | 'jsonl' });
951
+
952
+ // Extract inventory array - handle different JSON structures
953
+ let records: any[] = [];
954
+ if (format === 'jsonl') {
955
+ records = Array.isArray(parsed) ? parsed : [parsed];
956
+ } else {
957
+ if (Array.isArray(parsed)) {
958
+ records = parsed; // Root level array
959
+ } else if (parsed?.inventory && Array.isArray(parsed.inventory)) {
960
+ records = parsed.inventory; // { "inventory": [...] }
961
+ } else if (parsed?.records && Array.isArray(parsed.records)) {
962
+ records = parsed.records; // { "records": [...] }
963
+ } else {
964
+ records = [parsed]; // Single object
965
+ }
966
+ }
967
+ ```
968
+
969
+ ---
970
+
971
+ ## Service Implementation
972
+
973
+ ### File: `src/services/inventory-sync.service.ts`
974
+
975
+ **Complete implementation showing all SDK patterns:**
976
+
977
+ ```typescript
978
+ import { Buffer } from 'node:buffer'; // ✅ Required for Versori/Deno runtime
979
+ import {
980
+ createClient,
981
+ S3DataSource,
982
+ JSONParserService,
983
+ UniversalMapper,
984
+ VersoriFileTracker,
985
+ JobTracker,
986
+ extractFileName,
987
+ } from '@fluentcommerce/fc-connect-sdk';
988
+ import type { FileMetadata } from '@fluentcommerce/fc-connect-sdk';
989
+ import inventoryMapping from '../config/inventory.batch.json' with { type: 'json' }; // ✅ External JSON import
990
+
991
+ /**
992
+ * Shared inventory ingestion orchestration logic
993
+ * Used by both scheduled and webhook workflows
994
+ *
995
+ * @param ctx - Versori HTTP context
996
+ * @param jobId - Unique job identifier
997
+ * @param tracker - JobTracker instance
998
+ */
999
+ export async function runIngestion(
1000
+ ctx: any,
1001
+ jobId: string,
1002
+ tracker: JobTracker
1003
+ ) {
1004
+ const { log, activation, openKv } = ctx;
1005
+ const startTime = Date.now();
1006
+ let s3: S3DataSource | null = null;
1007
+
1008
+ // Optional feature toggles
1009
+ const enableFileTracking = activation.getVariable('enableFileTracking') !== 'false';
1010
+ const detailedLogging = activation.getVariable('detailedLogging') === 'true';
1011
+
1012
+ try {
1013
+ // ========================================
1014
+ // CLIENT INITIALIZATION (with retailerId and validation)
1015
+ // ========================================
1016
+ log.info('🔧 [InventorySync] Initializing Fluent Commerce client...');
1017
+
1018
+ const client = await createClient({
1019
+ ...ctx,
1020
+ validateConnection: true, // ✅ Validate connection on creation
1021
+ });
1022
+
1023
+ // ✅ CRITICAL: Set retailerId for Batch API
1024
+ const retailerId = activation.getVariable('fluentRetailerId');
1025
+ if (!retailerId) {
1026
+ log.error('❌ [InventorySync] Missing required fluentRetailerId activation variable');
1027
+ throw new Error('fluentRetailerId activation variable is required for Batch API');
1028
+ }
1029
+ client.setRetailerId(retailerId);
1030
+
1031
+ log.info('✅ [InventorySync] Client initialized and validated', { retailerId });
1032
+
1033
+ // ========================================
1034
+ // S3 DATA SOURCE INITIALIZATION
1035
+ // ========================================
1036
+ log.info('🔧 [InventorySync] Initializing S3 data source...');
1037
+
1038
+ const s3Config = {
1039
+ bucket: activation.getVariable('s3BucketName'),
1040
+ region: activation.getVariable('awsRegion') || 'us-east-1',
1041
+ accessKeyId: activation.getVariable('awsAccessKeyId'),
1042
+ secretAccessKey: activation.getVariable('awsSecretAccessKey'),
1043
+ prefix: activation.getVariable('s3Prefix') || 'inventory/',
1044
+ };
1045
+
1046
+ s3 = new S3DataSource(
1047
+ {
1048
+ type: 'S3_JSON',
1049
+ connectionId: 's3-inventory-sync',
1050
+ name: 'inventory-sync',
1051
+ s3Config,
1052
+ },
1053
+ log
1054
+ );
1055
+
1056
+ log.info('✅ [InventorySync] S3 data source initialized', {
1057
+ bucket: s3Config.bucket,
1058
+ region: s3Config.region,
1059
+ prefix: s3Config.prefix,
1060
+ });
1061
+
1062
+ // ========================================
1063
+ // SERVICE INITIALIZATION
1064
+ // ========================================
1065
+ const parser = new JSONParserService();
1066
+ const mapper = new UniversalMapper(inventoryMapping);
1067
+ const kv = openKv(':project:');
1068
+ const fileTracker = enableFileTracking
1069
+ ? new VersoriFileTracker(kv, 's3-json-inventory-sync')
1070
+ : null;
1071
+ const bppEnabled = activation.getVariable('bppEnabled') !== 'false';
1072
+
1073
+ if (detailedLogging) {
1074
+ log.info('🔍 [InventorySync] Configuration loaded', {
1075
+ bppEnabled,
1076
+ enableFileTracking,
1077
+ detailedLogging,
1078
+ });
1079
+ }
1080
+
1081
+ // ========================================
1082
+ // FILE DISCOVERY
1083
+ // ========================================
1084
+ log.info('🔍 [InventorySync] Discovering files on S3...');
1085
+
1086
+ const files = await s3.listFiles({ prefix: s3Config.prefix });
1087
+ log.info('📄 [InventorySync] File discovery complete', { count: files.length });
1088
+
1089
+ if (files.length === 0) {
1090
+ log.info('⚠️ [InventorySync] No files found to process');
1091
+ return { success: true, message: 'No files to process', filesProcessed: 0 };
1092
+ }
1093
+
1094
+ // ========================================
1095
+ // FILE PROCESSING
1096
+ // ========================================
1097
+ const results = [];
1098
+ let fileIndex = 0;
1099
+
1100
+ for (const file of files) {
1101
+ fileIndex++;
1102
+ const fileStartTime = Date.now();
1103
+ const fileName = extractFileName(file.path);
1104
+
1105
+ log.info(`📄 [FILE ${fileIndex}/${files.length}] Processing: ${fileName}`);
1106
+
1107
+ try {
1108
+ // Check if already processed
1109
+ if (fileTracker && (await fileTracker.wasFileProcessed(file.name))) {
1110
+ log.info(`⏭️ [FILE ${fileIndex}/${files.length}] Skipping already processed: ${fileName}`);
1111
+ results.push({ fileName, success: true, skipped: true });
1112
+ continue;
1113
+ }
1114
+
1115
+ // Download and parse
1116
+ if (detailedLogging) {
1117
+ log.info(`🔍 [FILE ${fileIndex}/${files.length}] Downloading: ${fileName}`);
1118
+ }
1119
+ const jsonContent = (await s3.downloadFile(file.path, { encoding: 'utf8' })) as string;
1120
+ const parsed = await parser.parse(jsonContent);
1121
+
1122
+ // Extract inventory array
1123
+ const records = Array.isArray(parsed) ? parsed :
1124
+ parsed?.inventory ? parsed.inventory :
1125
+ [parsed];
1126
+
1127
+ if (records.length === 0) {
1128
+ log.warn(`⚠️ [FILE ${fileIndex}/${files.length}] No records found in: ${fileName}`);
1129
+ continue;
1130
+ }
1131
+
1132
+ log.info(`🔄 [FILE ${fileIndex}/${files.length}] Transforming ${records.length} records`);
1133
+
1134
+ // Transform records
1135
+ const mappedRecords = [];
1136
+ const aggregatedSkippedFields: string[] = [];
1137
+ for (const rec of records) {
1138
+ const sourceDataWithContext = { ...rec, $context: { retailerId } };
1139
+ const mappingResult = await mapper.map(sourceDataWithContext);
1140
+ if (mappingResult.success) {
1141
+ mappedRecords.push(mappingResult.data);
1142
+ }
1143
+ // Aggregate skipped fields
1144
+ if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
1145
+ for (const fieldName of mappingResult.skippedFields) {
1146
+ if (!aggregatedSkippedFields.includes(fieldName)) {
1147
+ aggregatedSkippedFields.push(fieldName);
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ if (aggregatedSkippedFields.length > 0) {
1154
+ log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
1155
+ file: fileName,
1156
+ skippedFields: aggregatedSkippedFields,
1157
+ note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
1158
+ });
1159
+ }
1160
+
1161
+ if (mappedRecords.length === 0) {
1162
+ log.warn(`⚠️ [FILE ${fileIndex}/${files.length}] No valid records after mapping: ${fileName}`, {
1163
+ recommendation: 'Check mapping configuration and required fields',
1164
+ });
1165
+ continue;
1166
+ }
1167
+
1168
+ // Create job and send batches
1169
+ log.info(`📦 [FILE ${fileIndex}/${files.length}] Creating Batch API job for ${mappedRecords.length} records`);
1170
+
1171
+ const job = await client.createJob({
1172
+ name: `s3-json-${fileName}-${Date.now()}`,
1173
+ meta: bppEnabled ? undefined : { preprocessing: 'skip' },
1174
+ });
1175
+
1176
+ await tracker.updateJob(jobId, {
1177
+ status: 'processing',
1178
+ details: { fileName, recordCount: mappedRecords.length },
1179
+ });
1180
+
1181
+ // ? Enhanced: Extract context for progress logging
1182
+ const uniqueLocations = [...new Set(mappedRecords.map((r: any) => r.locationRef))];
1183
+ const sampleSKUs = mappedRecords.slice(0, 5).map((r: any) => r.skuRef);
1184
+
1185
+ // ? Enhanced: Start logging with context
1186
+ log.info(`📤 [BatchProcessor] Sending batch for file "${fileName}"`, {
1187
+ totalRecords: mappedRecords.length,
1188
+ locations: uniqueLocations.join(', '),
1189
+ sampleSKUs: sampleSKUs.join(', '),
1190
+ jobId: job.id
1191
+ });
1192
+
1193
+ // Send batches (fire-and-forget)
1194
+ await client.sendBatch(job.id, {
1195
+ action: 'UPSERT',
1196
+ entityType: 'INVENTORY',
1197
+ entities: mappedRecords,
1198
+ });
1199
+
1200
+ // ? Enhanced: Completion logging
1201
+ log.info(`✅ [BatchProcessor] Batch sent successfully for file "${fileName}"`, {
1202
+ totalRecords: mappedRecords.length,
1203
+ jobId: job.id,
1204
+ duration: Date.now() - fileStartTime
1205
+ });
1206
+
1207
+ // ? Enhanced: Clear large arrays after processing to free memory
1208
+ mappedRecords.length = 0;
1209
+
1210
+ // Mark as processed
1211
+ if (fileTracker) {
1212
+ await fileTracker.markFileProcessed(file.name, {
1213
+ recordCount: mappedRecords.length,
1214
+ });
1215
+ }
1216
+
1217
+ const fileDuration = Date.now() - fileStartTime;
1218
+ results.push({
1219
+ fileName,
1220
+ success: true,
1221
+ recordCount: mappedRecords.length,
1222
+ duration: fileDuration,
1223
+ });
1224
+
1225
+ log.info(`✅ [FILE ${fileIndex}/${files.length}] Successfully processed: ${fileName}`, {
1226
+ records: mappedRecords.length,
1227
+ jobId: job.id,
1228
+ duration: `${fileDuration}ms (${(fileDuration / 1000).toFixed(2)}s)`,
1229
+ });
1230
+
1231
+ } catch (error: any) {
1232
+ const fileDuration = Date.now() - fileStartTime;
1233
+ log.error(`❌ [FILE ${fileIndex}/${files.length}] Processing failed: ${fileName}`, {
1234
+ error: error?.message,
1235
+ stack: error?.stack,
1236
+ duration: `${fileDuration}ms`,
1237
+ recommendation: 'Check S3 permissions, file format, and mapping configuration',
1238
+ });
1239
+ results.push({ fileName, success: false, error: error?.message, duration: fileDuration });
1240
+ }
1241
+ }
1242
+
1243
+ const totalDuration = Date.now() - startTime;
1244
+ const successCount = results.filter((r) => r.success && !r.skipped).length;
1245
+
1246
+ log.info('✅ [InventorySync] Batch processing complete', {
1247
+ total: files.length,
1248
+ processed: successCount,
1249
+ skipped: results.filter((r) => r.skipped).length,
1250
+ failed: results.filter((r) => !r.success).length,
1251
+ duration: `${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)`,
1252
+ });
1253
+
1254
+ return {
1255
+ success: true,
1256
+ results,
1257
+ filesProcessed: successCount,
1258
+ duration: totalDuration,
1259
+ };
1260
+
1261
+ } catch (error: any) {
1262
+ const totalDuration = Date.now() - startTime;
1263
+ log.error('❌ [InventorySync] Fatal error during ingestion', {
1264
+ message: error?.message,
1265
+ stack: error?.stack,
1266
+ duration: `${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)`,
1267
+ recommendation: 'Check activation variables, S3 credentials, and Fluent API connectivity',
1268
+ });
1269
+ throw error; // Re-throw for workflow error handling
1270
+ } finally {
1271
+ // ✅ CRITICAL: Always dispose S3 connection
1272
+ if (s3) {
1273
+ try {
1274
+ await s3.dispose();
1275
+ log.info('✅ [InventorySync] S3 connection disposed successfully');
1276
+ } catch (disposeError: any) {
1277
+ log.error('⚠️ [InventorySync] Failed to dispose S3 connection', {
1278
+ error: disposeError?.message,
1279
+ });
1280
+ }
1281
+ }
1282
+ }
1283
+ }
1284
+ ```
1285
+
1286
+ **Key Patterns Demonstrated:**
1287
+ - ✅ Buffer import for Versori/Deno compatibility
1288
+ - ✅ External JSON mapping import with `{ type: 'json' }`
1289
+ - ✅ `validateConnection: true` for client initialization
1290
+ - ✅ `setRetailerId()` call after client creation (REQUIRED for Batch API)
1291
+ - ✅ Emoji-based logging (🚀 ✅ ❌ ⚠️ 🔍 📄) for visual log scanning
1292
+ - ✅ Execution boundaries with ═══ separators
1293
+ - ✅ `extractFileName()` usage for clean file names
1294
+ - ✅ Optional feature toggles (enableFileTracking, detailedLogging)
1295
+ - ✅ Duration tracking at file and workflow levels (ms and seconds)
1296
+ - ✅ Recommendations in error logs for actionable debugging
1297
+ - ✅ S3 `dispose()` in finally block with error handling
1298
+ - ✅ VersoriFileTracker for state management (optional via toggle)
1299
+ - ✅ JobTracker for job lifecycle tracking
1300
+
1301
+ ---
1302
+
1303
+ ## Code Flow Explanation
1304
+
1305
+ ### Initialization Phase
1306
+
1307
+ **Logger Setup:**
1308
+
1309
+ ```typescript
1310
+ // ✅ CORRECT: Use native Versori log from context
1311
+ const { log } = ctx;
1312
+ log.info('Starting workflow');
1313
+
1314
+ // ❌ WRONG: LoggingService (removed - use native log on Versori)
1315
+ // import { LoggingService } from '@fluentcommerce/fc-connect-sdk';
1316
+ // const logging = new LoggingService();
1317
+ // const log = logging.createLogger({ logLevel: 'info' });
1318
+ ```
1319
+
1320
+ - Versori provides `log` in the context - use it directly
1321
+
1322
+ **Client Creation:**
1323
+
1324
+ ```typescript
1325
+ // Pass entire Versori context object
1326
+ const client = await createClient(ctx);
1327
+ ```
1328
+
1329
+ - `createClient(ctx)` accepts entire Versori context (fetch, connections, log, activation)
1330
+ - Auto-detects Versori platform from context
1331
+ - Returns `FluentClient` configured with OAuth2 from connections.fluent_commerce
1332
+ - OAuth2 authentication handled automatically
1333
+ - This is the **CORRECT** pattern for Versori workflows
1334
+
1335
+ **S3 Data Source:**
1336
+
1337
+ ```typescript
1338
+ const s3 = new S3DataSource(
1339
+ {
1340
+ type: 'S3_JSON', // Type indicates JSON files (metadata only)
1341
+ connectionId: 's3-inventory-sync', // Unique identifier (required)
1342
+ name: 'inventory-sync', // Human-readable name (required)
1343
+ s3Config: {
1344
+ bucket: 'my-inventory-bucket',
1345
+ region: 'us-east-1',
1346
+ accessKeyId: 'AKIAXXXX',
1347
+ secretAccessKey: 'xxxxxxxx',
1348
+ },
1349
+ },
1350
+ log
1351
+ );
1352
+ ```
1353
+
1354
+ - **Constructor params:** `(config: S3DataSourceConfig, log)` - Versori native log, no explicit typing needed
1355
+ - `connectionId` and `name` are required in config
1356
+ - `type` must be `'S3_JSON'` for JSON files
1357
+ - Enhanced retry logic with exponential backoff
1358
+
1359
+ ### File Discovery Phase
1360
+
1361
+ **List Files:**
1362
+
1363
+ ```typescript
1364
+ const files = await s3.listFiles({
1365
+ prefix: config.s3.prefix, // Override config (optional)
1366
+ maxKeys: 1000, // Maximum files to retrieve
1367
+ });
1368
+ ```
1369
+
1370
+ - Returns `FileMetadata[]` with: `name`, `lastModified`, `size`, `path`, `source`
1371
+ - Files sorted by modification time (newest first)
1372
+ - Directories excluded automatically
1373
+
1374
+ **State Check:**
1375
+
1376
+ ```typescript
1377
+ const alreadyProcessed = await fileTracker.wasFileProcessed(file.name);
1378
+ if (alreadyProcessed) {
1379
+ continue; // Skip already processed files
1380
+ }
1381
+ ```
1382
+
1383
+ - Uses Versori KV store (`openKv()`) for distributed state
1384
+ - Prevents duplicate processing across workflow runs
1385
+ - State persists between executions
1386
+
1387
+ ### File Processing Phase
1388
+
1389
+ **Download File:**
1390
+
1391
+ ```typescript
1392
+ const jsonContent = (await s3.downloadFile(file.path, {
1393
+ encoding: 'utf8',
1394
+ })) as string;
1395
+ ```
1396
+
1397
+ - **Important:** Cast to `string` when `encoding` is specified
1398
+ - Without encoding, returns `Buffer`
1399
+ - Supports streaming for large files
1400
+
1401
+ **Parse JSON:**
1402
+
1403
+ ```typescript
1404
+ const jsonFormat = activation.getVariable('jsonFormat') || 'json';
1405
+ const parsed = await parser.parse(jsonContent, { format: jsonFormat });
1406
+
1407
+ // Extract inventory array - handle different JSON structures
1408
+ let records: any[] = [];
1409
+ if (jsonFormat === 'jsonl') {
1410
+ records = Array.isArray(parsed) ? parsed : [parsed];
1411
+ } else {
1412
+ if (Array.isArray(parsed)) {
1413
+ records = parsed; // Root level array
1414
+ } else if (parsed?.inventory && Array.isArray(parsed.inventory)) {
1415
+ records = parsed.inventory; // { "inventory": [...] }
1416
+ } else {
1417
+ records = [parsed]; // Single object
1418
+ }
1419
+ }
1420
+ ```
1421
+
1422
+ - Parses JSON into JavaScript object or array
1423
+ - **Format detection**: Auto-detects standard JSON vs JSON Lines
1424
+ - Always normalize to array for consistent processing
1425
+ - Throws `ParsingError` on invalid JSON
1426
+
1427
+ **JSON Field Handling:**
1428
+
1429
+ - Direct field access by name (no special prefix needed)
1430
+ - Example: `{ "locationRef": "LOC001" }` → `{ locationRef: 'LOC001' }`
1431
+ - Use simple paths like `"locationRef"` in mapping config
1432
+
1433
+ **Transform Data:**
1434
+
1435
+ ```typescript
1436
+ const mapper = new UniversalMapper(inventoryMapping);
1437
+
1438
+ // Map each record directly (no wrapper needed for JSON)
1439
+ const mappingResult = await mapper.map(rec);
1440
+
1441
+ if (!mappingResult.success) {
1442
+ // Mapping validation failed for required fields
1443
+ log.warn('Mapping failed:', mappingResult.errors);
1444
+ continue; // Skip invalid records
1445
+ }
1446
+
1447
+ mappedRecords.push(mappingResult.data);
1448
+ ```
1449
+
1450
+ - Direct mapping (no need to wrap in object like XML)
1451
+ - `MappingResult`: `{ success: boolean, data: any, errors?: string[] }`
1452
+ - `success: false` only if required fields fail
1453
+ - Optional field errors reported but don't fail mapping
1454
+
1455
+ ### Batch API Phase
1456
+
1457
+ **Create Job:**
1458
+
1459
+ ```typescript
1460
+ // Full snapshot (BPP enabled by default)
1461
+ const job = await client.createJob({
1462
+ name: 'Daily Full Inventory',
1463
+ retailerId: 'my-retailer',
1464
+ // BPP automatically enabled - filters unchanged records
1465
+ });
1466
+
1467
+ // Delta feed (skip BPP)
1468
+ const job = await client.createJob({
1469
+ name: 'Hourly Deltas',
1470
+ retailerId: 'my-retailer',
1471
+ meta: {
1472
+ preprocessing: 'skip', // All records are changes
1473
+ },
1474
+ });
1475
+ ```
1476
+
1477
+ - Returns job object with `id` and metadata
1478
+ - `retailerId` required for tenant isolation
1479
+ - BPP enabled by default, skip with `meta: { preprocessing: 'skip' }`
1480
+
1481
+ **Send Batches:**
1482
+
1483
+ ```typescript
1484
+ const batch = await client.sendBatch(jobId, {
1485
+ action: 'UPSERT', // Most common: create or update
1486
+ entityType: 'INVENTORY', // Entity type for inventory
1487
+ source: 'S3_JSON', // Data source identifier (optional)
1488
+ event: 'InventoryChanged', // Valid Rubix event (optional, defaults to InventoryChanged)
1489
+ entities: chunk, // Array of inventory records
1490
+ });
1491
+ ```
1492
+
1493
+ - `action`: `'UPSERT'` (currently the only action supported by Fluent Batch API)
1494
+ - `entityType`: `'INVENTORY'` for inventory records
1495
+ - `entities`: Array of transformed inventory objects
1496
+ - Returns batch object with `id` for tracking
1497
+
1498
+ **Fire-and-Forget Pattern:**
1499
+
1500
+ ```typescript
1501
+ const batch = await client.sendBatch(jobId, { ... });
1502
+ log.info('Batch sent (fire-and-forget)', { batchId: batch.id });
1503
+
1504
+ // Record batch details for audit trail
1505
+ result.batches.push({
1506
+ batchId: batch.id,
1507
+ recordCount: chunk.length,
1508
+ timestamp: new Date().toISOString(),
1509
+ status: 'SENT',
1510
+ });
1511
+ ```
1512
+
1513
+ - No polling - batches sent and tracked immediately
1514
+ - Batch details recorded for audit logging
1515
+ - Log files written to S3 for tracking (optional, configurable)
1516
+
1517
+ ### Cleanup Phase
1518
+
1519
+ **Archive File:**
1520
+
1521
+ ```typescript
1522
+ await s3.moveFile(
1523
+ file.path,
1524
+ `${config.s3.archivePrefix}${file.name}`
1525
+ );
1526
+ ```
1527
+
1528
+ - Moves file on S3 (atomic operation)
1529
+ - Creates destination prefix if needed
1530
+ - Original file removed after successful move
1531
+
1532
+ **Mark Processed:**
1533
+
1534
+ ```typescript
1535
+ await fileTracker.markFileProcessed(file.name, {
1536
+ recordCount: batchResults.totalSent,
1537
+ batchCount: batchResults.batchCount,
1538
+ jobId: job.id,
1539
+ });
1540
+ ```
1541
+
1542
+ - Stores metadata in Versori KV store
1543
+ - Metadata optional but useful for audit trails
1544
+ - Prevents reprocessing in future runs
1545
+
1546
+ **Dispose Resources:**
1547
+
1548
+ ```typescript
1549
+ await s3.dispose();
1550
+ ```
1551
+
1552
+ - Releases S3 connection pool
1553
+ - Should be called at end of workflow in `finally` block
1554
+ - Ensures proper cleanup
1555
+
1556
+ ---
1557
+
1558
+ ## Versori Activation Variables
1559
+
1560
+ Configure in Versori platform settings:
1561
+
1562
+ ```bash
1563
+ # S3 Configuration
1564
+ S3_BUCKET_NAME=my-inventory-bucket
1565
+ AWS_REGION=us-east-1
1566
+ AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXX
1567
+ AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1568
+
1569
+ # S3 Paths
1570
+ S3_PREFIX=inventory/
1571
+ ARCHIVE_PREFIX=processed/
1572
+ ERROR_PREFIX=errors/
1573
+ LOG_PREFIX=logs/
1574
+
1575
+ # File Processing
1576
+ FILE_PATTERN=.json
1577
+ JSON_FORMAT=json # or 'jsonl' for JSON Lines
1578
+ MAX_FILES=10
1579
+
1580
+ # Processing Mode Configuration
1581
+ # ⚠️ IMPORTANT: Choose ONE mode per connector (these are alternatives, not used together)
1582
+ # Options: "per-file" (default, recommended), "batch", "chunked"
1583
+ PROCESSING_MODE=per-file
1584
+ # For chunked mode only: number of files to process per chunk (default: 5)
1585
+ FILE_CHUNK_SIZE=5
1586
+
1587
+ # Batch Log Configuration
1588
+ LOG_ENABLED=true # Enable/disable batch log writing (default: true)
1589
+ LOG_FORMAT=json # Log format: json or text (default: json)
1590
+
1591
+ # Fluent Configuration (via Versori connection)
1592
+ # Connection: fluent_commerce (OAuth2)
1593
+ FLUENT_RETAILER_ID=my-retailer
1594
+
1595
+ # Batch Processing
1596
+ BATCH_SIZE=1000
1597
+
1598
+ # BPP Configuration
1599
+ # true = Full snapshot (BPP filters unchanged records - DEFAULT)
1600
+ # false = Delta feed (skip BPP, all records are changes)
1601
+ BPP_ENABLED=true
1602
+
1603
+ # Optional Feature Toggles (NEW)
1604
+ ENABLE_FILE_TRACKING=true # Track processed files to prevent duplicates (default: true)
1605
+ DETAILED_LOGGING=false # Enable verbose logging for debugging (default: false)
1606
+ ```
1607
+
1608
+ ---
1609
+
1610
+ ## Batch API Payload Example
1611
+
1612
+ What gets sent to Fluent Commerce Batch API:
1613
+
1614
+ ```json
1615
+ {
1616
+ "action": "UPSERT",
1617
+ "entityType": "INVENTORY",
1618
+ "source": "S3_JSON",
1619
+ "event": "InventoryChanged",
1620
+ "entities": [
1621
+ {
1622
+ "retailerId": 1,
1623
+ "locationRef": "LOC001",
1624
+ "skuRef": "SKU-12345",
1625
+ "qty": 100,
1626
+ "type": "LAST_ON_HAND",
1627
+ "status": "ACTIVE",
1628
+ "expectedOn": "2025-01-25T00:00:00.000Z",
1629
+ "attributes": {
1630
+ "expiryDate": "2026-12-31T00:00:00.000Z",
1631
+ "batchNumber": "BATCH-A001",
1632
+ "condition": "NEW",
1633
+ "storageZone": "ZONE-A"
1634
+ }
1635
+ },
1636
+ {
1637
+ "retailerId": 1,
1638
+ "locationRef": "LOC001",
1639
+ "skuRef": "SKU-67890",
1640
+ "qty": 50,
1641
+ "type": "LAST_ON_HAND",
1642
+ "status": "ACTIVE",
1643
+ "expectedOn": "2025-01-25T00:00:00.000Z",
1644
+ "attributes": {
1645
+ "expiryDate": "2026-06-30T00:00:00.000Z",
1646
+ "batchNumber": "BATCH-A002",
1647
+ "condition": "NEW",
1648
+ "storageZone": "ZONE-B"
1649
+ }
1650
+ }
1651
+ ]
1652
+ }
1653
+ ```
1654
+
1655
+ > **⚠️ CRITICAL:** Each entity MUST include `retailerId` - it's required in the entity payload, not just in createJob()
1656
+
1657
+ ---
1658
+
1659
+ ## Versori Deployment
1660
+
1661
+ Use the Versori CLI for deploy and operations:
1662
+
1663
+ ```bash
1664
+ # Install dependencies
1665
+ npm install
1666
+
1667
+ # Deploy to Versori
1668
+ versori deploy
1669
+
1670
+ # View logs
1671
+ versori logs s3-json-inventory-batch-sync
1672
+
1673
+ # Trigger manual run (if defined)
1674
+ versori run ingest-now
1675
+ ```
1676
+
1677
+ ---
1678
+
1679
+ ## Testing
1680
+
1681
+ ### Test Scheduled Batch
1682
+
1683
+ Upload a test JSON file to S3 incoming prefix and wait for the scheduled run.
1684
+
1685
+ **Check logs:**
1686
+
1687
+ ```
1688
+ [STEP 1/8] Initializing job tracking
1689
+ [STEP 2/8] Initializing Fluent Commerce client and S3
1690
+ [STEP 3/8] Discovering files on S3
1691
+ [FILE 1/1] Processing file: inventory_20250124.json
1692
+ [STEP 4/8] Downloading and parsing: inventory_20250124.json
1693
+ [STEP 5/8] Transforming 5000 inventory records from inventory_20250124.json
1694
+ [STEP 6/8] Creating batch job and sending 5 batches to Fluent Commerce
1695
+ [STEP 7/8] Archiving file: inventory_20250124.json
1696
+ [STEP 8/8] Completing job and calculating totals
1697
+ ```
1698
+
1699
+ ### Test Ad hoc Batch
1700
+
1701
+ ```bash
1702
+ # Process all pending files
1703
+ curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
1704
+ -H "Content-Type: application/json" \
1705
+ -d '{}'
1706
+
1707
+ # Process specific pattern
1708
+ curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
1709
+ -H "Content-Type: application/json" \
1710
+ -d '{
1711
+ "filePattern": "urgent_*.json"
1712
+ }'
1713
+
1714
+ # Force reprocess
1715
+ curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
1716
+ -H "Content-Type: application/json" \
1717
+ -d '{
1718
+ "forceReprocess": true,
1719
+ "filePattern": "inventory_20250124.json"
1720
+ }'
1721
+ ```
1722
+
1723
+ ### Test Job Status Query
1724
+
1725
+ ```bash
1726
+ curl -X POST https://api.versori.com/webhooks/inventory-batch-job-status \
1727
+ -H "Content-Type: application/json" \
1728
+ -d '{
1729
+ "jobId": "ADHOC_INV_20251024_183045_abc123"
1730
+ }'
1731
+ ```
1732
+
1733
+ ### Verify Batch Job in Fluent
1734
+
1735
+ After processing, check the Batch job status in Fluent Commerce:
1736
+
1737
+ ```bash
1738
+ # Upload test file to S3
1739
+ aws s3 cp inventory-test.json s3://my-bucket/inventory/
1740
+
1741
+ # Query job status via GraphQL
1742
+ curl -X POST https://your-fluent-instance.com/graphql \
1743
+ -H "Authorization: Bearer YOUR_TOKEN" \
1744
+ -H "Content-Type: application/json" \
1745
+ -d '{
1746
+ "query": "query { job(id: \"job-123456\") { id status recordCount processedCount } }"
1747
+ }'
1748
+ ```
1749
+
1750
+ ---
1751
+
1752
+ ## Monitoring
1753
+
1754
+ ### Success Response
1755
+
1756
+ ```json
1757
+ {
1758
+ "success": true,
1759
+ "filesProcessed": 1,
1760
+ "filesSkipped": 0,
1761
+ "filesFailed": 0,
1762
+ "results": [
1763
+ {
1764
+ "file": "inventory_2025-01-22.json",
1765
+ "success": true,
1766
+ "recordCount": 5000,
1767
+ "batchCount": 5,
1768
+ "jobId": "job-123456",
1769
+ "duration": 12345
1770
+ }
1771
+ ],
1772
+ "duration": 13456
1773
+ }
1774
+ ```
1775
+
1776
+ ### Error Response
1777
+
1778
+ ```json
1779
+ {
1780
+ "success": false,
1781
+ "filesProcessed": 0,
1782
+ "filesFailed": 1,
1783
+ "results": [
1784
+ {
1785
+ "file": "inventory_2025-01-22.json",
1786
+ "success": false,
1787
+ "error": "No valid records after mapping"
1788
+ }
1789
+ ],
1790
+ "duration": 876
1791
+ }
1792
+ ```
1793
+
1794
+ ---
1795
+
1796
+ ## Common Pitfalls and Solutions
1797
+
1798
+ ### 1. JSON Format Detection
1799
+
1800
+ ❌ **Wrong:**
1801
+
1802
+ ```typescript
1803
+ // Assuming format without checking
1804
+ const records = parsed.inventory;
1805
+ ```
1806
+
1807
+ ✅ **Correct:**
1808
+
1809
+ ```typescript
1810
+ // Handle both standard JSON and JSON Lines
1811
+ let records: any[] = [];
1812
+ if (format === 'jsonl') {
1813
+ records = Array.isArray(parsed) ? parsed : [parsed];
1814
+ } else {
1815
+ if (Array.isArray(parsed)) records = parsed;
1816
+ else if (parsed?.inventory) records = parsed.inventory;
1817
+ else records = [parsed];
1818
+ }
1819
+ ```
1820
+
1821
+ **Why:** JSON files can have different structures; always normalize.
1822
+
1823
+ ### 2. JSON Field Mapping
1824
+
1825
+ ❌ **Wrong:**
1826
+
1827
+ ```json
1828
+ {
1829
+ "fields": {
1830
+ "locationRef": { "source": "@locationRef" }
1831
+ }
1832
+ }
1833
+ ```
1834
+
1835
+ ✅ **Correct:**
1836
+
1837
+ ```json
1838
+ {
1839
+ "fields": {
1840
+ "locationRef": { "source": "locationRef" }
1841
+ }
1842
+ }
1843
+ ```
1844
+
1845
+ **Why:** JSON uses direct field access (no `@` prefix like XML attributes).
1846
+
1847
+ ### 3. Mapping Data Structure
1848
+
1849
+ ❌ **Wrong:**
1850
+
1851
+ ```typescript
1852
+ // Wrapping JSON record unnecessarily
1853
+ const mappingResult = await mapper.map({ inventory: rec });
1854
+ ```
1855
+
1856
+ ✅ **Correct:**
1857
+
1858
+ ```typescript
1859
+ // Map JSON record directly
1860
+ const mappingResult = await mapper.map(rec);
1861
+ ```
1862
+
1863
+ **Why:** Unlike XML, JSON records don't need wrapper objects for mapping.
1864
+
1865
+ ### 4. BPP Configuration
1866
+
1867
+ ❌ **Wrong:**
1868
+
1869
+ ```typescript
1870
+ // Using BPP for delta feeds (wastes processing time)
1871
+ const job = await client.createJob({
1872
+ name: 'Hourly Deltas',
1873
+ retailerId: 'my-retailer',
1874
+ // BPP enabled by default - unnecessary for delta feeds
1875
+ });
1876
+ ```
1877
+
1878
+ ✅ **Correct:**
1879
+
1880
+ ```typescript
1881
+ // Skip BPP for delta feeds (all records are changes)
1882
+ const job = await client.createJob({
1883
+ name: 'Hourly Deltas',
1884
+ retailerId: 'my-retailer',
1885
+ meta: {
1886
+ preprocessing: 'skip', // Skip BPP - all records are changes
1887
+ },
1888
+ });
1889
+ ```
1890
+
1891
+ **Why:** Delta feeds are pre-filtered by source system, BPP overhead is wasted.
1892
+
1893
+ ### 5. Large JSON Files
1894
+
1895
+ ❌ **Wrong:**
1896
+
1897
+ ```typescript
1898
+ // Loading entire 500MB JSON into memory
1899
+ const parsed = await parser.parse(largeJsonContent);
1900
+ ```
1901
+
1902
+ ✅ **Correct:**
1903
+
1904
+ ```typescript
1905
+ // Use JSON Lines format for streaming
1906
+ const format = 'jsonl';
1907
+ const parsed = await parser.parse(largeJsonContent, { format });
1908
+ ```
1909
+
1910
+ **Why:** JSON Lines processes one record at a time, preventing memory issues.
1911
+
1912
+ ---
1913
+
1914
+ ## Key Takeaways
1915
+
1916
+ - 🎯 **Use Batch API for inventory** - Not Event API (Event API is for products/orders/etc.)
1917
+ - 🎯 **Processing mode selection** - Per-file (default) for safety, batch for speed, chunked for scale
1918
+ - 🎯 **TRUE modular architecture** - Separate service files with clear responsibilities (see Modular Structure section)
1919
+ - 🎯 **BPP by default** - Enabled for full snapshots, skip for delta feeds
1920
+ - 🎯 **Format detection** - Auto-detects standard JSON vs JSON Lines
1921
+ - 🎯 **Direct field access** - Use simple paths like `"locationRef"` (no special notation)
1922
+ - 🎯 **Map records directly** - No need to wrap in object (unlike XML)
1923
+ - 🎯 **Chunk records** - Default 1000 per batch, tune based on record size
1924
+ - 🎯 **EntityType: INVENTORY** - Correct entity type for inventory records
1925
+ - 🎯 **State management** - VersoriFileTracker + JobTracker prevent duplicates
1926
+ - 🎯 **Enhanced retry logic** - S3 operations with exponential backoff
1927
+ - 🎯 **Always dispose** - Release S3 resources with `dispose()` in finally block
1928
+ - 🎯 **Error handling** - Move failed files to error folder, don't fail entire workflow
1929
+ - 🎯 **Native logging** - Use `log` from context on Versori platform
1930
+ - 🎯 **Streaming for large files** - Use JSON Lines format for files >100MB
1931
+ - 🎯 **Emoji logging** - Use 🚀 ✅ ❌ ⚠️ 🔍 📄 for visual log scanning
1932
+ - 🎯 **Execution boundaries** - Use ═══ separators for workflow start/end
1933
+ - 🎯 **validateConnection** - Enable connection validation on client creation
1934
+ - 🎯 **Optional toggles** - Support enableFileTracking and detailedLogging flags
1935
+ - 🎯 **Duration tracking** - Track execution time at file and workflow levels
1936
+ - 🎯 **Error recommendations** - Include actionable suggestions in error logs
1937
+
1938
+ ---
1939
+
1940
+ ## Related Documentation
1941
+
1942
+ - [Batch API vs Event API Decision Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
1943
+ - [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
1944
+ - [S3 Data Source](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md#s3-data-source)
1945
+ - [JSON Parser](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md#json-parser-service)
1946
+ - [State Management](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md)
1947
+ - [Job Tracker](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md)
1948
+ - [BPP Documentation](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md#batch-pre-processing-bpp-change-detection)
1949
+
1950
+ ---
1951
+
1952
+ [→ Back to Versori Scheduled Workflows](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) | [Versori Platform Guide →](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)