@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/clients/fluent-client.js +13 -6
  3. package/dist/cjs/utils/pagination-helpers.js +38 -2
  4. package/dist/cjs/versori/fluent-versori-client.js +11 -5
  5. package/dist/esm/clients/fluent-client.js +13 -6
  6. package/dist/esm/utils/pagination-helpers.js +38 -2
  7. package/dist/esm/versori/fluent-versori-client.js +11 -5
  8. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  9. package/dist/tsconfig.tsbuildinfo +1 -1
  10. package/dist/tsconfig.types.tsbuildinfo +1 -1
  11. package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
  12. package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
  13. package/docs/00-START-HERE/cli-documentation-index.md +202 -202
  14. package/docs/00-START-HERE/cli-quick-reference.md +252 -252
  15. package/docs/00-START-HERE/decision-tree.md +552 -552
  16. package/docs/00-START-HERE/getting-started.md +1070 -1070
  17. package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
  18. package/docs/00-START-HERE/readme.md +237 -237
  19. package/docs/00-START-HERE/retailerid-configuration.md +404 -404
  20. package/docs/00-START-HERE/sdk-philosophy.md +794 -794
  21. package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
  22. package/docs/01-TEMPLATES/faq.md +686 -686
  23. package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
  24. package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
  25. package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
  26. package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
  27. package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
  28. package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
  29. package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
  30. package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
  31. package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
  32. package/docs/01-TEMPLATES/readme.md +957 -957
  33. package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
  34. package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
  35. package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
  36. package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
  37. package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
  38. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
  39. package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
  40. package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
  41. package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
  42. package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
  43. package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
  44. package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
  45. package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
  46. package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
  47. package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
  48. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
  49. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
  50. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
  51. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
  52. package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
  53. package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
  54. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
  55. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
  56. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
  57. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
  58. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
  59. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
  60. package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
  61. package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
  62. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
  63. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
  64. package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
  65. package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
  66. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
  67. package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
  68. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
  69. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
  70. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
  71. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
  72. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
  73. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
  74. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
  75. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
  76. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
  77. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
  78. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
  79. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
  80. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
  81. package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
  82. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
  83. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
  84. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
  85. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
  86. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
  87. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
  88. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
  89. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
  90. package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
  91. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
  92. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
  93. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
  94. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
  95. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
  96. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
  97. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
  98. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
  99. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
  100. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
  101. package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
  102. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
  103. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
  104. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
  105. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
  106. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
  107. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
  108. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
  109. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
  110. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
  111. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
  112. package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
  113. package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
  114. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
  115. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
  116. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
  117. package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
  118. package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
  119. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
  120. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
  121. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
  122. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
  123. package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
  124. package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
  125. package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
  126. package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
  127. package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
  128. package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
  129. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
  130. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
  131. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
  132. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
  133. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
  134. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
  135. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
  136. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
  137. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
  138. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
  139. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
  140. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
  141. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
  142. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
  143. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
  144. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
  145. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
  146. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
  147. package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
  148. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
  149. package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
  150. package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
  151. package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
  152. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
  153. package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
  154. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
  155. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
  156. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
  157. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
  158. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
  159. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
  160. package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
  161. package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
  162. package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
  163. package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
  164. package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
  165. package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
  166. package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
  167. package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
  168. package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
  169. package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
  170. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
  171. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
  172. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
  173. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
  174. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
  175. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
  176. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
  177. package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
  178. package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
  179. package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
  180. package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
  181. package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
  182. package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
  183. package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
  184. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
  185. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
  186. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
  187. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
  188. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
  189. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
  190. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
  191. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
  192. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
  193. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
  194. package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
  195. package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
  196. package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
  197. package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
  198. package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
  199. package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
  200. package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
  201. package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
  202. package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
  203. package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
  204. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
  205. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
  206. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
  207. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
  208. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
  209. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
  210. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
  211. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
  212. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
  213. package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
  214. package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
  215. package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
  216. package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
  217. package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
  218. package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
  219. package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
  220. package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
  221. package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
  222. package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
  223. package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
  224. package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
  225. package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
  226. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
  227. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
  228. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
  229. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
  230. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
  231. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
  232. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
  233. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
  234. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
  235. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
  236. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
  237. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
  238. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
  239. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
  240. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
  241. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
  242. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
  243. package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
  244. package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
  245. package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
  246. package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
  247. package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
  248. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
  249. package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
  250. package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
  251. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
  252. package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
  253. package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
  254. package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
  255. package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
  256. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
  257. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
  258. package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
  259. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
  260. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
  261. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
  262. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
  263. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
  264. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
  265. package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
  266. package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
  267. package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
  268. package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
  269. package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
  270. package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
  271. package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
  272. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
  273. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
  274. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
  275. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
  276. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
  277. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
  278. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
  279. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
  280. package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
  281. package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
  282. package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
  283. package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
  284. package/docs/02-CORE-GUIDES/readme.md +194 -194
  285. package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
  286. package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
  287. package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
  288. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
  289. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
  290. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
  291. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
  292. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
  293. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
  294. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
  295. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
  296. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
  297. package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
  298. package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
  299. package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
  300. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
  301. package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
  302. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
  303. package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
  304. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
  305. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
  306. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
  307. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
  308. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
  309. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
  310. package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
  311. package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
  312. package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
  313. package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
  314. package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
  315. package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
  316. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
  317. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
  318. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
  319. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
  320. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
  321. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
  322. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
  323. package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
  324. package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
  325. package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
  326. package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
  327. package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
  328. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
  329. package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
  330. package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
  331. package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
  332. package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
  333. package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
  334. package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
  335. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
  336. package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
  337. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
  338. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
  339. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
  340. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
  341. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
  342. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
  343. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
  344. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
  345. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
  346. package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
  347. package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
  348. package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
  349. package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
  350. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
  351. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
  352. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
  353. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
  354. package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
  355. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
  356. package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
  357. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
  358. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
  359. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
  360. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
  361. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
  362. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
  363. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
  364. package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
  365. package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
  366. package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
  367. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
  368. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
  369. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
  370. package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
  371. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
  372. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
  373. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
  374. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
  375. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
  376. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
  377. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
  378. package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
  379. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
  380. package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
  381. package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
  382. package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
  383. package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
  384. package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
  385. package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
  386. package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
  387. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
  388. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
  389. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
  390. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
  391. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
  392. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
  393. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
  394. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
  395. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
  396. package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
  397. package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
  398. package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
  399. package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
  400. package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
  401. package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
  402. package/docs/03-PATTERN-GUIDES/readme.md +159 -159
  403. package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
  404. package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
  405. package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
  406. package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
  407. package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
  408. package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
  409. package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
  410. package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
  411. package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
  412. package/docs/04-REFERENCE/architecture/readme.md +279 -279
  413. package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
  414. package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
  415. package/docs/04-REFERENCE/platforms/readme.md +135 -135
  416. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
  417. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
  418. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
  419. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
  420. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
  421. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
  422. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
  423. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
  424. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
  425. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
  426. package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
  427. package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
  428. package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
  429. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
  430. package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
  431. package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
  432. package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
  433. package/docs/04-REFERENCE/readme.md +148 -148
  434. package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
  435. package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
  436. package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
  437. package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
  438. package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
  439. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
  440. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
  441. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
  442. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
  443. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
  444. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
  445. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
  446. package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
  447. package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
  448. package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
  449. package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
  450. package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
  451. package/docs/04-REFERENCE/schema/readme.md +141 -141
  452. package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
  453. package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
  454. package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
  455. package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
  456. package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
  457. package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
  458. package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
  459. package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
  460. package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
  461. package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
  462. package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
  463. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
  464. package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
  465. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
  466. package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
  467. package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
  468. package/docs/04-REFERENCE/testing/readme.md +86 -86
  469. package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
  470. package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
  471. package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
  472. package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
  473. package/docs/template-loading-matrix.md +242 -242
  474. package/package.json +5 -3
  475. package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
@@ -1,1893 +1,1893 @@
1
- # Module 9: Best Practices
2
-
3
- [← Back to Ingestion Guide](../ingestion-readme.md)
4
-
5
- **Module 9 of 9** | **Level**: All Levels | **Time**: 30 minutes
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- This module covers production-ready patterns for data ingestion workflows. Learn comprehensive error handling strategies, monitoring approaches, security best practices, scheduling patterns, data quality validation, and complete production deployment patterns.
12
-
13
- New in this version:
14
-
15
- - Preflight validation before runs (PreflightValidator)
16
- - Job lifecycle tracking (JobTracker)
17
- - Partial batch failure recovery (PartialBatchRecovery)
18
- - Versori file-level deduplication (VersoriFileTracker)
19
-
20
- ## Learning Objectives
21
-
22
- By the end of this module, you will:
23
-
24
- - ✅ Implement robust error handling with retry logic and circuit breakers
25
- - ✅ Set up comprehensive monitoring, logging, and alerting
26
- - ✅ Apply security best practices for credentials and data
27
- - ✅ Design effective scheduling and trigger strategies
28
- - ✅ Validate data quality before ingestion
29
- - ✅ Build production-ready ingestion pipelines
30
- - ✅ Troubleshoot common issues effectively
31
- - ✅ Follow deployment checklists for success
32
-
33
- ---
34
-
35
- ## Error Handling
36
-
37
- ### Retry Strategy with Exponential Backoff
38
-
39
- ```typescript
40
- async function retryWithBackoff<T>(
41
- operation: () => Promise<T>,
42
- maxRetries: number = 3
43
- ): Promise<T> {
44
- let lastError: Error;
45
- let delay = 1000; // Start with 1 second
46
-
47
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
48
- try {
49
- return await operation();
50
- } catch (error) {
51
- lastError = error;
52
-
53
- if (!isRetryable(error) || attempt === maxRetries) {
54
- throw error;
55
- }
56
-
57
- console.log(\`Retry \${attempt}/\${maxRetries} after \${delay}ms\`);
58
- await sleep(delay);
59
- delay *= 2; // Exponential backoff
60
- }
61
- }
62
-
63
- throw lastError;
64
- }
65
-
66
- function isRetryable(error: any): boolean {
67
- const retryableCodes = [
68
- 'RATE_LIMIT_ERROR',
69
- 'NETWORK_ERROR',
70
- 'TIMEOUT_ERROR',
71
- 'SERVICE_UNAVAILABLE'
72
- ];
73
-
74
- return (
75
- retryableCodes.includes(error.code) ||
76
- error.message.includes('timeout') ||
77
- error.message.includes('ECONNRESET')
78
- );
79
- }
80
- ```
81
-
82
- ### SDK-Specific Error Handling
83
-
84
- The SDK throws specific error types that require different handling strategies:
85
-
86
- ```typescript
87
- import {
88
- FileParsingError,
89
- MappingError,
90
- BatchAPIError,
91
- StateError,
92
- createConsoleLogger,
93
- toStructuredLogger,
94
- } from '@fluentcommerce/fc-connect-sdk';
95
-
96
- async function handleSDKErrors(fileKey: string) {
97
- try {
98
- await processFile(fileKey);
99
- } catch (error) {
100
- if (error instanceof FileParsingError) {
101
- // File format issues - log and skip
102
- console.error(`Invalid file format: ${fileKey}`, {
103
- line: error.line,
104
- column: error.column,
105
- message: error.message,
106
- });
107
-
108
- // Move to error bucket for manual review
109
- await moveToErrorBucket(fileKey, error);
110
- return { status: 'skipped', reason: 'parsing_error' };
111
- } else if (error instanceof MappingError) {
112
- // Field mapping failures - log details
113
- console.error(`Field mapping failed: ${fileKey}`, {
114
- invalidFields: error.invalidFields,
115
- recordIndex: error.recordIndex,
116
- errors: error.errors,
117
- });
118
-
119
- // Save error report
120
- await saveErrorReport(fileKey, error);
121
- return { status: 'failed', reason: 'mapping_error' };
122
- } else if (error instanceof BatchAPIError) {
123
- // Batch API rejections - check if retryable
124
- if (error.isRetryable) {
125
- console.warn(`Retryable batch error: ${error.code}`);
126
- throw error; // Will be caught by retry logic
127
- } else {
128
- console.error(`Permanent batch error: ${error.code}`, {
129
- jobId: error.jobId,
130
- batchId: error.batchId,
131
- details: error.details,
132
- });
133
-
134
- // Save to dead letter queue
135
- await saveToDLQ(fileKey, error);
136
- return { status: 'failed', reason: 'batch_api_error' };
137
- }
138
- } else if (error instanceof StateError) {
139
- // State management issues - critical
140
- console.error(`State management error: ${error.message}`);
141
-
142
- // Alert operations team
143
- await sendAlert('CRITICAL: State management failure', error);
144
- throw error; // Don't continue if state is broken
145
- } else {
146
- // Unknown error - log and alert
147
- console.error(`Unexpected error processing ${fileKey}:`, error);
148
- await sendAlert('Unknown ingestion error', error);
149
- throw error;
150
- }
151
- }
152
- }
153
- ```
154
-
155
- ### Circuit Breaker Pattern
156
-
157
- Prevent cascading failures when external services are down:
158
-
159
- ```typescript
160
- interface CircuitBreakerConfig {
161
- failureThreshold: number;
162
- resetTimeoutMs: number;
163
- monitoringWindowMs: number;
164
- }
165
-
166
- enum CircuitState {
167
- CLOSED = 'CLOSED', // Normal operation
168
- OPEN = 'OPEN', // Blocking requests
169
- HALF_OPEN = 'HALF_OPEN', // Testing recovery
170
- }
171
-
172
- class CircuitBreaker {
173
- private state: CircuitState = CircuitState.CLOSED;
174
- private failureCount: number = 0;
175
- private lastFailureTime: number = 0;
176
- private failures: number[] = [];
177
-
178
- constructor(private config: CircuitBreakerConfig) {}
179
-
180
- async execute<T>(operation: () => Promise<T>): Promise<T> {
181
- // Check if circuit should reset
182
- if (this.shouldAttemptReset()) {
183
- this.state = CircuitState.HALF_OPEN;
184
- }
185
-
186
- // Block if circuit is open
187
- if (this.state === CircuitState.OPEN) {
188
- throw new Error(
189
- `Circuit breaker OPEN. Last failure: ${new Date(this.lastFailureTime).toISOString()}`
190
- );
191
- }
192
-
193
- try {
194
- const result = await operation();
195
-
196
- // Success - reset if in half-open state
197
- if (this.state === CircuitState.HALF_OPEN) {
198
- console.log('Circuit breaker: Service recovered, closing circuit');
199
- this.reset();
200
- }
201
-
202
- return result;
203
- } catch (error) {
204
- this.recordFailure();
205
-
206
- // Open circuit if threshold exceeded
207
- if (this.failureCount >= this.config.failureThreshold) {
208
- this.state = CircuitState.OPEN;
209
- this.lastFailureTime = Date.now();
210
- console.error(`Circuit breaker OPENED after ${this.failureCount} failures`);
211
- }
212
-
213
- throw error;
214
- }
215
- }
216
-
217
- private recordFailure(): void {
218
- const now = Date.now();
219
- this.failures.push(now);
220
-
221
- // Remove old failures outside monitoring window
222
- this.failures = this.failures.filter(time => now - time < this.config.monitoringWindowMs);
223
-
224
- this.failureCount = this.failures.length;
225
- }
226
-
227
- private shouldAttemptReset(): boolean {
228
- if (this.state !== CircuitState.OPEN) {
229
- return false;
230
- }
231
-
232
- const timeSinceLastFailure = Date.now() - this.lastFailureTime;
233
- return timeSinceLastFailure >= this.config.resetTimeoutMs;
234
- }
235
-
236
- private reset(): void {
237
- this.state = CircuitState.CLOSED;
238
- this.failureCount = 0;
239
- this.failures = [];
240
- }
241
-
242
- getState(): CircuitState {
243
- return this.state;
244
- }
245
- }
246
-
247
- // Usage
248
- const fluentAPICircuit = new CircuitBreaker({
249
- failureThreshold: 5, // Open after 5 failures
250
- resetTimeoutMs: 60000, // Try again after 1 minute
251
- monitoringWindowMs: 120000, // Track failures over 2 minutes
252
- });
253
-
254
- async function processWithCircuitBreaker(fileKey: string) {
255
- try {
256
- await fluentAPICircuit.execute(async () => {
257
- return await processFile(fileKey);
258
- });
259
- } catch (error) {
260
- if (error.message.includes('Circuit breaker OPEN')) {
261
- console.log('Service temporarily unavailable, requeueing file');
262
- await requeueFile(fileKey);
263
- } else {
264
- throw error;
265
- }
266
- }
267
- }
268
- ```
269
-
270
- ### Dead Letter Queue Pattern
271
-
272
- Store failed records for later processing:
273
-
274
- ```typescript
275
- import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
276
-
277
- interface DLQRecord {
278
- originalFile: string;
279
- failedAt: string;
280
- errorType: string;
281
- errorMessage: string;
282
- recordData: any;
283
- retryCount: number;
284
- metadata: Record<string, any>;
285
- }
286
-
287
- class DeadLetterQueue {
288
- constructor(
289
- private s3: S3DataSource,
290
- private dlqBucket: string,
291
- private dlqPrefix: string = 'dlq/'
292
- ) {}
293
-
294
- /**
295
- * Save failed record to DLQ
296
- */
297
- async save(record: DLQRecord): Promise<void> {
298
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
299
- const key = `${this.dlqPrefix}${record.errorType}/${timestamp}-${record.originalFile}`;
300
-
301
- await this.s3.uploadFile(key, JSON.stringify(record, null, 2));
302
-
303
- console.log(`Saved to DLQ: ${key}`);
304
- }
305
-
306
- /**
307
- * Retry processing records from DLQ
308
- */
309
- async retryAll(
310
- processor: (record: any) => Promise<void>
311
- ): Promise<{ successful: number; failed: number }> {
312
- const files = await this.s3.listFiles({ prefix: this.dlqPrefix });
313
- let successful = 0;
314
- let failed = 0;
315
-
316
- for (const file of files) {
317
- try {
318
- const content = await this.s3.downloadFile(file.path);
319
- const dlqRecord: DLQRecord = JSON.parse(content);
320
-
321
- // Attempt reprocessing
322
- await processor(dlqRecord.recordData);
323
-
324
- // Success - delete from DLQ (Note: deleteObject method may not exist - use uploadFile with empty content or check SDK)
325
- // await this.s3.deleteObject(this.dlqBucket, file.path);
326
- successful++;
327
- } catch (error) {
328
- console.error(`DLQ retry failed for ${file.path}:`, error);
329
- failed++;
330
-
331
- // Update retry count
332
- await this.incrementRetryCount(file.path);
333
- }
334
- }
335
-
336
- return { successful, failed };
337
- }
338
-
339
- /**
340
- * Increment retry count in DLQ record
341
- */
342
- private async incrementRetryCount(key: string): Promise<void> {
343
- try {
344
- const content = await this.s3.downloadFile(key);
345
- const record: DLQRecord = JSON.parse(content);
346
-
347
- record.retryCount = (record.retryCount || 0) + 1;
348
- record.metadata.lastRetryAttempt = new Date().toISOString();
349
-
350
- await this.s3.uploadFile(key, JSON.stringify(record, null, 2));
351
- } catch (error) {
352
- console.error(`Failed to update retry count for ${key}:`, error);
353
- }
354
- }
355
- }
356
-
357
- // Usage
358
- const dlq = new DeadLetterQueue(s3DataSource, 'my-dlq-bucket');
359
-
360
- async function processFileWithDLQ(fileKey: string) {
361
- try {
362
- const records = await parseFile(fileKey);
363
-
364
- for (const [index, record] of records.entries()) {
365
- try {
366
- await processRecord(record);
367
- } catch (error) {
368
- // Save failed record to DLQ
369
- await dlq.save({
370
- originalFile: fileKey,
371
- failedAt: new Date().toISOString(),
372
- errorType: error.constructor.name,
373
- errorMessage: error.message,
374
- recordData: record,
375
- retryCount: 0,
376
- metadata: {
377
- recordIndex: index,
378
- totalRecords: records.length,
379
- },
380
- });
381
- }
382
- }
383
- } catch (error) {
384
- console.error(`Complete file failure: ${fileKey}`, error);
385
- throw error;
386
- }
387
- }
388
- ```
389
-
390
- ### Graceful Degradation
391
-
392
- ```typescript
393
- async function processFileWithFallback(fileKey: string) {
394
- try {
395
- // Primary processing path
396
- await processFileNormally(fileKey);
397
- } catch (error) {
398
- console.error(\`Primary processing failed: \${error.message}\`);
399
-
400
- try {
401
- // Fallback: Process with reduced batch size
402
- await processFileWithSmallerBatches(fileKey);
403
- } catch (fallbackError) {
404
- console.error(\`Fallback processing failed: \${fallbackError.message}\`);
405
-
406
- // Last resort: Save to dead letter queue
407
- await saveToDeadLetterQueue(fileKey, error);
408
- throw new Error(\`File processing failed completely: \${fileKey}\`);
409
- }
410
- }
411
- }
412
- ```
413
-
414
- ## Validation Best Practices
415
-
416
- ### Pre-Flight Validation
417
-
418
- ```typescript
419
- async function validateBeforeIngestion(records: any[]): Promise<ValidationResult> {
420
- const errors = [];
421
- const warnings = [];
422
-
423
- // Schema validation
424
- for (const [index, record] of records.entries()) {
425
- // Required fields
426
- if (!record.ref) {
427
- errors.push(\`Row \${index}: Missing required field 'ref'\`);
428
- }
429
-
430
- if (!record.productRef) {
431
- errors.push(\`Row \${index}: Missing required field 'productRef'\`);
432
- }
433
-
434
- if (!record.locationRef) {
435
- errors.push(\`Row \${index}: Missing required field 'locationRef'\`);
436
- }
437
-
438
- if (record.qty === undefined || record.qty === null) {
439
- errors.push(\`Row \${index}: Missing required field 'qty'\`);
440
- }
441
-
442
- // Type validation
443
- if (typeof record.qty !== 'number') {
444
- errors.push(\`Row \${index}: 'qty' must be a number\`);
445
- }
446
-
447
- // Business rules
448
- if (record.qty < 0) {
449
- errors.push(\`Row \${index}: Quantity cannot be negative\`);
450
- }
451
-
452
- if (record.qty > 1000000) {
453
- warnings.push(\`Row \${index}: Unusually large quantity\`);
454
- }
455
- }
456
-
457
- return {
458
- isValid: errors.length === 0,
459
- errors,
460
- warnings
461
- };
462
- }
463
-
464
- // Usage
465
- const validation = await validateBeforeIngestion(records);
466
-
467
- if (!validation.isValid) {
468
- console.error('Validation errors:', validation.errors);
469
- throw new Error('Data validation failed');
470
- }
471
-
472
- if (validation.warnings.length > 0) {
473
- console.warn('Validation warnings:', validation.warnings);
474
- }
475
- ```
476
-
477
- ## Scheduling and Triggers
478
-
479
- ###Cron Scheduling Patterns
480
-
481
- Different scheduling strategies for different ingestion scenarios:
482
-
483
- ```typescript
484
- // Daily midnight sync (00:00 UTC)
485
- const dailyMidnightCron = '0 0 * * *';
486
-
487
- // Every 6 hours
488
- const every6HoursCron = '0 */6 * * *';
489
-
490
- // Every hour during business hours (9 AM - 5 PM, Mon-Fri)
491
- const businessHoursCron = '0 9-17 * * 1-5';
492
-
493
- // Every 15 minutes
494
- const every15MinutesCron = '*/15 * * * *';
495
-
496
- // Weekly Sunday at 2 AM
497
- const weeklySundayCron = '0 2 * * 0';
498
- ```
499
-
500
- **Recommended schedules by use case:**
501
-
502
- | Use Case | Schedule | Cron Expression | Rationale |
503
- | ----------------------- | ------------- | ---------------- | ---------------------------- |
504
- | **WMS Daily Sync** | Once daily | `0 2 * * *` | Process overnight updates |
505
- | **3PL Cycle Counts** | Every 6 hours | `0 */6 * * *` | Keep inventory fresh |
506
- | **Real-time Updates** | Every 15 min | `*/15 * * * *` | Near real-time sync |
507
- | **Weekly Full Sync** | Sunday 2 AM | `0 2 * * 0` | Comprehensive reconciliation |
508
- | **Business Hours Only** | 9 AM - 5 PM | `0 9-17 * * 1-5` | During operational hours |
509
-
510
- ### Trigger Mechanisms
511
-
512
- Different ways to trigger ingestion workflows:
513
-
514
- #### 1. S3 Event Triggers
515
-
516
- ```typescript
517
- // Versori workflow triggered by S3 event
518
- export const s3TriggeredIngestion = webhook('s3-inventory-upload', {
519
- response: { mode: 'sync' },
520
- })
521
- .then(
522
- fn('parse-s3-event', async ({ data }) => {
523
- const event = JSON.parse(data);
524
- const bucket = event.Records[0].s3.bucket.name;
525
- const key = decodeURIComponent(event.Records[0].s3.object.key);
526
-
527
- return { bucket, key };
528
- })
529
- )
530
- .then(
531
- fn('process-file', async ({ data, connections, log, openKv }) => {
532
- const client = await createClient({ connections, log, openKv });
533
-
534
- // Initialize logger and state management
535
- const logger = toStructuredLogger(log, {
536
- service: 'ingestion',
537
- correlationId: generateCorrelationId()
538
- });
539
-
540
- const stateService = new StateService(logger);
541
- const kvAdapter = new VersoriKVAdapter(openKv());
542
-
543
- const s3 = new S3DataSource(
544
- {
545
- type: 'S3_CSV',
546
- connectionId: 's3-triggered',
547
- name: 'S3 Triggered Ingestion',
548
- s3Config: { region: 'us-east-1', bucket: data.bucket },
549
- },
550
- logger
551
- );
552
-
553
- // Check if already processed
554
- if (await stateService.isFileProcessed(kvAdapter, data.key)) {
555
- log.info(`File already processed: ${data.key}`);
556
- return { status: 'skipped', reason: 'already_processed' };
557
- }
558
-
559
- // Process file
560
- await processInventoryFile(client, s3, data.bucket, data.key);
561
-
562
- // Mark as processed
563
- await stateService.updateSyncState(kvAdapter, [{
564
- fileName: data.key,
565
- lastModified: new Date().toISOString(),
566
- }]);
567
-
568
- return { status: 'success', file: data.key };
569
- })
570
- )
571
- .catch(({ data, log }) => {
572
- log.error('S3 triggered ingestion failed:', data);
573
- return { status: 'error', error: data };
574
- });
575
- ```
576
-
577
- #### 2. SFTP Polling
578
-
579
- ```typescript
580
- import { SftpDataSource, StateService, VersoriKVAdapter, createConsoleLogger, toStructuredLogger } from '@fluentcommerce/fc-connect-sdk';
581
-
582
- async function pollSftpDirectory(kv: any) {
583
- const logger = toStructuredLogger(createConsoleLogger(), {
584
- service: 'ingestion',
585
- correlationId: generateCorrelationId()
586
- });
587
-
588
- const stateService = new StateService(logger);
589
- const kvAdapter = new VersoriKVAdapter(kv);
590
-
591
- const sftp = new SftpDataSource({
592
- type: 'SFTP_CSV',
593
- connectionId: 'sftp-polling',
594
- name: 'SFTP Polling',
595
- settings: {
596
- host: process.env.SFTP_HOST!,
597
- port: 22,
598
- username: process.env.SFTP_USER!,
599
- privateKey: fs.readFileSync('/path/to/private/key'),
600
- }
601
- }, logger);
602
-
603
- const remoteDir = '/inbound/inventory/';
604
- const files = await sftp.listFiles(remoteDir);
605
-
606
- for (const file of files) {
607
- if (await stateService.isFileProcessed(kvAdapter, file.name)) {
608
- continue; // Skip already processed
609
- }
610
-
611
- // Download and process
612
- const localPath = `/tmp/${file.name}`;
613
- await sftp.downloadFile(file.path, localPath); // Use file.path (full path) for download
614
-
615
- await processLocalFile(localPath);
616
-
617
- // Mark as processed
618
- await stateService.updateSyncState(kvAdapter, [{
619
- fileName: file.name,
620
- lastModified: new Date().toISOString(),
621
- }]);
622
-
623
- // Optionally move to processed folder
624
- await sftp.moveFile(`${remoteDir}${file.name}`, `/processed/${file.name}`);
625
- }
626
- }
627
- ```
628
-
629
- #### 3. Webhook Triggers
630
-
631
- ```typescript
632
- // Manual or external system triggered ingestion
633
- export const manualIngestion = webhook('manual-trigger', {
634
- response: { mode: 'sync' },
635
- })
636
- .then(
637
- fn('validate-request', ({ data }) => {
638
- const { bucket, prefix, batchSize = 2000 } = JSON.parse(data);
639
-
640
- if (!bucket || !prefix) {
641
- throw new Error('Missing required parameters: bucket, prefix');
642
- }
643
-
644
- return { bucket, prefix, batchSize };
645
- })
646
- )
647
- .then(
648
- fn('run-ingestion', async ({ data, connections, log }) => {
649
- const client = await createClient(ctx); // Auto-detects Versori context
650
- const s3 = new S3DataSource(
651
- {
652
- type: 'S3_CSV',
653
- s3Config: { region: 'us-east-1' },
654
- },
655
- log
656
- );
657
-
658
- const files = await s3.listFiles({ prefix: data.prefix });
659
-
660
- log.info(`Found ${files.length} files to process`);
661
-
662
- const results = await processFilesInParallel(client, s3, data.bucket, files, {
663
- batchSize: data.batchSize,
664
- });
665
-
666
- return {
667
- filesProcessed: results.successful,
668
- filesFailed: results.failed,
669
- totalRecords: results.totalRecords,
670
- };
671
- })
672
- );
673
- ```
674
-
675
- ### Backfill Strategies
676
-
677
- Handle missed files or catch-up processing:
678
-
679
- ```typescript
680
- async function backfillMissedFiles(startDate: Date, endDate: Date, kv: any): Promise<void> {
681
- const logger = toStructuredLogger(createConsoleLogger(), {
682
- service: 'backfill',
683
- correlationId: generateCorrelationId()
684
- });
685
-
686
- const stateService = new StateService(logger);
687
- const kvAdapter = new VersoriKVAdapter(kv);
688
-
689
- const s3 = new S3DataSource(
690
- {
691
- type: 'S3_CSV',
692
- connectionId: 's3-backfill',
693
- name: 'S3 Backfill',
694
- s3Config: s3Config,
695
- },
696
- logger
697
- );
698
-
699
- console.log(`Backfilling files from ${startDate} to ${endDate}`);
700
-
701
- // List all files in date range
702
- const allFiles = await s3.listFiles({ prefix: 'data/' });
703
-
704
- const filesInRange = allFiles.filter(file => {
705
- const fileDate = extractDateFromKey(file.path);
706
- return fileDate >= startDate && fileDate <= endDate;
707
- });
708
-
709
- console.log(`Found ${filesInRange.length} files in date range`);
710
-
711
- // Check processing status
712
- const unprocessedFiles = [];
713
- for (const file of filesInRange) {
714
- if (!(await stateService.isFileProcessed(kvAdapter, file.path))) {
715
- unprocessedFiles.push(file);
716
- }
717
- }
718
-
719
- console.log(`${unprocessedFiles.length} files need processing`);
720
-
721
- // Process with lower concurrency to avoid overwhelming system
722
- await processFilesInParallel(unprocessedFiles, 2); // Only 2 concurrent
723
- }
724
-
725
- function extractDateFromKey(key: string): Date {
726
- // Extract date from key like "data/inventory-2025-01-15.csv"
727
- const match = key.match(/(\d{4}-\d{2}-\d{2})/);
728
- return match ? new Date(match[1]) : new Date(0);
729
- }
730
- ```
731
-
732
- ## Data Quality and Validation
733
-
734
- ### Pre-Ingestion Data Quality Checks
735
-
736
- ```typescript
737
- interface DataQualityRule {
738
- name: string;
739
- validate: (record: any) => { isValid: boolean; error?: string };
740
- severity: 'error' | 'warning';
741
- }
742
-
743
- class DataQualityValidator {
744
- private rules: DataQualityRule[] = [];
745
-
746
- addRule(rule: DataQualityRule): void {
747
- this.rules.push(rule);
748
- }
749
-
750
- validate(records: any[]): {
751
- passed: any[];
752
- failed: Array<{ record: any; errors: string[] }>;
753
- warnings: Array<{ record: any; warnings: string[] }>;
754
- } {
755
- const passed: any[] = [];
756
- const failed: Array<{ record: any; errors: string[] }> = [];
757
- const warnings: Array<{ record: any; warnings: string[] }> = [];
758
-
759
- for (const record of records) {
760
- const errors: string[] = [];
761
- const warns: string[] = [];
762
-
763
- for (const rule of this.rules) {
764
- const result = rule.validate(record);
765
-
766
- if (!result.isValid) {
767
- if (rule.severity === 'error') {
768
- errors.push(`${rule.name}: ${result.error}`);
769
- } else {
770
- warns.push(`${rule.name}: ${result.error}`);
771
- }
772
- }
773
- }
774
-
775
- if (errors.length > 0) {
776
- failed.push({ record, errors });
777
- } else {
778
- passed.push(record);
779
-
780
- if (warns.length > 0) {
781
- warnings.push({ record, warnings: warns });
782
- }
783
- }
784
- }
785
-
786
- return { passed, failed, warnings };
787
- }
788
- }
789
-
790
- // Define quality rules
791
- const qualityValidator = new DataQualityValidator();
792
-
793
- qualityValidator.addRule({
794
- name: 'Required Fields',
795
- severity: 'error',
796
- validate: record => {
797
- const required = ['sku', 'warehouse', 'quantity'];
798
- const missing = required.filter(field => !record[field]);
799
-
800
- if (missing.length > 0) {
801
- return {
802
- isValid: false,
803
- error: `Missing required fields: ${missing.join(', ')}`,
804
- };
805
- }
806
-
807
- return { isValid: true };
808
- },
809
- });
810
-
811
- qualityValidator.addRule({
812
- name: 'Quantity Range',
813
- severity: 'error',
814
- validate: record => {
815
- const qty = parseInt(record.quantity, 10);
816
-
817
- if (isNaN(qty) || qty < 0) {
818
- return {
819
- isValid: false,
820
- error: `Invalid quantity: ${record.quantity}`,
821
- };
822
- }
823
-
824
- return { isValid: true };
825
- },
826
- });
827
-
828
- qualityValidator.addRule({
829
- name: 'SKU Format',
830
- severity: 'error',
831
- validate: record => {
832
- const skuPattern = /^SKU-[A-Z0-9]{2,10}$/;
833
-
834
- if (!skuPattern.test(record.sku)) {
835
- return {
836
- isValid: false,
837
- error: `Invalid SKU format: ${record.sku}`,
838
- };
839
- }
840
-
841
- return { isValid: true };
842
- },
843
- });
844
-
845
- qualityValidator.addRule({
846
- name: 'Suspiciously Large Quantity',
847
- severity: 'warning',
848
- validate: record => {
849
- const qty = parseInt(record.quantity, 10);
850
-
851
- if (qty > 100000) {
852
- return {
853
- isValid: false,
854
- error: `Unusually large quantity: ${qty}`,
855
- };
856
- }
857
-
858
- return { isValid: true };
859
- },
860
- });
861
-
862
- // Usage
863
- const records = await parseCSV(fileContent);
864
- const validation = qualityValidator.validate(records);
865
-
866
- console.log(`Passed: ${validation.passed.length}`);
867
- console.log(`Failed: ${validation.failed.length}`);
868
- console.log(`Warnings: ${validation.warnings.length}`);
869
-
870
- // Log failures
871
- if (validation.failed.length > 0) {
872
- console.error('Data quality failures:');
873
- validation.failed.forEach(({ record, errors }) => {
874
- console.error(` SKU ${record.sku}:`, errors);
875
- });
876
-
877
- // Save failed records for review
878
- await saveDataQualityReport(validation.failed);
879
- }
880
-
881
- // Proceed with passed records only
882
- await processRecords(validation.passed);
883
- ```
884
-
885
- ### Reconciliation Reports
886
-
887
- ```typescript
888
- interface ReconciliationReport {
889
- fileKey: string;
890
- processedAt: string;
891
- totalRecords: number;
892
- successfulRecords: number;
893
- failedRecords: number;
894
- duplicateRecords: number;
895
- dataQualityIssues: number;
896
- batchesCreated: number;
897
- processingTimeMs: number;
898
- }
899
-
900
- async function generateReconciliationReport(
901
- fileKey: string,
902
- results: ProcessingResult
903
- ): Promise<ReconciliationReport> {
904
- return {
905
- fileKey,
906
- processedAt: new Date().toISOString(),
907
- totalRecords: results.totalRecords,
908
- successfulRecords: results.successful,
909
- failedRecords: results.failed,
910
- duplicateRecords: results.duplicates || 0,
911
- dataQualityIssues: results.qualityIssues || 0,
912
- batchesCreated: results.batches,
913
- processingTimeMs: results.processingTime,
914
- };
915
- }
916
-
917
- // Save report to S3
918
- async function saveReconciliationReport(report: ReconciliationReport): Promise<void> {
919
- const s3 = new S3DataSource(
920
- {
921
- type: 'S3_JSON',
922
- connectionId: 's3-reports',
923
- name: 'S3 Reports',
924
- s3Config: s3Config,
925
- },
926
- logger
927
- );
928
- const reportKey = `reports/reconciliation/${report.fileKey}-${Date.now()}.json`;
929
-
930
- await s3.uploadFile(reportKey, JSON.stringify(report, null, 2));
931
-
932
- console.log(`Reconciliation report saved: ${reportKey}`);
933
- }
934
- ```
935
-
936
- ## Security Best Practices
937
-
938
- ### Credential Management
939
-
940
- ```typescript
941
- // ✅ CORRECT - Use environment variables
942
- const config = {
943
- fluent: {
944
- baseUrl: process.env.FLUENT_BASE_URL!,
945
- clientId: process.env.FLUENT_CLIENT_ID!,
946
- clientSecret: process.env.FLUENT_CLIENT_SECRET!,
947
- retailerId: process.env.FLUENT_RETAILER_ID!,
948
- },
949
- s3: {
950
- region: process.env.AWS_REGION!,
951
- credentials: {
952
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
953
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
954
- },
955
- },
956
- };
957
-
958
- // ❌ WRONG - Never hardcode credentials
959
- const badConfig = {
960
- fluent: {
961
- clientId: 'hardcoded-client-id', // ❌ Don't do this
962
- clientSecret: 'hardcoded-secret', // ❌ Security risk
963
- },
964
- };
965
- ```
966
-
967
- ### Secrets Manager Integration
968
-
969
- ```typescript
970
- import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
971
-
972
- async function loadSecretsFromAWS(): Promise<any> {
973
- const client = new SecretsManagerClient({ region: 'us-east-1' });
974
-
975
- const command = new GetSecretValueCommand({
976
- SecretId: 'fluent-commerce/production',
977
- });
978
-
979
- const response = await client.send(command);
980
- return JSON.parse(response.SecretString!);
981
- }
982
-
983
- // Usage
984
- const secrets = await loadSecretsFromAWS();
985
-
986
- const client = await createClient({
987
- config: {
988
- baseUrl: secrets.FLUENT_BASE_URL,
989
- clientId: secrets.FLUENT_CLIENT_ID,
990
- clientSecret: secrets.FLUENT_CLIENT_SECRET,
991
- retailerId: secrets.FLUENT_RETAILER_ID,
992
- }
993
- });
994
- ```
995
-
996
- ### Data Encryption
997
-
998
- ```typescript
999
- // Encrypt sensitive data before storing
1000
- import * as crypto from 'crypto';
1001
-
1002
- function encryptSensitiveData(data: string, key: string): string {
1003
- const iv = crypto.randomBytes(16);
1004
- const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
1005
-
1006
- let encrypted = cipher.update(data, 'utf8', 'hex');
1007
- encrypted += cipher.final('hex');
1008
-
1009
- return iv.toString('hex') + ':' + encrypted;
1010
- }
1011
-
1012
- function decryptSensitiveData(encrypted: string, key: string): string {
1013
- const parts = encrypted.split(':');
1014
- const iv = Buffer.from(parts[0], 'hex');
1015
- const encryptedData = parts[1];
1016
-
1017
- const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
1018
-
1019
- let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
1020
- decrypted += decipher.final('utf8');
1021
-
1022
- return decrypted;
1023
- }
1024
- ```
1025
-
1026
- ### Audit Logging
1027
-
1028
- ```typescript
1029
- interface AuditLog {
1030
- timestamp: string;
1031
- operation: string;
1032
- user: string;
1033
- resource: string;
1034
- status: 'success' | 'failure';
1035
- details: Record<string, any>;
1036
- }
1037
-
1038
- async function logAuditEvent(event: Omit<AuditLog, 'timestamp'>): Promise<void> {
1039
- const auditLog: AuditLog = {
1040
- timestamp: new Date().toISOString(),
1041
- ...event,
1042
- };
1043
-
1044
- // Log to CloudWatch, Datadog, or custom logging service
1045
- console.log('[AUDIT]', JSON.stringify(auditLog));
1046
-
1047
- // Optionally save to S3 for long-term retention
1048
- await saveAuditLog(auditLog);
1049
- }
1050
-
1051
- // Usage
1052
- await logAuditEvent({
1053
- operation: 'INVENTORY_INGESTION',
1054
- user: process.env.USER || 'system',
1055
- resource: `s3://bucket/file.csv`,
1056
- status: 'success',
1057
- details: {
1058
- recordsProcessed: 1000,
1059
- jobId: 'job-123',
1060
- duration: 5000,
1061
- },
1062
- });
1063
- ```
1064
-
1065
- ## Monitoring and Alerting
1066
-
1067
- ### Metrics Tracking
1068
-
1069
- ```typescript
1070
- interface IngestionMetrics {
1071
- timestamp: string;
1072
- filesProcessed: number;
1073
- recordsIngested: number;
1074
- failedRecords: number;
1075
- processingTimeMs: number;
1076
- avgRecordsPerSecond: number;
1077
- errorRate: number;
1078
- }
1079
-
1080
- class MetricsCollector {
1081
- private metrics: IngestionMetrics[] = [];
1082
-
1083
- recordIngestion(result: IngestionResult): void {
1084
- const metrics: IngestionMetrics = {
1085
- timestamp: new Date().toISOString(),
1086
- filesProcessed: result.filesProcessed,
1087
- recordsIngested: result.recordsIngested,
1088
- failedRecords: result.failedRecords,
1089
- processingTimeMs: result.processingTimeMs,
1090
- avgRecordsPerSecond: result.recordsIngested / (result.processingTimeMs / 1000),
1091
- errorRate: result.failedRecords / result.recordsIngested
1092
- };
1093
-
1094
- this.metrics.push(metrics);
1095
-
1096
- // Alert on high error rate
1097
- if (metrics.errorRate > 0.05) { // > 5% error rate
1098
- this.sendAlert('High error rate detected', metrics);
1099
- }
1100
-
1101
- // Alert on slow processing
1102
- if (metrics.avgRecordsPerSecond < 10) {
1103
- this.sendAlert('Slow processing speed', metrics);
1104
- }
1105
- }
1106
-
1107
- private sendAlert(message: string, metrics: IngestionMetrics): void {
1108
- console.error(\`[ALERT] \${message}\`, metrics);
1109
- // Send to monitoring service (e.g., Datadog, New Relic)
1110
- }
1111
- }
1112
- ```
1113
-
1114
- ### Health Checks
1115
-
1116
- ```typescript
1117
- async function performHealthCheck(): Promise<HealthCheckResult> {
1118
- const checks = [];
1119
-
1120
- // Check Fluent API connectivity
1121
- try {
1122
- await client.graphql({
1123
- query: '{ __typename }'
1124
- });
1125
- checks.push({ name: 'Fluent API', status: 'healthy' });
1126
- } catch (error) {
1127
- checks.push({ name: 'Fluent API', status: 'unhealthy', error: error.message });
1128
- }
1129
-
1130
- // Check S3 connectivity
1131
- try {
1132
- await s3.listFiles({ prefix: 'health-check/', maxKeys: 1 });
1133
- checks.push({ name: 'S3', status: 'healthy' });
1134
- } catch (error) {
1135
- checks.push({ name: 'S3', status: 'unhealthy', error: error.message });
1136
- }
1137
-
1138
- // Check state storage
1139
- try {
1140
- await state.set('health-check', Date.now());
1141
- await state.get('health-check');
1142
- checks.push({ name: 'State Storage', status: 'healthy' });
1143
- } catch (error) {
1144
- checks.push({ name: 'State Storage', status: 'unhealthy', error: error.message });
1145
- }
1146
-
1147
- const allHealthy = checks.every(c => c.status === 'healthy');
1148
-
1149
- return {
1150
- status: allHealthy ? 'healthy' : 'degraded',
1151
- checks,
1152
- timestamp: new Date().toISOString(),
1153
- };
1154
- }
1155
- ```
1156
-
1157
- ## Complete Production Pipeline Example
1158
-
1159
- Putting all best practices together into a production-ready ingestion pipeline:
1160
-
1161
- ```typescript
1162
- import {
1163
- createClient,
1164
- S3DataSource,
1165
- CSVParserService,
1166
- UniversalMapper,
1167
- StateService,
1168
- VersoriKVAdapter,
1169
- FluentClient,
1170
- FileParsingError,
1171
- MappingError,
1172
- BatchAPIError,
1173
- } from '@fluentcommerce/fc-connect-sdk';
1174
- import * as winston from 'winston';
1175
-
1176
- /**
1177
- * Production-grade ingestion pipeline with all best practices
1178
- * - Error handling with retry and circuit breaker
1179
- * - Comprehensive monitoring and logging
1180
- * - Security best practices
1181
- * - Data quality validation
1182
- * - State management
1183
- */
1184
- class ProductionIngestionPipeline {
1185
- private client: FluentClient;
1186
- private s3: S3DataSource;
1187
- private parser: CSVParserService;
1188
- private mapper: UniversalMapper;
1189
- private state: StateService;
1190
- private kv: KVStore;
1191
- private logger: winston.Logger;
1192
- private circuitBreaker: CircuitBreaker;
1193
- private dlq: DeadLetterQueue;
1194
- private metrics: MetricsCollector;
1195
- private qualityValidator: DataQualityValidator;
1196
-
1197
- constructor(private config: ProductionConfig) {}
1198
-
1199
- async initialize(): Promise<void> {
1200
- // Setup structured logging
1201
- this.logger = winston.createLogger({
1202
- level: 'info',
1203
- format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
1204
- transports: [
1205
- new winston.transports.Console(),
1206
- new winston.transports.File({ filename: 'ingestion-error.log', level: 'error' }),
1207
- new winston.transports.File({ filename: 'ingestion-combined.log' }),
1208
- ],
1209
- });
1210
-
1211
- this.logger.info('Initializing production ingestion pipeline');
1212
-
1213
- // Initialize Fluent client
1214
- this.client = await createClient({ config: this.config.fluent });
1215
-
1216
- // Initialize data sources
1217
- this.s3 = new S3DataSource(
1218
- {
1219
- type: 'S3_CSV',
1220
- connectionId: 's3-production',
1221
- name: 'S3 Production',
1222
- s3Config: this.config.s3,
1223
- },
1224
- this.logger
1225
- );
1226
- this.parser = new CSVParserService();
1227
-
1228
- // Initialize field mapper with custom resolvers
1229
- this.mapper = new UniversalMapper(this.config.mapping, {
1230
- customResolvers: this.config.customResolvers || {},
1231
- });
1232
-
1233
- // Initialize state management
1234
- const logger = toStructuredLogger(this.logger, {
1235
- service: 'production-pipeline',
1236
- correlationId: generateCorrelationId()
1237
- });
1238
-
1239
- this.kv = new VersoriKVAdapter(this.config.kv);
1240
- this.stateService = new StateService(logger);
1241
-
1242
- // Initialize circuit breaker
1243
- this.circuitBreaker = new CircuitBreaker({
1244
- failureThreshold: 5,
1245
- resetTimeoutMs: 60000,
1246
- monitoringWindowMs: 120000,
1247
- });
1248
-
1249
- // Initialize dead letter queue
1250
- this.dlq = new DeadLetterQueue(this.s3, this.config.dlqBucket, 'dlq/');
1251
-
1252
- // Initialize metrics collector
1253
- this.metrics = new MetricsCollector();
1254
-
1255
- // Initialize data quality validator
1256
- this.qualityValidator = this.createQualityValidator();
1257
-
1258
- this.logger.info('Pipeline initialization complete');
1259
- }
1260
-
1261
- /**
1262
- * Run the complete ingestion pipeline
1263
- */
1264
- async run(bucket: string, prefix: string): Promise<ExecutionReport> {
1265
- const startTime = Date.now();
1266
-
1267
- try {
1268
- this.logger.info('Starting ingestion pipeline', { bucket, prefix });
1269
-
1270
- // Health check before starting
1271
- const healthCheck = await this.performHealthCheck();
1272
- if (healthCheck.status !== 'healthy') {
1273
- throw new Error(`System unhealthy: ${JSON.stringify(healthCheck.checks)}`);
1274
- }
1275
-
1276
- // List files
1277
- const allFiles = await this.s3.listFiles({ prefix });
1278
- this.logger.info(`Found ${allFiles.length} total files`);
1279
-
1280
- // Filter unprocessed files
1281
- const unprocessedFiles = await this.filterUnprocessedFiles(allFiles);
1282
- this.logger.info(`${unprocessedFiles.length} files to process`);
1283
-
1284
- if (unprocessedFiles.length === 0) {
1285
- return this.createReport(startTime, { filesProcessed: 0 });
1286
- }
1287
-
1288
- // Process files with circuit breaker
1289
- const results = await this.circuitBreaker.execute(async () => {
1290
- return await this.processFilesWithRetry(bucket, unprocessedFiles);
1291
- });
1292
-
1293
- // Generate report
1294
- const report = this.createReport(startTime, results);
1295
-
1296
- // Log metrics
1297
- this.metrics.recordIngestion({
1298
- filesProcessed: results.filesProcessed,
1299
- recordsIngested: results.recordsIngested,
1300
- failedRecords: results.failedRecords,
1301
- processingTimeMs: report.totalTimeMs,
1302
- avgRecordsPerSecond: report.throughput,
1303
- errorRate: results.failedRecords / (results.recordsIngested || 1),
1304
- });
1305
-
1306
- this.logger.info('Pipeline execution complete', report);
1307
-
1308
- return report;
1309
- } catch (error) {
1310
- this.logger.error('Pipeline execution failed', {
1311
- error: error.message,
1312
- stack: error.stack,
1313
- });
1314
-
1315
- await this.sendAlert('CRITICAL: Ingestion pipeline failure', error);
1316
- throw error;
1317
- }
1318
- }
1319
-
1320
- /**
1321
- * Process files with retry logic
1322
- */
1323
- private async processFilesWithRetry(
1324
- bucket: string,
1325
- files: Array<{ key: string }>
1326
- ): Promise<ProcessingResults> {
1327
- const results: ProcessingResults = {
1328
- filesProcessed: 0,
1329
- filesFailed: 0,
1330
- recordsIngested: 0,
1331
- failedRecords: 0,
1332
- details: [],
1333
- };
1334
-
1335
- for (const file of files) {
1336
- const maxRetries = 3;
1337
- let attempt = 0;
1338
- let success = false;
1339
-
1340
- while (attempt < maxRetries && !success) {
1341
- attempt++;
1342
-
1343
- try {
1344
- const fileResult = await this.processFile(bucket, file.path);
1345
-
1346
- results.filesProcessed++;
1347
- results.recordsIngested += fileResult.recordsProcessed;
1348
- results.details.push({
1349
- file: file.path,
1350
- status: 'success',
1351
- records: fileResult.recordsProcessed,
1352
- });
1353
-
1354
- success = true;
1355
-
1356
- this.logger.info(`File processed successfully`, {
1357
- file: file.path,
1358
- records: fileResult.recordsProcessed,
1359
- attempt,
1360
- });
1361
- } catch (error) {
1362
- this.logger.warn(`File processing attempt ${attempt} failed`, {
1363
- file: file.path,
1364
- error: error.message,
1365
- });
1366
-
1367
- if (attempt < maxRetries && this.isRetryable(error)) {
1368
- const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
1369
- await new Promise(resolve => setTimeout(resolve, delay));
1370
- } else {
1371
- results.filesFailed++;
1372
- results.details.push({
1373
- file: file.path,
1374
- status: 'failed',
1375
- error: error.message,
1376
- });
1377
-
1378
- this.logger.error(`File processing failed permanently`, {
1379
- file: file.path,
1380
- attempts: attempt,
1381
- error: error.message,
1382
- });
1383
- }
1384
- }
1385
- }
1386
- }
1387
-
1388
- return results;
1389
- }
1390
-
1391
- /**
1392
- * Process a single file with all validations
1393
- */
1394
- private async processFile(bucket: string, fileKey: string): Promise<FileProcessingResult> {
1395
- this.logger.info(`Processing file: ${fileKey}`);
1396
-
1397
- try {
1398
- // Read file
1399
- const fileContent = await this.s3.downloadFile(fileKey);
1400
-
1401
- // Parse CSV
1402
- const records = await this.parser.parse(fileContent);
1403
- this.logger.debug(`Parsed ${records.length} records from ${fileKey}`);
1404
-
1405
- // Data quality validation
1406
- const validation = this.qualityValidator.validate(records);
1407
-
1408
- if (validation.failed.length > 0) {
1409
- this.logger.warn(`Data quality issues found`, {
1410
- file: fileKey,
1411
- failed: validation.failed.length,
1412
- warnings: validation.warnings.length,
1413
- });
1414
-
1415
- // Save failed records to DLQ
1416
- for (const { record, errors } of validation.failed) {
1417
- await this.dlq.save({
1418
- originalFile: fileKey,
1419
- failedAt: new Date().toISOString(),
1420
- errorType: 'DATA_QUALITY_ERROR',
1421
- errorMessage: errors.join('; '),
1422
- recordData: record,
1423
- retryCount: 0,
1424
- metadata: {},
1425
- });
1426
- }
1427
- }
1428
-
1429
- // Proceed with passed records only
1430
- if (validation.passed.length === 0) {
1431
- throw new Error('No valid records after quality validation');
1432
- }
1433
-
1434
- // Field mapping
1435
- const mappingResult = await this.mapper.map(validation.passed);
1436
-
1437
- if (!mappingResult.success) {
1438
- throw new MappingError(`Field mapping failed: ${mappingResult.errors.join(', ')}`);
1439
- }
1440
-
1441
- // Create job
1442
- const job = await this.client.createJob({
1443
- name: `Production Import - ${fileKey}`,
1444
- retailerId: this.config.fluent.retailerId,
1445
- metadata: {
1446
- fileName: fileKey,
1447
- recordCount: mappingResult.data.length,
1448
- pipeline: 'production',
1449
- },
1450
- });
1451
-
1452
- // Send to Batch API
1453
- await this.client.sendBatch(job.id, {
1454
- action: 'UPSERT',
1455
- entityType: 'INVENTORY',
1456
- entities: mappingResult.data,
1457
- });
1458
-
1459
- // Mark as processed
1460
- await this.state.markFileProcessed(fileKey, {
1461
- jobId: job.id,
1462
- recordCount: mappingResult.data.length,
1463
- timestamp: new Date().toISOString(),
1464
- });
1465
-
1466
- // Audit log
1467
- await this.logAuditEvent({
1468
- operation: 'INVENTORY_INGESTION',
1469
- user: 'system',
1470
- resource: `s3://${bucket}/${fileKey}`,
1471
- status: 'success',
1472
- details: {
1473
- recordsProcessed: mappingResult.data.length,
1474
- jobId: job.id,
1475
- },
1476
- });
1477
-
1478
- return {
1479
- recordsProcessed: mappingResult.data.length,
1480
- jobId: job.id,
1481
- };
1482
- } catch (error) {
1483
- // Audit log failure
1484
- await this.logAuditEvent({
1485
- operation: 'INVENTORY_INGESTION',
1486
- user: 'system',
1487
- resource: `s3://${bucket}/${fileKey}`,
1488
- status: 'failure',
1489
- details: {
1490
- error: error.message,
1491
- },
1492
- });
1493
-
1494
- throw error;
1495
- }
1496
- }
1497
-
1498
- /**
1499
- * Check if error is retryable
1500
- */
1501
- private isRetryable(error: any): boolean {
1502
- if (error instanceof FileParsingError) {
1503
- return false; // File format errors are not retryable
1504
- }
1505
-
1506
- if (error instanceof BatchAPIError) {
1507
- return error.isRetryable;
1508
- }
1509
-
1510
- // Network errors are retryable
1511
- return (
1512
- error.code === 'NETWORK_ERROR' ||
1513
- error.message.includes('timeout') ||
1514
- error.message.includes('ECONNRESET')
1515
- );
1516
- }
1517
-
1518
- /**
1519
- * Filter unprocessed files
1520
- */
1521
- private async filterUnprocessedFiles(
1522
- files: FileMetadata[]
1523
- ): Promise<FileMetadata[]> {
1524
- const unprocessed = [];
1525
-
1526
- for (const file of files) {
1527
- if (!(await this.state.isFileProcessed(this.kv, file.path))) {
1528
- unprocessed.push(file);
1529
- }
1530
- }
1531
-
1532
- return unprocessed;
1533
- }
1534
-
1535
- /**
1536
- * Create data quality validator
1537
- */
1538
- private createQualityValidator(): DataQualityValidator {
1539
- const validator = new DataQualityValidator();
1540
-
1541
- // Add validation rules based on configuration
1542
- validator.addRule({
1543
- name: 'Required Fields',
1544
- severity: 'error',
1545
- validate: record => {
1546
- const required = this.config.requiredFields || [];
1547
- const missing = required.filter(field => !record[field]);
1548
-
1549
- if (missing.length > 0) {
1550
- return {
1551
- isValid: false,
1552
- error: `Missing required fields: ${missing.join(', ')}`,
1553
- };
1554
- }
1555
-
1556
- return { isValid: true };
1557
- },
1558
- });
1559
-
1560
- return validator;
1561
- }
1562
-
1563
- /**
1564
- * Perform system health check
1565
- */
1566
- private async performHealthCheck(): Promise<HealthCheckResult> {
1567
- const checks = [];
1568
-
1569
- // Check Fluent API
1570
- try {
1571
- await this.client.graphql({
1572
- query: '{ __typename }'
1573
- });
1574
- checks.push({ name: 'Fluent API', status: 'healthy' });
1575
- } catch (error) {
1576
- checks.push({ name: 'Fluent API', status: 'unhealthy', error: error.message });
1577
- }
1578
-
1579
- // Check S3
1580
- try {
1581
- await this.s3.listObjects(this.config.s3Bucket, 'health-check/');
1582
- checks.push({ name: 'S3', status: 'healthy' });
1583
- } catch (error) {
1584
- checks.push({ name: 'S3', status: 'unhealthy', error: error.message });
1585
- }
1586
-
1587
- const allHealthy = checks.every(c => c.status === 'healthy');
1588
-
1589
- return {
1590
- status: allHealthy ? 'healthy' : 'degraded',
1591
- checks,
1592
- timestamp: new Date().toISOString(),
1593
- };
1594
- }
1595
-
1596
- /**
1597
- * Create execution report
1598
- */
1599
- private createReport(startTime: number, results: Partial<ProcessingResults>): ExecutionReport {
1600
- const totalTimeMs = Date.now() - startTime;
1601
-
1602
- return {
1603
- startTime: new Date(startTime).toISOString(),
1604
- endTime: new Date().toISOString(),
1605
- totalTimeMs,
1606
- filesProcessed: results.filesProcessed || 0,
1607
- filesFailed: results.filesFailed || 0,
1608
- recordsIngested: results.recordsIngested || 0,
1609
- failedRecords: results.failedRecords || 0,
1610
- throughput: (results.recordsIngested || 0) / (totalTimeMs / 1000),
1611
- successRate:
1612
- ((results.filesProcessed || 0) /
1613
- ((results.filesProcessed || 0) + (results.filesFailed || 0))) *
1614
- 100,
1615
- };
1616
- }
1617
-
1618
- /**
1619
- * Log audit event
1620
- */
1621
- private async logAuditEvent(event: Omit<AuditLog, 'timestamp'>): Promise<void> {
1622
- const auditLog: AuditLog = {
1623
- timestamp: new Date().toISOString(),
1624
- ...event,
1625
- };
1626
-
1627
- this.logger.info('[AUDIT]', auditLog);
1628
-
1629
- // Optionally save to S3 for compliance
1630
- // await this.s3.putObject(auditBucket, auditKey, JSON.stringify(auditLog));
1631
- }
1632
-
1633
- /**
1634
- * Send alert to monitoring service
1635
- */
1636
- private async sendAlert(message: string, error: any): Promise<void> {
1637
- this.logger.error('[ALERT]', { message, error: error.message });
1638
-
1639
- // Integration with alerting service (PagerDuty, Slack, etc.)
1640
- // await alertService.send({ message, error });
1641
- }
1642
- }
1643
-
1644
- // Usage
1645
- const pipeline = new ProductionIngestionPipeline({
1646
- fluent: {
1647
- baseUrl: process.env.FLUENT_BASE_URL!,
1648
- clientId: process.env.FLUENT_CLIENT_ID!,
1649
- clientSecret: process.env.FLUENT_CLIENT_SECRET!,
1650
- retailerId: process.env.FLUENT_RETAILER_ID!,
1651
- },
1652
- s3: {
1653
- region: process.env.AWS_REGION!,
1654
- credentials: {
1655
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
1656
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
1657
- },
1658
- },
1659
- s3Bucket: 'inventory-bucket',
1660
- dlqBucket: 'inventory-dlq-bucket',
1661
- mapping: {
1662
- fields: {
1663
- ref: { source: 'sku', required: true },
1664
- productRef: { source: 'product_id', required: true },
1665
- locationRef: { source: 'warehouse_code', required: true },
1666
- qty: { source: 'quantity', resolver: 'sdk.parseInt', required: true },
1667
- type: { source: 'inventory_type', default: 'ON_HAND' },
1668
- status: { source: 'status', default: 'AVAILABLE' },
1669
- },
1670
- },
1671
- requiredFields: ['sku', 'warehouse', 'quantity'],
1672
- kv: openKv(),
1673
- });
1674
-
1675
- await pipeline.initialize();
1676
- const report = await pipeline.run('inventory-bucket', 'data/');
1677
-
1678
- console.log('Ingestion complete:', report);
1679
- ```
1680
-
1681
- ## Common Errors and Solutions
1682
-
1683
- ### Error: JOB_EXPIRED
1684
-
1685
- **Cause:** Job exceeded 24-hour lifetime
1686
-
1687
- **Solution:**
1688
-
1689
- ```typescript
1690
- async function handleExpiredJob(originalJobId: string, remainingBatches: any[]) {
1691
- // Create new job
1692
- const newJob = await client.createJob({
1693
- name: \`Recovery - \${new Date().toISOString()}\`,
1694
- retailerId: process.env.FLUENT_RETAILER_ID!,
1695
- metadata: {
1696
- originalJobId,
1697
- reason: 'JOB_EXPIRED'
1698
- }
1699
- });
1700
-
1701
- // Resend remaining batches
1702
- for (const batch of remainingBatches) {
1703
- await client.sendBatch(newJob.id, batch);
1704
- }
1705
-
1706
- return newJob.id;
1707
- }
1708
- ```
1709
-
1710
- ### Error: VALIDATION_ERROR
1711
-
1712
- **Cause:** Invalid field values or missing required fields
1713
-
1714
- **Solution:**
1715
-
1716
- ```typescript
1717
- async function handleValidationError(records: any[], error: any) {
1718
- // Parse validation errors
1719
- const invalidRecords = extractInvalidRecords(error);
1720
-
1721
- // Filter out invalid records
1722
- const validRecords = records.filter(r => !invalidRecords.includes(r));
1723
-
1724
- // Log invalid records
1725
- console.error('Invalid records:', invalidRecords);
1726
- await saveToErrorLog(invalidRecords);
1727
-
1728
- // Retry with valid records only
1729
- return validRecords;
1730
- }
1731
- ```
1732
-
1733
- ### Error: RATE_LIMIT_ERROR
1734
-
1735
- **Cause:** Too many requests to Fluent API
1736
-
1737
- **Solution:**
1738
-
1739
- ```typescript
1740
- class RateLimitHandler {
1741
- async handleRateLimit(operation: () => Promise<any>): Promise<any> {
1742
- try {
1743
- return await operation();
1744
- } catch (error) {
1745
- if (error.code === 'RATE_LIMIT_ERROR') {
1746
- const retryAfter = error.retryAfter || 60000; // Default 60s
1747
- console.log(\`Rate limited. Waiting \${retryAfter}ms\`);
1748
- await sleep(retryAfter);
1749
- return await operation(); // Retry once
1750
- }
1751
- throw error;
1752
- }
1753
- }
1754
- }
1755
- ```
1756
-
1757
- ## Production Checklist
1758
-
1759
- ### Before Deployment
1760
-
1761
- - [ ] Schema validated against Fluent Commerce API
1762
- - [ ] Field mappings tested with sample data
1763
- - [ ] Error handling implemented for all operations
1764
- - [ ] State management configured and tested
1765
- - [ ] Monitoring and alerting configured
1766
- - [ ] Health checks implemented
1767
- - [ ] Rate limiting configured
1768
- - [ ] Dead letter queue configured
1769
- - [ ] Rollback plan documented
1770
-
1771
- ### During Deployment
1772
-
1773
- - [ ] Start with small batch sizes (100-500)
1774
- - [ ] Monitor error rates closely
1775
- - [ ] Test with sample files first
1776
- - [ ] Gradually increase batch sizes
1777
- - [ ] Verify state management works correctly
1778
-
1779
- ### After Deployment
1780
-
1781
- - [ ] Monitor ingestion metrics daily
1782
- - [ ] Review error logs regularly
1783
- - [ ] Track processing times
1784
- - [ ] Optimize based on metrics
1785
- - [ ] Document common issues and solutions
1786
-
1787
- ## Debugging Tips
1788
-
1789
- ### Enable Debug Logging
1790
-
1791
- ```typescript
1792
- const client = await createClient({
1793
- config: {
1794
- ...config
1795
- },
1796
- logger: {
1797
- debug: (...args) => console.log('[DEBUG]', ...args),
1798
- info: (...args) => console.log('[INFO]', ...args),
1799
- warn: (...args) => console.warn('[WARN]', ...args),
1800
- error: (...args) => console.error('[ERROR]', ...args),
1801
- },
1802
- });
1803
- ```
1804
-
1805
- ### Trace File Processing
1806
-
1807
- ```typescript
1808
- async function traceFileProcessing(fileKey: string) {
1809
- console.log(\`[TRACE] Starting processing: \${fileKey}\`);
1810
-
1811
- try {
1812
- console.log('[TRACE] Reading file from S3');
1813
- const data = await s3.downloadFile(fileKey);
1814
- console.log(\`[TRACE] File size: \${data.length} bytes\`);
1815
-
1816
- console.log('[TRACE] Parsing CSV');
1817
- const records = await parser.parse(data);
1818
- console.log(\`[TRACE] Parsed \${records.length} records\`);
1819
-
1820
- console.log('[TRACE] Mapping fields');
1821
- const result = await mapper.map(records);
1822
- console.log(\`[TRACE] Mapped \${result.data.length} valid records\`);
1823
-
1824
- console.log('[TRACE] Creating job');
1825
- const job = await client.createJob({ name: \`Import - \${fileKey}\` });
1826
- console.log(\`[TRACE] Job created: \${job.id}\`);
1827
-
1828
- console.log('[TRACE] Sending batch');
1829
- await client.sendBatch(job.id, { entities: result.data });
1830
- console.log('[TRACE] Batch sent successfully');
1831
-
1832
- } catch (error) {
1833
- console.error(\`[TRACE] Error at: \${error.message}\`);
1834
- throw error;
1835
- }
1836
- }
1837
- ```
1838
-
1839
- ## Key Takeaways
1840
-
1841
- - 🎯 **Always validate** - Check schema and data before sending
1842
- - 🎯 **Handle errors gracefully** - Retry transient failures, log permanent ones
1843
- - 🎯 **Monitor everything** - Track metrics, set up alerts
1844
- - 🎯 **Use state management** - Prevent duplicates, track history
1845
- - 🎯 **Test thoroughly** - Start small, scale gradually
1846
- - 🎯 **Document** - Keep runbooks for common issues
1847
-
1848
- ## Congratulations!
1849
-
1850
- You've completed the Data Ingestion Learning Path! You now know:
1851
-
1852
- - ✅ Core ingestion concepts and architecture
1853
- - ✅ How to read from multiple data sources
1854
- - ✅ How to parse CSV, Parquet, XML, and JSON
1855
- - ✅ How to transform data with UniversalMapper
1856
- - ✅ How to use the Batch API effectively
1857
- - ✅ How to implement state management
1858
- - ✅ How to optimize performance
1859
- - ✅ How to build production-ready ingestion workflows
1860
-
1861
- ## Next Steps
1862
-
1863
- Congratulations! You've completed the Data Ingestion Learning Path and mastered production-ready ingestion patterns.
1864
-
1865
- **Continue Learning:**
1866
-
1867
- - 📖 [Data Extraction Guide](../../extraction/) - Learn reverse workflows (Fluent → S3/Parquet/CSV)
1868
- - 📖 [Resolver Development Guide](../../mapping/resolvers/mapping-resolvers-readme.md) - Build custom data transformations
1869
- - 📖 [Universal Mapping Guide](../../mapping/mapping-readme.md) - Master field mapping patterns
1870
- - 📖 [Error Handling Guide](../../../03-PATTERN-GUIDES/error-handling/error-handling-readme.md) - Deep dive into error patterns
1871
- - 📖 [Versori Platform Integration](../../../04-REFERENCE/platforms/versori/) - Deploy to production
1872
-
1873
- **Real-World Examples:**
1874
-
1875
- - 🔍 [Complete Connector Examples](../../../01-TEMPLATES/versori/workflows/readme.md) - Production Versori connectors
1876
- - 📋 [Use Case Library](../../../01-TEMPLATES/readme.md) - Business-specific implementations
1877
- - 🛠️ [CLI Tools](../../../../bin/) - Code generation and validation utilities
1878
-
1879
- **Resources:**
1880
-
1881
- - 📚 [Complete API Reference](../../api-reference/api-reference-readme.md)
1882
- - 📖 [Quick Reference Cheat Sheet](../ingestion-quick-reference.md)
1883
- - 🐛 [Troubleshooting Guide](../../../00-START-HERE/troubleshooting-quick-reference.md)
1884
-
1885
- ---
1886
-
1887
- [← Back to Ingestion Guide](../ingestion-readme.md) | [Previous: Module 8 - Performance Optimization](./02-core-guides-ingestion-08-performance-optimization.md)
1888
-
1889
- **Need Help?**
1890
-
1891
- - 📧 Email: support@fluentcommerce.com
1892
- - 📚 Documentation: [FC Connect SDK Docs](../../../)
1893
- - 🔗 GitHub: Report issues or contribute
1
+ # Module 9: Best Practices
2
+
3
+ [← Back to Ingestion Guide](../ingestion-readme.md)
4
+
5
+ **Module 9 of 9** | **Level**: All Levels | **Time**: 30 minutes
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ This module covers production-ready patterns for data ingestion workflows. Learn comprehensive error handling strategies, monitoring approaches, security best practices, scheduling patterns, data quality validation, and complete production deployment patterns.
12
+
13
+ New in this version:
14
+
15
+ - Preflight validation before runs (PreflightValidator)
16
+ - Job lifecycle tracking (JobTracker)
17
+ - Partial batch failure recovery (PartialBatchRecovery)
18
+ - Versori file-level deduplication (VersoriFileTracker)
19
+
20
+ ## Learning Objectives
21
+
22
+ By the end of this module, you will:
23
+
24
+ - ✅ Implement robust error handling with retry logic and circuit breakers
25
+ - ✅ Set up comprehensive monitoring, logging, and alerting
26
+ - ✅ Apply security best practices for credentials and data
27
+ - ✅ Design effective scheduling and trigger strategies
28
+ - ✅ Validate data quality before ingestion
29
+ - ✅ Build production-ready ingestion pipelines
30
+ - ✅ Troubleshoot common issues effectively
31
+ - ✅ Follow deployment checklists for success
32
+
33
+ ---
34
+
35
+ ## Error Handling
36
+
37
+ ### Retry Strategy with Exponential Backoff
38
+
39
+ ```typescript
40
+ async function retryWithBackoff<T>(
41
+ operation: () => Promise<T>,
42
+ maxRetries: number = 3
43
+ ): Promise<T> {
44
+ let lastError: Error;
45
+ let delay = 1000; // Start with 1 second
46
+
47
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
48
+ try {
49
+ return await operation();
50
+ } catch (error) {
51
+ lastError = error;
52
+
53
+ if (!isRetryable(error) || attempt === maxRetries) {
54
+ throw error;
55
+ }
56
+
57
+ console.log(\`Retry \${attempt}/\${maxRetries} after \${delay}ms\`);
58
+ await sleep(delay);
59
+ delay *= 2; // Exponential backoff
60
+ }
61
+ }
62
+
63
+ throw lastError;
64
+ }
65
+
66
+ function isRetryable(error: any): boolean {
67
+ const retryableCodes = [
68
+ 'RATE_LIMIT_ERROR',
69
+ 'NETWORK_ERROR',
70
+ 'TIMEOUT_ERROR',
71
+ 'SERVICE_UNAVAILABLE'
72
+ ];
73
+
74
+ return (
75
+ retryableCodes.includes(error.code) ||
76
+ error.message.includes('timeout') ||
77
+ error.message.includes('ECONNRESET')
78
+ );
79
+ }
80
+ ```
81
+
82
+ ### SDK-Specific Error Handling
83
+
84
+ The SDK throws specific error types that require different handling strategies:
85
+
86
+ ```typescript
87
+ import {
88
+ FileParsingError,
89
+ MappingError,
90
+ BatchAPIError,
91
+ StateError,
92
+ createConsoleLogger,
93
+ toStructuredLogger,
94
+ } from '@fluentcommerce/fc-connect-sdk';
95
+
96
+ async function handleSDKErrors(fileKey: string) {
97
+ try {
98
+ await processFile(fileKey);
99
+ } catch (error) {
100
+ if (error instanceof FileParsingError) {
101
+ // File format issues - log and skip
102
+ console.error(`Invalid file format: ${fileKey}`, {
103
+ line: error.line,
104
+ column: error.column,
105
+ message: error.message,
106
+ });
107
+
108
+ // Move to error bucket for manual review
109
+ await moveToErrorBucket(fileKey, error);
110
+ return { status: 'skipped', reason: 'parsing_error' };
111
+ } else if (error instanceof MappingError) {
112
+ // Field mapping failures - log details
113
+ console.error(`Field mapping failed: ${fileKey}`, {
114
+ invalidFields: error.invalidFields,
115
+ recordIndex: error.recordIndex,
116
+ errors: error.errors,
117
+ });
118
+
119
+ // Save error report
120
+ await saveErrorReport(fileKey, error);
121
+ return { status: 'failed', reason: 'mapping_error' };
122
+ } else if (error instanceof BatchAPIError) {
123
+ // Batch API rejections - check if retryable
124
+ if (error.isRetryable) {
125
+ console.warn(`Retryable batch error: ${error.code}`);
126
+ throw error; // Will be caught by retry logic
127
+ } else {
128
+ console.error(`Permanent batch error: ${error.code}`, {
129
+ jobId: error.jobId,
130
+ batchId: error.batchId,
131
+ details: error.details,
132
+ });
133
+
134
+ // Save to dead letter queue
135
+ await saveToDLQ(fileKey, error);
136
+ return { status: 'failed', reason: 'batch_api_error' };
137
+ }
138
+ } else if (error instanceof StateError) {
139
+ // State management issues - critical
140
+ console.error(`State management error: ${error.message}`);
141
+
142
+ // Alert operations team
143
+ await sendAlert('CRITICAL: State management failure', error);
144
+ throw error; // Don't continue if state is broken
145
+ } else {
146
+ // Unknown error - log and alert
147
+ console.error(`Unexpected error processing ${fileKey}:`, error);
148
+ await sendAlert('Unknown ingestion error', error);
149
+ throw error;
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ ### Circuit Breaker Pattern
156
+
157
+ Prevent cascading failures when external services are down:
158
+
159
+ ```typescript
160
+ interface CircuitBreakerConfig {
161
+ failureThreshold: number;
162
+ resetTimeoutMs: number;
163
+ monitoringWindowMs: number;
164
+ }
165
+
166
+ enum CircuitState {
167
+ CLOSED = 'CLOSED', // Normal operation
168
+ OPEN = 'OPEN', // Blocking requests
169
+ HALF_OPEN = 'HALF_OPEN', // Testing recovery
170
+ }
171
+
172
+ class CircuitBreaker {
173
+ private state: CircuitState = CircuitState.CLOSED;
174
+ private failureCount: number = 0;
175
+ private lastFailureTime: number = 0;
176
+ private failures: number[] = [];
177
+
178
+ constructor(private config: CircuitBreakerConfig) {}
179
+
180
+ async execute<T>(operation: () => Promise<T>): Promise<T> {
181
+ // Check if circuit should reset
182
+ if (this.shouldAttemptReset()) {
183
+ this.state = CircuitState.HALF_OPEN;
184
+ }
185
+
186
+ // Block if circuit is open
187
+ if (this.state === CircuitState.OPEN) {
188
+ throw new Error(
189
+ `Circuit breaker OPEN. Last failure: ${new Date(this.lastFailureTime).toISOString()}`
190
+ );
191
+ }
192
+
193
+ try {
194
+ const result = await operation();
195
+
196
+ // Success - reset if in half-open state
197
+ if (this.state === CircuitState.HALF_OPEN) {
198
+ console.log('Circuit breaker: Service recovered, closing circuit');
199
+ this.reset();
200
+ }
201
+
202
+ return result;
203
+ } catch (error) {
204
+ this.recordFailure();
205
+
206
+ // Open circuit if threshold exceeded
207
+ if (this.failureCount >= this.config.failureThreshold) {
208
+ this.state = CircuitState.OPEN;
209
+ this.lastFailureTime = Date.now();
210
+ console.error(`Circuit breaker OPENED after ${this.failureCount} failures`);
211
+ }
212
+
213
+ throw error;
214
+ }
215
+ }
216
+
217
+ private recordFailure(): void {
218
+ const now = Date.now();
219
+ this.failures.push(now);
220
+
221
+ // Remove old failures outside monitoring window
222
+ this.failures = this.failures.filter(time => now - time < this.config.monitoringWindowMs);
223
+
224
+ this.failureCount = this.failures.length;
225
+ }
226
+
227
+ private shouldAttemptReset(): boolean {
228
+ if (this.state !== CircuitState.OPEN) {
229
+ return false;
230
+ }
231
+
232
+ const timeSinceLastFailure = Date.now() - this.lastFailureTime;
233
+ return timeSinceLastFailure >= this.config.resetTimeoutMs;
234
+ }
235
+
236
+ private reset(): void {
237
+ this.state = CircuitState.CLOSED;
238
+ this.failureCount = 0;
239
+ this.failures = [];
240
+ }
241
+
242
+ getState(): CircuitState {
243
+ return this.state;
244
+ }
245
+ }
246
+
247
+ // Usage
248
+ const fluentAPICircuit = new CircuitBreaker({
249
+ failureThreshold: 5, // Open after 5 failures
250
+ resetTimeoutMs: 60000, // Try again after 1 minute
251
+ monitoringWindowMs: 120000, // Track failures over 2 minutes
252
+ });
253
+
254
+ async function processWithCircuitBreaker(fileKey: string) {
255
+ try {
256
+ await fluentAPICircuit.execute(async () => {
257
+ return await processFile(fileKey);
258
+ });
259
+ } catch (error) {
260
+ if (error.message.includes('Circuit breaker OPEN')) {
261
+ console.log('Service temporarily unavailable, requeueing file');
262
+ await requeueFile(fileKey);
263
+ } else {
264
+ throw error;
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ ### Dead Letter Queue Pattern
271
+
272
+ Store failed records for later processing:
273
+
274
+ ```typescript
275
+ import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
276
+
277
+ interface DLQRecord {
278
+ originalFile: string;
279
+ failedAt: string;
280
+ errorType: string;
281
+ errorMessage: string;
282
+ recordData: any;
283
+ retryCount: number;
284
+ metadata: Record<string, any>;
285
+ }
286
+
287
+ class DeadLetterQueue {
288
+ constructor(
289
+ private s3: S3DataSource,
290
+ private dlqBucket: string,
291
+ private dlqPrefix: string = 'dlq/'
292
+ ) {}
293
+
294
+ /**
295
+ * Save failed record to DLQ
296
+ */
297
+ async save(record: DLQRecord): Promise<void> {
298
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
299
+ const key = `${this.dlqPrefix}${record.errorType}/${timestamp}-${record.originalFile}`;
300
+
301
+ await this.s3.uploadFile(key, JSON.stringify(record, null, 2));
302
+
303
+ console.log(`Saved to DLQ: ${key}`);
304
+ }
305
+
306
+ /**
307
+ * Retry processing records from DLQ
308
+ */
309
+ async retryAll(
310
+ processor: (record: any) => Promise<void>
311
+ ): Promise<{ successful: number; failed: number }> {
312
+ const files = await this.s3.listFiles({ prefix: this.dlqPrefix });
313
+ let successful = 0;
314
+ let failed = 0;
315
+
316
+ for (const file of files) {
317
+ try {
318
+ const content = await this.s3.downloadFile(file.path);
319
+ const dlqRecord: DLQRecord = JSON.parse(content);
320
+
321
+ // Attempt reprocessing
322
+ await processor(dlqRecord.recordData);
323
+
324
+ // Success - delete from DLQ (Note: deleteObject method may not exist - use uploadFile with empty content or check SDK)
325
+ // await this.s3.deleteObject(this.dlqBucket, file.path);
326
+ successful++;
327
+ } catch (error) {
328
+ console.error(`DLQ retry failed for ${file.path}:`, error);
329
+ failed++;
330
+
331
+ // Update retry count
332
+ await this.incrementRetryCount(file.path);
333
+ }
334
+ }
335
+
336
+ return { successful, failed };
337
+ }
338
+
339
+ /**
340
+ * Increment retry count in DLQ record
341
+ */
342
+ private async incrementRetryCount(key: string): Promise<void> {
343
+ try {
344
+ const content = await this.s3.downloadFile(key);
345
+ const record: DLQRecord = JSON.parse(content);
346
+
347
+ record.retryCount = (record.retryCount || 0) + 1;
348
+ record.metadata.lastRetryAttempt = new Date().toISOString();
349
+
350
+ await this.s3.uploadFile(key, JSON.stringify(record, null, 2));
351
+ } catch (error) {
352
+ console.error(`Failed to update retry count for ${key}:`, error);
353
+ }
354
+ }
355
+ }
356
+
357
+ // Usage
358
+ const dlq = new DeadLetterQueue(s3DataSource, 'my-dlq-bucket');
359
+
360
+ async function processFileWithDLQ(fileKey: string) {
361
+ try {
362
+ const records = await parseFile(fileKey);
363
+
364
+ for (const [index, record] of records.entries()) {
365
+ try {
366
+ await processRecord(record);
367
+ } catch (error) {
368
+ // Save failed record to DLQ
369
+ await dlq.save({
370
+ originalFile: fileKey,
371
+ failedAt: new Date().toISOString(),
372
+ errorType: error.constructor.name,
373
+ errorMessage: error.message,
374
+ recordData: record,
375
+ retryCount: 0,
376
+ metadata: {
377
+ recordIndex: index,
378
+ totalRecords: records.length,
379
+ },
380
+ });
381
+ }
382
+ }
383
+ } catch (error) {
384
+ console.error(`Complete file failure: ${fileKey}`, error);
385
+ throw error;
386
+ }
387
+ }
388
+ ```
389
+
390
+ ### Graceful Degradation
391
+
392
+ ```typescript
393
+ async function processFileWithFallback(fileKey: string) {
394
+ try {
395
+ // Primary processing path
396
+ await processFileNormally(fileKey);
397
+ } catch (error) {
398
+ console.error(\`Primary processing failed: \${error.message}\`);
399
+
400
+ try {
401
+ // Fallback: Process with reduced batch size
402
+ await processFileWithSmallerBatches(fileKey);
403
+ } catch (fallbackError) {
404
+ console.error(\`Fallback processing failed: \${fallbackError.message}\`);
405
+
406
+ // Last resort: Save to dead letter queue
407
+ await saveToDeadLetterQueue(fileKey, error);
408
+ throw new Error(\`File processing failed completely: \${fileKey}\`);
409
+ }
410
+ }
411
+ }
412
+ ```
413
+
414
+ ## Validation Best Practices
415
+
416
+ ### Pre-Flight Validation
417
+
418
+ ```typescript
419
+ async function validateBeforeIngestion(records: any[]): Promise<ValidationResult> {
420
+ const errors = [];
421
+ const warnings = [];
422
+
423
+ // Schema validation
424
+ for (const [index, record] of records.entries()) {
425
+ // Required fields
426
+ if (!record.ref) {
427
+ errors.push(\`Row \${index}: Missing required field 'ref'\`);
428
+ }
429
+
430
+ if (!record.productRef) {
431
+ errors.push(\`Row \${index}: Missing required field 'productRef'\`);
432
+ }
433
+
434
+ if (!record.locationRef) {
435
+ errors.push(\`Row \${index}: Missing required field 'locationRef'\`);
436
+ }
437
+
438
+ if (record.qty === undefined || record.qty === null) {
439
+ errors.push(\`Row \${index}: Missing required field 'qty'\`);
440
+ }
441
+
442
+ // Type validation
443
+ if (typeof record.qty !== 'number') {
444
+ errors.push(\`Row \${index}: 'qty' must be a number\`);
445
+ }
446
+
447
+ // Business rules
448
+ if (record.qty < 0) {
449
+ errors.push(\`Row \${index}: Quantity cannot be negative\`);
450
+ }
451
+
452
+ if (record.qty > 1000000) {
453
+ warnings.push(\`Row \${index}: Unusually large quantity\`);
454
+ }
455
+ }
456
+
457
+ return {
458
+ isValid: errors.length === 0,
459
+ errors,
460
+ warnings
461
+ };
462
+ }
463
+
464
+ // Usage
465
+ const validation = await validateBeforeIngestion(records);
466
+
467
+ if (!validation.isValid) {
468
+ console.error('Validation errors:', validation.errors);
469
+ throw new Error('Data validation failed');
470
+ }
471
+
472
+ if (validation.warnings.length > 0) {
473
+ console.warn('Validation warnings:', validation.warnings);
474
+ }
475
+ ```
476
+
477
+ ## Scheduling and Triggers
478
+
479
+ ###Cron Scheduling Patterns
480
+
481
+ Different scheduling strategies for different ingestion scenarios:
482
+
483
+ ```typescript
484
+ // Daily midnight sync (00:00 UTC)
485
+ const dailyMidnightCron = '0 0 * * *';
486
+
487
+ // Every 6 hours
488
+ const every6HoursCron = '0 */6 * * *';
489
+
490
+ // Every hour during business hours (9 AM - 5 PM, Mon-Fri)
491
+ const businessHoursCron = '0 9-17 * * 1-5';
492
+
493
+ // Every 15 minutes
494
+ const every15MinutesCron = '*/15 * * * *';
495
+
496
+ // Weekly Sunday at 2 AM
497
+ const weeklySundayCron = '0 2 * * 0';
498
+ ```
499
+
500
+ **Recommended schedules by use case:**
501
+
502
+ | Use Case | Schedule | Cron Expression | Rationale |
503
+ | ----------------------- | ------------- | ---------------- | ---------------------------- |
504
+ | **WMS Daily Sync** | Once daily | `0 2 * * *` | Process overnight updates |
505
+ | **3PL Cycle Counts** | Every 6 hours | `0 */6 * * *` | Keep inventory fresh |
506
+ | **Real-time Updates** | Every 15 min | `*/15 * * * *` | Near real-time sync |
507
+ | **Weekly Full Sync** | Sunday 2 AM | `0 2 * * 0` | Comprehensive reconciliation |
508
+ | **Business Hours Only** | 9 AM - 5 PM | `0 9-17 * * 1-5` | During operational hours |
509
+
510
+ ### Trigger Mechanisms
511
+
512
+ Different ways to trigger ingestion workflows:
513
+
514
+ #### 1. S3 Event Triggers
515
+
516
+ ```typescript
517
+ // Versori workflow triggered by S3 event
518
+ export const s3TriggeredIngestion = webhook('s3-inventory-upload', {
519
+ response: { mode: 'sync' },
520
+ })
521
+ .then(
522
+ fn('parse-s3-event', async ({ data }) => {
523
+ const event = JSON.parse(data);
524
+ const bucket = event.Records[0].s3.bucket.name;
525
+ const key = decodeURIComponent(event.Records[0].s3.object.key);
526
+
527
+ return { bucket, key };
528
+ })
529
+ )
530
+ .then(
531
+ fn('process-file', async ({ data, connections, log, openKv }) => {
532
+ const client = await createClient({ connections, log, openKv });
533
+
534
+ // Initialize logger and state management
535
+ const logger = toStructuredLogger(log, {
536
+ service: 'ingestion',
537
+ correlationId: generateCorrelationId()
538
+ });
539
+
540
+ const stateService = new StateService(logger);
541
+ const kvAdapter = new VersoriKVAdapter(openKv());
542
+
543
+ const s3 = new S3DataSource(
544
+ {
545
+ type: 'S3_CSV',
546
+ connectionId: 's3-triggered',
547
+ name: 'S3 Triggered Ingestion',
548
+ s3Config: { region: 'us-east-1', bucket: data.bucket },
549
+ },
550
+ logger
551
+ );
552
+
553
+ // Check if already processed
554
+ if (await stateService.isFileProcessed(kvAdapter, data.key)) {
555
+ log.info(`File already processed: ${data.key}`);
556
+ return { status: 'skipped', reason: 'already_processed' };
557
+ }
558
+
559
+ // Process file
560
+ await processInventoryFile(client, s3, data.bucket, data.key);
561
+
562
+ // Mark as processed
563
+ await stateService.updateSyncState(kvAdapter, [{
564
+ fileName: data.key,
565
+ lastModified: new Date().toISOString(),
566
+ }]);
567
+
568
+ return { status: 'success', file: data.key };
569
+ })
570
+ )
571
+ .catch(({ data, log }) => {
572
+ log.error('S3 triggered ingestion failed:', data);
573
+ return { status: 'error', error: data };
574
+ });
575
+ ```
576
+
577
+ #### 2. SFTP Polling
578
+
579
+ ```typescript
580
+ import { SftpDataSource, StateService, VersoriKVAdapter, createConsoleLogger, toStructuredLogger } from '@fluentcommerce/fc-connect-sdk';
581
+
582
+ async function pollSftpDirectory(kv: any) {
583
+ const logger = toStructuredLogger(createConsoleLogger(), {
584
+ service: 'ingestion',
585
+ correlationId: generateCorrelationId()
586
+ });
587
+
588
+ const stateService = new StateService(logger);
589
+ const kvAdapter = new VersoriKVAdapter(kv);
590
+
591
+ const sftp = new SftpDataSource({
592
+ type: 'SFTP_CSV',
593
+ connectionId: 'sftp-polling',
594
+ name: 'SFTP Polling',
595
+ settings: {
596
+ host: process.env.SFTP_HOST!,
597
+ port: 22,
598
+ username: process.env.SFTP_USER!,
599
+ privateKey: fs.readFileSync('/path/to/private/key'),
600
+ }
601
+ }, logger);
602
+
603
+ const remoteDir = '/inbound/inventory/';
604
+ const files = await sftp.listFiles(remoteDir);
605
+
606
+ for (const file of files) {
607
+ if (await stateService.isFileProcessed(kvAdapter, file.name)) {
608
+ continue; // Skip already processed
609
+ }
610
+
611
+ // Download and process
612
+ const localPath = `/tmp/${file.name}`;
613
+ await sftp.downloadFile(file.path, localPath); // Use file.path (full path) for download
614
+
615
+ await processLocalFile(localPath);
616
+
617
+ // Mark as processed
618
+ await stateService.updateSyncState(kvAdapter, [{
619
+ fileName: file.name,
620
+ lastModified: new Date().toISOString(),
621
+ }]);
622
+
623
+ // Optionally move to processed folder
624
+ await sftp.moveFile(`${remoteDir}${file.name}`, `/processed/${file.name}`);
625
+ }
626
+ }
627
+ ```
628
+
629
+ #### 3. Webhook Triggers
630
+
631
+ ```typescript
632
+ // Manual or external system triggered ingestion
633
+ export const manualIngestion = webhook('manual-trigger', {
634
+ response: { mode: 'sync' },
635
+ })
636
+ .then(
637
+ fn('validate-request', ({ data }) => {
638
+ const { bucket, prefix, batchSize = 2000 } = JSON.parse(data);
639
+
640
+ if (!bucket || !prefix) {
641
+ throw new Error('Missing required parameters: bucket, prefix');
642
+ }
643
+
644
+ return { bucket, prefix, batchSize };
645
+ })
646
+ )
647
+ .then(
648
+ fn('run-ingestion', async ({ data, connections, log }) => {
649
+ const client = await createClient(ctx); // Auto-detects Versori context
650
+ const s3 = new S3DataSource(
651
+ {
652
+ type: 'S3_CSV',
653
+ s3Config: { region: 'us-east-1' },
654
+ },
655
+ log
656
+ );
657
+
658
+ const files = await s3.listFiles({ prefix: data.prefix });
659
+
660
+ log.info(`Found ${files.length} files to process`);
661
+
662
+ const results = await processFilesInParallel(client, s3, data.bucket, files, {
663
+ batchSize: data.batchSize,
664
+ });
665
+
666
+ return {
667
+ filesProcessed: results.successful,
668
+ filesFailed: results.failed,
669
+ totalRecords: results.totalRecords,
670
+ };
671
+ })
672
+ );
673
+ ```
674
+
675
+ ### Backfill Strategies
676
+
677
+ Handle missed files or catch-up processing:
678
+
679
+ ```typescript
680
+ async function backfillMissedFiles(startDate: Date, endDate: Date, kv: any): Promise<void> {
681
+ const logger = toStructuredLogger(createConsoleLogger(), {
682
+ service: 'backfill',
683
+ correlationId: generateCorrelationId()
684
+ });
685
+
686
+ const stateService = new StateService(logger);
687
+ const kvAdapter = new VersoriKVAdapter(kv);
688
+
689
+ const s3 = new S3DataSource(
690
+ {
691
+ type: 'S3_CSV',
692
+ connectionId: 's3-backfill',
693
+ name: 'S3 Backfill',
694
+ s3Config: s3Config,
695
+ },
696
+ logger
697
+ );
698
+
699
+ console.log(`Backfilling files from ${startDate} to ${endDate}`);
700
+
701
+ // List all files in date range
702
+ const allFiles = await s3.listFiles({ prefix: 'data/' });
703
+
704
+ const filesInRange = allFiles.filter(file => {
705
+ const fileDate = extractDateFromKey(file.path);
706
+ return fileDate >= startDate && fileDate <= endDate;
707
+ });
708
+
709
+ console.log(`Found ${filesInRange.length} files in date range`);
710
+
711
+ // Check processing status
712
+ const unprocessedFiles = [];
713
+ for (const file of filesInRange) {
714
+ if (!(await stateService.isFileProcessed(kvAdapter, file.path))) {
715
+ unprocessedFiles.push(file);
716
+ }
717
+ }
718
+
719
+ console.log(`${unprocessedFiles.length} files need processing`);
720
+
721
+ // Process with lower concurrency to avoid overwhelming system
722
+ await processFilesInParallel(unprocessedFiles, 2); // Only 2 concurrent
723
+ }
724
+
725
+ function extractDateFromKey(key: string): Date {
726
+ // Extract date from key like "data/inventory-2025-01-15.csv"
727
+ const match = key.match(/(\d{4}-\d{2}-\d{2})/);
728
+ return match ? new Date(match[1]) : new Date(0);
729
+ }
730
+ ```
731
+
732
+ ## Data Quality and Validation
733
+
734
+ ### Pre-Ingestion Data Quality Checks
735
+
736
+ ```typescript
737
+ interface DataQualityRule {
738
+ name: string;
739
+ validate: (record: any) => { isValid: boolean; error?: string };
740
+ severity: 'error' | 'warning';
741
+ }
742
+
743
+ class DataQualityValidator {
744
+ private rules: DataQualityRule[] = [];
745
+
746
+ addRule(rule: DataQualityRule): void {
747
+ this.rules.push(rule);
748
+ }
749
+
750
+ validate(records: any[]): {
751
+ passed: any[];
752
+ failed: Array<{ record: any; errors: string[] }>;
753
+ warnings: Array<{ record: any; warnings: string[] }>;
754
+ } {
755
+ const passed: any[] = [];
756
+ const failed: Array<{ record: any; errors: string[] }> = [];
757
+ const warnings: Array<{ record: any; warnings: string[] }> = [];
758
+
759
+ for (const record of records) {
760
+ const errors: string[] = [];
761
+ const warns: string[] = [];
762
+
763
+ for (const rule of this.rules) {
764
+ const result = rule.validate(record);
765
+
766
+ if (!result.isValid) {
767
+ if (rule.severity === 'error') {
768
+ errors.push(`${rule.name}: ${result.error}`);
769
+ } else {
770
+ warns.push(`${rule.name}: ${result.error}`);
771
+ }
772
+ }
773
+ }
774
+
775
+ if (errors.length > 0) {
776
+ failed.push({ record, errors });
777
+ } else {
778
+ passed.push(record);
779
+
780
+ if (warns.length > 0) {
781
+ warnings.push({ record, warnings: warns });
782
+ }
783
+ }
784
+ }
785
+
786
+ return { passed, failed, warnings };
787
+ }
788
+ }
789
+
790
+ // Define quality rules
791
+ const qualityValidator = new DataQualityValidator();
792
+
793
+ qualityValidator.addRule({
794
+ name: 'Required Fields',
795
+ severity: 'error',
796
+ validate: record => {
797
+ const required = ['sku', 'warehouse', 'quantity'];
798
+ const missing = required.filter(field => !record[field]);
799
+
800
+ if (missing.length > 0) {
801
+ return {
802
+ isValid: false,
803
+ error: `Missing required fields: ${missing.join(', ')}`,
804
+ };
805
+ }
806
+
807
+ return { isValid: true };
808
+ },
809
+ });
810
+
811
+ qualityValidator.addRule({
812
+ name: 'Quantity Range',
813
+ severity: 'error',
814
+ validate: record => {
815
+ const qty = parseInt(record.quantity, 10);
816
+
817
+ if (isNaN(qty) || qty < 0) {
818
+ return {
819
+ isValid: false,
820
+ error: `Invalid quantity: ${record.quantity}`,
821
+ };
822
+ }
823
+
824
+ return { isValid: true };
825
+ },
826
+ });
827
+
828
+ qualityValidator.addRule({
829
+ name: 'SKU Format',
830
+ severity: 'error',
831
+ validate: record => {
832
+ const skuPattern = /^SKU-[A-Z0-9]{2,10}$/;
833
+
834
+ if (!skuPattern.test(record.sku)) {
835
+ return {
836
+ isValid: false,
837
+ error: `Invalid SKU format: ${record.sku}`,
838
+ };
839
+ }
840
+
841
+ return { isValid: true };
842
+ },
843
+ });
844
+
845
+ qualityValidator.addRule({
846
+ name: 'Suspiciously Large Quantity',
847
+ severity: 'warning',
848
+ validate: record => {
849
+ const qty = parseInt(record.quantity, 10);
850
+
851
+ if (qty > 100000) {
852
+ return {
853
+ isValid: false,
854
+ error: `Unusually large quantity: ${qty}`,
855
+ };
856
+ }
857
+
858
+ return { isValid: true };
859
+ },
860
+ });
861
+
862
+ // Usage
863
+ const records = await parseCSV(fileContent);
864
+ const validation = qualityValidator.validate(records);
865
+
866
+ console.log(`Passed: ${validation.passed.length}`);
867
+ console.log(`Failed: ${validation.failed.length}`);
868
+ console.log(`Warnings: ${validation.warnings.length}`);
869
+
870
+ // Log failures
871
+ if (validation.failed.length > 0) {
872
+ console.error('Data quality failures:');
873
+ validation.failed.forEach(({ record, errors }) => {
874
+ console.error(` SKU ${record.sku}:`, errors);
875
+ });
876
+
877
+ // Save failed records for review
878
+ await saveDataQualityReport(validation.failed);
879
+ }
880
+
881
+ // Proceed with passed records only
882
+ await processRecords(validation.passed);
883
+ ```
884
+
885
+ ### Reconciliation Reports
886
+
887
+ ```typescript
888
+ interface ReconciliationReport {
889
+ fileKey: string;
890
+ processedAt: string;
891
+ totalRecords: number;
892
+ successfulRecords: number;
893
+ failedRecords: number;
894
+ duplicateRecords: number;
895
+ dataQualityIssues: number;
896
+ batchesCreated: number;
897
+ processingTimeMs: number;
898
+ }
899
+
900
+ async function generateReconciliationReport(
901
+ fileKey: string,
902
+ results: ProcessingResult
903
+ ): Promise<ReconciliationReport> {
904
+ return {
905
+ fileKey,
906
+ processedAt: new Date().toISOString(),
907
+ totalRecords: results.totalRecords,
908
+ successfulRecords: results.successful,
909
+ failedRecords: results.failed,
910
+ duplicateRecords: results.duplicates || 0,
911
+ dataQualityIssues: results.qualityIssues || 0,
912
+ batchesCreated: results.batches,
913
+ processingTimeMs: results.processingTime,
914
+ };
915
+ }
916
+
917
+ // Save report to S3
918
+ async function saveReconciliationReport(report: ReconciliationReport): Promise<void> {
919
+ const s3 = new S3DataSource(
920
+ {
921
+ type: 'S3_JSON',
922
+ connectionId: 's3-reports',
923
+ name: 'S3 Reports',
924
+ s3Config: s3Config,
925
+ },
926
+ logger
927
+ );
928
+ const reportKey = `reports/reconciliation/${report.fileKey}-${Date.now()}.json`;
929
+
930
+ await s3.uploadFile(reportKey, JSON.stringify(report, null, 2));
931
+
932
+ console.log(`Reconciliation report saved: ${reportKey}`);
933
+ }
934
+ ```
935
+
936
+ ## Security Best Practices
937
+
938
+ ### Credential Management
939
+
940
+ ```typescript
941
+ // ✅ CORRECT - Use environment variables
942
+ const config = {
943
+ fluent: {
944
+ baseUrl: process.env.FLUENT_BASE_URL!,
945
+ clientId: process.env.FLUENT_CLIENT_ID!,
946
+ clientSecret: process.env.FLUENT_CLIENT_SECRET!,
947
+ retailerId: process.env.FLUENT_RETAILER_ID!,
948
+ },
949
+ s3: {
950
+ region: process.env.AWS_REGION!,
951
+ credentials: {
952
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
953
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
954
+ },
955
+ },
956
+ };
957
+
958
+ // ❌ WRONG - Never hardcode credentials
959
+ const badConfig = {
960
+ fluent: {
961
+ clientId: 'hardcoded-client-id', // ❌ Don't do this
962
+ clientSecret: 'hardcoded-secret', // ❌ Security risk
963
+ },
964
+ };
965
+ ```
966
+
967
+ ### Secrets Manager Integration
968
+
969
+ ```typescript
970
+ import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
971
+
972
+ async function loadSecretsFromAWS(): Promise<any> {
973
+ const client = new SecretsManagerClient({ region: 'us-east-1' });
974
+
975
+ const command = new GetSecretValueCommand({
976
+ SecretId: 'fluent-commerce/production',
977
+ });
978
+
979
+ const response = await client.send(command);
980
+ return JSON.parse(response.SecretString!);
981
+ }
982
+
983
+ // Usage
984
+ const secrets = await loadSecretsFromAWS();
985
+
986
+ const client = await createClient({
987
+ config: {
988
+ baseUrl: secrets.FLUENT_BASE_URL,
989
+ clientId: secrets.FLUENT_CLIENT_ID,
990
+ clientSecret: secrets.FLUENT_CLIENT_SECRET,
991
+ retailerId: secrets.FLUENT_RETAILER_ID,
992
+ }
993
+ });
994
+ ```
995
+
996
+ ### Data Encryption
997
+
998
+ ```typescript
999
+ // Encrypt sensitive data before storing
1000
+ import * as crypto from 'crypto';
1001
+
1002
+ function encryptSensitiveData(data: string, key: string): string {
1003
+ const iv = crypto.randomBytes(16);
1004
+ const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
1005
+
1006
+ let encrypted = cipher.update(data, 'utf8', 'hex');
1007
+ encrypted += cipher.final('hex');
1008
+
1009
+ return iv.toString('hex') + ':' + encrypted;
1010
+ }
1011
+
1012
+ function decryptSensitiveData(encrypted: string, key: string): string {
1013
+ const parts = encrypted.split(':');
1014
+ const iv = Buffer.from(parts[0], 'hex');
1015
+ const encryptedData = parts[1];
1016
+
1017
+ const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
1018
+
1019
+ let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
1020
+ decrypted += decipher.final('utf8');
1021
+
1022
+ return decrypted;
1023
+ }
1024
+ ```
1025
+
1026
+ ### Audit Logging
1027
+
1028
+ ```typescript
1029
+ interface AuditLog {
1030
+ timestamp: string;
1031
+ operation: string;
1032
+ user: string;
1033
+ resource: string;
1034
+ status: 'success' | 'failure';
1035
+ details: Record<string, any>;
1036
+ }
1037
+
1038
+ async function logAuditEvent(event: Omit<AuditLog, 'timestamp'>): Promise<void> {
1039
+ const auditLog: AuditLog = {
1040
+ timestamp: new Date().toISOString(),
1041
+ ...event,
1042
+ };
1043
+
1044
+ // Log to CloudWatch, Datadog, or custom logging service
1045
+ console.log('[AUDIT]', JSON.stringify(auditLog));
1046
+
1047
+ // Optionally save to S3 for long-term retention
1048
+ await saveAuditLog(auditLog);
1049
+ }
1050
+
1051
+ // Usage
1052
+ await logAuditEvent({
1053
+ operation: 'INVENTORY_INGESTION',
1054
+ user: process.env.USER || 'system',
1055
+ resource: `s3://bucket/file.csv`,
1056
+ status: 'success',
1057
+ details: {
1058
+ recordsProcessed: 1000,
1059
+ jobId: 'job-123',
1060
+ duration: 5000,
1061
+ },
1062
+ });
1063
+ ```
1064
+
1065
+ ## Monitoring and Alerting
1066
+
1067
+ ### Metrics Tracking
1068
+
1069
+ ```typescript
1070
+ interface IngestionMetrics {
1071
+ timestamp: string;
1072
+ filesProcessed: number;
1073
+ recordsIngested: number;
1074
+ failedRecords: number;
1075
+ processingTimeMs: number;
1076
+ avgRecordsPerSecond: number;
1077
+ errorRate: number;
1078
+ }
1079
+
1080
+ class MetricsCollector {
1081
+ private metrics: IngestionMetrics[] = [];
1082
+
1083
+ recordIngestion(result: IngestionResult): void {
1084
+ const metrics: IngestionMetrics = {
1085
+ timestamp: new Date().toISOString(),
1086
+ filesProcessed: result.filesProcessed,
1087
+ recordsIngested: result.recordsIngested,
1088
+ failedRecords: result.failedRecords,
1089
+ processingTimeMs: result.processingTimeMs,
1090
+ avgRecordsPerSecond: result.recordsIngested / (result.processingTimeMs / 1000),
1091
+ errorRate: result.failedRecords / result.recordsIngested
1092
+ };
1093
+
1094
+ this.metrics.push(metrics);
1095
+
1096
+ // Alert on high error rate
1097
+ if (metrics.errorRate > 0.05) { // > 5% error rate
1098
+ this.sendAlert('High error rate detected', metrics);
1099
+ }
1100
+
1101
+ // Alert on slow processing
1102
+ if (metrics.avgRecordsPerSecond < 10) {
1103
+ this.sendAlert('Slow processing speed', metrics);
1104
+ }
1105
+ }
1106
+
1107
+ private sendAlert(message: string, metrics: IngestionMetrics): void {
1108
+ console.error(\`[ALERT] \${message}\`, metrics);
1109
+ // Send to monitoring service (e.g., Datadog, New Relic)
1110
+ }
1111
+ }
1112
+ ```
1113
+
1114
+ ### Health Checks
1115
+
1116
+ ```typescript
1117
+ async function performHealthCheck(): Promise<HealthCheckResult> {
1118
+ const checks = [];
1119
+
1120
+ // Check Fluent API connectivity
1121
+ try {
1122
+ await client.graphql({
1123
+ query: '{ __typename }'
1124
+ });
1125
+ checks.push({ name: 'Fluent API', status: 'healthy' });
1126
+ } catch (error) {
1127
+ checks.push({ name: 'Fluent API', status: 'unhealthy', error: error.message });
1128
+ }
1129
+
1130
+ // Check S3 connectivity
1131
+ try {
1132
+ await s3.listFiles({ prefix: 'health-check/', maxKeys: 1 });
1133
+ checks.push({ name: 'S3', status: 'healthy' });
1134
+ } catch (error) {
1135
+ checks.push({ name: 'S3', status: 'unhealthy', error: error.message });
1136
+ }
1137
+
1138
+ // Check state storage
1139
+ try {
1140
+ await state.set('health-check', Date.now());
1141
+ await state.get('health-check');
1142
+ checks.push({ name: 'State Storage', status: 'healthy' });
1143
+ } catch (error) {
1144
+ checks.push({ name: 'State Storage', status: 'unhealthy', error: error.message });
1145
+ }
1146
+
1147
+ const allHealthy = checks.every(c => c.status === 'healthy');
1148
+
1149
+ return {
1150
+ status: allHealthy ? 'healthy' : 'degraded',
1151
+ checks,
1152
+ timestamp: new Date().toISOString(),
1153
+ };
1154
+ }
1155
+ ```
1156
+
1157
+ ## Complete Production Pipeline Example
1158
+
1159
+ Putting all best practices together into a production-ready ingestion pipeline:
1160
+
1161
+ ```typescript
1162
+ import {
1163
+ createClient,
1164
+ S3DataSource,
1165
+ CSVParserService,
1166
+ UniversalMapper,
1167
+ StateService,
1168
+ VersoriKVAdapter,
1169
+ FluentClient,
1170
+ FileParsingError,
1171
+ MappingError,
1172
+ BatchAPIError,
1173
+ } from '@fluentcommerce/fc-connect-sdk';
1174
+ import * as winston from 'winston';
1175
+
1176
+ /**
1177
+ * Production-grade ingestion pipeline with all best practices
1178
+ * - Error handling with retry and circuit breaker
1179
+ * - Comprehensive monitoring and logging
1180
+ * - Security best practices
1181
+ * - Data quality validation
1182
+ * - State management
1183
+ */
1184
+ class ProductionIngestionPipeline {
1185
+ private client: FluentClient;
1186
+ private s3: S3DataSource;
1187
+ private parser: CSVParserService;
1188
+ private mapper: UniversalMapper;
1189
+ private state: StateService;
1190
+ private kv: KVStore;
1191
+ private logger: winston.Logger;
1192
+ private circuitBreaker: CircuitBreaker;
1193
+ private dlq: DeadLetterQueue;
1194
+ private metrics: MetricsCollector;
1195
+ private qualityValidator: DataQualityValidator;
1196
+
1197
+ constructor(private config: ProductionConfig) {}
1198
+
1199
+ async initialize(): Promise<void> {
1200
+ // Setup structured logging
1201
+ this.logger = winston.createLogger({
1202
+ level: 'info',
1203
+ format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
1204
+ transports: [
1205
+ new winston.transports.Console(),
1206
+ new winston.transports.File({ filename: 'ingestion-error.log', level: 'error' }),
1207
+ new winston.transports.File({ filename: 'ingestion-combined.log' }),
1208
+ ],
1209
+ });
1210
+
1211
+ this.logger.info('Initializing production ingestion pipeline');
1212
+
1213
+ // Initialize Fluent client
1214
+ this.client = await createClient({ config: this.config.fluent });
1215
+
1216
+ // Initialize data sources
1217
+ this.s3 = new S3DataSource(
1218
+ {
1219
+ type: 'S3_CSV',
1220
+ connectionId: 's3-production',
1221
+ name: 'S3 Production',
1222
+ s3Config: this.config.s3,
1223
+ },
1224
+ this.logger
1225
+ );
1226
+ this.parser = new CSVParserService();
1227
+
1228
+ // Initialize field mapper with custom resolvers
1229
+ this.mapper = new UniversalMapper(this.config.mapping, {
1230
+ customResolvers: this.config.customResolvers || {},
1231
+ });
1232
+
1233
+ // Initialize state management
1234
+ const logger = toStructuredLogger(this.logger, {
1235
+ service: 'production-pipeline',
1236
+ correlationId: generateCorrelationId()
1237
+ });
1238
+
1239
+ this.kv = new VersoriKVAdapter(this.config.kv);
1240
+ this.stateService = new StateService(logger);
1241
+
1242
+ // Initialize circuit breaker
1243
+ this.circuitBreaker = new CircuitBreaker({
1244
+ failureThreshold: 5,
1245
+ resetTimeoutMs: 60000,
1246
+ monitoringWindowMs: 120000,
1247
+ });
1248
+
1249
+ // Initialize dead letter queue
1250
+ this.dlq = new DeadLetterQueue(this.s3, this.config.dlqBucket, 'dlq/');
1251
+
1252
+ // Initialize metrics collector
1253
+ this.metrics = new MetricsCollector();
1254
+
1255
+ // Initialize data quality validator
1256
+ this.qualityValidator = this.createQualityValidator();
1257
+
1258
+ this.logger.info('Pipeline initialization complete');
1259
+ }
1260
+
1261
+ /**
1262
+ * Run the complete ingestion pipeline
1263
+ */
1264
+ async run(bucket: string, prefix: string): Promise<ExecutionReport> {
1265
+ const startTime = Date.now();
1266
+
1267
+ try {
1268
+ this.logger.info('Starting ingestion pipeline', { bucket, prefix });
1269
+
1270
+ // Health check before starting
1271
+ const healthCheck = await this.performHealthCheck();
1272
+ if (healthCheck.status !== 'healthy') {
1273
+ throw new Error(`System unhealthy: ${JSON.stringify(healthCheck.checks)}`);
1274
+ }
1275
+
1276
+ // List files
1277
+ const allFiles = await this.s3.listFiles({ prefix });
1278
+ this.logger.info(`Found ${allFiles.length} total files`);
1279
+
1280
+ // Filter unprocessed files
1281
+ const unprocessedFiles = await this.filterUnprocessedFiles(allFiles);
1282
+ this.logger.info(`${unprocessedFiles.length} files to process`);
1283
+
1284
+ if (unprocessedFiles.length === 0) {
1285
+ return this.createReport(startTime, { filesProcessed: 0 });
1286
+ }
1287
+
1288
+ // Process files with circuit breaker
1289
+ const results = await this.circuitBreaker.execute(async () => {
1290
+ return await this.processFilesWithRetry(bucket, unprocessedFiles);
1291
+ });
1292
+
1293
+ // Generate report
1294
+ const report = this.createReport(startTime, results);
1295
+
1296
+ // Log metrics
1297
+ this.metrics.recordIngestion({
1298
+ filesProcessed: results.filesProcessed,
1299
+ recordsIngested: results.recordsIngested,
1300
+ failedRecords: results.failedRecords,
1301
+ processingTimeMs: report.totalTimeMs,
1302
+ avgRecordsPerSecond: report.throughput,
1303
+ errorRate: results.failedRecords / (results.recordsIngested || 1),
1304
+ });
1305
+
1306
+ this.logger.info('Pipeline execution complete', report);
1307
+
1308
+ return report;
1309
+ } catch (error) {
1310
+ this.logger.error('Pipeline execution failed', {
1311
+ error: error.message,
1312
+ stack: error.stack,
1313
+ });
1314
+
1315
+ await this.sendAlert('CRITICAL: Ingestion pipeline failure', error);
1316
+ throw error;
1317
+ }
1318
+ }
1319
+
1320
+ /**
1321
+ * Process files with retry logic
1322
+ */
1323
+ private async processFilesWithRetry(
1324
+ bucket: string,
1325
+ files: Array<{ key: string }>
1326
+ ): Promise<ProcessingResults> {
1327
+ const results: ProcessingResults = {
1328
+ filesProcessed: 0,
1329
+ filesFailed: 0,
1330
+ recordsIngested: 0,
1331
+ failedRecords: 0,
1332
+ details: [],
1333
+ };
1334
+
1335
+ for (const file of files) {
1336
+ const maxRetries = 3;
1337
+ let attempt = 0;
1338
+ let success = false;
1339
+
1340
+ while (attempt < maxRetries && !success) {
1341
+ attempt++;
1342
+
1343
+ try {
1344
+ const fileResult = await this.processFile(bucket, file.path);
1345
+
1346
+ results.filesProcessed++;
1347
+ results.recordsIngested += fileResult.recordsProcessed;
1348
+ results.details.push({
1349
+ file: file.path,
1350
+ status: 'success',
1351
+ records: fileResult.recordsProcessed,
1352
+ });
1353
+
1354
+ success = true;
1355
+
1356
+ this.logger.info(`File processed successfully`, {
1357
+ file: file.path,
1358
+ records: fileResult.recordsProcessed,
1359
+ attempt,
1360
+ });
1361
+ } catch (error) {
1362
+ this.logger.warn(`File processing attempt ${attempt} failed`, {
1363
+ file: file.path,
1364
+ error: error.message,
1365
+ });
1366
+
1367
+ if (attempt < maxRetries && this.isRetryable(error)) {
1368
+ const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
1369
+ await new Promise(resolve => setTimeout(resolve, delay));
1370
+ } else {
1371
+ results.filesFailed++;
1372
+ results.details.push({
1373
+ file: file.path,
1374
+ status: 'failed',
1375
+ error: error.message,
1376
+ });
1377
+
1378
+ this.logger.error(`File processing failed permanently`, {
1379
+ file: file.path,
1380
+ attempts: attempt,
1381
+ error: error.message,
1382
+ });
1383
+ }
1384
+ }
1385
+ }
1386
+ }
1387
+
1388
+ return results;
1389
+ }
1390
+
1391
+ /**
1392
+ * Process a single file with all validations
1393
+ */
1394
+ private async processFile(bucket: string, fileKey: string): Promise<FileProcessingResult> {
1395
+ this.logger.info(`Processing file: ${fileKey}`);
1396
+
1397
+ try {
1398
+ // Read file
1399
+ const fileContent = await this.s3.downloadFile(fileKey);
1400
+
1401
+ // Parse CSV
1402
+ const records = await this.parser.parse(fileContent);
1403
+ this.logger.debug(`Parsed ${records.length} records from ${fileKey}`);
1404
+
1405
+ // Data quality validation
1406
+ const validation = this.qualityValidator.validate(records);
1407
+
1408
+ if (validation.failed.length > 0) {
1409
+ this.logger.warn(`Data quality issues found`, {
1410
+ file: fileKey,
1411
+ failed: validation.failed.length,
1412
+ warnings: validation.warnings.length,
1413
+ });
1414
+
1415
+ // Save failed records to DLQ
1416
+ for (const { record, errors } of validation.failed) {
1417
+ await this.dlq.save({
1418
+ originalFile: fileKey,
1419
+ failedAt: new Date().toISOString(),
1420
+ errorType: 'DATA_QUALITY_ERROR',
1421
+ errorMessage: errors.join('; '),
1422
+ recordData: record,
1423
+ retryCount: 0,
1424
+ metadata: {},
1425
+ });
1426
+ }
1427
+ }
1428
+
1429
+ // Proceed with passed records only
1430
+ if (validation.passed.length === 0) {
1431
+ throw new Error('No valid records after quality validation');
1432
+ }
1433
+
1434
+ // Field mapping
1435
+ const mappingResult = await this.mapper.map(validation.passed);
1436
+
1437
+ if (!mappingResult.success) {
1438
+ throw new MappingError(`Field mapping failed: ${mappingResult.errors.join(', ')}`);
1439
+ }
1440
+
1441
+ // Create job
1442
+ const job = await this.client.createJob({
1443
+ name: `Production Import - ${fileKey}`,
1444
+ retailerId: this.config.fluent.retailerId,
1445
+ metadata: {
1446
+ fileName: fileKey,
1447
+ recordCount: mappingResult.data.length,
1448
+ pipeline: 'production',
1449
+ },
1450
+ });
1451
+
1452
+ // Send to Batch API
1453
+ await this.client.sendBatch(job.id, {
1454
+ action: 'UPSERT',
1455
+ entityType: 'INVENTORY',
1456
+ entities: mappingResult.data,
1457
+ });
1458
+
1459
+ // Mark as processed
1460
+ await this.state.markFileProcessed(fileKey, {
1461
+ jobId: job.id,
1462
+ recordCount: mappingResult.data.length,
1463
+ timestamp: new Date().toISOString(),
1464
+ });
1465
+
1466
+ // Audit log
1467
+ await this.logAuditEvent({
1468
+ operation: 'INVENTORY_INGESTION',
1469
+ user: 'system',
1470
+ resource: `s3://${bucket}/${fileKey}`,
1471
+ status: 'success',
1472
+ details: {
1473
+ recordsProcessed: mappingResult.data.length,
1474
+ jobId: job.id,
1475
+ },
1476
+ });
1477
+
1478
+ return {
1479
+ recordsProcessed: mappingResult.data.length,
1480
+ jobId: job.id,
1481
+ };
1482
+ } catch (error) {
1483
+ // Audit log failure
1484
+ await this.logAuditEvent({
1485
+ operation: 'INVENTORY_INGESTION',
1486
+ user: 'system',
1487
+ resource: `s3://${bucket}/${fileKey}`,
1488
+ status: 'failure',
1489
+ details: {
1490
+ error: error.message,
1491
+ },
1492
+ });
1493
+
1494
+ throw error;
1495
+ }
1496
+ }
1497
+
1498
+ /**
1499
+ * Check if error is retryable
1500
+ */
1501
+ private isRetryable(error: any): boolean {
1502
+ if (error instanceof FileParsingError) {
1503
+ return false; // File format errors are not retryable
1504
+ }
1505
+
1506
+ if (error instanceof BatchAPIError) {
1507
+ return error.isRetryable;
1508
+ }
1509
+
1510
+ // Network errors are retryable
1511
+ return (
1512
+ error.code === 'NETWORK_ERROR' ||
1513
+ error.message.includes('timeout') ||
1514
+ error.message.includes('ECONNRESET')
1515
+ );
1516
+ }
1517
+
1518
+ /**
1519
+ * Filter unprocessed files
1520
+ */
1521
+ private async filterUnprocessedFiles(
1522
+ files: FileMetadata[]
1523
+ ): Promise<FileMetadata[]> {
1524
+ const unprocessed = [];
1525
+
1526
+ for (const file of files) {
1527
+ if (!(await this.state.isFileProcessed(this.kv, file.path))) {
1528
+ unprocessed.push(file);
1529
+ }
1530
+ }
1531
+
1532
+ return unprocessed;
1533
+ }
1534
+
1535
+ /**
1536
+ * Create data quality validator
1537
+ */
1538
+ private createQualityValidator(): DataQualityValidator {
1539
+ const validator = new DataQualityValidator();
1540
+
1541
+ // Add validation rules based on configuration
1542
+ validator.addRule({
1543
+ name: 'Required Fields',
1544
+ severity: 'error',
1545
+ validate: record => {
1546
+ const required = this.config.requiredFields || [];
1547
+ const missing = required.filter(field => !record[field]);
1548
+
1549
+ if (missing.length > 0) {
1550
+ return {
1551
+ isValid: false,
1552
+ error: `Missing required fields: ${missing.join(', ')}`,
1553
+ };
1554
+ }
1555
+
1556
+ return { isValid: true };
1557
+ },
1558
+ });
1559
+
1560
+ return validator;
1561
+ }
1562
+
1563
+ /**
1564
+ * Perform system health check
1565
+ */
1566
+ private async performHealthCheck(): Promise<HealthCheckResult> {
1567
+ const checks = [];
1568
+
1569
+ // Check Fluent API
1570
+ try {
1571
+ await this.client.graphql({
1572
+ query: '{ __typename }'
1573
+ });
1574
+ checks.push({ name: 'Fluent API', status: 'healthy' });
1575
+ } catch (error) {
1576
+ checks.push({ name: 'Fluent API', status: 'unhealthy', error: error.message });
1577
+ }
1578
+
1579
+ // Check S3
1580
+ try {
1581
+ await this.s3.listObjects(this.config.s3Bucket, 'health-check/');
1582
+ checks.push({ name: 'S3', status: 'healthy' });
1583
+ } catch (error) {
1584
+ checks.push({ name: 'S3', status: 'unhealthy', error: error.message });
1585
+ }
1586
+
1587
+ const allHealthy = checks.every(c => c.status === 'healthy');
1588
+
1589
+ return {
1590
+ status: allHealthy ? 'healthy' : 'degraded',
1591
+ checks,
1592
+ timestamp: new Date().toISOString(),
1593
+ };
1594
+ }
1595
+
1596
+ /**
1597
+ * Create execution report
1598
+ */
1599
+ private createReport(startTime: number, results: Partial<ProcessingResults>): ExecutionReport {
1600
+ const totalTimeMs = Date.now() - startTime;
1601
+
1602
+ return {
1603
+ startTime: new Date(startTime).toISOString(),
1604
+ endTime: new Date().toISOString(),
1605
+ totalTimeMs,
1606
+ filesProcessed: results.filesProcessed || 0,
1607
+ filesFailed: results.filesFailed || 0,
1608
+ recordsIngested: results.recordsIngested || 0,
1609
+ failedRecords: results.failedRecords || 0,
1610
+ throughput: (results.recordsIngested || 0) / (totalTimeMs / 1000),
1611
+ successRate:
1612
+ ((results.filesProcessed || 0) /
1613
+ ((results.filesProcessed || 0) + (results.filesFailed || 0))) *
1614
+ 100,
1615
+ };
1616
+ }
1617
+
1618
+ /**
1619
+ * Log audit event
1620
+ */
1621
+ private async logAuditEvent(event: Omit<AuditLog, 'timestamp'>): Promise<void> {
1622
+ const auditLog: AuditLog = {
1623
+ timestamp: new Date().toISOString(),
1624
+ ...event,
1625
+ };
1626
+
1627
+ this.logger.info('[AUDIT]', auditLog);
1628
+
1629
+ // Optionally save to S3 for compliance
1630
+ // await this.s3.putObject(auditBucket, auditKey, JSON.stringify(auditLog));
1631
+ }
1632
+
1633
+ /**
1634
+ * Send alert to monitoring service
1635
+ */
1636
+ private async sendAlert(message: string, error: any): Promise<void> {
1637
+ this.logger.error('[ALERT]', { message, error: error.message });
1638
+
1639
+ // Integration with alerting service (PagerDuty, Slack, etc.)
1640
+ // await alertService.send({ message, error });
1641
+ }
1642
+ }
1643
+
1644
+ // Usage
1645
+ const pipeline = new ProductionIngestionPipeline({
1646
+ fluent: {
1647
+ baseUrl: process.env.FLUENT_BASE_URL!,
1648
+ clientId: process.env.FLUENT_CLIENT_ID!,
1649
+ clientSecret: process.env.FLUENT_CLIENT_SECRET!,
1650
+ retailerId: process.env.FLUENT_RETAILER_ID!,
1651
+ },
1652
+ s3: {
1653
+ region: process.env.AWS_REGION!,
1654
+ credentials: {
1655
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
1656
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
1657
+ },
1658
+ },
1659
+ s3Bucket: 'inventory-bucket',
1660
+ dlqBucket: 'inventory-dlq-bucket',
1661
+ mapping: {
1662
+ fields: {
1663
+ ref: { source: 'sku', required: true },
1664
+ productRef: { source: 'product_id', required: true },
1665
+ locationRef: { source: 'warehouse_code', required: true },
1666
+ qty: { source: 'quantity', resolver: 'sdk.parseInt', required: true },
1667
+ type: { source: 'inventory_type', default: 'ON_HAND' },
1668
+ status: { source: 'status', default: 'AVAILABLE' },
1669
+ },
1670
+ },
1671
+ requiredFields: ['sku', 'warehouse', 'quantity'],
1672
+ kv: openKv(),
1673
+ });
1674
+
1675
+ await pipeline.initialize();
1676
+ const report = await pipeline.run('inventory-bucket', 'data/');
1677
+
1678
+ console.log('Ingestion complete:', report);
1679
+ ```
1680
+
1681
+ ## Common Errors and Solutions
1682
+
1683
+ ### Error: JOB_EXPIRED
1684
+
1685
+ **Cause:** Job exceeded 24-hour lifetime
1686
+
1687
+ **Solution:**
1688
+
1689
+ ```typescript
1690
+ async function handleExpiredJob(originalJobId: string, remainingBatches: any[]) {
1691
+ // Create new job
1692
+ const newJob = await client.createJob({
1693
+ name: \`Recovery - \${new Date().toISOString()}\`,
1694
+ retailerId: process.env.FLUENT_RETAILER_ID!,
1695
+ metadata: {
1696
+ originalJobId,
1697
+ reason: 'JOB_EXPIRED'
1698
+ }
1699
+ });
1700
+
1701
+ // Resend remaining batches
1702
+ for (const batch of remainingBatches) {
1703
+ await client.sendBatch(newJob.id, batch);
1704
+ }
1705
+
1706
+ return newJob.id;
1707
+ }
1708
+ ```
1709
+
1710
+ ### Error: VALIDATION_ERROR
1711
+
1712
+ **Cause:** Invalid field values or missing required fields
1713
+
1714
+ **Solution:**
1715
+
1716
+ ```typescript
1717
+ async function handleValidationError(records: any[], error: any) {
1718
+ // Parse validation errors
1719
+ const invalidRecords = extractInvalidRecords(error);
1720
+
1721
+ // Filter out invalid records
1722
+ const validRecords = records.filter(r => !invalidRecords.includes(r));
1723
+
1724
+ // Log invalid records
1725
+ console.error('Invalid records:', invalidRecords);
1726
+ await saveToErrorLog(invalidRecords);
1727
+
1728
+ // Retry with valid records only
1729
+ return validRecords;
1730
+ }
1731
+ ```
1732
+
1733
+ ### Error: RATE_LIMIT_ERROR
1734
+
1735
+ **Cause:** Too many requests to Fluent API
1736
+
1737
+ **Solution:**
1738
+
1739
+ ```typescript
1740
+ class RateLimitHandler {
1741
+ async handleRateLimit(operation: () => Promise<any>): Promise<any> {
1742
+ try {
1743
+ return await operation();
1744
+ } catch (error) {
1745
+ if (error.code === 'RATE_LIMIT_ERROR') {
1746
+ const retryAfter = error.retryAfter || 60000; // Default 60s
1747
+ console.log(\`Rate limited. Waiting \${retryAfter}ms\`);
1748
+ await sleep(retryAfter);
1749
+ return await operation(); // Retry once
1750
+ }
1751
+ throw error;
1752
+ }
1753
+ }
1754
+ }
1755
+ ```
1756
+
1757
+ ## Production Checklist
1758
+
1759
+ ### Before Deployment
1760
+
1761
+ - [ ] Schema validated against Fluent Commerce API
1762
+ - [ ] Field mappings tested with sample data
1763
+ - [ ] Error handling implemented for all operations
1764
+ - [ ] State management configured and tested
1765
+ - [ ] Monitoring and alerting configured
1766
+ - [ ] Health checks implemented
1767
+ - [ ] Rate limiting configured
1768
+ - [ ] Dead letter queue configured
1769
+ - [ ] Rollback plan documented
1770
+
1771
+ ### During Deployment
1772
+
1773
+ - [ ] Start with small batch sizes (100-500)
1774
+ - [ ] Monitor error rates closely
1775
+ - [ ] Test with sample files first
1776
+ - [ ] Gradually increase batch sizes
1777
+ - [ ] Verify state management works correctly
1778
+
1779
+ ### After Deployment
1780
+
1781
+ - [ ] Monitor ingestion metrics daily
1782
+ - [ ] Review error logs regularly
1783
+ - [ ] Track processing times
1784
+ - [ ] Optimize based on metrics
1785
+ - [ ] Document common issues and solutions
1786
+
1787
+ ## Debugging Tips
1788
+
1789
+ ### Enable Debug Logging
1790
+
1791
+ ```typescript
1792
+ const client = await createClient({
1793
+ config: {
1794
+ ...config
1795
+ },
1796
+ logger: {
1797
+ debug: (...args) => console.log('[DEBUG]', ...args),
1798
+ info: (...args) => console.log('[INFO]', ...args),
1799
+ warn: (...args) => console.warn('[WARN]', ...args),
1800
+ error: (...args) => console.error('[ERROR]', ...args),
1801
+ },
1802
+ });
1803
+ ```
1804
+
1805
+ ### Trace File Processing
1806
+
1807
+ ```typescript
1808
+ async function traceFileProcessing(fileKey: string) {
1809
+ console.log(\`[TRACE] Starting processing: \${fileKey}\`);
1810
+
1811
+ try {
1812
+ console.log('[TRACE] Reading file from S3');
1813
+ const data = await s3.downloadFile(fileKey);
1814
+ console.log(\`[TRACE] File size: \${data.length} bytes\`);
1815
+
1816
+ console.log('[TRACE] Parsing CSV');
1817
+ const records = await parser.parse(data);
1818
+ console.log(\`[TRACE] Parsed \${records.length} records\`);
1819
+
1820
+ console.log('[TRACE] Mapping fields');
1821
+ const result = await mapper.map(records);
1822
+ console.log(\`[TRACE] Mapped \${result.data.length} valid records\`);
1823
+
1824
+ console.log('[TRACE] Creating job');
1825
+ const job = await client.createJob({ name: \`Import - \${fileKey}\` });
1826
+ console.log(\`[TRACE] Job created: \${job.id}\`);
1827
+
1828
+ console.log('[TRACE] Sending batch');
1829
+ await client.sendBatch(job.id, { entities: result.data });
1830
+ console.log('[TRACE] Batch sent successfully');
1831
+
1832
+ } catch (error) {
1833
+ console.error(\`[TRACE] Error at: \${error.message}\`);
1834
+ throw error;
1835
+ }
1836
+ }
1837
+ ```
1838
+
1839
+ ## Key Takeaways
1840
+
1841
+ - 🎯 **Always validate** - Check schema and data before sending
1842
+ - 🎯 **Handle errors gracefully** - Retry transient failures, log permanent ones
1843
+ - 🎯 **Monitor everything** - Track metrics, set up alerts
1844
+ - 🎯 **Use state management** - Prevent duplicates, track history
1845
+ - 🎯 **Test thoroughly** - Start small, scale gradually
1846
+ - 🎯 **Document** - Keep runbooks for common issues
1847
+
1848
+ ## Congratulations!
1849
+
1850
+ You've completed the Data Ingestion Learning Path! You now know:
1851
+
1852
+ - ✅ Core ingestion concepts and architecture
1853
+ - ✅ How to read from multiple data sources
1854
+ - ✅ How to parse CSV, Parquet, XML, and JSON
1855
+ - ✅ How to transform data with UniversalMapper
1856
+ - ✅ How to use the Batch API effectively
1857
+ - ✅ How to implement state management
1858
+ - ✅ How to optimize performance
1859
+ - ✅ How to build production-ready ingestion workflows
1860
+
1861
+ ## Next Steps
1862
+
1863
+ Congratulations! You've completed the Data Ingestion Learning Path and mastered production-ready ingestion patterns.
1864
+
1865
+ **Continue Learning:**
1866
+
1867
+ - 📖 [Data Extraction Guide](../../extraction/) - Learn reverse workflows (Fluent → S3/Parquet/CSV)
1868
+ - 📖 [Resolver Development Guide](../../mapping/resolvers/mapping-resolvers-readme.md) - Build custom data transformations
1869
+ - 📖 [Universal Mapping Guide](../../mapping/mapping-readme.md) - Master field mapping patterns
1870
+ - 📖 [Error Handling Guide](../../../03-PATTERN-GUIDES/error-handling/error-handling-readme.md) - Deep dive into error patterns
1871
+ - 📖 [Versori Platform Integration](../../../04-REFERENCE/platforms/versori/) - Deploy to production
1872
+
1873
+ **Real-World Examples:**
1874
+
1875
+ - 🔍 [Complete Connector Examples](../../../01-TEMPLATES/versori/workflows/readme.md) - Production Versori connectors
1876
+ - 📋 [Use Case Library](../../../01-TEMPLATES/readme.md) - Business-specific implementations
1877
+ - 🛠️ [CLI Tools](../../../../bin/) - Code generation and validation utilities
1878
+
1879
+ **Resources:**
1880
+
1881
+ - 📚 [Complete API Reference](../../api-reference/api-reference-readme.md)
1882
+ - 📖 [Quick Reference Cheat Sheet](../ingestion-quick-reference.md)
1883
+ - 🐛 [Troubleshooting Guide](../../../00-START-HERE/troubleshooting-quick-reference.md)
1884
+
1885
+ ---
1886
+
1887
+ [← Back to Ingestion Guide](../ingestion-readme.md) | [Previous: Module 8 - Performance Optimization](./02-core-guides-ingestion-08-performance-optimization.md)
1888
+
1889
+ **Need Help?**
1890
+
1891
+ - 📧 Email: support@fluentcommerce.com
1892
+ - 📚 Documentation: [FC Connect SDK Docs](../../../)
1893
+ - 🔗 GitHub: Report issues or contribute