@fluentcommerce/fc-connect-sdk 0.1.53 → 0.1.55

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