@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.56

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