@fluentcommerce/fc-connect-sdk 0.1.53 → 0.1.55

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