@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,2399 +1,2399 @@
1
- # Pattern: Master Data ETL - Generic Framework for Loading Any Entity
2
-
3
- **FC Connect SDK Use Case Guide**
4
-
5
- > **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
6
- > **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
7
-
8
- **Status**: Production Ready
9
-
10
- **Complexity**: Intermediate
11
-
12
- **Est. Time**: 30-60 minutes
13
-
14
- **Use Cases**: Locations, Products, Controls, Carriers, Customers, Pricing, Categories, etc.
15
-
16
- ## Table of Contents
17
-
18
- - [Overview](#overview)
19
- - [What You'll Build](#what-youll-build)
20
- - [SDK Methods Used](#sdk-methods-used)
21
- - [The Generic Pattern](#the-generic-pattern)
22
- - [Example 1: Location Master Data](#example-1-location-master-data)
23
- - [Example 2: Product Catalog](#example-2-product-catalog)
24
- - [Example 3: Control/Config Data](#example-3-controlconfig-data)
25
- - [Source Strategies](#source-strategies)
26
- - [Load Strategies](#load-strategies)
27
- - [Configuration Schema](#configuration-schema)
28
- - [Extending to Other Entities](#extending-to-other-entities)
29
- - [Testing](#testing)
30
- - [Common Issues](#common-issues)
31
- - [Related Guides](#related-guides)
32
-
33
- ---
34
-
35
- ## Overview
36
-
37
- Master data ETL is the process of extracting reference data from external systems and loading it into Fluent Commerce. Unlike transactional data (orders, inventory updates), master data changes infrequently and defines the core entities of your commerce platform.
38
-
39
- **Common Master Data Entities:**
40
-
41
- - **Locations**: Stores, warehouses, distribution centers
42
- - **Products**: SKUs, product catalogs, variants
43
- - **Controls**: Business rules, configuration parameters
44
- - **Carriers**: Shipping carriers, service levels
45
- - **Customers**: Customer profiles, segments
46
- - **Categories**: Product taxonomies, merchandising hierarchies
47
- - **Pricing**: Price lists, promotional rules
48
-
49
- **Why This Pattern?**
50
-
51
- This guide provides a **generic, configuration-driven framework** that works for ANY master data entity. Instead of writing custom code for each entity type, you configure JSON mappings and reuse the same pipeline.
52
-
53
- **Key Benefits:**
54
-
55
- - ✅ **One Pattern for All Entities**: Same code works for locations, products, controls, etc.
56
- - ✅ **Configuration-Driven**: No code changes needed for new entity types
57
- - ✅ **Multiple Source Formats**: CSV, JSON, XML support out-of-the-box
58
- - ✅ **Multiple Load Strategies**: GraphQL mutations or Event API
59
- - ✅ **Automatic Deduplication**: State management prevents duplicate loads
60
- - ✅ **Production-Ready Error Handling**: Comprehensive logging and retry logic
61
-
62
- ---
63
-
64
- ## What You'll Build
65
-
66
- A **reusable ETL framework** with these capabilities:
67
-
68
- 1. **Extract**: Read master data from S3/SFTP in CSV, JSON, or XML format
69
- 2. **Parse**: Convert file format to JavaScript objects
70
- 3. **Transform**: Map source fields to Fluent schema using field mappings
71
- 4. **Load**: Submit to Fluent via GraphQL mutations or Event API
72
- 5. **Track**: Prevent duplicate processing using state management
73
-
74
- **Pipeline Flow:**
75
-
76
- ```
77
- Source File (S3/SFTP)
78
-
79
- Extract (DataSource)
80
-
81
- Parse (CSVParserService/JSONParser/XMLParser)
82
-
83
- Transform (UniversalMapper)
84
-
85
- Load (GraphQL Mutation or Event API)
86
-
87
- Track (StateService)
88
- ```
89
-
90
- **Configuration-Driven Design:**
91
-
92
- Instead of hardcoded logic:
93
-
94
- ```typescript
95
- // ❌ WRONG: Hardcoded for each entity type
96
- if (entityType === 'location') {
97
- // Custom location loading logic
98
- } else if (entityType === 'product') {
99
- // Custom product loading logic
100
- }
101
- ```
102
-
103
- You use JSON configuration:
104
-
105
- ```typescript
106
- // ✅ CORRECT: Generic pipeline with config
107
- const config = loadConfig('location-mapping.json');
108
- await etlPipeline.run(sourceFile, config);
109
- ```
110
-
111
- > Tips:
112
- >
113
- > - For identifiers that may look numeric (SKU/GTIN/UPC), add `resolver: "sdk.toString"` in mappings to force string output.
114
- > - When parsing XML sources where leading zeros matter, configure the XML parser with `parseNumbers: false` to prevent numeric coercion.
115
-
116
- ---
117
-
118
- ## SDK Methods Used
119
-
120
- | Method | Purpose | Pattern |
121
- | ---------------------------------------------------------- | -------------------------------------- | --------------- |
122
- | `S3DataSource.downloadFile()` | Download master data file from S3 | Source |
123
- | `SftpDataSource.downloadFile()` | Download master data file from SFTP | Source |
124
- | `CSVParserService.parse()` | Parse CSV master data | Parse |
125
- | `JSONParserService.parse()` | Parse JSON master data | Parse |
126
- | `XMLParserService.parse()` | Parse XML master data | Parse |
127
- | `UniversalMapper.map()` | Transform source data to Fluent schema | Transform |
128
- | `GraphQLMutationMapper.map()` | Generate GraphQL mutations from data | Load (Option 1) |
129
- | `FluentClient.graphql()` | Execute GraphQL mutations | Load (Option 1) |
130
- | `FluentClient.sendEvent()` | Send events to Event API | Load (Option 2) |
131
- | `StateService.markFileProcessed(kv, fileName, workflowId)` | Prevent duplicate processing | Track |
132
-
133
- ---
134
-
135
- ## The Generic Pattern
136
-
137
- ### Architecture Overview
138
-
139
- The master data ETL pattern follows a **four-phase pipeline** that adapts to any entity type through configuration:
140
-
141
- ```
142
- ┌──────────────────────────────────────────────────────────────┐
143
- │ PHASE 1: EXTRACT │
144
- │ - List files from S3/SFTP │
145
- │ - Filter by pattern (*.csv, locations_*.json, etc.) │
146
- │ - Download file content │
147
- │ - Skip already-processed files (state check) │
148
- └──────────────────────────────────────────────────────────────┘
149
-
150
- ┌──────────────────────────────────────────────────────────────┐
151
- │ PHASE 2: PARSE │
152
- │ - Auto-detect format (CSV, JSON, XML) │
153
- │ - Parse content to JavaScript objects │
154
- │ - Validate structure (required fields, types) │
155
- │ - Handle parsing errors gracefully │
156
- └──────────────────────────────────────────────────────────────┘
157
-
158
- ┌──────────────────────────────────────────────────────────────┐
159
- │ PHASE 3: TRANSFORM │
160
- │ - Apply field mappings (source to Fluent schema) │
161
- │ - Execute resolvers (transformations, calculations) │
162
- │ - Validate required fields │
163
- │ - Enrich with defaults/constants │
164
- └──────────────────────────────────────────────────────────────┘
165
-
166
- ┌──────────────────────────────────────────────────────────────┐
167
- │ PHASE 4: LOAD │
168
- │ - Choose strategy (GraphQL Mutation vs Event API) │
169
- │ - Batch if needed (large datasets) │
170
- │ - Execute load operation │
171
- │ - Handle errors and retries │
172
- │ - Mark file as processed (state update) │
173
- └──────────────────────────────────────────────────────────────┘
174
- ```
175
-
176
- ### Configuration-Driven Design
177
-
178
- **Core Principle**: All entity-specific logic lives in JSON configuration files, not in code.
179
-
180
- **Generic Configuration Structure:**
181
-
182
- ```json
183
- {
184
- "entityType": "location", // What you're loading
185
- "sourceConfig": {
186
- // Where to get data
187
- "type": "S3_CSV",
188
- "bucket": "master-data",
189
- "prefix": "locations/",
190
- "filePattern": "*.csv"
191
- },
192
- "parseConfig": {
193
- // How to parse data
194
- "format": "csv",
195
- "delimiter": ",",
196
- "headers": true
197
- },
198
- "mappingConfig": {
199
- // How to transform data
200
- "version": "1.0",
201
- "fields": {
202
- "ref": { "source": "location_id", "required": true },
203
- "name": { "source": "location_name" },
204
- "status": { "value": "ACTIVE" }
205
- }
206
- },
207
- "loadConfig": {
208
- // How to load into Fluent
209
- "strategy": "graphql",
210
- "mutation": "createLocation",
211
- "batchSize": 100
212
- }
213
- }
214
- ```
215
-
216
- **Reusability**: Change `entityType` to "product", update field mappings → same code loads products!
217
-
218
- ### Works for ANY Entity Type
219
-
220
- This pattern is entity-agnostic because:
221
-
222
- 1. **Generic Source Reading**: S3DataSource/SftpDataSource work with any file
223
- 2. **Format-Agnostic Parsing**: Parsers handle CSV/JSON/XML regardless of entity type
224
- 3. **Flexible Mapping**: UniversalMapper adapts to any source→target schema
225
- 4. **Mutation Generation**: GraphQLMutationMapper works with any mutation
226
- 5. **State Management**: StateService tracks processing for any entity
227
-
228
- **Example Entity Types:**
229
-
230
- | Entity | Source Format | Mutation | Complexity |
231
- | ---------- | ------------- | ---------------- | ------------------- |
232
- | Locations | CSV | `createLocation` | Simple |
233
- | Products | JSON | `createProduct` | Medium (variants) |
234
- | Controls | JSON | `createControl` | Simple |
235
- | Carriers | XML | `createCarrier` | Simple |
236
- | Categories | JSON | `createCategory` | Medium (hierarchy) |
237
- | Customers | CSV | `createCustomer` | Medium (attributes) |
238
-
239
- All use the **same pipeline** with different **configurations**.
240
-
241
- ---
242
-
243
- ## Example 1: Location Master Data
244
-
245
- ### Overview
246
-
247
- Load store/warehouse locations from CSV files into Fluent Commerce.
248
-
249
- **Business Context:**
250
-
251
- - Retail locations change infrequently (openings, closings, updates)
252
- - Source: Retail management system exports CSV daily
253
- - Destination: Fluent Location entities
254
- - Frequency: Daily batch, event-driven on new file
255
-
256
- ### Source Data Formats
257
-
258
- **CSV Format:**
259
-
260
- ```csv
261
- location_id,location_name,type,address_line1,city,state,zip,country,latitude,longitude,status
262
- LOC001,Downtown Store,STORE,123 Main St,New York,NY,10001,US,40.7128,-74.0060,ACTIVE
263
- LOC002,Warehouse East,WAREHOUSE,456 Industrial Rd,Newark,NJ,07102,US,40.7357,-74.1724,ACTIVE
264
- LOC003,Pop-Up Shop,STORE,789 Fashion Ave,New York,NY,10018,US,40.7549,-73.9840,INACTIVE
265
- ```
266
-
267
- **JSON Format:**
268
-
269
- ```json
270
- {
271
- "locations": [
272
- {
273
- "locationId": "LOC001",
274
- "name": "Downtown Store",
275
- "type": "STORE",
276
- "address": {
277
- "street": "123 Main St",
278
- "city": "New York",
279
- "state": "NY",
280
- "zip": "10001",
281
- "country": "US"
282
- },
283
- "coordinates": {
284
- "lat": 40.7128,
285
- "lng": -74.006
286
- },
287
- "status": "ACTIVE"
288
- }
289
- ]
290
- }
291
- ```
292
-
293
- **XML Format:**
294
-
295
- ```xml
296
- <?xml version="1.0" encoding="UTF-8"?>
297
- <LocationFeed>
298
- <Location>
299
- <ID>LOC001</ID>
300
- <Name>Downtown Store</Name>
301
- <Type>STORE</Type>
302
- <Address>
303
- <Line1>123 Main St</Line1>
304
- <City>New York</City>
305
- <State>NY</State>
306
- <Zip>10001</Zip>
307
- <Country>US</Country>
308
- </Address>
309
- <Latitude>40.7128</Latitude>
310
- <Longitude>-74.0060</Longitude>
311
- <Status>ACTIVE</Status>
312
- </Location>
313
- </LocationFeed>
314
- ```
315
-
316
- ### Field Mapping Configuration
317
-
318
- **`config/location-mapping.json`:**
319
-
320
- ```json
321
- {
322
- "version": "1.0",
323
- "description": "Map external location data to Fluent Location schema",
324
- "fields": {
325
- "ref": {
326
- "source": "location_id",
327
- "required": true,
328
- "resolver": "sdk.trim"
329
- },
330
- "type": {
331
- "source": "type",
332
- "required": true,
333
- "resolver": "sdk.uppercase"
334
- },
335
- "name": {
336
- "source": "location_name",
337
- "required": true
338
- },
339
- "status": {
340
- "source": "status",
341
- "defaultValue": "ACTIVE",
342
- "resolver": "sdk.uppercase"
343
- },
344
- "primaryAddress": {
345
- "fields": {
346
- "street": { "source": "address_line1" },
347
- "city": { "source": "city" },
348
- "state": { "source": "state" },
349
- "postcode": { "source": "zip" },
350
- "country": { "source": "country" }
351
- }
352
- },
353
- "coordinates": {
354
- "fields": {
355
- "latitude": { "source": "latitude", "resolver": "sdk.parseFloat" },
356
- "longitude": { "source": "longitude", "resolver": "sdk.parseFloat" }
357
- }
358
- },
359
- "retailerId": {
360
- "value": "${RETAILER_ID}",
361
- "required": true
362
- }
363
- }
364
- }
365
- ```
366
-
367
- **Key Features:**
368
-
369
- - ✅ Required field validation (`ref`, `type`, `name`)
370
- - ✅ Default values (`status` defaults to "ACTIVE")
371
- - ✅ Built-in resolvers (`sdk.trim`, `sdk.uppercase`, `sdk.parseFloat`)
372
- - ✅ Nested object mapping (`primaryAddress`, `coordinates`)
373
- - ✅ Environment variable support (`${RETAILER_ID}`)
374
-
375
- ### GraphQL Mutation Approach
376
-
377
- **Target Mutation:**
378
-
379
- ```graphql
380
- mutation CreateLocation($input: CreateLocationInput!) {
381
- createLocation(input: $input) {
382
- id
383
- ref
384
- name
385
- status
386
- }
387
- }
388
- ```
389
-
390
- **Mutation Variables (after transformation):**
391
-
392
- ```json
393
- {
394
- "input": {
395
- "ref": "LOC001",
396
- "type": "STORE",
397
- "name": "Downtown Store",
398
- "status": "ACTIVE",
399
- "primaryAddress": {
400
- "street": "123 Main St",
401
- "city": "New York",
402
- "state": "NY",
403
- "postcode": "10001",
404
- "country": "US"
405
- },
406
- "coordinates": {
407
- "latitude": 40.7128,
408
- "longitude": -74.006
409
- },
410
- "retailerId": "my-retailer"
411
- }
412
- }
413
- ```
414
-
415
- ### Complete Working Code
416
-
417
- **`location-etl.ts`:**
418
-
419
- ```typescript
420
- // FC Connect SDK+
421
- // Install: npm install @fluentcommerce/fc-connect-sdk@latest
422
- // Docs: https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk
423
- // GitHub: https://github.com/fluentcommerce/fc-connect-sdk
424
-
425
- import { createClient } from '@fluentcommerce/fc-connect-sdk';
426
- import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
427
- import { CSVParserService } from '@fluentcommerce/fc-connect-sdk';
428
- import { UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
429
- import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
430
- // Access openKv from context: const { openKv } = ctx;
431
- import * as fs from 'fs';
432
-
433
- // Initialize state service (prevents duplicate processing)
434
- // ✅ CORRECT: Access openKv from Versori context
435
- export async function masterDataETL(ctx: any, configPath: string) {
436
- // Load configuration
437
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
438
-
439
- // Initialize SDK components
440
- const logger = console; // Replace with proper logger
441
- const { openKv } = ctx;
442
- const fluentClient = await createClient({
443
- config: {
444
- baseUrl: process.env.FLUENT_BASE_URL!,
445
- clientId: process.env.FLUENT_CLIENT_ID!,
446
- clientSecret: process.env.FLUENT_CLIENT_SECRET!,
447
- retailerId: process.env.FLUENT_RETAILER_ID!,
448
- },
449
- logger,
450
- });
451
-
452
- // Initialize data source (S3 in this example)
453
- const s3DataSource = new S3DataSource(
454
- {
455
- type: 'S3_CSV',
456
- s3Config: {
457
- bucket: config.sourceConfig.bucket,
458
- region: process.env.AWS_REGION!,
459
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
460
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
461
- },
462
- },
463
- logger
464
- );
465
-
466
- // Initialize state service (prevents duplicate processing)
467
- const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
468
- const stateService = new StateService(logger);
469
-
470
- // Initialize parser based on format
471
- const parser = config.parseConfig.format === 'csv' ? new CSVParserService() : null; // Add JSON/XML parsers as needed
472
-
473
- // Initialize mapper
474
- const mapper = new UniversalMapper(config.mappingConfig, { logger, fluentClient });
475
-
476
- logger.info(`Starting ${config.entityType} ETL process`);
477
-
478
- try {
479
- // PHASE 1: EXTRACT - List files from source
480
- const files = await s3DataSource.listFiles({
481
- prefix: config.sourceConfig.prefix,
482
- });
483
-
484
- logger.info(`Found ${files.length} files to process`);
485
-
486
- // Process each file
487
- for (const file of files) {
488
- const fileKey = `${config.entityType}:${file.name}`;
489
-
490
- // Check if already processed
491
- const alreadyProcessed = await stateService.isFileProcessed(kvAdapter, fileKey);
492
- if (alreadyProcessed) {
493
- logger.info(`Skipping already processed file: ${file.name}`);
494
- continue;
495
- }
496
-
497
- logger.info(`Processing file: ${file.name}`);
498
-
499
- try {
500
- // PHASE 2: PARSE - Download and parse file
501
- const fileContent = await s3DataSource.downloadFile(file.path);
502
- const records = await parser!.parse(fileContent as string);
503
-
504
- logger.info(`Parsed ${records.length} records from ${file.name}`);
505
-
506
- // PHASE 3: TRANSFORM - Map each record
507
- const transformedRecords = [];
508
- for (const record of records) {
509
- const result = await mapper.map(record);
510
- if (result.success) {
511
- transformedRecords.push(result.data);
512
- } else {
513
- logger.error(`Mapping failed for record:`, {
514
- record,
515
- errors: result.errors,
516
- });
517
- }
518
- }
519
-
520
- logger.info(`Transformed ${transformedRecords.length} records successfully`);
521
-
522
- // PHASE 4: LOAD - Submit to Fluent Commerce
523
- if (config.loadConfig.strategy === 'graphql') {
524
- await loadViaGraphQL(fluentClient, transformedRecords, config.loadConfig, logger);
525
- } else if (config.loadConfig.strategy === 'event') {
526
- await loadViaEventAPI(fluentClient, transformedRecords, config.loadConfig, logger);
527
- }
528
-
529
- // Update sync state with processed file metadata
530
- await stateService.updateSyncState(
531
- kvAdapter,
532
- [
533
- {
534
- fileName: file.name,
535
- lastModified: new Date().toISOString(),
536
- recordCount: transformedRecords.length,
537
- },
538
- ],
539
- 'master-data-etl'
540
- );
541
-
542
- logger.info(`Successfully processed file: ${file.name}`);
543
- } catch (error) {
544
- logger.error(`Failed to process file: ${file.name}`, error);
545
- // Continue to next file instead of failing entire batch
546
- continue;
547
- }
548
- }
549
-
550
- logger.info(`${config.entityType} ETL process completed`);
551
- } catch (error) {
552
- logger.error(`${config.entityType} ETL process failed`, error);
553
- throw error;
554
- }
555
- }
556
-
557
- /**
558
- * Load data via GraphQL mutations
559
- */
560
- async function loadViaGraphQL(client: any, records: any[], loadConfig: any, logger: any) {
561
- const batchSize = loadConfig.batchSize || 100;
562
- const mutation = loadConfig.mutation;
563
-
564
- logger.info(`Loading ${records.length} records via GraphQL mutation: ${mutation}`);
565
-
566
- // Process in batches
567
- for (let i = 0; i < records.length; i += batchSize) {
568
- const batch = records.slice(i, i + batchSize);
569
- logger.info(`Processing batch ${i / batchSize + 1} (${batch.length} records)`);
570
-
571
- // Execute mutations for batch
572
- for (const record of batch) {
573
- const query = `
574
- mutation ${mutation}($input: ${capitalize(mutation)}Input!) {
575
- ${mutation}(input: $input) {
576
- id
577
- ref
578
- }
579
- }
580
- `;
581
-
582
- try {
583
- const result = await client.graphql({
584
- query,
585
- variables: { input: record },
586
- });
587
-
588
- logger.debug(`Created ${mutation}:`, result.data[mutation]);
589
- } catch (error) {
590
- logger.error(`Failed to create ${mutation}:`, { record, error });
591
- // Continue to next record instead of failing entire batch
592
- }
593
- }
594
- }
595
-
596
- logger.info(`GraphQL load completed`);
597
- }
598
-
599
- /**
600
- * Load data via Event API
601
- */
602
- async function loadViaEventAPI(client: any, records: any[], loadConfig: any, logger: any) {
603
- const eventName = loadConfig.eventName;
604
-
605
- logger.info(`Loading ${records.length} records via Event API: ${eventName}`);
606
-
607
- for (const record of records) {
608
- try {
609
- await client.sendEvent({
610
- name: eventName,
611
- entityRef: record.ref,
612
- entityType: loadConfig.entityType,
613
- retailerId: record.retailerId,
614
- attributes: record,
615
- });
616
-
617
- logger.debug(`Sent event ${eventName} for ${record.ref}`);
618
- } catch (error) {
619
- logger.error(`Failed to send event for ${record.ref}:`, error);
620
- }
621
- }
622
-
623
- logger.info(`Event API load completed`);
624
- }
625
-
626
- // Helper function
627
- function capitalize(str: string): string {
628
- return str.charAt(0).toUpperCase() + str.slice(1);
629
- }
630
-
631
- // Example usage
632
- if (require.main === module) {
633
- masterDataETL('config/location-etl-config.json')
634
- .then(() => console.log('ETL completed'))
635
- .catch(err => {
636
- console.error('ETL failed:', err);
637
- process.exit(1);
638
- });
639
- }
640
- ```
641
-
642
- **Configuration File (`config/location-etl-config.json`):**
643
-
644
- ```json
645
- {
646
- "entityType": "location",
647
- "sourceConfig": {
648
- "type": "S3_CSV",
649
- "bucket": "master-data",
650
- "prefix": "locations/",
651
- "filePattern": "*.csv"
652
- },
653
- "parseConfig": {
654
- "format": "csv",
655
- "delimiter": ",",
656
- "headers": true
657
- },
658
- "mappingConfig": {
659
- "version": "1.0",
660
- "fields": {
661
- "ref": { "source": "location_id", "required": true },
662
- "name": { "source": "location_name", "required": true },
663
- "type": { "source": "type", "required": true },
664
- "status": { "source": "status", "defaultValue": "ACTIVE" },
665
- "primaryAddress": {
666
- "fields": {
667
- "street": { "source": "address_line1" },
668
- "city": { "source": "city" },
669
- "state": { "source": "state" },
670
- "postcode": { "source": "zip" },
671
- "country": { "source": "country" }
672
- }
673
- },
674
- "retailerId": { "value": "${RETAILER_ID}" }
675
- }
676
- },
677
- "loadConfig": {
678
- "strategy": "graphql",
679
- "mutation": "createLocation",
680
- "batchSize": 100
681
- }
682
- }
683
- ```
684
-
685
- **Key Design Decisions:**
686
-
687
- 1. **Configuration-Driven**: All entity logic in JSON config, not code
688
- 2. **Error Handling**: Continue processing on individual record failures
689
- 3. **State Management**: Prevent duplicate processing of files
690
- 4. **Batching**: Process large datasets in manageable chunks
691
- 5. **Logging**: Comprehensive logging for debugging and monitoring
692
-
693
- ---
694
-
695
- ## Example 2: Product Catalog
696
-
697
- ### Overview
698
-
699
- Load product catalog with variants (parent-child relationships) from JSON files.
700
-
701
- **Business Context:**
702
-
703
- - Product data includes parent products and child variants (size, color, etc.)
704
- - Source: Product Information Management (PIM) system exports JSON
705
- - Destination: Fluent Product entities
706
- - Complexity: Parent-child relationships require nested mapping
707
-
708
- ### Source Data Formats
709
-
710
- **JSON Format (with variants):**
711
-
712
- ```json
713
- {
714
- "products": [
715
- {
716
- "productId": "PROD001",
717
- "name": "Classic T-Shirt",
718
- "description": "Premium cotton t-shirt",
719
- "brand": "MyBrand",
720
- "category": "Apparel",
721
- "variants": [
722
- {
723
- "sku": "PROD001-S-RED",
724
- "size": "S",
725
- "color": "Red",
726
- "price": 29.99,
727
- "barcode": "123456789001"
728
- },
729
- {
730
- "sku": "PROD001-M-RED",
731
- "size": "M",
732
- "color": "Red",
733
- "price": 29.99,
734
- "barcode": "123456789002"
735
- }
736
- ]
737
- }
738
- ]
739
- }
740
- ```
741
-
742
- **CSV Format (flattened variants):**
743
-
744
- ```csv
745
- product_id,product_name,sku,size,color,price,barcode
746
- PROD001,Classic T-Shirt,PROD001-S-RED,S,Red,29.99,123456789001
747
- PROD001,Classic T-Shirt,PROD001-M-RED,M,Red,29.99,123456789002
748
- ```
749
-
750
- ### Field Mapping Configuration
751
-
752
- **`config/product-mapping.json`:**
753
-
754
- ```json
755
- {
756
- "version": "1.0",
757
- "description": "Map PIM product data to Fluent Product schema",
758
- "fields": {
759
- "ref": {
760
- "source": "productId",
761
- "required": true
762
- },
763
- "type": {
764
- "value": "STANDARD",
765
- "required": true
766
- },
767
- "name": {
768
- "source": "name",
769
- "required": true
770
- },
771
- "summary": {
772
- "source": "description"
773
- },
774
- "gtin": {
775
- "source": "barcode"
776
- },
777
- "status": {
778
- "value": "ACTIVE"
779
- },
780
- "retailerId": {
781
- "value": "${RETAILER_ID}"
782
- },
783
- "attributes": {
784
- "fields": {
785
- "brand": { "source": "brand" },
786
- "category": { "source": "category" }
787
- }
788
- },
789
- "variants": {
790
- "source": "variants",
791
- "isArray": true,
792
- "fields": {
793
- "ref": { "source": "$.sku", "required": true },
794
- "attributes": {
795
- "fields": {
796
- "size": { "source": "$.size" },
797
- "color": { "source": "$.color" }
798
- }
799
- },
800
- "prices": {
801
- "fields": {
802
- "currency": { "value": "USD" },
803
- "value": { "source": "$.price", "resolver": "sdk.parseFloat" }
804
- }
805
- },
806
- "gtin": { "source": "$.barcode" }
807
- }
808
- }
809
- }
810
- }
811
- ```
812
-
813
- **Key Features:**
814
-
815
- - ✅ Parent-child relationships (`variants[]` array mapping)
816
- - ✅ Relative paths within arrays (`$.sku` references current variant)
817
- - ✅ Nested objects (`attributes`, `prices`)
818
- - ✅ Static values (`type: "STANDARD"`, `currency: "USD"`)
819
-
820
- ### Complete Working Code
821
-
822
- **`product-etl.ts`:**
823
-
824
- ```typescript
825
- import { createClient } from '@fluentcommerce/fc-connect-sdk';
826
- import { S3DataSource, JSONParserService } from '@fluentcommerce/fc-connect-sdk';
827
- import { UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
828
- import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
829
- // Access openKv from context: const { openKv } = ctx;
830
- import * as fs from 'fs';
831
-
832
- /**
833
- * Product Catalog ETL with Parent-Child Relationships
834
- */
835
- export async function productCatalogETL(ctx: any) {
836
- const logger = console;
837
- const { openKv } = ctx;
838
- const fluentClient = await createClient({
839
- config: {
840
- baseUrl: process.env.FLUENT_BASE_URL!,
841
- clientId: process.env.FLUENT_CLIENT_ID!,
842
- clientSecret: process.env.FLUENT_CLIENT_SECRET!,
843
- retailerId: process.env.FLUENT_RETAILER_ID!,
844
- },
845
- logger,
846
- });
847
-
848
- // Initialize components
849
- const s3DataSource = new S3DataSource(
850
- {
851
- type: 'S3_CSV',
852
- s3Config: {
853
- bucket: 'product-catalog',
854
- region: process.env.AWS_REGION!,
855
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
856
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
857
- },
858
- },
859
- logger
860
- );
861
-
862
- const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
863
- const stateService = new StateService(logger);
864
-
865
- const jsonParser = new JSONParserService();
866
-
867
- // Load mapping configuration
868
- const mappingConfig = JSON.parse(fs.readFileSync('config/product-mapping.json', 'utf-8'));
869
-
870
- const mapper = new UniversalMapper(mappingConfig, { logger, fluentClient });
871
-
872
- logger.info('Starting product catalog ETL');
873
-
874
- try {
875
- // List JSON files
876
- const files = await s3DataSource.listFiles({ prefix: 'products/' });
877
-
878
- for (const file of files) {
879
- if (!file.name.endsWith('.json')) continue;
880
-
881
- const fileKey = `product:${file.name}`;
882
-
883
- // Check if processed
884
- if (await stateService.isFileProcessed(kvAdapter, fileKey)) {
885
- logger.info(`Skipping processed file: ${file.name}`);
886
- continue;
887
- }
888
-
889
- logger.info(`Processing file: ${file.name}`);
890
-
891
- try {
892
- // Download and parse JSON
893
- const fileContent = await s3DataSource.downloadFile(file.path);
894
- const data = await jsonParser.parse(fileContent as string, {
895
- dataPath: 'products', // Extract products array from root
896
- });
897
-
898
- const products = Array.isArray(data) ? data : [data];
899
- logger.info(`Parsed ${products.length} products from ${file.name}`);
900
-
901
- // Transform and load each product
902
- for (const product of products) {
903
- const result = await mapper.map(product);
904
-
905
- if (result.success) {
906
- // Create parent product
907
- await createProduct(fluentClient, result.data, logger);
908
-
909
- // Create variants if present
910
- if (result.data.variants && result.data.variants.length > 0) {
911
- for (const variant of result.data.variants) {
912
- await createVariant(fluentClient, result.data.ref, variant, logger);
913
- }
914
- }
915
- } else {
916
- logger.error('Product mapping failed:', {
917
- product,
918
- errors: result.errors,
919
- });
920
- }
921
- }
922
-
923
- // Mark as processed
924
- await stateService.updateSyncState(
925
- kvAdapter,
926
- [
927
- {
928
- fileName: file.name,
929
- lastModified: new Date().toISOString(),
930
- recordCount: products.length,
931
- },
932
- ],
933
- 'product-catalog-etl'
934
- );
935
-
936
- logger.info(`Successfully processed: ${file.name}`);
937
- } catch (error) {
938
- logger.error(`Failed to process file: ${file.name}`, error);
939
- continue;
940
- }
941
- }
942
-
943
- logger.info('Product catalog ETL completed');
944
- } catch (error) {
945
- logger.error('Product catalog ETL failed', error);
946
- throw error;
947
- }
948
- }
949
-
950
- /**
951
- * Create product via GraphQL
952
- */
953
- async function createProduct(client: any, productData: any, logger: any) {
954
- const mutation = `
955
- mutation CreateProduct($input: CreateProductInput!) {
956
- createProduct(input: $input) {
957
- id
958
- ref
959
- name
960
- status
961
- }
962
- }
963
- `;
964
-
965
- try {
966
- const result = await client.graphql({
967
- query: mutation,
968
- variables: { input: productData },
969
- });
970
-
971
- logger.info(`Created product: ${productData.ref}`, result.data.createProduct);
972
- } catch (error) {
973
- logger.error(`Failed to create product: ${productData.ref}`, error);
974
- throw error;
975
- }
976
- }
977
-
978
- /**
979
- * Create variant via GraphQL
980
- */
981
- async function createVariant(client: any, parentRef: string, variantData: any, logger: any) {
982
- const mutation = `
983
- mutation CreateVariant($input: CreateVariantInput!) {
984
- createVariant(input: $input) {
985
- id
986
- ref
987
- attributes
988
- }
989
- }
990
- `;
991
-
992
- try {
993
- const result = await client.graphql({
994
- query: mutation,
995
- variables: {
996
- input: {
997
- ...variantData,
998
- parentRef,
999
- },
1000
- },
1001
- });
1002
-
1003
- logger.info(`Created variant: ${variantData.ref}`, result.data.createVariant);
1004
- } catch (error) {
1005
- logger.error(`Failed to create variant: ${variantData.ref}`, error);
1006
- // Don't throw - continue with other variants
1007
- }
1008
- }
1009
-
1010
- // Example usage
1011
- if (require.main === module) {
1012
- productCatalogETL()
1013
- .then(() => console.log('ETL completed'))
1014
- .catch(err => {
1015
- console.error('ETL failed:', err);
1016
- process.exit(1);
1017
- });
1018
- }
1019
- ```
1020
-
1021
- **Key Design Decisions:**
1022
-
1023
- 1. **Parent-Child Processing**: Create parent product first, then variants
1024
- 2. **Array Mapping**: `variants[]` notation handles nested arrays
1025
- 3. **Relative Paths**: `$.sku` resolves relative to current variant item
1026
- 4. **Error Isolation**: Variant creation failures don't stop parent processing
1027
-
1028
- ---
1029
-
1030
- ## Example 3: Control/Config Data
1031
-
1032
- ### Overview
1033
-
1034
- Load business rules and configuration parameters from JSON files.
1035
-
1036
- **Business Context:**
1037
-
1038
- - Controls define business rules, thresholds, flags
1039
- - Source: Configuration management system exports JSON
1040
- - Destination: Fluent Control entities
1041
- - Characteristics: Simple structure, validation logic important
1042
-
1043
- ### Source Data Format
1044
-
1045
- **JSON Format:**
1046
-
1047
- ```json
1048
- {
1049
- "controls": [
1050
- {
1051
- "name": "ORDER_TIMEOUT_MINUTES",
1052
- "value": "30",
1053
- "type": "INTEGER",
1054
- "context": "ORDER_MANAGEMENT",
1055
- "description": "Order allocation timeout in minutes"
1056
- },
1057
- {
1058
- "name": "ENABLE_AUTO_ALLOCATION",
1059
- "value": "true",
1060
- "type": "BOOLEAN",
1061
- "context": "ORDER_MANAGEMENT",
1062
- "description": "Enable automatic order allocation"
1063
- },
1064
- {
1065
- "name": "DEFAULT_CARRIER",
1066
- "value": "FEDEX",
1067
- "type": "STRING",
1068
- "context": "FULFILLMENT",
1069
- "description": "Default shipping carrier"
1070
- }
1071
- ]
1072
- }
1073
- ```
1074
-
1075
- ### Field Mapping Configuration
1076
-
1077
- **`config/control-mapping.json`:**
1078
-
1079
- ```json
1080
- {
1081
- "version": "1.0",
1082
- "description": "Map configuration controls to Fluent Control schema",
1083
- "fields": {
1084
- "name": {
1085
- "source": "name",
1086
- "required": true,
1087
- "resolver": "sdk.uppercase"
1088
- },
1089
- "value": {
1090
- "source": "value",
1091
- "required": true,
1092
- "resolver": "custom.validateControlValue"
1093
- },
1094
- "type": {
1095
- "source": "type",
1096
- "required": true,
1097
- "resolver": "sdk.uppercase"
1098
- },
1099
- "context": {
1100
- "source": "context",
1101
- "defaultValue": "GLOBAL"
1102
- },
1103
- "description": {
1104
- "source": "description"
1105
- },
1106
- "status": {
1107
- "value": "ACTIVE"
1108
- },
1109
- "retailerId": {
1110
- "value": "${RETAILER_ID}"
1111
- }
1112
- }
1113
- }
1114
- ```
1115
-
1116
- ### Validation Logic
1117
-
1118
- **Custom resolver for control value validation:**
1119
-
1120
- ```typescript
1121
- import { FieldResolverFunction } from '@fluentcommerce/fc-connect-sdk';
1122
-
1123
- /**
1124
- * Validate control value based on type
1125
- */
1126
- export const validateControlValue: FieldResolverFunction = (
1127
- value: any,
1128
- sourceData: any,
1129
- config: any,
1130
- helpers: any
1131
- ) => {
1132
- const type = sourceData.type;
1133
-
1134
- switch (type) {
1135
- case 'INTEGER':
1136
- const intValue = helpers.parseIntSafe(value, null);
1137
- if (intValue === null) {
1138
- throw new Error(`Invalid INTEGER value: ${value}`);
1139
- }
1140
- return intValue.toString();
1141
-
1142
- case 'FLOAT':
1143
- const floatValue = helpers.parseFloatSafe(value, null);
1144
- if (floatValue === null) {
1145
- throw new Error(`Invalid FLOAT value: ${value}`);
1146
- }
1147
- return floatValue.toString();
1148
-
1149
- case 'BOOLEAN':
1150
- if (!['true', 'false'].includes(String(value).toLowerCase())) {
1151
- throw new Error(`Invalid BOOLEAN value: ${value}`);
1152
- }
1153
- return String(value).toLowerCase();
1154
-
1155
- case 'STRING':
1156
- return String(value);
1157
-
1158
- case 'JSON':
1159
- try {
1160
- JSON.parse(value);
1161
- return value;
1162
- } catch (error) {
1163
- throw new Error(`Invalid JSON value: ${value}`);
1164
- }
1165
-
1166
- default:
1167
- helpers.log.warn(`Unknown control type: ${type}, treating as STRING`);
1168
- return String(value);
1169
- }
1170
- };
1171
- ```
1172
-
1173
- ### Complete Working Code
1174
-
1175
- **`control-etl.ts`:**
1176
-
1177
- ```typescript
1178
- import { createClient } from '@fluentcommerce/fc-connect-sdk';
1179
- import { S3DataSource, JSONParserService } from '@fluentcommerce/fc-connect-sdk';
1180
- import { UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
1181
- import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
1182
- // Access openKv from context: const { openKv } = ctx;
1183
- import * as fs from 'fs';
1184
- import { validateControlValue } from './resolvers/control-validators';
1185
-
1186
- /**
1187
- * Control/Config Data ETL
1188
- */
1189
- export async function controlDataETL(ctx: any) {
1190
- const logger = console;
1191
- const { openKv } = ctx;
1192
- const fluentClient = await createClient({
1193
- config: {
1194
- baseUrl: process.env.FLUENT_BASE_URL!,
1195
- clientId: process.env.FLUENT_CLIENT_ID!,
1196
- clientSecret: process.env.FLUENT_CLIENT_SECRET!,
1197
- retailerId: process.env.FLUENT_RETAILER_ID!,
1198
- },
1199
- logger,
1200
- });
1201
-
1202
- // Initialize components
1203
- const s3DataSource = new S3DataSource(
1204
- {
1205
- type: 'S3_CSV',
1206
- s3Config: {
1207
- bucket: 'config-data',
1208
- region: process.env.AWS_REGION!,
1209
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
1210
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
1211
- },
1212
- },
1213
- logger
1214
- );
1215
-
1216
- const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
1217
- const stateService = new StateService(logger);
1218
-
1219
- const jsonParser = new JSONParserService();
1220
-
1221
- // Load mapping configuration
1222
- const mappingConfig = JSON.parse(fs.readFileSync('config/control-mapping.json', 'utf-8'));
1223
-
1224
- // Initialize mapper with custom resolvers
1225
- const mapper = new UniversalMapper(mappingConfig, {
1226
- logger,
1227
- fluentClient,
1228
- customResolvers: {
1229
- 'custom.validateControlValue': validateControlValue,
1230
- },
1231
- });
1232
-
1233
- logger.info('Starting control data ETL');
1234
-
1235
- try {
1236
- // List JSON files
1237
- const files = await s3DataSource.listFiles({ prefix: 'controls/' });
1238
-
1239
- for (const file of files) {
1240
- if (!file.name.endsWith('.json')) continue;
1241
-
1242
- const fileKey = `control:${file.name}`;
1243
-
1244
- // Check if processed
1245
- if (await stateService.isFileProcessed(kvAdapter, fileKey)) {
1246
- logger.info(`Skipping processed file: ${file.name}`);
1247
- continue;
1248
- }
1249
-
1250
- logger.info(`Processing file: ${file.name}`);
1251
-
1252
- try {
1253
- // Download and parse JSON
1254
- const fileContent = await s3DataSource.downloadFile(file.path);
1255
- const data = await jsonParser.parse(fileContent as string, {
1256
- dataPath: 'controls',
1257
- });
1258
-
1259
- const controls = Array.isArray(data) ? data : [data];
1260
- logger.info(`Parsed ${controls.length} controls from ${file.name}`);
1261
-
1262
- // Transform and load each control
1263
- let successCount = 0;
1264
- let failureCount = 0;
1265
-
1266
- for (const control of controls) {
1267
- try {
1268
- const result = await mapper.map(control);
1269
-
1270
- if (result.success) {
1271
- await createControl(fluentClient, result.data, logger);
1272
- successCount++;
1273
- } else {
1274
- logger.error('Control mapping failed:', {
1275
- control,
1276
- errors: result.errors,
1277
- });
1278
- failureCount++;
1279
- }
1280
- } catch (error) {
1281
- logger.error(`Failed to process control: ${control.name}`, error);
1282
- failureCount++;
1283
- }
1284
- }
1285
-
1286
- logger.info(`Control processing summary:`, {
1287
- total: controls.length,
1288
- success: successCount,
1289
- failures: failureCount,
1290
- });
1291
-
1292
- // Mark as processed
1293
- await stateService.updateSyncState(
1294
- kvAdapter,
1295
- [
1296
- {
1297
- fileName: file.name,
1298
- lastModified: new Date().toISOString(),
1299
- recordCount: controls.length,
1300
- },
1301
- ],
1302
- 'control-data-etl'
1303
- );
1304
-
1305
- logger.info(`Successfully processed: ${file.name}`);
1306
- } catch (error) {
1307
- logger.error(`Failed to process file: ${file.name}`, error);
1308
- continue;
1309
- }
1310
- }
1311
-
1312
- logger.info('Control data ETL completed');
1313
- } catch (error) {
1314
- logger.error('Control data ETL failed', error);
1315
- throw error;
1316
- }
1317
- }
1318
-
1319
- /**
1320
- * Create control via GraphQL
1321
- */
1322
- async function createControl(client: any, controlData: any, logger: any) {
1323
- const mutation = `
1324
- mutation CreateControl($input: CreateControlInput!) {
1325
- createControl(input: $input) {
1326
- id
1327
- name
1328
- value
1329
- type
1330
- context
1331
- }
1332
- }
1333
- `;
1334
-
1335
- try {
1336
- const result = await client.graphql({
1337
- query: mutation,
1338
- variables: { input: controlData },
1339
- });
1340
-
1341
- logger.info(`Created control: ${controlData.name}`, {
1342
- value: controlData.value,
1343
- type: controlData.type,
1344
- });
1345
- } catch (error) {
1346
- logger.error(`Failed to create control: ${controlData.name}`, error);
1347
- throw error;
1348
- }
1349
- }
1350
-
1351
- // Example usage
1352
- if (require.main === module) {
1353
- controlDataETL()
1354
- .then(() => console.log('ETL completed'))
1355
- .catch(err => {
1356
- console.error('ETL failed:', err);
1357
- process.exit(1);
1358
- });
1359
- }
1360
- ```
1361
-
1362
- **Key Design Decisions:**
1363
-
1364
- 1. **Type Validation**: Custom resolver validates value based on type field
1365
- 2. **Error Tracking**: Count successes/failures for summary reporting
1366
- 3. **Atomic Processing**: Each control processed independently
1367
- 4. **Detailed Logging**: Track validation failures with context
1368
-
1369
- ---
1370
-
1371
- ## Source Strategies
1372
-
1373
- ### S3 with Event Notifications
1374
-
1375
- **Use Case**: Process files as soon as they're uploaded to S3
1376
-
1377
- **Setup:**
1378
-
1379
- ```typescript
1380
- import { webhook } from '@versori/run/webhooks';
1381
- import { masterDataETL } from './location-etl';
1382
-
1383
- export const s3LocationETL = webhook('s3-location-upload', {
1384
- response: { mode: 'sync' },
1385
- })
1386
- .then(async ({ data }) => {
1387
- // Parse S3 event notification
1388
- const s3Event = data.Records[0].s3;
1389
- const bucket = s3Event.bucket.name;
1390
- const key = decodeURIComponent(s3Event.object.key.replace(/\+/g, ' '));
1391
-
1392
- console.log(`S3 event received: ${bucket}/${key}`);
1393
-
1394
- // Run ETL for this specific file
1395
- await masterDataETL('config/location-etl-config.json');
1396
-
1397
- return { success: true, message: 'Location ETL completed' };
1398
- })
1399
- .catch(({ error }) => {
1400
- console.error('S3 location ETL failed:', error);
1401
- return { success: false, error: error.message };
1402
- });
1403
- ```
1404
-
1405
- **S3 Bucket Configuration:**
1406
-
1407
- ```json
1408
- {
1409
- "LambdaFunctionConfigurations": [
1410
- {
1411
- "LambdaFunctionArn": "arn:aws:lambda:...:function:versori-webhook",
1412
- "Events": ["s3:ObjectCreated:*"],
1413
- "Filter": {
1414
- "Key": {
1415
- "FilterRules": [
1416
- {
1417
- "Name": "prefix",
1418
- "Value": "locations/"
1419
- },
1420
- {
1421
- "Name": "suffix",
1422
- "Value": ".csv"
1423
- }
1424
- ]
1425
- }
1426
- }
1427
- }
1428
- ]
1429
- }
1430
- ```
1431
-
1432
- ### SFTP with Polling
1433
-
1434
- **Use Case**: Poll SFTP server periodically for new files
1435
-
1436
- #### SFTP Credential Access
1437
-
1438
- **Versori Platform** has three methods for accessing SFTP credentials:
1439
-
1440
- 1. **Connection Variables (Recommended)** - Direct access to connection config:
1441
- ```typescript
1442
- const { host, port, username, password, privateKey } = ctx.activation.connections.sftp_server;
1443
- ```
1444
-
1445
- 2. **Credentials API** - For base64-encoded credentials:
1446
- ```typescript
1447
- const creds = await ctx.credentials().getAccessToken('sftp_server');
1448
- ```
1449
-
1450
- 3. **Connection String Parsing** - Decode `connectionVariables.connectionString`:
1451
- ```typescript
1452
- const connStr = ctx.activation.connections.sftp_server.connectionString;
1453
- // Parse: sftp://username:password@host:port
1454
- ```
1455
-
1456
- **Standalone Node.js/Deno**: Use environment variables directly:
1457
- ```typescript
1458
- const config = {
1459
- host: process.env.SFTP_HOST!,
1460
- port: parseInt(process.env.SFTP_PORT || '22'),
1461
- username: process.env.SFTP_USERNAME!,
1462
- password: process.env.SFTP_PASSWORD,
1463
- privateKey: process.env.SFTP_PRIVATE_KEY,
1464
- };
1465
- ```
1466
-
1467
- **Security Best Practices:**
1468
- - Always prefer SSH keys over passwords
1469
- - Never log credential values
1470
- - Use scoped credentials (read-only when possible)
1471
- - Rotate credentials regularly
1472
-
1473
- **See:** [SFTP Credential Access Security Guide](../../02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md) for complete details.
1474
-
1475
- #### Setup Example
1476
-
1477
- ```typescript
1478
- import { schedule } from '@versori/run/schedule';
1479
- import { SftpDataSource } from '@fluentcommerce/fc-connect-sdk';
1480
- import { Buffer } from 'node:buffer'; // Required for Deno/Versori
1481
-
1482
- export const sftpPolling = schedule('location-sftp-poll', {
1483
- cron: '0 */6 * * *', // Every 6 hours
1484
- retry: { attempts: 3 },
1485
- }).then(async (ctx) => {
1486
- const { log, activation } = ctx;
1487
-
1488
- // Access SFTP credentials (Versori)
1489
- const { host, port, username, password, privateKey } = activation.connections.sftp_server;
1490
-
1491
- // Initialize SFTP data source
1492
- const sftpSource = new SftpDataSource(
1493
- {
1494
- type: 'SFTP_CSV',
1495
- settings: {
1496
- host,
1497
- port: port || 22,
1498
- username,
1499
- password,
1500
- privateKey,
1501
- remotePath: '/data/locations',
1502
- filePattern: '*.csv',
1503
- },
1504
- },
1505
- log
1506
- );
1507
-
1508
- // List new files
1509
- const files = await sftpSource.listFiles();
1510
- log.info(`Found ${files.length} files on SFTP`);
1511
-
1512
- for (const file of files) {
1513
- try {
1514
- // Download file
1515
- const content = await sftpSource.downloadFile(file.path);
1516
-
1517
- // Process file (same ETL logic as S3)
1518
- // ... (extract, parse, transform, load)
1519
-
1520
- // Move to processed folder
1521
- await sftpSource.moveFile(file.path, `/data/locations/processed/${file.name}`);
1522
- } catch (error) {
1523
- log.error(`Failed to process ${file.name}:`, error);
1524
- }
1525
- }
1526
-
1527
- return { success: true, filesProcessed: files.length };
1528
- });
1529
- ```
1530
-
1531
- ---
1532
-
1533
- ## Load Strategies
1534
-
1535
- ### GraphQL Mutation Approach
1536
-
1537
- **When to Use:**
1538
-
1539
- - Simple entity creation/updates
1540
- - Direct control over mutations
1541
- - Schema validation needed
1542
- - Small to medium datasets (<10K records)
1543
-
1544
- **Advantages:**
1545
-
1546
- - ✅ Type-safe with GraphQL schema
1547
- - ✅ Immediate validation feedback
1548
- - ✅ Fine-grained control over mutations
1549
- - ✅ Can return created IDs
1550
-
1551
- **Example:**
1552
-
1553
- ```typescript
1554
- async function loadViaGraphQL(records: any[], mutation: string, logger: any) {
1555
- const client = await createClient({
1556
- config: {
1557
- baseUrl: process.env.FLUENT_BASE_URL!,
1558
- clientId: process.env.FLUENT_CLIENT_ID!,
1559
- clientSecret: process.env.FLUENT_CLIENT_SECRET!,
1560
- retailerId: process.env.FLUENT_RETAILER_ID!,
1561
- },
1562
- });
1563
-
1564
- for (const record of records) {
1565
- const query = `
1566
- mutation ${mutation}($input: ${capitalize(mutation)}Input!) {
1567
- ${mutation}(input: $input) {
1568
- id
1569
- ref
1570
- }
1571
- }
1572
- `;
1573
-
1574
- try {
1575
- const result = await client.graphql({
1576
- query,
1577
- variables: { input: record },
1578
- });
1579
-
1580
- logger.info(`Created ${mutation}:`, result.data[mutation]);
1581
- } catch (error: any) {
1582
- logger.error(`Failed ${mutation}:`, {
1583
- record,
1584
- error: error.message,
1585
- details: error.response?.errors,
1586
- });
1587
- }
1588
- }
1589
- }
1590
- ```
1591
-
1592
- ### Event API Approach
1593
-
1594
- **When to Use:**
1595
-
1596
- - Asynchronous processing acceptable
1597
- - Triggering workflows/rules needed
1598
- - Large datasets (>10K records)
1599
- - Need event-driven architecture
1600
-
1601
- **Advantages:**
1602
-
1603
- - ✅ Asynchronous (better for large datasets)
1604
- - ✅ Triggers workflows and rules
1605
- - ✅ Decoupled from mutations
1606
- - ✅ Better performance for bulk loads
1607
-
1608
- **Example:**
1609
-
1610
- ```typescript
1611
- async function loadViaEventAPI(records: any[], eventName: string, logger: any) {
1612
- const client = await createClient({
1613
- config: {
1614
- baseUrl: process.env.FLUENT_BASE_URL!,
1615
- clientId: process.env.FLUENT_CLIENT_ID!,
1616
- clientSecret: process.env.FLUENT_CLIENT_SECRET!,
1617
- retailerId: process.env.FLUENT_RETAILER_ID!,
1618
- },
1619
- });
1620
-
1621
- for (const record of records) {
1622
- try {
1623
- await client.sendEvent({
1624
- name: eventName,
1625
- entityRef: record.ref,
1626
- entityType: 'LOCATION',
1627
- retailerId: record.retailerId,
1628
- attributes: record,
1629
- });
1630
-
1631
- logger.info(`Sent event ${eventName} for ${record.ref}`);
1632
- } catch (error: any) {
1633
- logger.error(`Failed to send event for ${record.ref}:`, error);
1634
- }
1635
- }
1636
- }
1637
- ```
1638
-
1639
- **Event-Driven Workflow:**
1640
-
1641
- ```
1642
- ETL Process Fluent Commerce
1643
-
1644
- Send Event (LOCATION_CREATED)
1645
- ↓ ↓
1646
- Workflow Triggers
1647
-
1648
- Process Event
1649
-
1650
- Create Location
1651
-
1652
- Apply Business Rules
1653
-
1654
- Send Notifications
1655
- ```
1656
-
1657
- ---
1658
-
1659
- ## Configuration Schema
1660
-
1661
- ### Generic Configuration Template
1662
-
1663
- Use this template for ANY entity type:
1664
-
1665
- ```json
1666
- {
1667
- "entityType": "<entity-name>",
1668
- "description": "<what this ETL does>",
1669
- "sourceConfig": {
1670
- "type": "S3_CSV | SFTP_CSV | S3_JSON | SFTP_JSON",
1671
- "bucket": "<s3-bucket-name>",
1672
- "prefix": "<folder-prefix>",
1673
- "filePattern": "*.csv | *.json | *.xml",
1674
- "sftp": {
1675
- "host": "${SFTP_HOST}",
1676
- "port": 22,
1677
- "username": "${SFTP_USERNAME}",
1678
- "password": "${SFTP_PASSWORD}",
1679
- "privateKey": "${SFTP_PRIVATE_KEY}", // Recommended over password
1680
- "remotePath": "/data/<entity>",
1681
- "filePattern": "*.<format>"
1682
- }
1683
- },
1684
- "parseConfig": {
1685
- "format": "csv | json | xml",
1686
- "delimiter": "," | "|" | "\t",
1687
- "headers": true | false,
1688
- "encoding": "utf8 | utf16",
1689
- "json": {
1690
- "dataPath": "root.path.to.array",
1691
- "jsonLines": false
1692
- },
1693
- "xml": {
1694
- "itemPath": "//Item",
1695
- "includeAttributes": true
1696
- }
1697
- },
1698
- "mappingConfig": {
1699
- "version": "1.0",
1700
- "description": "Field mappings",
1701
- "fields": {
1702
- "targetField": {
1703
- "source": "sourceField",
1704
- "required": true | false,
1705
- "defaultValue": "default",
1706
- "resolver": "sdk.* | custom.*"
1707
- }
1708
- }
1709
- },
1710
- "loadConfig": {
1711
- "strategy": "graphql | event",
1712
- "mutation": "createEntity | updateEntity",
1713
- "eventName": "ENTITY_CREATED",
1714
- "batchSize": 100,
1715
- "retryAttempts": 3
1716
- },
1717
- "scheduleConfig": {
1718
- "enabled": true,
1719
- "cron": "0 0 * * *",
1720
- "timezone": "UTC"
1721
- }
1722
- }
1723
- ```
1724
-
1725
- ### How to Adapt for New Entity Types
1726
-
1727
- **Step-by-Step Guide:**
1728
-
1729
- 1. **Copy Template**: Start with generic configuration template
1730
- 2. **Set Entity Type**: Change `entityType` to your entity name
1731
- 3. **Configure Source**: Set bucket/path for your data source
1732
- 4. **Configure Parser**: Set format and parsing options
1733
- 5. **Define Field Mappings**: Map source fields to Fluent schema
1734
- 6. **Configure Load Strategy**: Choose GraphQL or Event API
1735
- 7. **Test**: Run ETL with sample data
1736
- 8. **Deploy**: Schedule or trigger via webhook
1737
-
1738
- **Example Adaptation (Customer Entity):**
1739
-
1740
- ```json
1741
- {
1742
- "entityType": "customer",
1743
- "description": "Load customer data from CRM system",
1744
- "sourceConfig": {
1745
- "type": "S3_CSV",
1746
- "bucket": "crm-exports",
1747
- "prefix": "customers/",
1748
- "filePattern": "customers_*.csv"
1749
- },
1750
- "parseConfig": {
1751
- "format": "csv",
1752
- "delimiter": ",",
1753
- "headers": true
1754
- },
1755
- "mappingConfig": {
1756
- "version": "1.0",
1757
- "fields": {
1758
- "ref": {
1759
- "source": "customer_id",
1760
- "required": true
1761
- },
1762
- "firstName": {
1763
- "source": "first_name",
1764
- "required": true
1765
- },
1766
- "lastName": {
1767
- "source": "last_name",
1768
- "required": true
1769
- },
1770
- "email": {
1771
- "source": "email_address",
1772
- "required": true,
1773
- "resolver": "sdk.lowercase"
1774
- },
1775
- "primaryPhone": {
1776
- "source": "phone_number",
1777
- "resolver": "custom.formatPhoneNumber"
1778
- },
1779
- "retailerId": {
1780
- "value": "${RETAILER_ID}"
1781
- }
1782
- }
1783
- },
1784
- "loadConfig": {
1785
- "strategy": "graphql",
1786
- "mutation": "createCustomer",
1787
- "batchSize": 50
1788
- }
1789
- }
1790
- ```
1791
-
1792
- ---
1793
-
1794
- ## Extending to Other Entities
1795
-
1796
- ### Step-by-Step Guide
1797
-
1798
- **1. Identify Entity Schema**
1799
-
1800
- Understand the Fluent schema for your entity:
1801
-
1802
- ```graphql
1803
- # Example: Carrier schema
1804
- type Carrier {
1805
- id: ID!
1806
- ref: String!
1807
- name: String!
1808
- type: String!
1809
- status: String
1810
- services: [CarrierService]
1811
- retailerId: ID!
1812
- }
1813
-
1814
- input CreateCarrierInput {
1815
- ref: String!
1816
- name: String!
1817
- type: String!
1818
- status: String
1819
- services: [CarrierServiceInput]
1820
- retailerId: ID!
1821
- }
1822
- ```
1823
-
1824
- **2. Create Field Mapping Configuration**
1825
-
1826
- Map source data to schema:
1827
-
1828
- ```json
1829
- {
1830
- "version": "1.0",
1831
- "fields": {
1832
- "ref": {
1833
- "source": "carrier_code",
1834
- "required": true,
1835
- "resolver": "sdk.uppercase"
1836
- },
1837
- "name": {
1838
- "source": "carrier_name",
1839
- "required": true
1840
- },
1841
- "type": {
1842
- "source": "carrier_type",
1843
- "required": true
1844
- },
1845
- "status": {
1846
- "value": "ACTIVE"
1847
- },
1848
- "services": {
1849
- "source": "services",
1850
- "isArray": true,
1851
- "fields": {
1852
- "name": { "source": "$.service_name" },
1853
- "code": { "source": "$.service_code" },
1854
- "deliveryDays": { "source": "$.delivery_days", "resolver": "sdk.parseInt" }
1855
- }
1856
- },
1857
- "retailerId": {
1858
- "value": "${RETAILER_ID}"
1859
- }
1860
- }
1861
- }
1862
- ```
1863
-
1864
- **3. Set Up Data Source**
1865
-
1866
- Configure where data comes from:
1867
-
1868
- ```typescript
1869
- const carrierSource = new S3DataSource(
1870
- {
1871
- type: 'S3_CSV',
1872
- s3Config: {
1873
- bucket: 'carrier-data',
1874
- region: process.env.AWS_REGION!,
1875
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
1876
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
1877
- },
1878
- },
1879
- logger
1880
- );
1881
- ```
1882
-
1883
- **4. Run Generic ETL Pipeline**
1884
-
1885
- Use the same ETL code:
1886
-
1887
- ```typescript
1888
- await masterDataETL('config/carrier-etl-config.json');
1889
- ```
1890
-
1891
- ### Customer Example
1892
-
1893
- **Source CSV:**
1894
-
1895
- ```csv
1896
- customer_id,first_name,last_name,email,phone,segment,status
1897
- CUST001,John,Doe,john.doe@email.com,555-1234,VIP,ACTIVE
1898
- CUST002,Jane,Smith,jane.smith@email.com,555-5678,STANDARD,ACTIVE
1899
- ```
1900
-
1901
- **Mapping Configuration:**
1902
-
1903
- ```json
1904
- {
1905
- "version": "1.0",
1906
- "fields": {
1907
- "ref": { "source": "customer_id", "required": true },
1908
- "firstName": { "source": "first_name", "required": true },
1909
- "lastName": { "source": "last_name", "required": true },
1910
- "email": { "source": "email", "required": true, "resolver": "sdk.lowercase" },
1911
- "primaryPhone": { "source": "phone" },
1912
- "status": { "source": "status", "defaultValue": "ACTIVE" },
1913
- "attributes": {
1914
- "fields": {
1915
- "segment": { "source": "segment" }
1916
- }
1917
- },
1918
- "retailerId": { "value": "${RETAILER_ID}" }
1919
- }
1920
- }
1921
- ```
1922
-
1923
- ### Carrier Example
1924
-
1925
- **Source JSON:**
1926
-
1927
- ```json
1928
- {
1929
- "carriers": [
1930
- {
1931
- "code": "FEDEX",
1932
- "name": "FedEx",
1933
- "type": "PARCEL",
1934
- "services": [
1935
- { "service_code": "GROUND", "service_name": "FedEx Ground", "delivery_days": 3 },
1936
- { "service_code": "2DAY", "service_name": "FedEx 2 Day", "delivery_days": 2 }
1937
- ]
1938
- }
1939
- ]
1940
- }
1941
- ```
1942
-
1943
- **Mapping Configuration:**
1944
-
1945
- ```json
1946
- {
1947
- "version": "1.0",
1948
- "fields": {
1949
- "ref": { "source": "code", "required": true },
1950
- "name": { "source": "name", "required": true },
1951
- "type": { "source": "type", "required": true },
1952
- "status": { "value": "ACTIVE" },
1953
- "services": {
1954
- "source": "services",
1955
- "isArray": true,
1956
- "fields": {
1957
- "code": { "source": "$.service_code" },
1958
- "name": { "source": "$.service_name" },
1959
- "transitDays": { "source": "$.delivery_days", "resolver": "sdk.parseInt" }
1960
- }
1961
- },
1962
- "retailerId": { "value": "${RETAILER_ID}" }
1963
- }
1964
- }
1965
- ```
1966
-
1967
- ### Pricing Example
1968
-
1969
- **Source CSV:**
1970
-
1971
- ```csv
1972
- sku,price_list,currency,base_price,sale_price,start_date,end_date
1973
- PROD001-S-RED,US_RETAIL,USD,29.99,24.99,2024-01-01,2024-12-31
1974
- PROD001-M-RED,US_RETAIL,USD,29.99,24.99,2024-01-01,2024-12-31
1975
- ```
1976
-
1977
- **Mapping Configuration:**
1978
-
1979
- ```json
1980
- {
1981
- "version": "1.0",
1982
- "fields": {
1983
- "ref": {
1984
- "resolver": "custom.generatePriceRef",
1985
- "required": true
1986
- },
1987
- "sku": {
1988
- "source": "sku",
1989
- "required": true
1990
- },
1991
- "priceList": {
1992
- "source": "price_list",
1993
- "required": true
1994
- },
1995
- "currency": {
1996
- "source": "currency",
1997
- "required": true,
1998
- "resolver": "sdk.uppercase"
1999
- },
2000
- "value": {
2001
- "source": "base_price",
2002
- "required": true,
2003
- "resolver": "sdk.parseFloat"
2004
- },
2005
- "salePrice": {
2006
- "source": "sale_price",
2007
- "resolver": "sdk.parseFloat"
2008
- },
2009
- "validFrom": {
2010
- "source": "start_date",
2011
- "resolver": "sdk.formatDate"
2012
- },
2013
- "validTo": {
2014
- "source": "end_date",
2015
- "resolver": "sdk.formatDate"
2016
- },
2017
- "retailerId": {
2018
- "value": "${RETAILER_ID}"
2019
- }
2020
- }
2021
- }
2022
- ```
2023
-
2024
- **Custom Resolver:**
2025
-
2026
- ```typescript
2027
- export const generatePriceRef: FieldResolverFunction = (
2028
- value: any,
2029
- sourceData: any,
2030
- config: any,
2031
- helpers: any
2032
- ) => {
2033
- // Generate unique ref: SKU-PRICELIST
2034
- return `${sourceData.sku}-${sourceData.price_list}`;
2035
- };
2036
- ```
2037
-
2038
- ---
2039
-
2040
- ## Testing
2041
-
2042
- ### Unit Testing Field Mappings
2043
-
2044
- ```typescript
2045
- import { UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
2046
- import * as fs from 'fs';
2047
-
2048
- describe('Location Mapping', () => {
2049
- let mapper: UniversalMapper;
2050
-
2051
- beforeEach(() => {
2052
- const config = JSON.parse(fs.readFileSync('config/location-mapping.json', 'utf-8'));
2053
- mapper = new UniversalMapper(config);
2054
- });
2055
-
2056
- it('should map location data correctly', async () => {
2057
- const sourceData = {
2058
- location_id: 'LOC001',
2059
- location_name: 'Downtown Store',
2060
- type: 'store',
2061
- status: 'active',
2062
- address_line1: '123 Main St',
2063
- city: 'New York',
2064
- state: 'NY',
2065
- zip: '10001',
2066
- country: 'US',
2067
- latitude: '40.7128',
2068
- longitude: '-74.0060',
2069
- };
2070
-
2071
- const result = await mapper.map(sourceData);
2072
-
2073
- expect(result.success).toBe(true);
2074
- expect(result.data).toMatchObject({
2075
- ref: 'LOC001',
2076
- name: 'Downtown Store',
2077
- type: 'STORE',
2078
- status: 'ACTIVE',
2079
- primaryAddress: {
2080
- street: '123 Main St',
2081
- city: 'New York',
2082
- state: 'NY',
2083
- postcode: '10001',
2084
- country: 'US',
2085
- },
2086
- coordinates: {
2087
- latitude: 40.7128,
2088
- longitude: -74.006,
2089
- },
2090
- });
2091
- });
2092
-
2093
- it('should handle required field validation', async () => {
2094
- const sourceData = {
2095
- location_name: 'Store Without ID',
2096
- // Missing location_id (required)
2097
- };
2098
-
2099
- const result = await mapper.map(sourceData);
2100
-
2101
- expect(result.success).toBe(false);
2102
- expect(result.errors).toContain("Required field 'ref' is missing or empty");
2103
- });
2104
-
2105
- it('should apply default values', async () => {
2106
- const sourceData = {
2107
- location_id: 'LOC001',
2108
- location_name: 'Store',
2109
- type: 'STORE',
2110
- // Missing status - should default to "ACTIVE"
2111
- };
2112
-
2113
- const result = await mapper.map(sourceData);
2114
-
2115
- expect(result.success).toBe(true);
2116
- expect(result.data.status).toBe('ACTIVE');
2117
- });
2118
- });
2119
- ```
2120
-
2121
- ### Integration Testing ETL Pipeline
2122
-
2123
- ```typescript
2124
- import { masterDataETL } from './location-etl';
2125
- import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
2126
- import * as fs from 'fs';
2127
-
2128
- describe('Location ETL Integration', () => {
2129
- let s3DataSource: S3DataSource;
2130
-
2131
- beforeEach(() => {
2132
- s3DataSource = new S3DataSource(
2133
- {
2134
- type: 'S3_CSV',
2135
- s3Config: {
2136
- bucket: 'test-bucket',
2137
- region: 'us-east-1',
2138
- accessKeyId: process.env.TEST_AWS_KEY!,
2139
- secretAccessKey: process.env.TEST_AWS_SECRET!,
2140
- },
2141
- },
2142
- console
2143
- );
2144
- });
2145
-
2146
- it('should process location CSV file end-to-end', async () => {
2147
- // Upload test file to S3
2148
- const testData = `location_id,location_name,type,status
2149
- LOC001,Test Store,STORE,ACTIVE
2150
- LOC002,Test Warehouse,WAREHOUSE,ACTIVE`;
2151
-
2152
- await s3DataSource.uploadFile('locations/test.csv', testData, { contentType: 'text/csv' });
2153
-
2154
- // Run ETL
2155
- await masterDataETL('config/location-etl-config.json');
2156
-
2157
- // Verify locations were created (query Fluent API)
2158
- const result = await fluentClient.graphql({
2159
- query: `
2160
- query {
2161
- locations(first: 10, filter: { ref: { in: ["LOC001", "LOC002"] } }) {
2162
- edges {
2163
- node {
2164
- ref
2165
- name
2166
- type
2167
- }
2168
- }
2169
- }
2170
- }
2171
- `,
2172
- });
2173
-
2174
- expect(result.data.locations.edges).toHaveLength(2);
2175
- expect(result.data.locations.edges[0].node.ref).toBe('LOC001');
2176
- }, 30000); // 30s timeout for integration test
2177
- });
2178
- ```
2179
-
2180
- ### End-to-End Testing
2181
-
2182
- ```typescript
2183
- import { masterDataETL } from './location-etl';
2184
-
2185
- describe('Location ETL E2E', () => {
2186
- it('should handle full ETL lifecycle', async () => {
2187
- // 1. Upload test data to S3
2188
- // 2. Trigger ETL process
2189
- // 3. Verify data in Fluent
2190
- // 4. Verify state tracking (file marked as processed)
2191
- // 5. Verify idempotency (re-running doesn't duplicate)
2192
- // TODO: Implement full E2E test scenario
2193
- });
2194
- });
2195
- ```
2196
-
2197
- ---
2198
-
2199
- ## Common Issues
2200
-
2201
- ### Issue: Duplicate Records Created
2202
-
2203
- **Symptom**: Same entity created multiple times
2204
-
2205
- **Root Cause**: State management not working or file processed multiple times
2206
-
2207
- **Solution:**
2208
-
2209
- ```typescript
2210
- // Ensure state service is initialized
2211
- const kvAdapter = new VersoriKVAdapter(openKv());
2212
- const stateService = new StateService(logger);
2213
-
2214
- // Check BEFORE processing
2215
- const fileKey = `${entityType}:${file.name}`;
2216
- if (await stateService.isFileProcessed(kvAdapter, fileKey)) {
2217
- logger.info(`Skipping already processed file: ${file.name}`);
2218
- continue;
2219
- }
2220
-
2221
- // Mark AFTER successful processing
2222
- await stateService.updateSyncState(
2223
- kvAdapter,
2224
- [
2225
- {
2226
- fileName: file.name,
2227
- lastModified: new Date().toISOString(),
2228
- recordCount: records.length,
2229
- },
2230
- ],
2231
- 'master-data-etl'
2232
- );
2233
- ```
2234
-
2235
- ### Issue: Field Mapping Errors
2236
-
2237
- **Symptom**: "Required field missing" or "Mapping failed"
2238
-
2239
- **Root Cause**: Source field name doesn't match configuration
2240
-
2241
- **Solution:**
2242
-
2243
- ```typescript
2244
- // Debug source data structure
2245
- logger.debug('Source data:', JSON.stringify(sourceData, null, 2));
2246
-
2247
- // Check field names match exactly (case-sensitive)
2248
- {
2249
- "ref": {
2250
- "source": "location_id", // Must match CSV header exactly
2251
- "required": true
2252
- }
2253
- }
2254
-
2255
- // Use custom resolver for flexible field access
2256
- {
2257
- "ref": {
2258
- "resolver": "custom.extractRef",
2259
- "required": true
2260
- }
2261
- }
2262
- ```
2263
-
2264
- ### Issue: Large File Memory Issues
2265
-
2266
- **Symptom**: Out of memory errors with large CSV/JSON files
2267
-
2268
- **Root Cause**: Loading entire file into memory
2269
-
2270
- **Solution:**
2271
-
2272
- ```typescript
2273
- // Use streaming parsers for large files
2274
- const csvParser = new CSVParserService();
2275
-
2276
- // Parse with streaming (yields records one-by-one)
2277
- for await (const record of csvParser.parseStreaming(fileContent)) {
2278
- const result = await mapper.map(record);
2279
- if (result.success) {
2280
- await loadRecord(result.data);
2281
- }
2282
- }
2283
-
2284
- // Or batch process
2285
- for await (const batch of csvParser.parseStreaming(fileContent, {}, 100)) {
2286
- await loadBatch(batch);
2287
- }
2288
- ```
2289
-
2290
- ### Issue: GraphQL Mutation Timeouts
2291
-
2292
- **Symptom**: Mutations timing out for large datasets
2293
-
2294
- **Root Cause**: Synchronous processing of many records
2295
-
2296
- **Solution:**
2297
-
2298
- ```typescript
2299
- // Use batching and concurrency limits
2300
- import pLimit from 'p-limit';
2301
-
2302
- const limit = pLimit(5); // Max 5 concurrent mutations
2303
-
2304
- const promises = records.map(record => limit(() => createViaGraphQL(record)));
2305
-
2306
- await Promise.all(promises);
2307
-
2308
- // Or use Event API for async processing
2309
- await loadViaEventAPI(records, 'LOCATION_CREATED', logger);
2310
- ```
2311
-
2312
- ### Issue: Type Coercion Errors
2313
-
2314
- **Symptom**: "Expected number, got string" in GraphQL mutations
2315
-
2316
- **Root Cause**: CSV parsers return all values as strings
2317
-
2318
- **Solution:**
2319
-
2320
- ```typescript
2321
- // Use SDK resolvers for type coercion
2322
- {
2323
- "latitude": {
2324
- "source": "latitude",
2325
- "resolver": "sdk.parseFloat" // String → number
2326
- },
2327
- "active": {
2328
- "source": "is_active",
2329
- "resolver": "sdk.boolean" // "true" → true
2330
- },
2331
- "quantity": {
2332
- "source": "qty",
2333
- "resolver": "sdk.parseInt" // "10" → 10
2334
- }
2335
- }
2336
- ```
2337
-
2338
- ---
2339
-
2340
- ## Related Guides
2341
-
2342
- ### SDK Documentation
2343
-
2344
- - [Universal Mapping Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Complete field mapping reference
2345
- - [S3 Data Source](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - S3 integration details
2346
- - [SFTP Data Source](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - SFTP integration details
2347
- - [SFTP Credential Access Security](../../02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md) - Secure credential handling for SFTP
2348
- - [CSV Parser](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - CSV parsing options
2349
- - [JSON Parser](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - JSON parsing options
2350
- - [State Management](../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md) - Preventing duplicates
2351
-
2352
- ### Use Case Patterns
2353
-
2354
- - [Inventory Ingestion](../../02-CORE-GUIDES/ingestion/ingestion-readme.md) - Similar pattern for inventory data
2355
- - [Order Integration](../../03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md) - Transaction data vs master data
2356
- - [Catalog Sync](../../01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md) - Product catalog patterns
2357
-
2358
- ### Platform Integration
2359
-
2360
- - [Versori Connector Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Deploy ETL as connector
2361
- - [Webhook Triggers](../../04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md#webhook-functions-receiving-external-requests) - Event-driven ETL
2362
- - [Scheduled Jobs](../../04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md#scheduled-functions-time-based-recurring-tasks) - Periodic ETL execution
2363
-
2364
- ---
2365
-
2366
- ## Summary
2367
-
2368
- This **Master Data ETL Pattern** provides a **generic, configuration-driven framework** for loading ANY entity type into Fluent Commerce.
2369
-
2370
- **Key Takeaways:**
2371
-
2372
- 1. ✅ **One Pattern for All Entities**: Same code works for locations, products, controls, carriers, etc.
2373
- 2. ✅ **Configuration-Driven**: No code changes needed for new entity types
2374
- 3. ✅ **Four-Phase Pipeline**: Extract → Parse → Transform → Load
2375
- 4. ✅ **Multiple Source Formats**: CSV, JSON, XML support
2376
- 5. ✅ **Multiple Load Strategies**: GraphQL mutations or Event API
2377
- 6. ✅ **Production-Ready**: State management, error handling, logging
2378
-
2379
- **Getting Started:**
2380
-
2381
- 1. Copy the generic ETL code
2382
- 2. Create field mapping configuration for your entity
2383
- 3. Configure data source (S3/SFTP)
2384
- 4. Run ETL pipeline
2385
- 5. Monitor logs and verify data in Fluent
2386
-
2387
- **Next Steps:**
2388
-
2389
- - Review the [Universal Mapping Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) for advanced mapping patterns
2390
- - Explore [S3 Data Source](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) for source configuration options
2391
- - Check [Versori Connector Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) to deploy as a production connector
2392
-
2393
- ---
2394
-
2395
- **Need Help?**
2396
-
2397
- - 📖 Documentation: `fc-connect-sdk/docs/`
2398
- - 💬 Support: Fluent Commerce support team
2399
- - 🐛 Issues: GitHub repository issues
1
+ # Pattern: Master Data ETL - Generic Framework for Loading Any Entity
2
+
3
+ **FC Connect SDK Use Case Guide**
4
+
5
+ > **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
6
+ > **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
7
+
8
+ **Status**: Production Ready
9
+
10
+ **Complexity**: Intermediate
11
+
12
+ **Est. Time**: 30-60 minutes
13
+
14
+ **Use Cases**: Locations, Products, Controls, Carriers, Customers, Pricing, Categories, etc.
15
+
16
+ ## Table of Contents
17
+
18
+ - [Overview](#overview)
19
+ - [What You'll Build](#what-youll-build)
20
+ - [SDK Methods Used](#sdk-methods-used)
21
+ - [The Generic Pattern](#the-generic-pattern)
22
+ - [Example 1: Location Master Data](#example-1-location-master-data)
23
+ - [Example 2: Product Catalog](#example-2-product-catalog)
24
+ - [Example 3: Control/Config Data](#example-3-controlconfig-data)
25
+ - [Source Strategies](#source-strategies)
26
+ - [Load Strategies](#load-strategies)
27
+ - [Configuration Schema](#configuration-schema)
28
+ - [Extending to Other Entities](#extending-to-other-entities)
29
+ - [Testing](#testing)
30
+ - [Common Issues](#common-issues)
31
+ - [Related Guides](#related-guides)
32
+
33
+ ---
34
+
35
+ ## Overview
36
+
37
+ Master data ETL is the process of extracting reference data from external systems and loading it into Fluent Commerce. Unlike transactional data (orders, inventory updates), master data changes infrequently and defines the core entities of your commerce platform.
38
+
39
+ **Common Master Data Entities:**
40
+
41
+ - **Locations**: Stores, warehouses, distribution centers
42
+ - **Products**: SKUs, product catalogs, variants
43
+ - **Controls**: Business rules, configuration parameters
44
+ - **Carriers**: Shipping carriers, service levels
45
+ - **Customers**: Customer profiles, segments
46
+ - **Categories**: Product taxonomies, merchandising hierarchies
47
+ - **Pricing**: Price lists, promotional rules
48
+
49
+ **Why This Pattern?**
50
+
51
+ This guide provides a **generic, configuration-driven framework** that works for ANY master data entity. Instead of writing custom code for each entity type, you configure JSON mappings and reuse the same pipeline.
52
+
53
+ **Key Benefits:**
54
+
55
+ - ✅ **One Pattern for All Entities**: Same code works for locations, products, controls, etc.
56
+ - ✅ **Configuration-Driven**: No code changes needed for new entity types
57
+ - ✅ **Multiple Source Formats**: CSV, JSON, XML support out-of-the-box
58
+ - ✅ **Multiple Load Strategies**: GraphQL mutations or Event API
59
+ - ✅ **Automatic Deduplication**: State management prevents duplicate loads
60
+ - ✅ **Production-Ready Error Handling**: Comprehensive logging and retry logic
61
+
62
+ ---
63
+
64
+ ## What You'll Build
65
+
66
+ A **reusable ETL framework** with these capabilities:
67
+
68
+ 1. **Extract**: Read master data from S3/SFTP in CSV, JSON, or XML format
69
+ 2. **Parse**: Convert file format to JavaScript objects
70
+ 3. **Transform**: Map source fields to Fluent schema using field mappings
71
+ 4. **Load**: Submit to Fluent via GraphQL mutations or Event API
72
+ 5. **Track**: Prevent duplicate processing using state management
73
+
74
+ **Pipeline Flow:**
75
+
76
+ ```
77
+ Source File (S3/SFTP)
78
+
79
+ Extract (DataSource)
80
+
81
+ Parse (CSVParserService/JSONParser/XMLParser)
82
+
83
+ Transform (UniversalMapper)
84
+
85
+ Load (GraphQL Mutation or Event API)
86
+
87
+ Track (StateService)
88
+ ```
89
+
90
+ **Configuration-Driven Design:**
91
+
92
+ Instead of hardcoded logic:
93
+
94
+ ```typescript
95
+ // ❌ WRONG: Hardcoded for each entity type
96
+ if (entityType === 'location') {
97
+ // Custom location loading logic
98
+ } else if (entityType === 'product') {
99
+ // Custom product loading logic
100
+ }
101
+ ```
102
+
103
+ You use JSON configuration:
104
+
105
+ ```typescript
106
+ // ✅ CORRECT: Generic pipeline with config
107
+ const config = loadConfig('location-mapping.json');
108
+ await etlPipeline.run(sourceFile, config);
109
+ ```
110
+
111
+ > Tips:
112
+ >
113
+ > - For identifiers that may look numeric (SKU/GTIN/UPC), add `resolver: "sdk.toString"` in mappings to force string output.
114
+ > - When parsing XML sources where leading zeros matter, configure the XML parser with `parseNumbers: false` to prevent numeric coercion.
115
+
116
+ ---
117
+
118
+ ## SDK Methods Used
119
+
120
+ | Method | Purpose | Pattern |
121
+ | ---------------------------------------------------------- | -------------------------------------- | --------------- |
122
+ | `S3DataSource.downloadFile()` | Download master data file from S3 | Source |
123
+ | `SftpDataSource.downloadFile()` | Download master data file from SFTP | Source |
124
+ | `CSVParserService.parse()` | Parse CSV master data | Parse |
125
+ | `JSONParserService.parse()` | Parse JSON master data | Parse |
126
+ | `XMLParserService.parse()` | Parse XML master data | Parse |
127
+ | `UniversalMapper.map()` | Transform source data to Fluent schema | Transform |
128
+ | `GraphQLMutationMapper.map()` | Generate GraphQL mutations from data | Load (Option 1) |
129
+ | `FluentClient.graphql()` | Execute GraphQL mutations | Load (Option 1) |
130
+ | `FluentClient.sendEvent()` | Send events to Event API | Load (Option 2) |
131
+ | `StateService.markFileProcessed(kv, fileName, workflowId)` | Prevent duplicate processing | Track |
132
+
133
+ ---
134
+
135
+ ## The Generic Pattern
136
+
137
+ ### Architecture Overview
138
+
139
+ The master data ETL pattern follows a **four-phase pipeline** that adapts to any entity type through configuration:
140
+
141
+ ```
142
+ ┌──────────────────────────────────────────────────────────────┐
143
+ │ PHASE 1: EXTRACT │
144
+ │ - List files from S3/SFTP │
145
+ │ - Filter by pattern (*.csv, locations_*.json, etc.) │
146
+ │ - Download file content │
147
+ │ - Skip already-processed files (state check) │
148
+ └──────────────────────────────────────────────────────────────┘
149
+
150
+ ┌──────────────────────────────────────────────────────────────┐
151
+ │ PHASE 2: PARSE │
152
+ │ - Auto-detect format (CSV, JSON, XML) │
153
+ │ - Parse content to JavaScript objects │
154
+ │ - Validate structure (required fields, types) │
155
+ │ - Handle parsing errors gracefully │
156
+ └──────────────────────────────────────────────────────────────┘
157
+
158
+ ┌──────────────────────────────────────────────────────────────┐
159
+ │ PHASE 3: TRANSFORM │
160
+ │ - Apply field mappings (source to Fluent schema) │
161
+ │ - Execute resolvers (transformations, calculations) │
162
+ │ - Validate required fields │
163
+ │ - Enrich with defaults/constants │
164
+ └──────────────────────────────────────────────────────────────┘
165
+
166
+ ┌──────────────────────────────────────────────────────────────┐
167
+ │ PHASE 4: LOAD │
168
+ │ - Choose strategy (GraphQL Mutation vs Event API) │
169
+ │ - Batch if needed (large datasets) │
170
+ │ - Execute load operation │
171
+ │ - Handle errors and retries │
172
+ │ - Mark file as processed (state update) │
173
+ └──────────────────────────────────────────────────────────────┘
174
+ ```
175
+
176
+ ### Configuration-Driven Design
177
+
178
+ **Core Principle**: All entity-specific logic lives in JSON configuration files, not in code.
179
+
180
+ **Generic Configuration Structure:**
181
+
182
+ ```json
183
+ {
184
+ "entityType": "location", // What you're loading
185
+ "sourceConfig": {
186
+ // Where to get data
187
+ "type": "S3_CSV",
188
+ "bucket": "master-data",
189
+ "prefix": "locations/",
190
+ "filePattern": "*.csv"
191
+ },
192
+ "parseConfig": {
193
+ // How to parse data
194
+ "format": "csv",
195
+ "delimiter": ",",
196
+ "headers": true
197
+ },
198
+ "mappingConfig": {
199
+ // How to transform data
200
+ "version": "1.0",
201
+ "fields": {
202
+ "ref": { "source": "location_id", "required": true },
203
+ "name": { "source": "location_name" },
204
+ "status": { "value": "ACTIVE" }
205
+ }
206
+ },
207
+ "loadConfig": {
208
+ // How to load into Fluent
209
+ "strategy": "graphql",
210
+ "mutation": "createLocation",
211
+ "batchSize": 100
212
+ }
213
+ }
214
+ ```
215
+
216
+ **Reusability**: Change `entityType` to "product", update field mappings → same code loads products!
217
+
218
+ ### Works for ANY Entity Type
219
+
220
+ This pattern is entity-agnostic because:
221
+
222
+ 1. **Generic Source Reading**: S3DataSource/SftpDataSource work with any file
223
+ 2. **Format-Agnostic Parsing**: Parsers handle CSV/JSON/XML regardless of entity type
224
+ 3. **Flexible Mapping**: UniversalMapper adapts to any source→target schema
225
+ 4. **Mutation Generation**: GraphQLMutationMapper works with any mutation
226
+ 5. **State Management**: StateService tracks processing for any entity
227
+
228
+ **Example Entity Types:**
229
+
230
+ | Entity | Source Format | Mutation | Complexity |
231
+ | ---------- | ------------- | ---------------- | ------------------- |
232
+ | Locations | CSV | `createLocation` | Simple |
233
+ | Products | JSON | `createProduct` | Medium (variants) |
234
+ | Controls | JSON | `createControl` | Simple |
235
+ | Carriers | XML | `createCarrier` | Simple |
236
+ | Categories | JSON | `createCategory` | Medium (hierarchy) |
237
+ | Customers | CSV | `createCustomer` | Medium (attributes) |
238
+
239
+ All use the **same pipeline** with different **configurations**.
240
+
241
+ ---
242
+
243
+ ## Example 1: Location Master Data
244
+
245
+ ### Overview
246
+
247
+ Load store/warehouse locations from CSV files into Fluent Commerce.
248
+
249
+ **Business Context:**
250
+
251
+ - Retail locations change infrequently (openings, closings, updates)
252
+ - Source: Retail management system exports CSV daily
253
+ - Destination: Fluent Location entities
254
+ - Frequency: Daily batch, event-driven on new file
255
+
256
+ ### Source Data Formats
257
+
258
+ **CSV Format:**
259
+
260
+ ```csv
261
+ location_id,location_name,type,address_line1,city,state,zip,country,latitude,longitude,status
262
+ LOC001,Downtown Store,STORE,123 Main St,New York,NY,10001,US,40.7128,-74.0060,ACTIVE
263
+ LOC002,Warehouse East,WAREHOUSE,456 Industrial Rd,Newark,NJ,07102,US,40.7357,-74.1724,ACTIVE
264
+ LOC003,Pop-Up Shop,STORE,789 Fashion Ave,New York,NY,10018,US,40.7549,-73.9840,INACTIVE
265
+ ```
266
+
267
+ **JSON Format:**
268
+
269
+ ```json
270
+ {
271
+ "locations": [
272
+ {
273
+ "locationId": "LOC001",
274
+ "name": "Downtown Store",
275
+ "type": "STORE",
276
+ "address": {
277
+ "street": "123 Main St",
278
+ "city": "New York",
279
+ "state": "NY",
280
+ "zip": "10001",
281
+ "country": "US"
282
+ },
283
+ "coordinates": {
284
+ "lat": 40.7128,
285
+ "lng": -74.006
286
+ },
287
+ "status": "ACTIVE"
288
+ }
289
+ ]
290
+ }
291
+ ```
292
+
293
+ **XML Format:**
294
+
295
+ ```xml
296
+ <?xml version="1.0" encoding="UTF-8"?>
297
+ <LocationFeed>
298
+ <Location>
299
+ <ID>LOC001</ID>
300
+ <Name>Downtown Store</Name>
301
+ <Type>STORE</Type>
302
+ <Address>
303
+ <Line1>123 Main St</Line1>
304
+ <City>New York</City>
305
+ <State>NY</State>
306
+ <Zip>10001</Zip>
307
+ <Country>US</Country>
308
+ </Address>
309
+ <Latitude>40.7128</Latitude>
310
+ <Longitude>-74.0060</Longitude>
311
+ <Status>ACTIVE</Status>
312
+ </Location>
313
+ </LocationFeed>
314
+ ```
315
+
316
+ ### Field Mapping Configuration
317
+
318
+ **`config/location-mapping.json`:**
319
+
320
+ ```json
321
+ {
322
+ "version": "1.0",
323
+ "description": "Map external location data to Fluent Location schema",
324
+ "fields": {
325
+ "ref": {
326
+ "source": "location_id",
327
+ "required": true,
328
+ "resolver": "sdk.trim"
329
+ },
330
+ "type": {
331
+ "source": "type",
332
+ "required": true,
333
+ "resolver": "sdk.uppercase"
334
+ },
335
+ "name": {
336
+ "source": "location_name",
337
+ "required": true
338
+ },
339
+ "status": {
340
+ "source": "status",
341
+ "defaultValue": "ACTIVE",
342
+ "resolver": "sdk.uppercase"
343
+ },
344
+ "primaryAddress": {
345
+ "fields": {
346
+ "street": { "source": "address_line1" },
347
+ "city": { "source": "city" },
348
+ "state": { "source": "state" },
349
+ "postcode": { "source": "zip" },
350
+ "country": { "source": "country" }
351
+ }
352
+ },
353
+ "coordinates": {
354
+ "fields": {
355
+ "latitude": { "source": "latitude", "resolver": "sdk.parseFloat" },
356
+ "longitude": { "source": "longitude", "resolver": "sdk.parseFloat" }
357
+ }
358
+ },
359
+ "retailerId": {
360
+ "value": "${RETAILER_ID}",
361
+ "required": true
362
+ }
363
+ }
364
+ }
365
+ ```
366
+
367
+ **Key Features:**
368
+
369
+ - ✅ Required field validation (`ref`, `type`, `name`)
370
+ - ✅ Default values (`status` defaults to "ACTIVE")
371
+ - ✅ Built-in resolvers (`sdk.trim`, `sdk.uppercase`, `sdk.parseFloat`)
372
+ - ✅ Nested object mapping (`primaryAddress`, `coordinates`)
373
+ - ✅ Environment variable support (`${RETAILER_ID}`)
374
+
375
+ ### GraphQL Mutation Approach
376
+
377
+ **Target Mutation:**
378
+
379
+ ```graphql
380
+ mutation CreateLocation($input: CreateLocationInput!) {
381
+ createLocation(input: $input) {
382
+ id
383
+ ref
384
+ name
385
+ status
386
+ }
387
+ }
388
+ ```
389
+
390
+ **Mutation Variables (after transformation):**
391
+
392
+ ```json
393
+ {
394
+ "input": {
395
+ "ref": "LOC001",
396
+ "type": "STORE",
397
+ "name": "Downtown Store",
398
+ "status": "ACTIVE",
399
+ "primaryAddress": {
400
+ "street": "123 Main St",
401
+ "city": "New York",
402
+ "state": "NY",
403
+ "postcode": "10001",
404
+ "country": "US"
405
+ },
406
+ "coordinates": {
407
+ "latitude": 40.7128,
408
+ "longitude": -74.006
409
+ },
410
+ "retailerId": "my-retailer"
411
+ }
412
+ }
413
+ ```
414
+
415
+ ### Complete Working Code
416
+
417
+ **`location-etl.ts`:**
418
+
419
+ ```typescript
420
+ // FC Connect SDK+
421
+ // Install: npm install @fluentcommerce/fc-connect-sdk@latest
422
+ // Docs: https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk
423
+ // GitHub: https://github.com/fluentcommerce/fc-connect-sdk
424
+
425
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
426
+ import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
427
+ import { CSVParserService } from '@fluentcommerce/fc-connect-sdk';
428
+ import { UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
429
+ import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
430
+ // Access openKv from context: const { openKv } = ctx;
431
+ import * as fs from 'fs';
432
+
433
+ // Initialize state service (prevents duplicate processing)
434
+ // ✅ CORRECT: Access openKv from Versori context
435
+ export async function masterDataETL(ctx: any, configPath: string) {
436
+ // Load configuration
437
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
438
+
439
+ // Initialize SDK components
440
+ const logger = console; // Replace with proper logger
441
+ const { openKv } = ctx;
442
+ const fluentClient = await createClient({
443
+ config: {
444
+ baseUrl: process.env.FLUENT_BASE_URL!,
445
+ clientId: process.env.FLUENT_CLIENT_ID!,
446
+ clientSecret: process.env.FLUENT_CLIENT_SECRET!,
447
+ retailerId: process.env.FLUENT_RETAILER_ID!,
448
+ },
449
+ logger,
450
+ });
451
+
452
+ // Initialize data source (S3 in this example)
453
+ const s3DataSource = new S3DataSource(
454
+ {
455
+ type: 'S3_CSV',
456
+ s3Config: {
457
+ bucket: config.sourceConfig.bucket,
458
+ region: process.env.AWS_REGION!,
459
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
460
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
461
+ },
462
+ },
463
+ logger
464
+ );
465
+
466
+ // Initialize state service (prevents duplicate processing)
467
+ const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
468
+ const stateService = new StateService(logger);
469
+
470
+ // Initialize parser based on format
471
+ const parser = config.parseConfig.format === 'csv' ? new CSVParserService() : null; // Add JSON/XML parsers as needed
472
+
473
+ // Initialize mapper
474
+ const mapper = new UniversalMapper(config.mappingConfig, { logger, fluentClient });
475
+
476
+ logger.info(`Starting ${config.entityType} ETL process`);
477
+
478
+ try {
479
+ // PHASE 1: EXTRACT - List files from source
480
+ const files = await s3DataSource.listFiles({
481
+ prefix: config.sourceConfig.prefix,
482
+ });
483
+
484
+ logger.info(`Found ${files.length} files to process`);
485
+
486
+ // Process each file
487
+ for (const file of files) {
488
+ const fileKey = `${config.entityType}:${file.name}`;
489
+
490
+ // Check if already processed
491
+ const alreadyProcessed = await stateService.isFileProcessed(kvAdapter, fileKey);
492
+ if (alreadyProcessed) {
493
+ logger.info(`Skipping already processed file: ${file.name}`);
494
+ continue;
495
+ }
496
+
497
+ logger.info(`Processing file: ${file.name}`);
498
+
499
+ try {
500
+ // PHASE 2: PARSE - Download and parse file
501
+ const fileContent = await s3DataSource.downloadFile(file.path);
502
+ const records = await parser!.parse(fileContent as string);
503
+
504
+ logger.info(`Parsed ${records.length} records from ${file.name}`);
505
+
506
+ // PHASE 3: TRANSFORM - Map each record
507
+ const transformedRecords = [];
508
+ for (const record of records) {
509
+ const result = await mapper.map(record);
510
+ if (result.success) {
511
+ transformedRecords.push(result.data);
512
+ } else {
513
+ logger.error(`Mapping failed for record:`, {
514
+ record,
515
+ errors: result.errors,
516
+ });
517
+ }
518
+ }
519
+
520
+ logger.info(`Transformed ${transformedRecords.length} records successfully`);
521
+
522
+ // PHASE 4: LOAD - Submit to Fluent Commerce
523
+ if (config.loadConfig.strategy === 'graphql') {
524
+ await loadViaGraphQL(fluentClient, transformedRecords, config.loadConfig, logger);
525
+ } else if (config.loadConfig.strategy === 'event') {
526
+ await loadViaEventAPI(fluentClient, transformedRecords, config.loadConfig, logger);
527
+ }
528
+
529
+ // Update sync state with processed file metadata
530
+ await stateService.updateSyncState(
531
+ kvAdapter,
532
+ [
533
+ {
534
+ fileName: file.name,
535
+ lastModified: new Date().toISOString(),
536
+ recordCount: transformedRecords.length,
537
+ },
538
+ ],
539
+ 'master-data-etl'
540
+ );
541
+
542
+ logger.info(`Successfully processed file: ${file.name}`);
543
+ } catch (error) {
544
+ logger.error(`Failed to process file: ${file.name}`, error);
545
+ // Continue to next file instead of failing entire batch
546
+ continue;
547
+ }
548
+ }
549
+
550
+ logger.info(`${config.entityType} ETL process completed`);
551
+ } catch (error) {
552
+ logger.error(`${config.entityType} ETL process failed`, error);
553
+ throw error;
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Load data via GraphQL mutations
559
+ */
560
+ async function loadViaGraphQL(client: any, records: any[], loadConfig: any, logger: any) {
561
+ const batchSize = loadConfig.batchSize || 100;
562
+ const mutation = loadConfig.mutation;
563
+
564
+ logger.info(`Loading ${records.length} records via GraphQL mutation: ${mutation}`);
565
+
566
+ // Process in batches
567
+ for (let i = 0; i < records.length; i += batchSize) {
568
+ const batch = records.slice(i, i + batchSize);
569
+ logger.info(`Processing batch ${i / batchSize + 1} (${batch.length} records)`);
570
+
571
+ // Execute mutations for batch
572
+ for (const record of batch) {
573
+ const query = `
574
+ mutation ${mutation}($input: ${capitalize(mutation)}Input!) {
575
+ ${mutation}(input: $input) {
576
+ id
577
+ ref
578
+ }
579
+ }
580
+ `;
581
+
582
+ try {
583
+ const result = await client.graphql({
584
+ query,
585
+ variables: { input: record },
586
+ });
587
+
588
+ logger.debug(`Created ${mutation}:`, result.data[mutation]);
589
+ } catch (error) {
590
+ logger.error(`Failed to create ${mutation}:`, { record, error });
591
+ // Continue to next record instead of failing entire batch
592
+ }
593
+ }
594
+ }
595
+
596
+ logger.info(`GraphQL load completed`);
597
+ }
598
+
599
+ /**
600
+ * Load data via Event API
601
+ */
602
+ async function loadViaEventAPI(client: any, records: any[], loadConfig: any, logger: any) {
603
+ const eventName = loadConfig.eventName;
604
+
605
+ logger.info(`Loading ${records.length} records via Event API: ${eventName}`);
606
+
607
+ for (const record of records) {
608
+ try {
609
+ await client.sendEvent({
610
+ name: eventName,
611
+ entityRef: record.ref,
612
+ entityType: loadConfig.entityType,
613
+ retailerId: record.retailerId,
614
+ attributes: record,
615
+ });
616
+
617
+ logger.debug(`Sent event ${eventName} for ${record.ref}`);
618
+ } catch (error) {
619
+ logger.error(`Failed to send event for ${record.ref}:`, error);
620
+ }
621
+ }
622
+
623
+ logger.info(`Event API load completed`);
624
+ }
625
+
626
+ // Helper function
627
+ function capitalize(str: string): string {
628
+ return str.charAt(0).toUpperCase() + str.slice(1);
629
+ }
630
+
631
+ // Example usage
632
+ if (require.main === module) {
633
+ masterDataETL('config/location-etl-config.json')
634
+ .then(() => console.log('ETL completed'))
635
+ .catch(err => {
636
+ console.error('ETL failed:', err);
637
+ process.exit(1);
638
+ });
639
+ }
640
+ ```
641
+
642
+ **Configuration File (`config/location-etl-config.json`):**
643
+
644
+ ```json
645
+ {
646
+ "entityType": "location",
647
+ "sourceConfig": {
648
+ "type": "S3_CSV",
649
+ "bucket": "master-data",
650
+ "prefix": "locations/",
651
+ "filePattern": "*.csv"
652
+ },
653
+ "parseConfig": {
654
+ "format": "csv",
655
+ "delimiter": ",",
656
+ "headers": true
657
+ },
658
+ "mappingConfig": {
659
+ "version": "1.0",
660
+ "fields": {
661
+ "ref": { "source": "location_id", "required": true },
662
+ "name": { "source": "location_name", "required": true },
663
+ "type": { "source": "type", "required": true },
664
+ "status": { "source": "status", "defaultValue": "ACTIVE" },
665
+ "primaryAddress": {
666
+ "fields": {
667
+ "street": { "source": "address_line1" },
668
+ "city": { "source": "city" },
669
+ "state": { "source": "state" },
670
+ "postcode": { "source": "zip" },
671
+ "country": { "source": "country" }
672
+ }
673
+ },
674
+ "retailerId": { "value": "${RETAILER_ID}" }
675
+ }
676
+ },
677
+ "loadConfig": {
678
+ "strategy": "graphql",
679
+ "mutation": "createLocation",
680
+ "batchSize": 100
681
+ }
682
+ }
683
+ ```
684
+
685
+ **Key Design Decisions:**
686
+
687
+ 1. **Configuration-Driven**: All entity logic in JSON config, not code
688
+ 2. **Error Handling**: Continue processing on individual record failures
689
+ 3. **State Management**: Prevent duplicate processing of files
690
+ 4. **Batching**: Process large datasets in manageable chunks
691
+ 5. **Logging**: Comprehensive logging for debugging and monitoring
692
+
693
+ ---
694
+
695
+ ## Example 2: Product Catalog
696
+
697
+ ### Overview
698
+
699
+ Load product catalog with variants (parent-child relationships) from JSON files.
700
+
701
+ **Business Context:**
702
+
703
+ - Product data includes parent products and child variants (size, color, etc.)
704
+ - Source: Product Information Management (PIM) system exports JSON
705
+ - Destination: Fluent Product entities
706
+ - Complexity: Parent-child relationships require nested mapping
707
+
708
+ ### Source Data Formats
709
+
710
+ **JSON Format (with variants):**
711
+
712
+ ```json
713
+ {
714
+ "products": [
715
+ {
716
+ "productId": "PROD001",
717
+ "name": "Classic T-Shirt",
718
+ "description": "Premium cotton t-shirt",
719
+ "brand": "MyBrand",
720
+ "category": "Apparel",
721
+ "variants": [
722
+ {
723
+ "sku": "PROD001-S-RED",
724
+ "size": "S",
725
+ "color": "Red",
726
+ "price": 29.99,
727
+ "barcode": "123456789001"
728
+ },
729
+ {
730
+ "sku": "PROD001-M-RED",
731
+ "size": "M",
732
+ "color": "Red",
733
+ "price": 29.99,
734
+ "barcode": "123456789002"
735
+ }
736
+ ]
737
+ }
738
+ ]
739
+ }
740
+ ```
741
+
742
+ **CSV Format (flattened variants):**
743
+
744
+ ```csv
745
+ product_id,product_name,sku,size,color,price,barcode
746
+ PROD001,Classic T-Shirt,PROD001-S-RED,S,Red,29.99,123456789001
747
+ PROD001,Classic T-Shirt,PROD001-M-RED,M,Red,29.99,123456789002
748
+ ```
749
+
750
+ ### Field Mapping Configuration
751
+
752
+ **`config/product-mapping.json`:**
753
+
754
+ ```json
755
+ {
756
+ "version": "1.0",
757
+ "description": "Map PIM product data to Fluent Product schema",
758
+ "fields": {
759
+ "ref": {
760
+ "source": "productId",
761
+ "required": true
762
+ },
763
+ "type": {
764
+ "value": "STANDARD",
765
+ "required": true
766
+ },
767
+ "name": {
768
+ "source": "name",
769
+ "required": true
770
+ },
771
+ "summary": {
772
+ "source": "description"
773
+ },
774
+ "gtin": {
775
+ "source": "barcode"
776
+ },
777
+ "status": {
778
+ "value": "ACTIVE"
779
+ },
780
+ "retailerId": {
781
+ "value": "${RETAILER_ID}"
782
+ },
783
+ "attributes": {
784
+ "fields": {
785
+ "brand": { "source": "brand" },
786
+ "category": { "source": "category" }
787
+ }
788
+ },
789
+ "variants": {
790
+ "source": "variants",
791
+ "isArray": true,
792
+ "fields": {
793
+ "ref": { "source": "$.sku", "required": true },
794
+ "attributes": {
795
+ "fields": {
796
+ "size": { "source": "$.size" },
797
+ "color": { "source": "$.color" }
798
+ }
799
+ },
800
+ "prices": {
801
+ "fields": {
802
+ "currency": { "value": "USD" },
803
+ "value": { "source": "$.price", "resolver": "sdk.parseFloat" }
804
+ }
805
+ },
806
+ "gtin": { "source": "$.barcode" }
807
+ }
808
+ }
809
+ }
810
+ }
811
+ ```
812
+
813
+ **Key Features:**
814
+
815
+ - ✅ Parent-child relationships (`variants[]` array mapping)
816
+ - ✅ Relative paths within arrays (`$.sku` references current variant)
817
+ - ✅ Nested objects (`attributes`, `prices`)
818
+ - ✅ Static values (`type: "STANDARD"`, `currency: "USD"`)
819
+
820
+ ### Complete Working Code
821
+
822
+ **`product-etl.ts`:**
823
+
824
+ ```typescript
825
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
826
+ import { S3DataSource, JSONParserService } from '@fluentcommerce/fc-connect-sdk';
827
+ import { UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
828
+ import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
829
+ // Access openKv from context: const { openKv } = ctx;
830
+ import * as fs from 'fs';
831
+
832
+ /**
833
+ * Product Catalog ETL with Parent-Child Relationships
834
+ */
835
+ export async function productCatalogETL(ctx: any) {
836
+ const logger = console;
837
+ const { openKv } = ctx;
838
+ const fluentClient = await createClient({
839
+ config: {
840
+ baseUrl: process.env.FLUENT_BASE_URL!,
841
+ clientId: process.env.FLUENT_CLIENT_ID!,
842
+ clientSecret: process.env.FLUENT_CLIENT_SECRET!,
843
+ retailerId: process.env.FLUENT_RETAILER_ID!,
844
+ },
845
+ logger,
846
+ });
847
+
848
+ // Initialize components
849
+ const s3DataSource = new S3DataSource(
850
+ {
851
+ type: 'S3_CSV',
852
+ s3Config: {
853
+ bucket: 'product-catalog',
854
+ region: process.env.AWS_REGION!,
855
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
856
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
857
+ },
858
+ },
859
+ logger
860
+ );
861
+
862
+ const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
863
+ const stateService = new StateService(logger);
864
+
865
+ const jsonParser = new JSONParserService();
866
+
867
+ // Load mapping configuration
868
+ const mappingConfig = JSON.parse(fs.readFileSync('config/product-mapping.json', 'utf-8'));
869
+
870
+ const mapper = new UniversalMapper(mappingConfig, { logger, fluentClient });
871
+
872
+ logger.info('Starting product catalog ETL');
873
+
874
+ try {
875
+ // List JSON files
876
+ const files = await s3DataSource.listFiles({ prefix: 'products/' });
877
+
878
+ for (const file of files) {
879
+ if (!file.name.endsWith('.json')) continue;
880
+
881
+ const fileKey = `product:${file.name}`;
882
+
883
+ // Check if processed
884
+ if (await stateService.isFileProcessed(kvAdapter, fileKey)) {
885
+ logger.info(`Skipping processed file: ${file.name}`);
886
+ continue;
887
+ }
888
+
889
+ logger.info(`Processing file: ${file.name}`);
890
+
891
+ try {
892
+ // Download and parse JSON
893
+ const fileContent = await s3DataSource.downloadFile(file.path);
894
+ const data = await jsonParser.parse(fileContent as string, {
895
+ dataPath: 'products', // Extract products array from root
896
+ });
897
+
898
+ const products = Array.isArray(data) ? data : [data];
899
+ logger.info(`Parsed ${products.length} products from ${file.name}`);
900
+
901
+ // Transform and load each product
902
+ for (const product of products) {
903
+ const result = await mapper.map(product);
904
+
905
+ if (result.success) {
906
+ // Create parent product
907
+ await createProduct(fluentClient, result.data, logger);
908
+
909
+ // Create variants if present
910
+ if (result.data.variants && result.data.variants.length > 0) {
911
+ for (const variant of result.data.variants) {
912
+ await createVariant(fluentClient, result.data.ref, variant, logger);
913
+ }
914
+ }
915
+ } else {
916
+ logger.error('Product mapping failed:', {
917
+ product,
918
+ errors: result.errors,
919
+ });
920
+ }
921
+ }
922
+
923
+ // Mark as processed
924
+ await stateService.updateSyncState(
925
+ kvAdapter,
926
+ [
927
+ {
928
+ fileName: file.name,
929
+ lastModified: new Date().toISOString(),
930
+ recordCount: products.length,
931
+ },
932
+ ],
933
+ 'product-catalog-etl'
934
+ );
935
+
936
+ logger.info(`Successfully processed: ${file.name}`);
937
+ } catch (error) {
938
+ logger.error(`Failed to process file: ${file.name}`, error);
939
+ continue;
940
+ }
941
+ }
942
+
943
+ logger.info('Product catalog ETL completed');
944
+ } catch (error) {
945
+ logger.error('Product catalog ETL failed', error);
946
+ throw error;
947
+ }
948
+ }
949
+
950
+ /**
951
+ * Create product via GraphQL
952
+ */
953
+ async function createProduct(client: any, productData: any, logger: any) {
954
+ const mutation = `
955
+ mutation CreateProduct($input: CreateProductInput!) {
956
+ createProduct(input: $input) {
957
+ id
958
+ ref
959
+ name
960
+ status
961
+ }
962
+ }
963
+ `;
964
+
965
+ try {
966
+ const result = await client.graphql({
967
+ query: mutation,
968
+ variables: { input: productData },
969
+ });
970
+
971
+ logger.info(`Created product: ${productData.ref}`, result.data.createProduct);
972
+ } catch (error) {
973
+ logger.error(`Failed to create product: ${productData.ref}`, error);
974
+ throw error;
975
+ }
976
+ }
977
+
978
+ /**
979
+ * Create variant via GraphQL
980
+ */
981
+ async function createVariant(client: any, parentRef: string, variantData: any, logger: any) {
982
+ const mutation = `
983
+ mutation CreateVariant($input: CreateVariantInput!) {
984
+ createVariant(input: $input) {
985
+ id
986
+ ref
987
+ attributes
988
+ }
989
+ }
990
+ `;
991
+
992
+ try {
993
+ const result = await client.graphql({
994
+ query: mutation,
995
+ variables: {
996
+ input: {
997
+ ...variantData,
998
+ parentRef,
999
+ },
1000
+ },
1001
+ });
1002
+
1003
+ logger.info(`Created variant: ${variantData.ref}`, result.data.createVariant);
1004
+ } catch (error) {
1005
+ logger.error(`Failed to create variant: ${variantData.ref}`, error);
1006
+ // Don't throw - continue with other variants
1007
+ }
1008
+ }
1009
+
1010
+ // Example usage
1011
+ if (require.main === module) {
1012
+ productCatalogETL()
1013
+ .then(() => console.log('ETL completed'))
1014
+ .catch(err => {
1015
+ console.error('ETL failed:', err);
1016
+ process.exit(1);
1017
+ });
1018
+ }
1019
+ ```
1020
+
1021
+ **Key Design Decisions:**
1022
+
1023
+ 1. **Parent-Child Processing**: Create parent product first, then variants
1024
+ 2. **Array Mapping**: `variants[]` notation handles nested arrays
1025
+ 3. **Relative Paths**: `$.sku` resolves relative to current variant item
1026
+ 4. **Error Isolation**: Variant creation failures don't stop parent processing
1027
+
1028
+ ---
1029
+
1030
+ ## Example 3: Control/Config Data
1031
+
1032
+ ### Overview
1033
+
1034
+ Load business rules and configuration parameters from JSON files.
1035
+
1036
+ **Business Context:**
1037
+
1038
+ - Controls define business rules, thresholds, flags
1039
+ - Source: Configuration management system exports JSON
1040
+ - Destination: Fluent Control entities
1041
+ - Characteristics: Simple structure, validation logic important
1042
+
1043
+ ### Source Data Format
1044
+
1045
+ **JSON Format:**
1046
+
1047
+ ```json
1048
+ {
1049
+ "controls": [
1050
+ {
1051
+ "name": "ORDER_TIMEOUT_MINUTES",
1052
+ "value": "30",
1053
+ "type": "INTEGER",
1054
+ "context": "ORDER_MANAGEMENT",
1055
+ "description": "Order allocation timeout in minutes"
1056
+ },
1057
+ {
1058
+ "name": "ENABLE_AUTO_ALLOCATION",
1059
+ "value": "true",
1060
+ "type": "BOOLEAN",
1061
+ "context": "ORDER_MANAGEMENT",
1062
+ "description": "Enable automatic order allocation"
1063
+ },
1064
+ {
1065
+ "name": "DEFAULT_CARRIER",
1066
+ "value": "FEDEX",
1067
+ "type": "STRING",
1068
+ "context": "FULFILLMENT",
1069
+ "description": "Default shipping carrier"
1070
+ }
1071
+ ]
1072
+ }
1073
+ ```
1074
+
1075
+ ### Field Mapping Configuration
1076
+
1077
+ **`config/control-mapping.json`:**
1078
+
1079
+ ```json
1080
+ {
1081
+ "version": "1.0",
1082
+ "description": "Map configuration controls to Fluent Control schema",
1083
+ "fields": {
1084
+ "name": {
1085
+ "source": "name",
1086
+ "required": true,
1087
+ "resolver": "sdk.uppercase"
1088
+ },
1089
+ "value": {
1090
+ "source": "value",
1091
+ "required": true,
1092
+ "resolver": "custom.validateControlValue"
1093
+ },
1094
+ "type": {
1095
+ "source": "type",
1096
+ "required": true,
1097
+ "resolver": "sdk.uppercase"
1098
+ },
1099
+ "context": {
1100
+ "source": "context",
1101
+ "defaultValue": "GLOBAL"
1102
+ },
1103
+ "description": {
1104
+ "source": "description"
1105
+ },
1106
+ "status": {
1107
+ "value": "ACTIVE"
1108
+ },
1109
+ "retailerId": {
1110
+ "value": "${RETAILER_ID}"
1111
+ }
1112
+ }
1113
+ }
1114
+ ```
1115
+
1116
+ ### Validation Logic
1117
+
1118
+ **Custom resolver for control value validation:**
1119
+
1120
+ ```typescript
1121
+ import { FieldResolverFunction } from '@fluentcommerce/fc-connect-sdk';
1122
+
1123
+ /**
1124
+ * Validate control value based on type
1125
+ */
1126
+ export const validateControlValue: FieldResolverFunction = (
1127
+ value: any,
1128
+ sourceData: any,
1129
+ config: any,
1130
+ helpers: any
1131
+ ) => {
1132
+ const type = sourceData.type;
1133
+
1134
+ switch (type) {
1135
+ case 'INTEGER':
1136
+ const intValue = helpers.parseIntSafe(value, null);
1137
+ if (intValue === null) {
1138
+ throw new Error(`Invalid INTEGER value: ${value}`);
1139
+ }
1140
+ return intValue.toString();
1141
+
1142
+ case 'FLOAT':
1143
+ const floatValue = helpers.parseFloatSafe(value, null);
1144
+ if (floatValue === null) {
1145
+ throw new Error(`Invalid FLOAT value: ${value}`);
1146
+ }
1147
+ return floatValue.toString();
1148
+
1149
+ case 'BOOLEAN':
1150
+ if (!['true', 'false'].includes(String(value).toLowerCase())) {
1151
+ throw new Error(`Invalid BOOLEAN value: ${value}`);
1152
+ }
1153
+ return String(value).toLowerCase();
1154
+
1155
+ case 'STRING':
1156
+ return String(value);
1157
+
1158
+ case 'JSON':
1159
+ try {
1160
+ JSON.parse(value);
1161
+ return value;
1162
+ } catch (error) {
1163
+ throw new Error(`Invalid JSON value: ${value}`);
1164
+ }
1165
+
1166
+ default:
1167
+ helpers.log.warn(`Unknown control type: ${type}, treating as STRING`);
1168
+ return String(value);
1169
+ }
1170
+ };
1171
+ ```
1172
+
1173
+ ### Complete Working Code
1174
+
1175
+ **`control-etl.ts`:**
1176
+
1177
+ ```typescript
1178
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
1179
+ import { S3DataSource, JSONParserService } from '@fluentcommerce/fc-connect-sdk';
1180
+ import { UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
1181
+ import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
1182
+ // Access openKv from context: const { openKv } = ctx;
1183
+ import * as fs from 'fs';
1184
+ import { validateControlValue } from './resolvers/control-validators';
1185
+
1186
+ /**
1187
+ * Control/Config Data ETL
1188
+ */
1189
+ export async function controlDataETL(ctx: any) {
1190
+ const logger = console;
1191
+ const { openKv } = ctx;
1192
+ const fluentClient = await createClient({
1193
+ config: {
1194
+ baseUrl: process.env.FLUENT_BASE_URL!,
1195
+ clientId: process.env.FLUENT_CLIENT_ID!,
1196
+ clientSecret: process.env.FLUENT_CLIENT_SECRET!,
1197
+ retailerId: process.env.FLUENT_RETAILER_ID!,
1198
+ },
1199
+ logger,
1200
+ });
1201
+
1202
+ // Initialize components
1203
+ const s3DataSource = new S3DataSource(
1204
+ {
1205
+ type: 'S3_CSV',
1206
+ s3Config: {
1207
+ bucket: 'config-data',
1208
+ region: process.env.AWS_REGION!,
1209
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
1210
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
1211
+ },
1212
+ },
1213
+ logger
1214
+ );
1215
+
1216
+ const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
1217
+ const stateService = new StateService(logger);
1218
+
1219
+ const jsonParser = new JSONParserService();
1220
+
1221
+ // Load mapping configuration
1222
+ const mappingConfig = JSON.parse(fs.readFileSync('config/control-mapping.json', 'utf-8'));
1223
+
1224
+ // Initialize mapper with custom resolvers
1225
+ const mapper = new UniversalMapper(mappingConfig, {
1226
+ logger,
1227
+ fluentClient,
1228
+ customResolvers: {
1229
+ 'custom.validateControlValue': validateControlValue,
1230
+ },
1231
+ });
1232
+
1233
+ logger.info('Starting control data ETL');
1234
+
1235
+ try {
1236
+ // List JSON files
1237
+ const files = await s3DataSource.listFiles({ prefix: 'controls/' });
1238
+
1239
+ for (const file of files) {
1240
+ if (!file.name.endsWith('.json')) continue;
1241
+
1242
+ const fileKey = `control:${file.name}`;
1243
+
1244
+ // Check if processed
1245
+ if (await stateService.isFileProcessed(kvAdapter, fileKey)) {
1246
+ logger.info(`Skipping processed file: ${file.name}`);
1247
+ continue;
1248
+ }
1249
+
1250
+ logger.info(`Processing file: ${file.name}`);
1251
+
1252
+ try {
1253
+ // Download and parse JSON
1254
+ const fileContent = await s3DataSource.downloadFile(file.path);
1255
+ const data = await jsonParser.parse(fileContent as string, {
1256
+ dataPath: 'controls',
1257
+ });
1258
+
1259
+ const controls = Array.isArray(data) ? data : [data];
1260
+ logger.info(`Parsed ${controls.length} controls from ${file.name}`);
1261
+
1262
+ // Transform and load each control
1263
+ let successCount = 0;
1264
+ let failureCount = 0;
1265
+
1266
+ for (const control of controls) {
1267
+ try {
1268
+ const result = await mapper.map(control);
1269
+
1270
+ if (result.success) {
1271
+ await createControl(fluentClient, result.data, logger);
1272
+ successCount++;
1273
+ } else {
1274
+ logger.error('Control mapping failed:', {
1275
+ control,
1276
+ errors: result.errors,
1277
+ });
1278
+ failureCount++;
1279
+ }
1280
+ } catch (error) {
1281
+ logger.error(`Failed to process control: ${control.name}`, error);
1282
+ failureCount++;
1283
+ }
1284
+ }
1285
+
1286
+ logger.info(`Control processing summary:`, {
1287
+ total: controls.length,
1288
+ success: successCount,
1289
+ failures: failureCount,
1290
+ });
1291
+
1292
+ // Mark as processed
1293
+ await stateService.updateSyncState(
1294
+ kvAdapter,
1295
+ [
1296
+ {
1297
+ fileName: file.name,
1298
+ lastModified: new Date().toISOString(),
1299
+ recordCount: controls.length,
1300
+ },
1301
+ ],
1302
+ 'control-data-etl'
1303
+ );
1304
+
1305
+ logger.info(`Successfully processed: ${file.name}`);
1306
+ } catch (error) {
1307
+ logger.error(`Failed to process file: ${file.name}`, error);
1308
+ continue;
1309
+ }
1310
+ }
1311
+
1312
+ logger.info('Control data ETL completed');
1313
+ } catch (error) {
1314
+ logger.error('Control data ETL failed', error);
1315
+ throw error;
1316
+ }
1317
+ }
1318
+
1319
+ /**
1320
+ * Create control via GraphQL
1321
+ */
1322
+ async function createControl(client: any, controlData: any, logger: any) {
1323
+ const mutation = `
1324
+ mutation CreateControl($input: CreateControlInput!) {
1325
+ createControl(input: $input) {
1326
+ id
1327
+ name
1328
+ value
1329
+ type
1330
+ context
1331
+ }
1332
+ }
1333
+ `;
1334
+
1335
+ try {
1336
+ const result = await client.graphql({
1337
+ query: mutation,
1338
+ variables: { input: controlData },
1339
+ });
1340
+
1341
+ logger.info(`Created control: ${controlData.name}`, {
1342
+ value: controlData.value,
1343
+ type: controlData.type,
1344
+ });
1345
+ } catch (error) {
1346
+ logger.error(`Failed to create control: ${controlData.name}`, error);
1347
+ throw error;
1348
+ }
1349
+ }
1350
+
1351
+ // Example usage
1352
+ if (require.main === module) {
1353
+ controlDataETL()
1354
+ .then(() => console.log('ETL completed'))
1355
+ .catch(err => {
1356
+ console.error('ETL failed:', err);
1357
+ process.exit(1);
1358
+ });
1359
+ }
1360
+ ```
1361
+
1362
+ **Key Design Decisions:**
1363
+
1364
+ 1. **Type Validation**: Custom resolver validates value based on type field
1365
+ 2. **Error Tracking**: Count successes/failures for summary reporting
1366
+ 3. **Atomic Processing**: Each control processed independently
1367
+ 4. **Detailed Logging**: Track validation failures with context
1368
+
1369
+ ---
1370
+
1371
+ ## Source Strategies
1372
+
1373
+ ### S3 with Event Notifications
1374
+
1375
+ **Use Case**: Process files as soon as they're uploaded to S3
1376
+
1377
+ **Setup:**
1378
+
1379
+ ```typescript
1380
+ import { webhook } from '@versori/run/webhooks';
1381
+ import { masterDataETL } from './location-etl';
1382
+
1383
+ export const s3LocationETL = webhook('s3-location-upload', {
1384
+ response: { mode: 'sync' },
1385
+ })
1386
+ .then(async ({ data }) => {
1387
+ // Parse S3 event notification
1388
+ const s3Event = data.Records[0].s3;
1389
+ const bucket = s3Event.bucket.name;
1390
+ const key = decodeURIComponent(s3Event.object.key.replace(/\+/g, ' '));
1391
+
1392
+ console.log(`S3 event received: ${bucket}/${key}`);
1393
+
1394
+ // Run ETL for this specific file
1395
+ await masterDataETL('config/location-etl-config.json');
1396
+
1397
+ return { success: true, message: 'Location ETL completed' };
1398
+ })
1399
+ .catch(({ error }) => {
1400
+ console.error('S3 location ETL failed:', error);
1401
+ return { success: false, error: error.message };
1402
+ });
1403
+ ```
1404
+
1405
+ **S3 Bucket Configuration:**
1406
+
1407
+ ```json
1408
+ {
1409
+ "LambdaFunctionConfigurations": [
1410
+ {
1411
+ "LambdaFunctionArn": "arn:aws:lambda:...:function:versori-webhook",
1412
+ "Events": ["s3:ObjectCreated:*"],
1413
+ "Filter": {
1414
+ "Key": {
1415
+ "FilterRules": [
1416
+ {
1417
+ "Name": "prefix",
1418
+ "Value": "locations/"
1419
+ },
1420
+ {
1421
+ "Name": "suffix",
1422
+ "Value": ".csv"
1423
+ }
1424
+ ]
1425
+ }
1426
+ }
1427
+ }
1428
+ ]
1429
+ }
1430
+ ```
1431
+
1432
+ ### SFTP with Polling
1433
+
1434
+ **Use Case**: Poll SFTP server periodically for new files
1435
+
1436
+ #### SFTP Credential Access
1437
+
1438
+ **Versori Platform** has three methods for accessing SFTP credentials:
1439
+
1440
+ 1. **Connection Variables (Recommended)** - Direct access to connection config:
1441
+ ```typescript
1442
+ const { host, port, username, password, privateKey } = ctx.activation.connections.sftp_server;
1443
+ ```
1444
+
1445
+ 2. **Credentials API** - For base64-encoded credentials:
1446
+ ```typescript
1447
+ const creds = await ctx.credentials().getAccessToken('sftp_server');
1448
+ ```
1449
+
1450
+ 3. **Connection String Parsing** - Decode `connectionVariables.connectionString`:
1451
+ ```typescript
1452
+ const connStr = ctx.activation.connections.sftp_server.connectionString;
1453
+ // Parse: sftp://username:password@host:port
1454
+ ```
1455
+
1456
+ **Standalone Node.js/Deno**: Use environment variables directly:
1457
+ ```typescript
1458
+ const config = {
1459
+ host: process.env.SFTP_HOST!,
1460
+ port: parseInt(process.env.SFTP_PORT || '22'),
1461
+ username: process.env.SFTP_USERNAME!,
1462
+ password: process.env.SFTP_PASSWORD,
1463
+ privateKey: process.env.SFTP_PRIVATE_KEY,
1464
+ };
1465
+ ```
1466
+
1467
+ **Security Best Practices:**
1468
+ - Always prefer SSH keys over passwords
1469
+ - Never log credential values
1470
+ - Use scoped credentials (read-only when possible)
1471
+ - Rotate credentials regularly
1472
+
1473
+ **See:** [SFTP Credential Access Security Guide](../../02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md) for complete details.
1474
+
1475
+ #### Setup Example
1476
+
1477
+ ```typescript
1478
+ import { schedule } from '@versori/run/schedule';
1479
+ import { SftpDataSource } from '@fluentcommerce/fc-connect-sdk';
1480
+ import { Buffer } from 'node:buffer'; // Required for Deno/Versori
1481
+
1482
+ export const sftpPolling = schedule('location-sftp-poll', {
1483
+ cron: '0 */6 * * *', // Every 6 hours
1484
+ retry: { attempts: 3 },
1485
+ }).then(async (ctx) => {
1486
+ const { log, activation } = ctx;
1487
+
1488
+ // Access SFTP credentials (Versori)
1489
+ const { host, port, username, password, privateKey } = activation.connections.sftp_server;
1490
+
1491
+ // Initialize SFTP data source
1492
+ const sftpSource = new SftpDataSource(
1493
+ {
1494
+ type: 'SFTP_CSV',
1495
+ settings: {
1496
+ host,
1497
+ port: port || 22,
1498
+ username,
1499
+ password,
1500
+ privateKey,
1501
+ remotePath: '/data/locations',
1502
+ filePattern: '*.csv',
1503
+ },
1504
+ },
1505
+ log
1506
+ );
1507
+
1508
+ // List new files
1509
+ const files = await sftpSource.listFiles();
1510
+ log.info(`Found ${files.length} files on SFTP`);
1511
+
1512
+ for (const file of files) {
1513
+ try {
1514
+ // Download file
1515
+ const content = await sftpSource.downloadFile(file.path);
1516
+
1517
+ // Process file (same ETL logic as S3)
1518
+ // ... (extract, parse, transform, load)
1519
+
1520
+ // Move to processed folder
1521
+ await sftpSource.moveFile(file.path, `/data/locations/processed/${file.name}`);
1522
+ } catch (error) {
1523
+ log.error(`Failed to process ${file.name}:`, error);
1524
+ }
1525
+ }
1526
+
1527
+ return { success: true, filesProcessed: files.length };
1528
+ });
1529
+ ```
1530
+
1531
+ ---
1532
+
1533
+ ## Load Strategies
1534
+
1535
+ ### GraphQL Mutation Approach
1536
+
1537
+ **When to Use:**
1538
+
1539
+ - Simple entity creation/updates
1540
+ - Direct control over mutations
1541
+ - Schema validation needed
1542
+ - Small to medium datasets (<10K records)
1543
+
1544
+ **Advantages:**
1545
+
1546
+ - ✅ Type-safe with GraphQL schema
1547
+ - ✅ Immediate validation feedback
1548
+ - ✅ Fine-grained control over mutations
1549
+ - ✅ Can return created IDs
1550
+
1551
+ **Example:**
1552
+
1553
+ ```typescript
1554
+ async function loadViaGraphQL(records: any[], mutation: string, logger: any) {
1555
+ const client = await createClient({
1556
+ config: {
1557
+ baseUrl: process.env.FLUENT_BASE_URL!,
1558
+ clientId: process.env.FLUENT_CLIENT_ID!,
1559
+ clientSecret: process.env.FLUENT_CLIENT_SECRET!,
1560
+ retailerId: process.env.FLUENT_RETAILER_ID!,
1561
+ },
1562
+ });
1563
+
1564
+ for (const record of records) {
1565
+ const query = `
1566
+ mutation ${mutation}($input: ${capitalize(mutation)}Input!) {
1567
+ ${mutation}(input: $input) {
1568
+ id
1569
+ ref
1570
+ }
1571
+ }
1572
+ `;
1573
+
1574
+ try {
1575
+ const result = await client.graphql({
1576
+ query,
1577
+ variables: { input: record },
1578
+ });
1579
+
1580
+ logger.info(`Created ${mutation}:`, result.data[mutation]);
1581
+ } catch (error: any) {
1582
+ logger.error(`Failed ${mutation}:`, {
1583
+ record,
1584
+ error: error.message,
1585
+ details: error.response?.errors,
1586
+ });
1587
+ }
1588
+ }
1589
+ }
1590
+ ```
1591
+
1592
+ ### Event API Approach
1593
+
1594
+ **When to Use:**
1595
+
1596
+ - Asynchronous processing acceptable
1597
+ - Triggering workflows/rules needed
1598
+ - Large datasets (>10K records)
1599
+ - Need event-driven architecture
1600
+
1601
+ **Advantages:**
1602
+
1603
+ - ✅ Asynchronous (better for large datasets)
1604
+ - ✅ Triggers workflows and rules
1605
+ - ✅ Decoupled from mutations
1606
+ - ✅ Better performance for bulk loads
1607
+
1608
+ **Example:**
1609
+
1610
+ ```typescript
1611
+ async function loadViaEventAPI(records: any[], eventName: string, logger: any) {
1612
+ const client = await createClient({
1613
+ config: {
1614
+ baseUrl: process.env.FLUENT_BASE_URL!,
1615
+ clientId: process.env.FLUENT_CLIENT_ID!,
1616
+ clientSecret: process.env.FLUENT_CLIENT_SECRET!,
1617
+ retailerId: process.env.FLUENT_RETAILER_ID!,
1618
+ },
1619
+ });
1620
+
1621
+ for (const record of records) {
1622
+ try {
1623
+ await client.sendEvent({
1624
+ name: eventName,
1625
+ entityRef: record.ref,
1626
+ entityType: 'LOCATION',
1627
+ retailerId: record.retailerId,
1628
+ attributes: record,
1629
+ });
1630
+
1631
+ logger.info(`Sent event ${eventName} for ${record.ref}`);
1632
+ } catch (error: any) {
1633
+ logger.error(`Failed to send event for ${record.ref}:`, error);
1634
+ }
1635
+ }
1636
+ }
1637
+ ```
1638
+
1639
+ **Event-Driven Workflow:**
1640
+
1641
+ ```
1642
+ ETL Process Fluent Commerce
1643
+
1644
+ Send Event (LOCATION_CREATED)
1645
+ ↓ ↓
1646
+ Workflow Triggers
1647
+
1648
+ Process Event
1649
+
1650
+ Create Location
1651
+
1652
+ Apply Business Rules
1653
+
1654
+ Send Notifications
1655
+ ```
1656
+
1657
+ ---
1658
+
1659
+ ## Configuration Schema
1660
+
1661
+ ### Generic Configuration Template
1662
+
1663
+ Use this template for ANY entity type:
1664
+
1665
+ ```json
1666
+ {
1667
+ "entityType": "<entity-name>",
1668
+ "description": "<what this ETL does>",
1669
+ "sourceConfig": {
1670
+ "type": "S3_CSV | SFTP_CSV | S3_JSON | SFTP_JSON",
1671
+ "bucket": "<s3-bucket-name>",
1672
+ "prefix": "<folder-prefix>",
1673
+ "filePattern": "*.csv | *.json | *.xml",
1674
+ "sftp": {
1675
+ "host": "${SFTP_HOST}",
1676
+ "port": 22,
1677
+ "username": "${SFTP_USERNAME}",
1678
+ "password": "${SFTP_PASSWORD}",
1679
+ "privateKey": "${SFTP_PRIVATE_KEY}", // Recommended over password
1680
+ "remotePath": "/data/<entity>",
1681
+ "filePattern": "*.<format>"
1682
+ }
1683
+ },
1684
+ "parseConfig": {
1685
+ "format": "csv | json | xml",
1686
+ "delimiter": "," | "|" | "\t",
1687
+ "headers": true | false,
1688
+ "encoding": "utf8 | utf16",
1689
+ "json": {
1690
+ "dataPath": "root.path.to.array",
1691
+ "jsonLines": false
1692
+ },
1693
+ "xml": {
1694
+ "itemPath": "//Item",
1695
+ "includeAttributes": true
1696
+ }
1697
+ },
1698
+ "mappingConfig": {
1699
+ "version": "1.0",
1700
+ "description": "Field mappings",
1701
+ "fields": {
1702
+ "targetField": {
1703
+ "source": "sourceField",
1704
+ "required": true | false,
1705
+ "defaultValue": "default",
1706
+ "resolver": "sdk.* | custom.*"
1707
+ }
1708
+ }
1709
+ },
1710
+ "loadConfig": {
1711
+ "strategy": "graphql | event",
1712
+ "mutation": "createEntity | updateEntity",
1713
+ "eventName": "ENTITY_CREATED",
1714
+ "batchSize": 100,
1715
+ "retryAttempts": 3
1716
+ },
1717
+ "scheduleConfig": {
1718
+ "enabled": true,
1719
+ "cron": "0 0 * * *",
1720
+ "timezone": "UTC"
1721
+ }
1722
+ }
1723
+ ```
1724
+
1725
+ ### How to Adapt for New Entity Types
1726
+
1727
+ **Step-by-Step Guide:**
1728
+
1729
+ 1. **Copy Template**: Start with generic configuration template
1730
+ 2. **Set Entity Type**: Change `entityType` to your entity name
1731
+ 3. **Configure Source**: Set bucket/path for your data source
1732
+ 4. **Configure Parser**: Set format and parsing options
1733
+ 5. **Define Field Mappings**: Map source fields to Fluent schema
1734
+ 6. **Configure Load Strategy**: Choose GraphQL or Event API
1735
+ 7. **Test**: Run ETL with sample data
1736
+ 8. **Deploy**: Schedule or trigger via webhook
1737
+
1738
+ **Example Adaptation (Customer Entity):**
1739
+
1740
+ ```json
1741
+ {
1742
+ "entityType": "customer",
1743
+ "description": "Load customer data from CRM system",
1744
+ "sourceConfig": {
1745
+ "type": "S3_CSV",
1746
+ "bucket": "crm-exports",
1747
+ "prefix": "customers/",
1748
+ "filePattern": "customers_*.csv"
1749
+ },
1750
+ "parseConfig": {
1751
+ "format": "csv",
1752
+ "delimiter": ",",
1753
+ "headers": true
1754
+ },
1755
+ "mappingConfig": {
1756
+ "version": "1.0",
1757
+ "fields": {
1758
+ "ref": {
1759
+ "source": "customer_id",
1760
+ "required": true
1761
+ },
1762
+ "firstName": {
1763
+ "source": "first_name",
1764
+ "required": true
1765
+ },
1766
+ "lastName": {
1767
+ "source": "last_name",
1768
+ "required": true
1769
+ },
1770
+ "email": {
1771
+ "source": "email_address",
1772
+ "required": true,
1773
+ "resolver": "sdk.lowercase"
1774
+ },
1775
+ "primaryPhone": {
1776
+ "source": "phone_number",
1777
+ "resolver": "custom.formatPhoneNumber"
1778
+ },
1779
+ "retailerId": {
1780
+ "value": "${RETAILER_ID}"
1781
+ }
1782
+ }
1783
+ },
1784
+ "loadConfig": {
1785
+ "strategy": "graphql",
1786
+ "mutation": "createCustomer",
1787
+ "batchSize": 50
1788
+ }
1789
+ }
1790
+ ```
1791
+
1792
+ ---
1793
+
1794
+ ## Extending to Other Entities
1795
+
1796
+ ### Step-by-Step Guide
1797
+
1798
+ **1. Identify Entity Schema**
1799
+
1800
+ Understand the Fluent schema for your entity:
1801
+
1802
+ ```graphql
1803
+ # Example: Carrier schema
1804
+ type Carrier {
1805
+ id: ID!
1806
+ ref: String!
1807
+ name: String!
1808
+ type: String!
1809
+ status: String
1810
+ services: [CarrierService]
1811
+ retailerId: ID!
1812
+ }
1813
+
1814
+ input CreateCarrierInput {
1815
+ ref: String!
1816
+ name: String!
1817
+ type: String!
1818
+ status: String
1819
+ services: [CarrierServiceInput]
1820
+ retailerId: ID!
1821
+ }
1822
+ ```
1823
+
1824
+ **2. Create Field Mapping Configuration**
1825
+
1826
+ Map source data to schema:
1827
+
1828
+ ```json
1829
+ {
1830
+ "version": "1.0",
1831
+ "fields": {
1832
+ "ref": {
1833
+ "source": "carrier_code",
1834
+ "required": true,
1835
+ "resolver": "sdk.uppercase"
1836
+ },
1837
+ "name": {
1838
+ "source": "carrier_name",
1839
+ "required": true
1840
+ },
1841
+ "type": {
1842
+ "source": "carrier_type",
1843
+ "required": true
1844
+ },
1845
+ "status": {
1846
+ "value": "ACTIVE"
1847
+ },
1848
+ "services": {
1849
+ "source": "services",
1850
+ "isArray": true,
1851
+ "fields": {
1852
+ "name": { "source": "$.service_name" },
1853
+ "code": { "source": "$.service_code" },
1854
+ "deliveryDays": { "source": "$.delivery_days", "resolver": "sdk.parseInt" }
1855
+ }
1856
+ },
1857
+ "retailerId": {
1858
+ "value": "${RETAILER_ID}"
1859
+ }
1860
+ }
1861
+ }
1862
+ ```
1863
+
1864
+ **3. Set Up Data Source**
1865
+
1866
+ Configure where data comes from:
1867
+
1868
+ ```typescript
1869
+ const carrierSource = new S3DataSource(
1870
+ {
1871
+ type: 'S3_CSV',
1872
+ s3Config: {
1873
+ bucket: 'carrier-data',
1874
+ region: process.env.AWS_REGION!,
1875
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
1876
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
1877
+ },
1878
+ },
1879
+ logger
1880
+ );
1881
+ ```
1882
+
1883
+ **4. Run Generic ETL Pipeline**
1884
+
1885
+ Use the same ETL code:
1886
+
1887
+ ```typescript
1888
+ await masterDataETL('config/carrier-etl-config.json');
1889
+ ```
1890
+
1891
+ ### Customer Example
1892
+
1893
+ **Source CSV:**
1894
+
1895
+ ```csv
1896
+ customer_id,first_name,last_name,email,phone,segment,status
1897
+ CUST001,John,Doe,john.doe@email.com,555-1234,VIP,ACTIVE
1898
+ CUST002,Jane,Smith,jane.smith@email.com,555-5678,STANDARD,ACTIVE
1899
+ ```
1900
+
1901
+ **Mapping Configuration:**
1902
+
1903
+ ```json
1904
+ {
1905
+ "version": "1.0",
1906
+ "fields": {
1907
+ "ref": { "source": "customer_id", "required": true },
1908
+ "firstName": { "source": "first_name", "required": true },
1909
+ "lastName": { "source": "last_name", "required": true },
1910
+ "email": { "source": "email", "required": true, "resolver": "sdk.lowercase" },
1911
+ "primaryPhone": { "source": "phone" },
1912
+ "status": { "source": "status", "defaultValue": "ACTIVE" },
1913
+ "attributes": {
1914
+ "fields": {
1915
+ "segment": { "source": "segment" }
1916
+ }
1917
+ },
1918
+ "retailerId": { "value": "${RETAILER_ID}" }
1919
+ }
1920
+ }
1921
+ ```
1922
+
1923
+ ### Carrier Example
1924
+
1925
+ **Source JSON:**
1926
+
1927
+ ```json
1928
+ {
1929
+ "carriers": [
1930
+ {
1931
+ "code": "FEDEX",
1932
+ "name": "FedEx",
1933
+ "type": "PARCEL",
1934
+ "services": [
1935
+ { "service_code": "GROUND", "service_name": "FedEx Ground", "delivery_days": 3 },
1936
+ { "service_code": "2DAY", "service_name": "FedEx 2 Day", "delivery_days": 2 }
1937
+ ]
1938
+ }
1939
+ ]
1940
+ }
1941
+ ```
1942
+
1943
+ **Mapping Configuration:**
1944
+
1945
+ ```json
1946
+ {
1947
+ "version": "1.0",
1948
+ "fields": {
1949
+ "ref": { "source": "code", "required": true },
1950
+ "name": { "source": "name", "required": true },
1951
+ "type": { "source": "type", "required": true },
1952
+ "status": { "value": "ACTIVE" },
1953
+ "services": {
1954
+ "source": "services",
1955
+ "isArray": true,
1956
+ "fields": {
1957
+ "code": { "source": "$.service_code" },
1958
+ "name": { "source": "$.service_name" },
1959
+ "transitDays": { "source": "$.delivery_days", "resolver": "sdk.parseInt" }
1960
+ }
1961
+ },
1962
+ "retailerId": { "value": "${RETAILER_ID}" }
1963
+ }
1964
+ }
1965
+ ```
1966
+
1967
+ ### Pricing Example
1968
+
1969
+ **Source CSV:**
1970
+
1971
+ ```csv
1972
+ sku,price_list,currency,base_price,sale_price,start_date,end_date
1973
+ PROD001-S-RED,US_RETAIL,USD,29.99,24.99,2024-01-01,2024-12-31
1974
+ PROD001-M-RED,US_RETAIL,USD,29.99,24.99,2024-01-01,2024-12-31
1975
+ ```
1976
+
1977
+ **Mapping Configuration:**
1978
+
1979
+ ```json
1980
+ {
1981
+ "version": "1.0",
1982
+ "fields": {
1983
+ "ref": {
1984
+ "resolver": "custom.generatePriceRef",
1985
+ "required": true
1986
+ },
1987
+ "sku": {
1988
+ "source": "sku",
1989
+ "required": true
1990
+ },
1991
+ "priceList": {
1992
+ "source": "price_list",
1993
+ "required": true
1994
+ },
1995
+ "currency": {
1996
+ "source": "currency",
1997
+ "required": true,
1998
+ "resolver": "sdk.uppercase"
1999
+ },
2000
+ "value": {
2001
+ "source": "base_price",
2002
+ "required": true,
2003
+ "resolver": "sdk.parseFloat"
2004
+ },
2005
+ "salePrice": {
2006
+ "source": "sale_price",
2007
+ "resolver": "sdk.parseFloat"
2008
+ },
2009
+ "validFrom": {
2010
+ "source": "start_date",
2011
+ "resolver": "sdk.formatDate"
2012
+ },
2013
+ "validTo": {
2014
+ "source": "end_date",
2015
+ "resolver": "sdk.formatDate"
2016
+ },
2017
+ "retailerId": {
2018
+ "value": "${RETAILER_ID}"
2019
+ }
2020
+ }
2021
+ }
2022
+ ```
2023
+
2024
+ **Custom Resolver:**
2025
+
2026
+ ```typescript
2027
+ export const generatePriceRef: FieldResolverFunction = (
2028
+ value: any,
2029
+ sourceData: any,
2030
+ config: any,
2031
+ helpers: any
2032
+ ) => {
2033
+ // Generate unique ref: SKU-PRICELIST
2034
+ return `${sourceData.sku}-${sourceData.price_list}`;
2035
+ };
2036
+ ```
2037
+
2038
+ ---
2039
+
2040
+ ## Testing
2041
+
2042
+ ### Unit Testing Field Mappings
2043
+
2044
+ ```typescript
2045
+ import { UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
2046
+ import * as fs from 'fs';
2047
+
2048
+ describe('Location Mapping', () => {
2049
+ let mapper: UniversalMapper;
2050
+
2051
+ beforeEach(() => {
2052
+ const config = JSON.parse(fs.readFileSync('config/location-mapping.json', 'utf-8'));
2053
+ mapper = new UniversalMapper(config);
2054
+ });
2055
+
2056
+ it('should map location data correctly', async () => {
2057
+ const sourceData = {
2058
+ location_id: 'LOC001',
2059
+ location_name: 'Downtown Store',
2060
+ type: 'store',
2061
+ status: 'active',
2062
+ address_line1: '123 Main St',
2063
+ city: 'New York',
2064
+ state: 'NY',
2065
+ zip: '10001',
2066
+ country: 'US',
2067
+ latitude: '40.7128',
2068
+ longitude: '-74.0060',
2069
+ };
2070
+
2071
+ const result = await mapper.map(sourceData);
2072
+
2073
+ expect(result.success).toBe(true);
2074
+ expect(result.data).toMatchObject({
2075
+ ref: 'LOC001',
2076
+ name: 'Downtown Store',
2077
+ type: 'STORE',
2078
+ status: 'ACTIVE',
2079
+ primaryAddress: {
2080
+ street: '123 Main St',
2081
+ city: 'New York',
2082
+ state: 'NY',
2083
+ postcode: '10001',
2084
+ country: 'US',
2085
+ },
2086
+ coordinates: {
2087
+ latitude: 40.7128,
2088
+ longitude: -74.006,
2089
+ },
2090
+ });
2091
+ });
2092
+
2093
+ it('should handle required field validation', async () => {
2094
+ const sourceData = {
2095
+ location_name: 'Store Without ID',
2096
+ // Missing location_id (required)
2097
+ };
2098
+
2099
+ const result = await mapper.map(sourceData);
2100
+
2101
+ expect(result.success).toBe(false);
2102
+ expect(result.errors).toContain("Required field 'ref' is missing or empty");
2103
+ });
2104
+
2105
+ it('should apply default values', async () => {
2106
+ const sourceData = {
2107
+ location_id: 'LOC001',
2108
+ location_name: 'Store',
2109
+ type: 'STORE',
2110
+ // Missing status - should default to "ACTIVE"
2111
+ };
2112
+
2113
+ const result = await mapper.map(sourceData);
2114
+
2115
+ expect(result.success).toBe(true);
2116
+ expect(result.data.status).toBe('ACTIVE');
2117
+ });
2118
+ });
2119
+ ```
2120
+
2121
+ ### Integration Testing ETL Pipeline
2122
+
2123
+ ```typescript
2124
+ import { masterDataETL } from './location-etl';
2125
+ import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
2126
+ import * as fs from 'fs';
2127
+
2128
+ describe('Location ETL Integration', () => {
2129
+ let s3DataSource: S3DataSource;
2130
+
2131
+ beforeEach(() => {
2132
+ s3DataSource = new S3DataSource(
2133
+ {
2134
+ type: 'S3_CSV',
2135
+ s3Config: {
2136
+ bucket: 'test-bucket',
2137
+ region: 'us-east-1',
2138
+ accessKeyId: process.env.TEST_AWS_KEY!,
2139
+ secretAccessKey: process.env.TEST_AWS_SECRET!,
2140
+ },
2141
+ },
2142
+ console
2143
+ );
2144
+ });
2145
+
2146
+ it('should process location CSV file end-to-end', async () => {
2147
+ // Upload test file to S3
2148
+ const testData = `location_id,location_name,type,status
2149
+ LOC001,Test Store,STORE,ACTIVE
2150
+ LOC002,Test Warehouse,WAREHOUSE,ACTIVE`;
2151
+
2152
+ await s3DataSource.uploadFile('locations/test.csv', testData, { contentType: 'text/csv' });
2153
+
2154
+ // Run ETL
2155
+ await masterDataETL('config/location-etl-config.json');
2156
+
2157
+ // Verify locations were created (query Fluent API)
2158
+ const result = await fluentClient.graphql({
2159
+ query: `
2160
+ query {
2161
+ locations(first: 10, filter: { ref: { in: ["LOC001", "LOC002"] } }) {
2162
+ edges {
2163
+ node {
2164
+ ref
2165
+ name
2166
+ type
2167
+ }
2168
+ }
2169
+ }
2170
+ }
2171
+ `,
2172
+ });
2173
+
2174
+ expect(result.data.locations.edges).toHaveLength(2);
2175
+ expect(result.data.locations.edges[0].node.ref).toBe('LOC001');
2176
+ }, 30000); // 30s timeout for integration test
2177
+ });
2178
+ ```
2179
+
2180
+ ### End-to-End Testing
2181
+
2182
+ ```typescript
2183
+ import { masterDataETL } from './location-etl';
2184
+
2185
+ describe('Location ETL E2E', () => {
2186
+ it('should handle full ETL lifecycle', async () => {
2187
+ // 1. Upload test data to S3
2188
+ // 2. Trigger ETL process
2189
+ // 3. Verify data in Fluent
2190
+ // 4. Verify state tracking (file marked as processed)
2191
+ // 5. Verify idempotency (re-running doesn't duplicate)
2192
+ // TODO: Implement full E2E test scenario
2193
+ });
2194
+ });
2195
+ ```
2196
+
2197
+ ---
2198
+
2199
+ ## Common Issues
2200
+
2201
+ ### Issue: Duplicate Records Created
2202
+
2203
+ **Symptom**: Same entity created multiple times
2204
+
2205
+ **Root Cause**: State management not working or file processed multiple times
2206
+
2207
+ **Solution:**
2208
+
2209
+ ```typescript
2210
+ // Ensure state service is initialized
2211
+ const kvAdapter = new VersoriKVAdapter(openKv());
2212
+ const stateService = new StateService(logger);
2213
+
2214
+ // Check BEFORE processing
2215
+ const fileKey = `${entityType}:${file.name}`;
2216
+ if (await stateService.isFileProcessed(kvAdapter, fileKey)) {
2217
+ logger.info(`Skipping already processed file: ${file.name}`);
2218
+ continue;
2219
+ }
2220
+
2221
+ // Mark AFTER successful processing
2222
+ await stateService.updateSyncState(
2223
+ kvAdapter,
2224
+ [
2225
+ {
2226
+ fileName: file.name,
2227
+ lastModified: new Date().toISOString(),
2228
+ recordCount: records.length,
2229
+ },
2230
+ ],
2231
+ 'master-data-etl'
2232
+ );
2233
+ ```
2234
+
2235
+ ### Issue: Field Mapping Errors
2236
+
2237
+ **Symptom**: "Required field missing" or "Mapping failed"
2238
+
2239
+ **Root Cause**: Source field name doesn't match configuration
2240
+
2241
+ **Solution:**
2242
+
2243
+ ```typescript
2244
+ // Debug source data structure
2245
+ logger.debug('Source data:', JSON.stringify(sourceData, null, 2));
2246
+
2247
+ // Check field names match exactly (case-sensitive)
2248
+ {
2249
+ "ref": {
2250
+ "source": "location_id", // Must match CSV header exactly
2251
+ "required": true
2252
+ }
2253
+ }
2254
+
2255
+ // Use custom resolver for flexible field access
2256
+ {
2257
+ "ref": {
2258
+ "resolver": "custom.extractRef",
2259
+ "required": true
2260
+ }
2261
+ }
2262
+ ```
2263
+
2264
+ ### Issue: Large File Memory Issues
2265
+
2266
+ **Symptom**: Out of memory errors with large CSV/JSON files
2267
+
2268
+ **Root Cause**: Loading entire file into memory
2269
+
2270
+ **Solution:**
2271
+
2272
+ ```typescript
2273
+ // Use streaming parsers for large files
2274
+ const csvParser = new CSVParserService();
2275
+
2276
+ // Parse with streaming (yields records one-by-one)
2277
+ for await (const record of csvParser.parseStreaming(fileContent)) {
2278
+ const result = await mapper.map(record);
2279
+ if (result.success) {
2280
+ await loadRecord(result.data);
2281
+ }
2282
+ }
2283
+
2284
+ // Or batch process
2285
+ for await (const batch of csvParser.parseStreaming(fileContent, {}, 100)) {
2286
+ await loadBatch(batch);
2287
+ }
2288
+ ```
2289
+
2290
+ ### Issue: GraphQL Mutation Timeouts
2291
+
2292
+ **Symptom**: Mutations timing out for large datasets
2293
+
2294
+ **Root Cause**: Synchronous processing of many records
2295
+
2296
+ **Solution:**
2297
+
2298
+ ```typescript
2299
+ // Use batching and concurrency limits
2300
+ import pLimit from 'p-limit';
2301
+
2302
+ const limit = pLimit(5); // Max 5 concurrent mutations
2303
+
2304
+ const promises = records.map(record => limit(() => createViaGraphQL(record)));
2305
+
2306
+ await Promise.all(promises);
2307
+
2308
+ // Or use Event API for async processing
2309
+ await loadViaEventAPI(records, 'LOCATION_CREATED', logger);
2310
+ ```
2311
+
2312
+ ### Issue: Type Coercion Errors
2313
+
2314
+ **Symptom**: "Expected number, got string" in GraphQL mutations
2315
+
2316
+ **Root Cause**: CSV parsers return all values as strings
2317
+
2318
+ **Solution:**
2319
+
2320
+ ```typescript
2321
+ // Use SDK resolvers for type coercion
2322
+ {
2323
+ "latitude": {
2324
+ "source": "latitude",
2325
+ "resolver": "sdk.parseFloat" // String → number
2326
+ },
2327
+ "active": {
2328
+ "source": "is_active",
2329
+ "resolver": "sdk.boolean" // "true" → true
2330
+ },
2331
+ "quantity": {
2332
+ "source": "qty",
2333
+ "resolver": "sdk.parseInt" // "10" → 10
2334
+ }
2335
+ }
2336
+ ```
2337
+
2338
+ ---
2339
+
2340
+ ## Related Guides
2341
+
2342
+ ### SDK Documentation
2343
+
2344
+ - [Universal Mapping Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Complete field mapping reference
2345
+ - [S3 Data Source](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - S3 integration details
2346
+ - [SFTP Data Source](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - SFTP integration details
2347
+ - [SFTP Credential Access Security](../../02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md) - Secure credential handling for SFTP
2348
+ - [CSV Parser](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - CSV parsing options
2349
+ - [JSON Parser](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - JSON parsing options
2350
+ - [State Management](../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md) - Preventing duplicates
2351
+
2352
+ ### Use Case Patterns
2353
+
2354
+ - [Inventory Ingestion](../../02-CORE-GUIDES/ingestion/ingestion-readme.md) - Similar pattern for inventory data
2355
+ - [Order Integration](../../03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md) - Transaction data vs master data
2356
+ - [Catalog Sync](../../01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md) - Product catalog patterns
2357
+
2358
+ ### Platform Integration
2359
+
2360
+ - [Versori Connector Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Deploy ETL as connector
2361
+ - [Webhook Triggers](../../04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md#webhook-functions-receiving-external-requests) - Event-driven ETL
2362
+ - [Scheduled Jobs](../../04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md#scheduled-functions-time-based-recurring-tasks) - Periodic ETL execution
2363
+
2364
+ ---
2365
+
2366
+ ## Summary
2367
+
2368
+ This **Master Data ETL Pattern** provides a **generic, configuration-driven framework** for loading ANY entity type into Fluent Commerce.
2369
+
2370
+ **Key Takeaways:**
2371
+
2372
+ 1. ✅ **One Pattern for All Entities**: Same code works for locations, products, controls, carriers, etc.
2373
+ 2. ✅ **Configuration-Driven**: No code changes needed for new entity types
2374
+ 3. ✅ **Four-Phase Pipeline**: Extract → Parse → Transform → Load
2375
+ 4. ✅ **Multiple Source Formats**: CSV, JSON, XML support
2376
+ 5. ✅ **Multiple Load Strategies**: GraphQL mutations or Event API
2377
+ 6. ✅ **Production-Ready**: State management, error handling, logging
2378
+
2379
+ **Getting Started:**
2380
+
2381
+ 1. Copy the generic ETL code
2382
+ 2. Create field mapping configuration for your entity
2383
+ 3. Configure data source (S3/SFTP)
2384
+ 4. Run ETL pipeline
2385
+ 5. Monitor logs and verify data in Fluent
2386
+
2387
+ **Next Steps:**
2388
+
2389
+ - Review the [Universal Mapping Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) for advanced mapping patterns
2390
+ - Explore [S3 Data Source](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) for source configuration options
2391
+ - Check [Versori Connector Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) to deploy as a production connector
2392
+
2393
+ ---
2394
+
2395
+ **Need Help?**
2396
+
2397
+ - 📖 Documentation: `fc-connect-sdk/docs/`
2398
+ - 💬 Support: Fluent Commerce support team
2399
+ - 🐛 Issues: GitHub repository issues