@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.56

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