@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,2384 +1,2384 @@
1
- ---
2
- template_id: tpl-extract-products-graphql-to-s3-json
3
- canonical_filename: template-extraction-products-to-s3-json.md
4
- sdk_version: ^0.1.39
5
- runtime: versori
6
- direction: extraction
7
- source: fluent-graphql
8
- destination: s3-json
9
- entity: products
10
- format: json
11
- logging: versori
12
- status: stable
13
- features:
14
- - memory-management
15
- - enhanced-logging
16
- - pagination-progress
17
- ---
18
-
19
- # Template: Extraction - Products GraphQL to S3 JSON
20
-
21
- **SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
22
- **Last Updated:** 2025-01-24
23
- **Deployment Target:** Versori Platform
24
-
25
- ---
26
-
27
- ## 📋 Implementation Prompt
28
-
29
- Copy/paste the standardized prompt from `docs/template-loading-matrix.md#prompts`.
30
-
31
- ---
32
-
33
- ## 💻 STEP 3: Implementation (Verified Imports)
34
-
35
- ```ts
36
- import { Buffer } from 'node:buffer';
37
- import {
38
- createClient,
39
- UniversalMapper,
40
- S3DataSource,
41
- JSONBuilder,
42
- VersoriKVAdapter,
43
- ExtractionOrchestrator,
44
- JobTracker,
45
- } from '@fluentcommerce/fc-connect-sdk';
46
- ```
47
-
48
- These are the only SDK imports required for this template. Keep type-only imports out of code samples. Prefer the orchestrator-based flow when you need built-in pagination and stats.
49
-
50
- ---
51
-
52
- # Versori Workflows: Product Catalog Extraction to S3 JSON
53
-
54
- **FC Connect SDK Use Case Guide**
55
-
56
- > SDK: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
57
- > Version: ^0.1.39
58
-
59
- Context: Three Versori workflows for extracting product catalog from Fluent Commerce via GraphQL query with **incremental timestamp tracking**, **ad hoc extraction**, and **job status monitoring**. Transforms with `UniversalMapper`, writes JSON files to S3 for marketplace integration (Amazon, eBay, etc).
60
-
61
- **Pattern**: EXTRACTION (Fluent → S3 JSON)
62
- **Complexity**: Medium | Runtime: Versori Platform
63
- **Format**: JSON with metadata wrapper
64
-
65
- ---
66
-
67
- ## ⚠️ IMPORTANT: Production-Ready Base Template
68
-
69
- > **📋 BASE TEMPLATE - Ready for Production (Customize for Your Needs)**
70
- >
71
- > This is a **production-ready base template** demonstrating FC Connect SDK best practices for product extraction workflows with JSON output to S3.
72
- >
73
- > **✅ INCLUDED FEATURES:**
74
- >
75
- > - ✅ Comprehensive error handling with retry logic
76
- > - ✅ S3 upload with proper error handling
77
- > - ✅ State management with overlap buffer (prevents missed records)
78
- > - ✅ Job tracking with lifecycle management
79
- > - ✅ Security (credential masking in logs)
80
- > - ✅ UTC time enforcement (prevents timezone bugs)
81
- > - ✅ Incremental extraction (safe, efficient, production-ready)
82
- > - ✅ Natural rate limiting via timestamps
83
- >
84
- > **📝 BEFORE DEPLOYING:**
85
- >
86
- > 1. Review and customize activation variables for your environment
87
- > 2. Test with sample data in your Versori workspace
88
- > 3. Adjust safety limits (pageSize, maxRecords) if needed
89
- > 4. Configure monitoring alerts for extraction failures
90
- > 5. Verify S3 bucket credentials and paths
91
- >
92
- > **This base template follows SDK best practices - tweak specific to your needs.**
93
-
94
- ---
95
-
96
- ## What You'll Build
97
-
98
- **Three Versori Workflows:**
99
-
100
- 1. **Scheduled Extraction** - Daily/hourly incremental product updates
101
- 2. **Ad Hoc Extraction** - On-demand HTTP webhook for manual triggers
102
- 3. **Job Status Checker** - Monitor extraction job status via JobTracker
103
-
104
- **Features:**
105
-
106
- - **Incremental extraction** using `updatedOn > lastRunTime` filter
107
- - **State management** with VersoriKVAdapter to track last successful run
108
- - **Job tracking** with JobTracker for status monitoring
109
- - GraphQL query with auto-pagination
110
- - UniversalMapper transformation for marketplace schema
111
- - JSON file generation with product catalog
112
- - S3 upload to marketplace integration bucket
113
- - **Failure recovery** with timestamp tracking
114
- - **Pretty print option** for human-readable JSON
115
-
116
- ## Business Use Case
117
-
118
- **Daily product catalog sync to marketplaces:**
119
-
120
- - Extract only changed products since last run
121
- - Export as JSON for marketplace API consumption
122
- - Run daily at midnight for overnight processing
123
- - Include SKU, title, description, pricing, attributes
124
- - Enable Amazon/eBay listing updates
125
- - Support ad hoc manual refresh on demand
126
- - Monitor job status for workflow integration
127
-
128
- ## SDK Methods Used
129
-
130
- ```typescript
131
- import { Buffer } from 'node:buffer';
132
- import {
133
- createClient,
134
- UniversalMapper,
135
- S3DataSource,
136
- JSONBuilder,
137
- VersoriKVAdapter,
138
- JobTracker,
139
- } from '@fluentcommerce/fc-connect-sdk';
140
-
141
- await createClient(ctx); // Versori-aware client
142
- await client.graphql({ query, variables, pagination }); // GraphQL with auto-pagination
143
- new VersoriKVAdapter(ctx.openKv(':project:')); // State management
144
- new JobTracker(ctx.openKv(':project:'), ctx.log); // Job tracking
145
- new UniversalMapper(exportMapping); // Field transformation
146
- const jsonBuilder = new JSONBuilder({ prettyPrint: true, indent: 2 });
147
- const jsonContent = jsonBuilder.build(dataObject); // JSON generation
148
- await s3.upload(key, Buffer.from(jsonContent, 'utf8'), options); // S3 upload
149
- ```
150
-
151
- ## Activation Variables
152
-
153
- ```json
154
- {
155
- "retailerId": "your-retailer-id",
156
- "s3BucketName": "marketplace-catalog-exports",
157
- "awsAccessKeyId": "AKIAXXXXXXXXXXXX",
158
- "awsSecretAccessKey": "********",
159
- "awsRegion": "us-east-1",
160
- "s3Prefix": "products/daily/",
161
- "pageSize": 200,
162
- "maxRecords": 50000,
163
- "fallbackStartDate": "2024-01-01T00:00:00Z",
164
- "overlapBufferSeconds": "60",
165
- "prettyPrint": "false",
166
- "validateConnectionOnStart": "false"
167
- }
168
- ```
169
-
170
- **New Variables (v2.1.0):**
171
- - `validateConnectionOnStart`: Optional fail-fast connection validation. When `"true"`, validates connection before extraction. Default: `"false"` (validation on first API call)
172
-
173
- **Configuration Notes:**
174
- - All variables are required except `validateConnectionOnStart`, `overlapBufferSeconds`, and `prettyPrint`
175
- - Credentials should be stored securely in Versori activation variables
176
- - `pageSize` and `maxRecords` can be adjusted based on data volume and performance requirements
177
-
178
- ## Export Mapping Configuration
179
-
180
- Create file: `./config/products.export.json`
181
-
182
- ```json
183
- {
184
- "name": "products.export",
185
- "version": "1.0.0",
186
- "description": "Fluent Products → Marketplace JSON Export",
187
- "fields": {
188
- "sku": { "source": "ref", "required": true, "resolver": "sdk.trim" },
189
- "title": { "source": "name", "required": true, "resolver": "sdk.trim" },
190
- "description": { "source": "summary", "required": false, "resolver": "sdk.trim" },
191
- "gtin": { "source": "gtin", "required": false, "resolver": "sdk.trim" },
192
- "type": { "source": "type", "required": false, "resolver": "sdk.uppercase" },
193
- "status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
194
- "createdOn": { "source": "createdOn", "required": false, "resolver": "sdk.toString" },
195
- "lastUpdated": { "source": "updatedOn", "required": true, "resolver": "sdk.toString" }
196
- }
197
- }
198
- ```
199
-
200
- **Note**: Actual GraphQL query structure should be verified against your Fluent schema using introspection. The fields above are examples - adjust based on your actual schema.
201
-
202
- ## GraphQL Query
203
-
204
- **Note**: This query is an **example**. Verify against your Fluent Commerce schema using introspection.
205
-
206
- ```graphql
207
- query GetProducts($retailerId: ID!, $updatedAfter: DateTime, $first: Int!, $after: String) {
208
- products(
209
- retailerId: $retailerId
210
- updatedOn: { after: $updatedAfter }
211
- first: $first
212
- after: $after
213
- ) {
214
- edges {
215
- node {
216
- id
217
- ref
218
- name
219
- summary
220
- gtin
221
- type
222
- status
223
- createdOn
224
- updatedOn
225
- }
226
- cursor
227
- }
228
- pageInfo {
229
- hasNextPage
230
- }
231
- }
232
- }
233
- ```
234
-
235
- ---
236
-
237
- ## Versori Workflows Structure
238
-
239
- **Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
240
-
241
- **Trigger Types:**
242
- - **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
243
- - **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
244
- - **`workflow()`** → Durable workflows (advanced) - Multi-step processes with state persistence
245
-
246
- **Execution Steps (chained to triggers):**
247
- - **`http()`** → External API calls (chained from schedule/webhook)
248
- - **`fn()`** → Internal processing (chained from schedule/webhook)
249
-
250
- ### Durable Workflows (Advanced Pattern)
251
-
252
- **⚠️ Note:** Durable workflows are an advanced pattern not yet used in this template. They're documented here for completeness.
253
-
254
- **Use Case:** Multi-step processes requiring state persistence, long-running operations (hours/days), human approval workflows, or complex retry/error handling across steps.
255
-
256
- **Example Pattern:**
257
- ```typescript
258
- import { workflow } from '@versori/run';
259
-
260
- workflow('order-fulfillment', { connection: 'fluent_commerce' }, async (ctx, step) => {
261
- // Step 1: Validate order
262
- const order = await step.run('validate-order', async () => {
263
- return await validateOrder(ctx);
264
- });
265
-
266
- // Step 2: Allocate inventory (with retry)
267
- const allocation = await step.run('allocate-inventory', {
268
- retry: { max: 3, delay: '1s' }
269
- }, async () => {
270
- return await allocateInventory(order);
271
- });
272
-
273
- // Step 3: Wait for approval (human-in-loop)
274
- await step.sleep('wait-for-approval', '1h');
275
-
276
- // Step 4: Create fulfillment
277
- return await step.run('create-fulfillment', async () => {
278
- return await createFulfillment(allocation);
279
- });
280
- });
281
- ```
282
-
283
- **When to use workflow():**
284
- - ✅ Multi-step processes requiring state persistence
285
- - ✅ Long-running operations (hours/days)
286
- - ✅ Human approval workflows
287
- - ✅ Complex retry/error handling across steps
288
- - ❌ Simple CRUD operations (use schedule/webhook + http)
289
- - ❌ Fire-and-forget patterns (use schedule + http)
290
-
291
- ### Recommended Project Structure
292
-
293
- ```
294
- products-extraction/
295
- ├── index.ts # Entry point - exports all workflows
296
- └── src/
297
- ├── workflows/
298
- │ ├── scheduled/
299
- │ │ └── daily-products-extraction.ts # Scheduled: Daily products extraction
300
- │ │
301
- │ └── webhook/
302
- │ ├── adhoc-products-extraction.ts # Webhook: Manual trigger
303
- │ └── job-status-check.ts # Webhook: Status query
304
-
305
- ├── services/
306
- │ └── products-extraction.service.ts # Shared orchestration logic (reusable)
307
-
308
- └── config/
309
- └── products.export.json.json # Mapping configuration
310
- ```
311
-
312
- ---
313
-
314
- ```json
315
- {
316
- "metadata": {
317
- "extractedAt": "2025-01-22T00:00:00.000Z",
318
- "productCount": 2,
319
- "incrementalFrom": "2025-01-21T00:00:00.000Z",
320
- "incrementalTo": "2025-01-22T00:00:00.000Z"
321
- },
322
- "products": [
323
- {
324
- "sku": "SKU-001",
325
- "title": "Premium Widget",
326
- "description": "High-quality widget for all purposes",
327
- "gtin": "012345678901",
328
- "type": "STANDARD",
329
- "status": "ACTIVE",
330
- "createdOn": "2025-01-15T10:00:00Z",
331
- "lastUpdated": "2025-01-21T10:30:00Z"
332
- },
333
- {
334
- "sku": "SKU-002",
335
- "title": "Deluxe Gadget",
336
- "description": "Advanced gadget with premium features",
337
- "gtin": "012345678902",
338
- "type": "STANDARD",
339
- "status": "ACTIVE",
340
- "createdOn": "2025-01-16T11:00:00Z",
341
- "lastUpdated": "2025-01-21T14:15:00Z"
342
- }
343
- ]
344
- }
345
- ```
346
-
347
- ## Mapping & Resolvers Explained
348
-
349
- ### SDK Resolvers Used
350
-
351
- | Field | Resolver | Why? | Example |
352
- | ------------- | --------------- | ----------------------- | ----------------------------- |
353
- | `sku` | `sdk.trim` | Clean SKU | " SKU-001 " → "SKU-001" |
354
- | `title` | `sdk.trim` | Clean names | " Widget " → "Widget" |
355
- | `description` | `sdk.trim` | Remove extra whitespace | " desc " → "desc" |
356
- | `gtin` | `sdk.trim` | Normalize GTIN/UPC | " 012345 " → "012345" |
357
- | `type` | `sdk.uppercase` | Normalize type | "standard" → "STANDARD" |
358
- | `status` | `sdk.uppercase` | Normalize status | "active" → "ACTIVE" |
359
- | `createdOn` | `sdk.toString` | Ensure ISO string | Date → "2025-01-15T10:00:00Z" |
360
- | `lastUpdated` | `sdk.toString` | Ensure ISO string | Date → "2025-01-22T14:30:00Z" |
361
-
362
- ### Transformation Flow
363
-
364
- ```typescript
365
- // 1) GraphQL node (example)
366
- {
367
- ref: " SKU-001 ",
368
- name: " Widget ",
369
- summary: " High quality ",
370
- gtin: " 012345678901 ",
371
- type: "standard",
372
- status: "active",
373
- createdOn: "2025-01-15T10:00:00.000Z",
374
- updatedOn: "2025-01-21T10:30:00.000Z"
375
- }
376
-
377
- // 2) Map with UniversalMapper
378
- const mapper = new UniversalMapper(productsExportMapping);
379
- const result = await mapper.map(node);
380
-
381
- // 3) Output
382
- result.data = {
383
- sku: "SKU-001",
384
- title: "Widget",
385
- description: "High quality",
386
- gtin: "012345678901",
387
- type: "STANDARD",
388
- status: "ACTIVE",
389
- createdOn: "2025-01-15T10:00:00.000Z",
390
- lastUpdated: "2025-01-21T10:30:00.000Z"
391
- };
392
- ```
393
-
394
- ### Custom Resolvers for Product-Specific Logic
395
-
396
- While the mapping above uses built-in SDK resolvers, you can extend with custom business logic for marketplace integration:
397
-
398
- ```typescript
399
- const customResolvers = {
400
- /**
401
- * Extract category hierarchy for marketplace navigation
402
- */
403
- 'custom.formatCategoryPath': (product: any) => {
404
- const categories = product.categories || [];
405
- return categories
406
- .map((c: any) => c.name?.trim())
407
- .filter(Boolean)
408
- .join(' > ');
409
- // Example: "Electronics > Computers > Laptops"
410
- },
411
-
412
- /**
413
- * Generate marketplace-compatible SKU with prefix
414
- */
415
- 'custom.generateMarketplaceSKU': (ref: string, retailerId: string) => {
416
- return `${retailerId}-${ref.trim()}`.toUpperCase();
417
- // Example: "RETAILER123-SKU-001"
418
- },
419
-
420
- /**
421
- * Format price for marketplace API (convert cents to dollars)
422
- */
423
- 'custom.formatPrice': (priceInCents: number) => {
424
- const dollars = (priceInCents || 0) / 100;
425
- return dollars.toFixed(2);
426
- // Example: 2499 → "24.99"
427
- },
428
-
429
- /**
430
- * Extract primary image URL from attributes
431
- */
432
- 'custom.getPrimaryImage': (product: any) => {
433
- const images = product.attributes?.images || [];
434
- return images.length > 0 ? images[0]?.url : null;
435
- },
436
-
437
- /**
438
- * Generate marketplace title with brand and product name
439
- */
440
- 'custom.generateMarketplaceTitle': (product: any) => {
441
- const brand = product.attributes?.brand?.trim() || '';
442
- const name = product.name?.trim() || '';
443
- const maxLength = 200; // Amazon/eBay limit
444
-
445
- const title = brand ? `${brand} - ${name}` : name;
446
- return title.length > maxLength ? title.substring(0, maxLength - 3) + '...' : title;
447
- },
448
-
449
- /**
450
- * Extract product attributes for marketplace listing
451
- */
452
- 'custom.extractAttributes': (product: any) => {
453
- const attrs = product.attributes || {};
454
- return {
455
- brand: attrs.brand?.trim() || 'Unbranded',
456
- color: attrs.color?.trim() || null,
457
- size: attrs.size?.trim() || null,
458
- weight: attrs.weight ? `${attrs.weight} ${attrs.weightUnit || 'lb'}` : null,
459
- dimensions: attrs.dimensions || null,
460
- material: attrs.material?.trim() || null,
461
- };
462
- },
463
-
464
- /**
465
- * Determine product eligibility for marketplaces
466
- */
467
- 'custom.checkMarketplaceEligibility': (product: any) => {
468
- const status = (product.status || '').toUpperCase();
469
- const hasGTIN = !!product.gtin;
470
- const hasImages = (product.attributes?.images || []).length > 0;
471
- const hasPrice = (product.prices || []).length > 0;
472
-
473
- return {
474
- isActive: status === 'ACTIVE',
475
- hasRequiredFields: hasGTIN && hasImages && hasPrice,
476
- isEligibleForAmazon: status === 'ACTIVE' && hasGTIN && hasImages && hasPrice,
477
- isEligibleForEbay: status === 'ACTIVE' && hasImages && hasPrice,
478
- missingFields: [!hasGTIN && 'GTIN', !hasImages && 'Images', !hasPrice && 'Price'].filter(
479
- Boolean
480
- ),
481
- };
482
- },
483
-
484
- /**
485
- * Format for marketplace API submission
486
- */
487
- 'custom.formatForMarketplace': (product: any) => {
488
- return {
489
- sku: product.ref?.trim(),
490
- title: product.name?.trim(),
491
- description: product.summary?.trim() || product.name?.trim(),
492
- category: product.categories?.[0]?.name || 'Uncategorized',
493
- brand: product.attributes?.brand?.trim() || 'Generic',
494
- gtin: product.gtin?.trim() || null,
495
- price: (product.prices?.[0]?.value || 0) / 100, // cents to dollars
496
- currency: product.prices?.[0]?.currency || 'USD',
497
- images: (product.attributes?.images || []).map((img: any) => img.url),
498
- status: product.status?.toUpperCase(),
499
- lastUpdated: product.updatedOn,
500
- };
501
- },
502
- };
503
-
504
- // Use custom resolvers with UniversalMapper
505
- const mapper = new UniversalMapper(productsExportMapping, {
506
- customResolvers,
507
- });
508
- ```
509
-
510
- ### Available SDK Resolvers
511
-
512
- The SDK provides these built-in resolvers (no custom code needed):
513
-
514
- **String Transformations:**
515
-
516
- - `sdk.trim` - Remove leading/trailing whitespace
517
- - `sdk.uppercase` - Convert to uppercase
518
- - `sdk.lowercase` - Convert to lowercase
519
- - `sdk.toString` - Convert to string
520
-
521
- **Number Parsing:**
522
-
523
- - `sdk.parseInt` - Parse as integer
524
- - `sdk.parseFloat` - Parse as decimal (ideal for prices)
525
- - `sdk.number` - Parse as number (auto-detect int/float)
526
-
527
- **Date Formatting:**
528
-
529
- - `sdk.formatDate` - ISO 8601 date only (YYYY-MM-DD)
530
- - `sdk.formatDateShort` - Short date format
531
- - `sdk.parseDate` - Parse various date formats
532
-
533
- **Type Conversions:**
534
-
535
- - `sdk.boolean` - Convert to boolean
536
- - `sdk.parseJson` - Parse JSON strings (useful for attributes)
537
- - `sdk.toJson` - Convert to JSON string
538
-
539
- **Utilities:**
540
-
541
- - `sdk.identity` - Return value unchanged
542
- - `sdk.coalesce` - Return first non-null value
543
-
544
- See [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) for complete resolver documentation.
545
-
546
- ## Production Safety & Guardrails
547
-
548
- ### Overview
549
-
550
- Even with **incremental-only** extraction, product catalogs need safeguards to prevent runtime failures:
551
-
552
- - **Memory limits**: Product records can be large (descriptions, attributes, images)
553
- - **S3 upload limits**: Single files > 5GB require multipart upload
554
- - **Processing time**: Large catalogs can timeout
555
- - **JSON parsing**: Massive JSON files stress memory and downstream parsers
556
-
557
- ### Hard Limits
558
-
559
- ```typescript
560
- const SAFETY_LIMITS = {
561
- MAX_RECORDS_PER_RUN: 100000, // 100k products per run
562
- MAX_RECORDS_PER_FILE: 25000, // 25k per JSON file
563
- MAX_FILE_SIZE_MB: 250, // 250MB per file
564
- MAX_JSON_SIZE_MB: 500, // Total extraction size
565
- CHUNK_SIZE: 10000, // Process in chunks
566
- ESTIMATED_BYTES_PER_PRODUCT: 2048, // Conservative estimate (2KB per product)
567
- };
568
- ```
569
-
570
- **Why these limits?**
571
-
572
- - Products have more fields than orders/fulfillments (descriptions, attributes, variants)
573
- - JSON is less compact than CSV
574
- - Marketplace APIs often have file size limits (Amazon: 100MB compressed)
575
-
576
- ### Runtime Validation Function
577
-
578
- ```typescript
579
- /**
580
- * Validate extraction safety limits before processing
581
- */
582
- function validateExtractionLimits(recordCount: number, estimatedSizeMB: number) {
583
- const MAX_RECORDS_PER_RUN = 100000;
584
- const MAX_JSON_SIZE_MB = 500;
585
-
586
- if (recordCount > MAX_RECORDS_PER_RUN) {
587
- return {
588
- valid: false,
589
- error: `Extraction limit exceeded: ${recordCount} records (max: ${MAX_RECORDS_PER_RUN})`,
590
- recommendation: `Catalog too large for single extraction. Consider:
591
- 1. Increase extraction frequency to reduce batch size
592
- 2. Filter by product status (ACTIVE only)
593
- 3. Split by product type or category
594
- 4. Use multiple extractions with different filters`,
595
- recordCount,
596
- maxAllowed: MAX_RECORDS_PER_RUN,
597
- };
598
- }
599
-
600
- if (estimatedSizeMB > MAX_JSON_SIZE_MB) {
601
- return {
602
- valid: false,
603
- error: `JSON size limit exceeded: ${estimatedSizeMB}MB (max: ${MAX_JSON_SIZE_MB}MB)`,
604
- recommendation: 'File splitting required. Consider filtering or splitting by category.',
605
- estimatedSizeMB,
606
- maxAllowed: MAX_JSON_SIZE_MB,
607
- };
608
- }
609
-
610
- return { valid: true };
611
- }
612
- ```
613
-
614
- ## Complete Workflows Implementation
615
-
616
- The code examples below demonstrate what goes in each file according to the modular structure shown in the "Recommended Project Structure" section above.
617
-
618
- ### Workflow 1: Scheduled Incremental Extraction
619
- **File:** `src/workflows/scheduled/daily-products-extraction.ts`
620
-
621
- ```typescript
622
- import { schedule, http } from '@versori/run';
623
- import { Buffer } from 'node:buffer';
624
- import {
625
- createClient,
626
- UniversalMapper,
627
- S3DataSource,
628
- JSONBuilder,
629
- VersoriKVAdapter,
630
- JobTracker,
631
- } from '@fluentcommerce/fc-connect-sdk';
632
- import productsExportMapping from './config/products.export.json' with { type: 'json' };
633
-
634
- const PRODUCTS_QUERY = `
635
- query GetProducts(
636
- $retailerId: ID!
637
- $updatedAfter: DateTime
638
- $first: Int!
639
- $after: String
640
- ) {
641
- products(
642
- retailerId: $retailerId
643
- updatedOn: { after: $updatedAfter }
644
- first: $first
645
- after: $after
646
- ) {
647
- edges {
648
- node {
649
- id
650
- ref
651
- name
652
- summary
653
- gtin
654
- type
655
- status
656
- createdOn
657
- updatedOn
658
- }
659
- cursor
660
- }
661
- pageInfo {
662
- hasNextPage
663
- }
664
- }
665
- }
666
- `;
667
-
668
- /**
669
- * WORKFLOW 1/3: Scheduled Daily Product Extraction (Incremental)
670
- *
671
- * Runs daily at midnight to extract changed products
672
- */
673
- export const scheduledProductsExtraction = schedule('scheduled-products-extraction', '0 0 * * *').then(
674
- http('extract-products', { connection: 'fluent_commerce' }, async ctx => {
675
- const { log, openKv, activation } = ctx;
676
- const executionStartTime = Date.now();
677
-
678
- log.info('=== EXECUTION START ===', { timestamp: new Date().toISOString() });
679
-
680
- // STEP 1/8: Parse activation variables
681
- const retailerId = activation?.getVariable('retailerId');
682
- const pageSize = parseInt(activation?.getVariable('pageSize') || '200', 10);
683
- const maxRecords = parseInt(activation?.getVariable('maxRecords') || '50000', 10);
684
- const fallbackStartDate =
685
- activation?.getVariable('fallbackStartDate') || '2024-01-01T00:00:00Z';
686
- const prettyPrint = activation?.getVariable('prettyPrint') === 'true';
687
-
688
- const s3Config = {
689
- bucket: activation?.getVariable('s3BucketName'),
690
- region: activation?.getVariable('awsRegion') || 'us-east-1',
691
- accessKeyId: activation?.getVariable('awsAccessKeyId'),
692
- secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
693
- };
694
- const s3Prefix = activation?.getVariable('s3Prefix') || 'products/daily/';
695
-
696
- // Validate required variables
697
- const missing: string[] = [];
698
- if (!retailerId) missing.push('retailerId');
699
- if (!s3Config.bucket) missing.push('s3BucketName');
700
- if (!s3Config.accessKeyId) missing.push('awsAccessKeyId');
701
- if (!s3Config.secretAccessKey) missing.push('awsSecretAccessKey');
702
- if (missing.length) {
703
- return { success: false, error: `Missing required variables: ${missing.join(', ')}` };
704
- }
705
-
706
- try {
707
- // STEP 2/8: Initialize JobTracker and start tracking
708
- const tracker = new JobTracker(openKv(':project:'), log);
709
- const jobId = `products-extraction-${Date.now()}`;
710
-
711
- await tracker.createJob(jobId, {
712
- type: 'extraction',
713
- entity: 'products',
714
- mode: 'scheduled',
715
- retailerId,
716
- startTime: executionStartTime,
717
- });
718
-
719
- // STEP 3/8: Load last successful extraction timestamp with overlap buffer
720
- const kv = new VersoriKVAdapter(openKv(':project:'));
721
- const stateKey = ['extraction', 'products', 'lastRunTime'];
722
- const lastRunState = await kv.get(stateKey);
723
- const rawLastRunTime = lastRunState?.value?.timestamp || fallbackStartDate;
724
-
725
- // Overlap buffer configuration (default: 60 seconds)
726
- const overlapBufferSeconds = parseInt(
727
- activation?.getVariable('overlapBufferSeconds') || '60',
728
- 10
729
- );
730
- const OVERLAP_BUFFER_MS = overlapBufferSeconds * 1000;
731
-
732
- // Apply overlap buffer for query (safety window)
733
- const bufferedLastRunTime = new Date(
734
- new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
735
- ).toISOString();
736
-
737
- const toDate = undefined; // No manual override for scheduled extraction
738
-
739
- log.info('🔍 Starting incremental products extraction with overlap buffer', {
740
- jobId,
741
- rawLastRunTime,
742
- bufferedLastRunTime,
743
- effectiveEndTime: toDate || new Date().toISOString(),
744
- overlapBufferSeconds,
745
- retailerId,
746
- maxRecords,
747
- });
748
-
749
- // STEP 4/8: Initialize Fluent client + ExtractionOrchestrator
750
- // ✅ Optional: Validate connection immediately (fail-fast mode)
751
- // Set activation variable 'validateConnectionOnStart' = 'true' to enable
752
- // When enabled: Executes query { me { ref } } to verify authentication
753
- // When disabled: Fast creation, validation happens on first API call (default)
754
- const validateConnection = activation.getVariable('validateConnectionOnStart') === 'true';
755
- const client = await createClient(ctx, validateConnection ? { validateConnection: true } : undefined);
756
-
757
- if (validateConnection) {
758
- log.info('✅ Connection validated successfully - authentication confirmed', { jobId });
759
- }
760
-
761
- const orchestrator = new ExtractionOrchestrator(client, log);
762
-
763
- // STEP 5/8: Execute extraction with auto-pagination (WITH overlap buffer)
764
- const effectiveEndTime = toDate || new Date().toISOString();
765
-
766
- // ? Enhanced: Extract context for progress logging
767
- const dateRangeInfo = {
768
- start: bufferedLastRunTime,
769
- end: effectiveEndTime,
770
- retailerId
771
- };
772
-
773
- // ? Enhanced: Start logging with context
774
- log.info(`📊 [ExtractionOrchestrator] Starting extraction`, {
775
- query: 'products',
776
- pageSize,
777
- maxRecords,
778
- dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
779
- retailerId: dateRangeInfo.retailerId,
780
- jobId
781
- });
782
-
783
- const extractionResult = await orchestrator.extract({
784
- query: PRODUCTS_QUERY,
785
- resultPath: 'products.edges.node',
786
- variables: {
787
- retailerId,
788
- dateRangeFilter: {
789
- after: bufferedLastRunTime,
790
- before: effectiveEndTime, // End of extraction window
791
- },
792
- },
793
- pageSize,
794
- maxRecords,
795
- validateItem: item => !!item.ref,
796
- });
797
-
798
- const rawRecords = extractionResult.data;
799
-
800
- log.info('Products extraction completed', {
801
- jobId,
802
- totalRecords: extractionResult.stats.totalRecords,
803
- totalPages: extractionResult.stats.totalPages,
804
- validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
805
- errors: extractionResult.errors ? extractionResult.errors.length : 0,
806
- });
807
-
808
- // ? Enhanced: Completion logging with summary
809
- log.info(`✅ [ExtractionOrchestrator] Extraction completed`, {
810
- totalRecords: extractionResult.stats.totalRecords,
811
- totalPages: extractionResult.stats.totalPages,
812
- validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
813
- failedValidations: extractionResult.stats.failedValidations,
814
- truncated: extractionResult.stats.truncated,
815
- truncationReason: extractionResult.stats.truncationReason,
816
- dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
817
- jobId
818
- });
819
-
820
- if (extractionResult.errors && extractionResult.errors.length > 0) {
821
- log.warn('Non-fatal extraction errors encountered', {
822
- jobId,
823
- errorCount: extractionResult.errors.length,
824
- sampleErrors: extractionResult.errors.slice(0, 3),
825
- });
826
- }
827
-
828
- if (rawRecords.length === 0) {
829
- log.info('No new products to extract');
830
- await kv.set(stateKey, {
831
- timestamp: new Date().toISOString(),
832
- productCount: 0,
833
- extractedAt: new Date().toISOString(),
834
- });
835
- await tracker.markCompleted(jobId, {
836
- recordsProcessed: 0,
837
- message: 'No new products to extract',
838
- });
839
- return {
840
- success: true,
841
- message: 'No new products to extract',
842
- jobId,
843
- lastRunTime: rawLastRunTime,
844
- };
845
- }
846
-
847
- log.info('Products retrieved', { jobId, count: rawRecords.length });
848
-
849
- // STEP 6/8: Validate extraction limits
850
- const MAX_RECORDS_PER_RUN = 100000;
851
- const ESTIMATED_BYTES_PER_PRODUCT = 2048;
852
- const estimatedSizeMB = (rawRecords.length * ESTIMATED_BYTES_PER_PRODUCT) / (1024 * 1024);
853
- const MAX_JSON_SIZE_MB = 500;
854
-
855
- if (rawRecords.length > MAX_RECORDS_PER_RUN) {
856
- await tracker.markFailed(jobId, {
857
- error: 'Extraction limit exceeded',
858
- recordCount: rawRecords.length,
859
- maxAllowed: MAX_RECORDS_PER_RUN,
860
- });
861
- log.error('Extraction limit exceeded', {
862
- recordCount: rawRecords.length,
863
- maxAllowed: MAX_RECORDS_PER_RUN,
864
- });
865
- return {
866
- success: false,
867
- error: `Extraction limit exceeded: ${rawRecords.length} records (max: ${MAX_RECORDS_PER_RUN})`,
868
- recommendation: `Catalog too large for single extraction. Consider:
869
- 1. Increase extraction frequency to reduce batch size
870
- 2. Filter by product status (ACTIVE only)
871
- 3. Split by product type or category
872
- 4. Use multiple extractions with different filters`,
873
- recordCount: rawRecords.length,
874
- maxAllowed: MAX_RECORDS_PER_RUN,
875
- };
876
- }
877
-
878
- if (estimatedSizeMB > MAX_JSON_SIZE_MB) {
879
- log.warn('JSON size approaching limit', {
880
- estimatedSizeMB: estimatedSizeMB.toFixed(2),
881
- maxAllowed: MAX_JSON_SIZE_MB,
882
- recommendation: 'Consider file splitting or category-based filtering',
883
- });
884
- }
885
-
886
- await tracker.updateJobProgress(jobId, {
887
- stage: 'validation_complete',
888
- recordCount: rawRecords.length,
889
- estimatedSizeMB: estimatedSizeMB.toFixed(2),
890
- });
891
-
892
- // STEP 7/8: Transform with UniversalMapper
893
- const mapper = new UniversalMapper(productsExportMapping);
894
- const mappingResult = await mapper.map(rawRecords);
895
-
896
- if (!mappingResult.success) {
897
- const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
898
- await tracker.markFailed(
899
- jobId,
900
- mappingErrors[0] || 'UniversalMapper returned unsuccessful result',
901
- {
902
- failedCount: mappingErrors.length,
903
- errors: mappingErrors,
904
- }
905
- );
906
- return {
907
- success: false,
908
- error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
909
- errors: mappingErrors,
910
- };
911
- }
912
-
913
- const transformedProducts = Array.isArray(mappingResult.data) ? mappingResult.data : [];
914
- const mappingErrors = mappingResult.errors || [];
915
-
916
- if (mappingErrors.length > 0) {
917
- log.warn('Some products failed transformation', {
918
- jobId,
919
- errorCount: mappingErrors.length,
920
- sampleErrors: mappingErrors.slice(0, 3),
921
- });
922
- }
923
-
924
- if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
925
- log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
926
- jobId,
927
- skippedFields: mappingResult.skippedFields,
928
- note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
929
- });
930
- }
931
-
932
- if (transformedProducts.length === 0) {
933
- await tracker.markFailed(jobId, 'All records failed mapping', {
934
- failedCount: mappingErrors.length,
935
- errors: mappingErrors,
936
- });
937
- return {
938
- success: false,
939
- error: 'All records failed mapping',
940
- errors: mappingErrors,
941
- };
942
- }
943
-
944
- log.info('Records transformed', {
945
- jobId,
946
- successful: transformedProducts.length,
947
- skippedRecords: rawRecords.length - transformedProducts.length,
948
- });
949
-
950
- await tracker.updateJobProgress(jobId, {
951
- stage: 'transformation_complete',
952
- transformedCount: transformedProducts.length,
953
- failedCount: mappingErrors.length,
954
- });
955
-
956
- // Calculate max updatedOn for next run (WITHOUT buffer)
957
- const maxUpdatedOn = transformedProducts.reduce((max, product) => {
958
- const productTime = new Date(product.lastUpdated).getTime();
959
- return productTime > max ? productTime : max;
960
- }, new Date(rawLastRunTime).getTime());
961
-
962
- const newTimestamp = new Date(maxUpdatedOn).toISOString();
963
-
964
- // Build JSON with metadata
965
- const jsonOutput = {
966
- metadata: {
967
- extractedAt: new Date().toISOString(),
968
- productCount: transformedProducts.length,
969
- incrementalFrom: rawLastRunTime,
970
- incrementalTo: newTimestamp,
971
- },
972
- products: transformedProducts,
973
- };
974
-
975
- // Use JSONBuilder for consistent JSON generation
976
- const jsonBuilder = new JSONBuilder({
977
- prettyPrint,
978
- indent: 2,
979
- });
980
- const jsonContent = jsonBuilder.build(jsonOutput);
981
-
982
- // Generate timestamped filename
983
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
984
- const fileName = `products-${timestamp}.json`;
985
- const s3Key = `${s3Prefix}${fileName}`;
986
-
987
- log.info('Generated JSON file', {
988
- jobId,
989
- fileName,
990
- size: jsonContent.length,
991
- productCount: transformedProducts.length,
992
- });
993
-
994
- // STEP 8/8: Upload to S3
995
- const s3 = new S3DataSource(
996
- {
997
- type: 'S3_JSON',
998
- connectionId: 's3-products-export',
999
- name: 'S3 Products Export',
1000
- s3Config,
1001
- },
1002
- log
1003
- );
1004
-
1005
- await s3.upload(s3Key, Buffer.from(jsonContent, 'utf8'), {
1006
- contentType: 'application/json',
1007
- metadata: {
1008
- productCount: String(transformedProducts.length),
1009
- extractedAt: new Date().toISOString(),
1010
- incrementalFrom: rawLastRunTime,
1011
- incrementalTo: newTimestamp,
1012
- },
1013
- });
1014
-
1015
- log.info('JSON file uploaded to S3', { jobId, s3Key });
1016
-
1017
- // Update state with new timestamp (WITHOUT buffer)
1018
- await kv.set(stateKey, {
1019
- timestamp: newTimestamp, // ← NO buffer applied
1020
- productCount: transformedProducts.length,
1021
- extractedAt: new Date().toISOString(),
1022
- overlapBufferSeconds,
1023
- fileName,
1024
- s3Key,
1025
- errors: mappingErrors.length > 0 ? mappingErrors : undefined,
1026
- });
1027
-
1028
- log.info('State updated with new timestamp (without buffer)', {
1029
- jobId,
1030
- newTimestamp,
1031
- overlapBufferSeconds,
1032
- });
1033
-
1034
- // Complete job tracking
1035
- const executionDurationMs = Date.now() - executionStartTime;
1036
- await tracker.markCompleted(jobId, {
1037
- recordsProcessed: transformedProducts.length,
1038
- recordsFailed: mappingErrors.length,
1039
- fileName,
1040
- s3Key,
1041
- newTimestamp,
1042
- executionDurationMs,
1043
- errors: mappingErrors,
1044
- });
1045
-
1046
- log.info('=== EXECUTION END ===', {
1047
- timestamp: new Date().toISOString(),
1048
- durationMs: executionDurationMs,
1049
- success: true,
1050
- });
1051
-
1052
- return {
1053
- success: true,
1054
- jobId,
1055
- productsExtracted: transformedProducts.length,
1056
- recordsFailed: mappingErrors.length,
1057
- fileName,
1058
- s3Key,
1059
- lastRunTime: rawLastRunTime,
1060
- newTimestamp,
1061
- executionDurationMs,
1062
- errors: mappingErrors.length > 0 ? mappingErrors : undefined,
1063
- };
1064
- } catch (error: any) {
1065
- const executionDurationMs = Date.now() - executionStartTime;
1066
- log.error('Extraction failed', error, {
1067
- message: error?.message,
1068
- executionDurationMs,
1069
- });
1070
-
1071
- log.info('=== EXECUTION END ===', {
1072
- timestamp: new Date().toISOString(),
1073
- durationMs: executionDurationMs,
1074
- success: false,
1075
- });
1076
-
1077
- return {
1078
- success: false,
1079
- message: error instanceof Error ? error.message : String(error),
1080
- stack: error instanceof Error ? error.stack : undefined,
1081
- errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
1082
- executionDurationMs,
1083
- };
1084
- }
1085
- })
1086
- );
1087
- ```
1088
-
1089
- ### Workflow 2: Ad Hoc HTTP Extraction
1090
- **File:** `src/workflows/webhook/adhoc-products-extraction.ts`
1091
-
1092
- ```typescript
1093
- /**
1094
- * WORKFLOW 2/3: Ad Hoc Product Extraction (HTTP Webhook)
1095
- *
1096
- * Manual trigger for on-demand product catalog extraction
1097
- *
1098
- * Usage (payload examples):
1099
- * 1) Incremental (use stored state):
1100
- * {}
1101
- *
1102
- * 2) Date range override (preferred keys):
1103
- * {
1104
- * "fromDate": "2025-01-01T00:00:00Z",
1105
- * "toDate": "2025-01-22T00:00:00Z",
1106
- * "updateState": false
1107
- * }
1108
- *
1109
- * 3) Backward-compatible keys (startDate/endDate) are also accepted and mapped:
1110
- * {
1111
- * "startDate": "2025-01-01T00:00:00Z",
1112
- * "endDate": "2025-01-22T00:00:00Z"
1113
- * }
1114
- */
1115
- export const adHocProductsExtraction = webhook('extract-products-adhoc', {
1116
- connection: 'products-adhoc',
1117
- }).then(
1118
- http('execute-adhoc-extraction', { connection: 'fluent_commerce' }, async ctx => {
1119
- const { log, openKv, activation, data } = ctx;
1120
- const executionStartTime = Date.now();
1121
-
1122
- log.info('=== EXECUTION START ===', { timestamp: new Date().toISOString() });
1123
-
1124
- // Parse request body and support both new (fromDate/toDate) and alternative (startDate/endDate) keys
1125
- const requestData = typeof data === 'string' ? JSON.parse(data) : data;
1126
- const fromDate = requestData?.fromDate || requestData?.startDate;
1127
- const toDate = requestData?.toDate || requestData?.endDate;
1128
- const updateState = requestData?.updateState === true; // default: false
1129
- const maxRecordsOverride = requestData?.maxRecords;
1130
-
1131
- const retailerId = activation?.getVariable('retailerId');
1132
- const pageSize = parseInt(activation?.getVariable('pageSize') || '200', 10);
1133
- const maxRecords =
1134
- maxRecordsOverride || parseInt(activation?.getVariable('maxRecords') || '50000', 10);
1135
- const prettyPrint = activation?.getVariable('prettyPrint') === 'true';
1136
-
1137
- const s3Config = {
1138
- bucket: activation?.getVariable('s3BucketName'),
1139
- region: activation?.getVariable('awsRegion') || 'us-east-1',
1140
- accessKeyId: activation?.getVariable('awsAccessKeyId'),
1141
- secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
1142
- };
1143
- const s3Prefix = activation?.getVariable('s3Prefix') || 'products/adhoc/';
1144
-
1145
- // Validate required variables
1146
- const missing: string[] = [];
1147
- if (!retailerId) missing.push('retailerId');
1148
- if (!s3Config.bucket) missing.push('s3BucketName');
1149
- if (!s3Config.accessKeyId) missing.push('awsAccessKeyId');
1150
- if (!s3Config.secretAccessKey) missing.push('awsSecretAccessKey');
1151
- if (missing.length) {
1152
- return { success: false, error: `Missing required variables: ${missing.join(', ')}` };
1153
- }
1154
-
1155
- try {
1156
- // Initialize JobTracker
1157
- const tracker = new JobTracker(openKv(':project:'), log);
1158
- const jobId = `products-adhoc-${Date.now()}`;
1159
-
1160
- await tracker.createJob(jobId, {
1161
- type: 'extraction',
1162
- entity: 'products',
1163
- mode: 'adhoc',
1164
- retailerId,
1165
- fromDate,
1166
- toDate,
1167
- updateState,
1168
- startTime: executionStartTime,
1169
- });
1170
-
1171
- log.info('Starting ad hoc products extraction', {
1172
- jobId,
1173
- retailerId,
1174
- fromDate: fromDate || 'not specified',
1175
- toDate: toDate || 'not specified',
1176
- maxRecords,
1177
- updateState,
1178
- });
1179
-
1180
- // Initialize Fluent client + Orchestrator
1181
- // ✅ Optional: Validate connection immediately (fail-fast mode)
1182
- // Set activation variable 'validateConnectionOnStart' = 'true' to enable
1183
- // When enabled: Executes query { me { ref } } to verify authentication
1184
- // When disabled: Fast creation, validation happens on first API call (default)
1185
- const validateConnection = activation.getVariable('validateConnectionOnStart') === 'true';
1186
- const client = await createClient(ctx, validateConnection ? { validateConnection: true } : undefined);
1187
-
1188
- if (validateConnection) {
1189
- log.info('✅ Connection validated successfully - authentication confirmed', { jobId });
1190
- }
1191
-
1192
- const orchestrator = new ExtractionOrchestrator(client, log);
1193
-
1194
- // Execute extraction with auto-pagination
1195
- // ? Enhanced: Extract context for progress logging
1196
- const dateRangeInfo = {
1197
- start: fromDate || 'all',
1198
- end: toDate || 'now',
1199
- retailerId
1200
- };
1201
-
1202
- // ? Enhanced: Start logging with context
1203
- log.info(`📊 [ExtractionOrchestrator] Starting extraction`, {
1204
- query: 'products',
1205
- pageSize,
1206
- maxRecords,
1207
- dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
1208
- retailerId: dateRangeInfo.retailerId,
1209
- jobId
1210
- });
1211
-
1212
- const extraction = await orchestrator.extract({
1213
- query: PRODUCTS_QUERY,
1214
- resultPath: 'products.edges.node',
1215
- variables: {
1216
- retailerId,
1217
- ...(fromDate ? { updatedAfter: fromDate } : {}),
1218
- },
1219
- pageSize,
1220
- maxRecords,
1221
- validateItem: (item: any) => !!item.ref,
1222
- });
1223
-
1224
- const nodes = extraction.data || [];
1225
-
1226
- // ? Enhanced: Completion logging with summary
1227
- log.info(`✅ [ExtractionOrchestrator] Extraction completed`, {
1228
- totalRecords: extraction.stats.totalRecords,
1229
- totalPages: extraction.stats.totalPages,
1230
- validRecords: extraction.stats.validRecords ?? nodes.length,
1231
- failedValidations: extraction.stats.failedValidations,
1232
- truncated: extraction.stats.truncated,
1233
- truncationReason: extraction.stats.truncationReason,
1234
- dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
1235
- jobId
1236
- });
1237
-
1238
- if (nodes.length === 0) {
1239
- await tracker.markCompleted(jobId, {
1240
- recordsProcessed: 0,
1241
- message: 'No products found',
1242
- });
1243
- return {
1244
- success: true,
1245
- message: 'No products found',
1246
- jobId,
1247
- fromDate,
1248
- toDate,
1249
- stateUpdated: false,
1250
- };
1251
- }
1252
-
1253
- // Transform with UniversalMapper (bulk mapping)
1254
- const mapper = new UniversalMapper(productsExportMapping);
1255
- const mappingResult = await mapper.map(nodes);
1256
-
1257
- if (!mappingResult.success) {
1258
- const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
1259
- log.error('Mapping failed - terminating job', {
1260
- jobId,
1261
- errorCount: mappingErrors.length,
1262
- sampleErrors: mappingErrors.slice(0, 3),
1263
- });
1264
-
1265
- await tracker.markFailed(
1266
- jobId,
1267
- mappingErrors[0] || 'UniversalMapper returned unsuccessful result',
1268
- {
1269
- errors: mappingErrors,
1270
- failedCount: mappingErrors.length,
1271
- }
1272
- );
1273
- return {
1274
- success: false,
1275
- error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
1276
- jobId,
1277
- errors: mappingErrors,
1278
- };
1279
- }
1280
-
1281
- const transformedProducts = mappingResult.data || [];
1282
- const mappingErrors = mappingResult.errors || [];
1283
-
1284
- if (mappingErrors.length > 0) {
1285
- log.warn('Some records failed transformation', {
1286
- jobId,
1287
- errorCount: mappingErrors.length,
1288
- sampleErrors: mappingErrors.slice(0, 3),
1289
- });
1290
- }
1291
-
1292
- if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
1293
- log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
1294
- jobId,
1295
- skippedFields: mappingResult.skippedFields,
1296
- note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
1297
- });
1298
- }
1299
-
1300
- if (transformedProducts.length === 0) {
1301
- await tracker.markFailed(jobId, 'All records failed mapping', {
1302
- failedCount: mappingErrors.length,
1303
- errors: mappingErrors,
1304
- });
1305
- return {
1306
- success: false,
1307
- error: 'All records failed mapping',
1308
- jobId,
1309
- errors: mappingErrors,
1310
- };
1311
- }
1312
-
1313
- // Compute newTimestamp from transformed records (WITHOUT buffer)
1314
- const newTimestamp = new Date(
1315
- transformedProducts.reduce(
1316
- (max, product) => {
1317
- const t = new Date(product.lastUpdated).getTime();
1318
- return t > max ? t : max;
1319
- },
1320
- fromDate ? new Date(fromDate).getTime() : 0
1321
- )
1322
- ).toISOString();
1323
-
1324
- // Build JSON output
1325
- const jsonOutput = {
1326
- metadata: {
1327
- extractedAt: new Date().toISOString(),
1328
- productCount: transformedProducts.length,
1329
- mode: 'adhoc',
1330
- ...(fromDate && { fromDate }),
1331
- ...(toDate && { toDate }),
1332
- stateUpdated: updateState,
1333
- },
1334
- products: transformedProducts,
1335
- };
1336
-
1337
- // Use JSONBuilder for consistent JSON generation
1338
- const jsonBuilder = new JSONBuilder({
1339
- prettyPrint,
1340
- indent: 2,
1341
- });
1342
- const jsonContent = jsonBuilder.build(jsonOutput);
1343
-
1344
- // Upload to S3
1345
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1346
- const fileName = `products-adhoc-${timestamp}.json`;
1347
- const s3Key = `${s3Prefix}${fileName}`;
1348
-
1349
- const s3 = new S3DataSource(
1350
- {
1351
- type: 'S3_JSON',
1352
- connectionId: 's3-products-export',
1353
- name: 'S3 Products Export',
1354
- s3Config,
1355
- },
1356
- log
1357
- );
1358
-
1359
- await s3.upload(s3Key, Buffer.from(jsonContent, 'utf8'), {
1360
- contentType: 'application/json',
1361
- metadata: {
1362
- productCount: String(transformedProducts.length),
1363
- extractedAt: new Date().toISOString(),
1364
- mode: 'adhoc',
1365
- },
1366
- });
1367
-
1368
- // Optionally update state (adhoc): respects updateState flag
1369
- let stateUpdated = false;
1370
- if (updateState) {
1371
- const kv = new VersoriKVAdapter(openKv(':project:'));
1372
- const stateKey = ['extraction', 'products', 'lastRunTime'];
1373
- await kv.set(stateKey, {
1374
- timestamp: newTimestamp, // WITHOUT buffer
1375
- productCount: transformedProducts.length,
1376
- extractedAt: new Date().toISOString(),
1377
- fileName,
1378
- s3Key,
1379
- errors: mappingErrors.length > 0 ? mappingErrors : undefined,
1380
- });
1381
- stateUpdated = true;
1382
- }
1383
-
1384
- // Complete job tracking
1385
- const executionDurationMs = Date.now() - executionStartTime;
1386
- await tracker.markCompleted(jobId, {
1387
- recordsProcessed: transformedProducts.length,
1388
- recordsFailed: mappingErrors.length,
1389
- fileName,
1390
- s3Key,
1391
- newTimestamp,
1392
- stateUpdated,
1393
- executionDurationMs,
1394
- errors: mappingErrors,
1395
- });
1396
-
1397
- log.info('=== EXECUTION END ===', {
1398
- timestamp: new Date().toISOString(),
1399
- durationMs: executionDurationMs,
1400
- success: true,
1401
- });
1402
-
1403
- return {
1404
- success: true,
1405
- jobId,
1406
- productsExtracted: transformedProducts.length,
1407
- recordsFailed: mappingErrors.length,
1408
- fileName,
1409
- s3Key,
1410
- newTimestamp,
1411
- stateUpdated,
1412
- executionDurationMs,
1413
- errors: mappingErrors.length > 0 ? mappingErrors : undefined,
1414
- };
1415
- } catch (error: any) {
1416
- const executionDurationMs = Date.now() - executionStartTime;
1417
- log.error('Ad hoc extraction failed', error, { executionDurationMs });
1418
-
1419
- log.info('=== EXECUTION END ===', {
1420
- timestamp: new Date().toISOString(),
1421
- durationMs: executionDurationMs,
1422
- success: false,
1423
- });
1424
-
1425
- return {
1426
- success: false,
1427
- message: error instanceof Error ? error.message : String(error),
1428
- stack: error instanceof Error ? error.stack : undefined,
1429
- errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
1430
- executionDurationMs,
1431
- };
1432
- }
1433
- })
1434
- );
1435
- ```
1436
-
1437
- ### Workflow 3: Job Status Checker
1438
- **File:** `src/workflows/webhook/job-status-check.ts`
1439
-
1440
- ```typescript
1441
- /**
1442
- * WORKFLOW 3/3: Job Status Checker
1443
- *
1444
- * Check status of extraction job
1445
- *
1446
- * Usage:
1447
- * POST /job-status
1448
- * {
1449
- * "jobId": "products-extraction-1234567890"
1450
- * }
1451
- */
1452
- export const checkJobStatus = webhook('products-job-status', {
1453
- connection: 'products-job-status',
1454
- }).then(
1455
- http('query-job-status', {}, async ctx => {
1456
- const { log, openKv, data } = ctx;
1457
-
1458
- const requestData = typeof data === 'string' ? JSON.parse(data) : data;
1459
- const jobId = requestData?.jobId;
1460
-
1461
- if (!jobId) {
1462
- return {
1463
- success: false,
1464
- error: 'Missing required field: jobId',
1465
- };
1466
- }
1467
-
1468
- try {
1469
- const tracker = new JobTracker(openKv(':project:'), log);
1470
- const job = await tracker.getJob(jobId);
1471
-
1472
- if (!job) {
1473
- return {
1474
- success: false,
1475
- error: `Job not found: ${jobId}`,
1476
- };
1477
- }
1478
-
1479
- return {
1480
- success: true,
1481
- job,
1482
- };
1483
- } catch (error: any) {
1484
- log.error('Failed to get job status', error);
1485
- return {
1486
- success: false,
1487
- message: error instanceof Error ? error.message : String(error),
1488
-
1489
- stack: error instanceof Error ? error.stack : undefined,
1490
-
1491
- errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
1492
- };
1493
- }
1494
- })
1495
- );
1496
- ```
1497
-
1498
- ### Entry Point (Workflow Registration)
1499
- **File:** `index.ts`
1500
-
1501
- ```typescript
1502
- /**
1503
- * Entry point - Export all workflows for Versori platform
1504
- *
1505
- * This file exports all workflows to be registered with Versori.
1506
- * Each workflow is defined in its own file for better organization.
1507
- */
1508
-
1509
- // Scheduled workflows
1510
- export { scheduledProductsExtraction } from './src/workflows/scheduled/daily-products-extraction';
1511
-
1512
- // Webhook workflows
1513
- export { adHocProductsExtraction } from './src/workflows/webhook/adhoc-products-extraction';
1514
- export { checkJobStatus } from './src/workflows/webhook/job-status-check';
1515
- ```
1516
-
1517
- ---
1518
-
1519
- ## Important: Schema Verification
1520
-
1521
- **Before using this template**, verify the GraphQL query structure against your actual Fluent Commerce schema:
1522
-
1523
- ```bash
1524
- # Introspect your schema
1525
- cd fc-connect-sdk
1526
- npx fc-connect introspect-schema --url https://your-fluent-api.com/graphql
1527
-
1528
- # Check available fields on Product type
1529
- npx fc-connect introspect-schema --url <url> --output schema.json
1530
- # Then search for "Product" type in schema.json
1531
- ```
1532
-
1533
- The query structure shown above is an **example**. Adjust field names to match your actual schema.
1534
-
1535
- ## Use Cases
1536
-
1537
- **1. Amazon Seller Central Integration:**
1538
-
1539
- - Daily product feed for listing updates
1540
- - Include GTIN/UPC for catalog matching
1541
- - Export pricing/inventory separately
1542
-
1543
- **2. eBay Marketplace Sync:**
1544
-
1545
- - Catalog updates for active listings
1546
- - Include product descriptions and attributes
1547
- - Handle variation products
1548
-
1549
- **3. Internal Product Master:**
1550
-
1551
- - Central product catalog for all systems
1552
- - Include extended attributes
1553
- - Version control for catalog changes
1554
-
1555
- **4. Ad Hoc Refresh:**
1556
-
1557
- - Manual catalog refresh for urgent updates
1558
- - Integration testing with sample data
1559
- - Troubleshooting specific product changes
1560
-
1561
- ## Production Checklist
1562
-
1563
- - [ ] **Verify GraphQL query against actual schema** (use introspection)
1564
- - [ ] Set appropriate extraction frequency (daily for catalog)
1565
- - [ ] Configure correct product status filter (ACTIVE only?)
1566
- - [ ] Test with real product data changes
1567
- - [ ] Verify S3 bucket permissions
1568
- - [ ] Set up monitoring/alerts for failed extractions
1569
- - [ ] Document JSON schema for downstream consumers
1570
- - [ ] Configure S3 lifecycle policy for old files
1571
- - [ ] Test incremental extraction (only changed products)
1572
- - [ ] Test ad hoc extraction workflow
1573
- - [ ] Test job status monitoring
1574
- - [ ] Verify overlap buffer prevents missed records
1575
-
1576
- ---
1577
-
1578
- ### Pattern 7: State Management & Date Overrides
1579
-
1580
- **Use Case**: Understand how state management works with scheduled and ad-hoc extractions.
1581
-
1582
- **How it works**:
1583
-
1584
- VersoriKV stores the last successful extraction timestamp to enable incremental sync:
1585
-
1586
- ```typescript
1587
- interface ExtractionState {
1588
- timestamp: string; // Last run timestamp (WITHOUT overlap buffer)
1589
- recordCount: number; // Number of records extracted
1590
- extractedAt: string; // When extraction completed
1591
- fileName?: string; // Generated filename
1592
- s3Key?: string; // S3 upload path
1593
- overlapBufferSeconds?: number; // Buffer configuration
1594
- }
1595
- ```
1596
-
1597
- **State Priority Chain** (highest to lowest):
1598
-
1599
- 1. **`fromDate` override** (manual date in webhook payload) - Highest priority
1600
- 2. **Stored state** (`await kv.get(stateKey)`) - Normal incremental mode
1601
- 3. **`fallbackStartDate`** (activation variable) - First run fallback
1602
-
1603
- **Three Scenarios**:
1604
-
1605
- #### Scenario 1: Normal Scheduled Runs (Incremental)
1606
-
1607
- ```typescript
1608
- // Payload: {} (empty - no overrides)
1609
-
1610
- // Behavior:
1611
- // 1. Load last timestamp from KV: "2025-01-22T10:00:00Z"
1612
- // 2. Apply overlap buffer: "2025-01-22T09:59:00Z" (query WITH buffer)
1613
- // 3. Extract records updated since buffered time
1614
- // 4. Calculate MAX(updatedOn) from results: "2025-01-22T14:30:00Z"
1615
- // 5. Save new timestamp WITHOUT buffer: "2025-01-22T14:30:00Z"
1616
- // 6. Next run starts from "2025-01-22T14:29:00Z" (with buffer)
1617
- ```
1618
-
1619
- **Test**:
1620
-
1621
- ```bash
1622
- # Trigger scheduled run (no payload needed)
1623
- # State advances automatically
1624
- curl -X POST https://workspace.versori.run/products-extract-daily
1625
- ```
1626
-
1627
- #### Scenario 2: Ad-hoc Extraction WITH State Update
1628
-
1629
- ```typescript
1630
- // Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": true }
1631
-
1632
- // Behavior:
1633
- // 1. Ignore stored state
1634
- // 2. Use fromDate: "2025-01-01T00:00:00Z" (no buffer applied to manual dates)
1635
- // 3. Extract all records since 2025-01-01
1636
- // 4. Calculate MAX(updatedOn): "2025-01-22T14:30:00Z"
1637
- // 5. Save new timestamp: "2025-01-22T14:30:00Z" (updates state!)
1638
- // 6. Next scheduled run starts from this new timestamp
1639
- ```
1640
-
1641
- **Use Case**: One-time catch-up extraction that advances the state pointer.
1642
-
1643
- **Test**:
1644
-
1645
- ```bash
1646
- curl -X POST https://workspace.versori.run/products-extract-webhook \
1647
- -H "x-api-key: your-api-key" \
1648
- -H "Content-Type: application/json" \
1649
- -d '{
1650
- "fromDate": "2025-01-01T00:00:00Z",
1651
- "updateState": true
1652
- }'
1653
- ```
1654
-
1655
- #### Scenario 3: Ad-hoc Extraction WITHOUT State Update
1656
-
1657
- ```typescript
1658
- // Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": false }
1659
-
1660
- // Behavior:
1661
- // 1. Ignore stored state
1662
- // 2. Use fromDate: "2025-01-01T00:00:00Z"
1663
- // 3. Extract all records since 2025-01-01
1664
- // 4. DO NOT update state
1665
- // 5. Next scheduled run uses previous timestamp (unaffected)
1666
- ```
1667
-
1668
- **Use Case**: Historical backfill or testing without affecting incremental sync.
1669
-
1670
- **Test**:
1671
-
1672
- ```bash
1673
- curl -X POST https://workspace.versori.run/products-extract-webhook \
1674
- -H "x-api-key: your-api-key" \
1675
- -H "Content-Type: application/json" \
1676
- -d '{
1677
- "fromDate": "2025-01-01T00:00:00Z",
1678
- "toDate": "2025-01-31T23:59:59Z",
1679
- "updateState": false
1680
- }'
1681
- ```
1682
-
1683
- **Why this matters**:
1684
-
1685
- - **Incremental sync** relies on state continuity
1686
- - **Manual overrides** allow catch-up without breaking incremental flow
1687
- - **Overlap buffer** prevents missed records at time boundaries
1688
- - **State isolation** lets you test/backfill without affecting production sync
1689
-
1690
- ---
1691
-
1692
- ### Pattern 8: Optional GraphQL Query Logging
1693
-
1694
- **Use Case**: Debug extraction issues by logging the exact GraphQL query sent to Fluent Commerce API.
1695
-
1696
- **When to use**:
1697
-
1698
- - ✅ Debugging pagination issues
1699
- - ✅ Verifying query variables (dates, filters, limits)
1700
- - ✅ Development and testing
1701
- - ❌ Production (verbose logs, potential secrets in variables)
1702
-
1703
- **How to enable**:
1704
-
1705
- Set `DEBUG_GRAPHQL=true` environment variable in Versori activation settings.
1706
-
1707
- **Implementation**:
1708
-
1709
- ```typescript
1710
- // In your extraction workflow
1711
- const DEBUG_GRAPHQL = activation?.getVariable('DEBUG_GRAPHQL') === 'true';
1712
-
1713
- if (DEBUG_GRAPHQL) {
1714
- log.info('GraphQL Query Debug', {
1715
- query: PRODUCTS_QUERY,
1716
- variables: {
1717
- catalogues,
1718
- dateRangeFilter,
1719
- first: pageSize,
1720
- after: null, // First page
1721
- },
1722
- pagination: {
1723
- pageSize,
1724
- maxRecords,
1725
- currentPage: 1,
1726
- },
1727
- });
1728
- }
1729
-
1730
- const extractionResult = await orchestrator.extract({
1731
- query: PRODUCTS_QUERY,
1732
- resultPath: 'products.edges.node',
1733
- variables: {
1734
- catalogues,
1735
- dateRangeFilter,
1736
- },
1737
- pageSize,
1738
- maxRecords,
1739
- });
1740
-
1741
- if (DEBUG_GRAPHQL) {
1742
- log.info('GraphQL Response Debug', {
1743
- totalRecords: extractionResult.stats.totalRecords,
1744
- totalPages: extractionResult.stats.totalPages,
1745
- truncated: extractionResult.stats.truncated,
1746
- truncationReason: extractionResult.stats.truncationReason,
1747
- validRecords: extractionResult.stats.validRecords ?? extractionResult.data.length,
1748
- firstRecordId: extractionResult.data[0]?.id,
1749
- lastRecordId: extractionResult.data[extractionResult.data.length - 1]?.id,
1750
- });
1751
- }
1752
- ```
1753
-
1754
- **What gets logged**:
1755
-
1756
- ```json
1757
- {
1758
- "level": "info",
1759
- "message": "GraphQL Query Debug",
1760
- "query": "query GetProducts($catalogues: [ProductCatalogueKey], $dateRangeFilter: DateRange, ...)",
1761
- "variables": {
1762
- "catalogues": [{ "ref": "DEFAULT_CATALOGUE" }],
1763
- "dateRangeFilter": "2025-01-22T09:59:00Z",
1764
- "first": 200,
1765
- "after": null
1766
- },
1767
- "pagination": {
1768
- "pageSize": 200,
1769
- "maxRecords": 50000,
1770
- "currentPage": 1
1771
- }
1772
- }
1773
- ```
1774
-
1775
- **Versori Environment Variables**:
1776
-
1777
- Add to activation settings:
1778
-
1779
- ```json
1780
- {
1781
- "DEBUG_GRAPHQL": "true"
1782
- }
1783
- ```
1784
-
1785
- **Testing**:
1786
-
1787
- ```bash
1788
- # Enable debug logging
1789
- curl -X POST https://workspace.versori.run/products-extract-daily
1790
-
1791
- # Check Versori logs for "GraphQL Query Debug" entries
1792
- # Verify query structure and variables are correct
1793
- ```
1794
-
1795
- **Sample Debug Output**:
1796
-
1797
- ```
1798
- [INFO] GraphQL Query Debug
1799
- query: "query GetProducts($catalogues: [ProductCatalogueKey], $dateRangeFilter: DateRange, ...)"
1800
- variables: { catalogues: [{ ref: "DEFAULT_CATALOGUE" }], dateRangeFilter: "2025-01-22T09:59:00Z", first: 200, after: null }
1801
- pagination: { pageSize: 200, maxRecords: 50000, currentPage: 1 }
1802
-
1803
- [INFO] Extraction complete
1804
- totalRecords: 1250
1805
- totalPages: 7
1806
- truncated: false
1807
- validRecords: 1250
1808
-
1809
- [INFO] GraphQL Response Debug
1810
- totalRecords: 1250
1811
- totalPages: 7
1812
- truncated: false
1813
- truncationReason: null
1814
- validRecords: 1250
1815
- firstRecordId: "product_abc"
1816
- lastRecordId: "product_xyz"
1817
- ```
1818
-
1819
- **Key Benefits**:
1820
-
1821
- - Quickly identify pagination configuration issues
1822
- - Verify date filters are applied correctly
1823
- - Debug "no records found" scenarios
1824
- - Validate ExtractionOrchestrator variable injection
1825
-
1826
- **Production Best Practice**: Disable `DEBUG_GRAPHQL` in production to reduce log volume and avoid logging sensitive data.
1827
-
1828
- ---
1829
-
1830
- ## Troubleshooting Guide
1831
-
1832
- **Issue**: "Extraction timeout after 10 minutes"
1833
-
1834
- - **Cause**: Too many records
1835
- - **Fix**: Reduce maxRecords, increase frequency
1836
-
1837
- **Issue**: "Mapping errors for 50% of records"
1838
-
1839
- - **Cause**: Schema mismatch
1840
- - **Fix**: Run schema validation, check field names
1841
-
1842
- **Issue**: "State not updating"
1843
-
1844
- - **Cause**: KV write failure or intentional retry
1845
- - **Fix**: Check KV logs, verify state update code
1846
-
1847
- **Issue**: "First run exceeds limits"
1848
-
1849
- - **Cause**: No previous timestamp, fetches all
1850
- - **Fix**: Set fallbackStartDate close to current, apply filters
1851
-
1852
- **Issue**: "Excessive duplicates"
1853
-
1854
- - **Cause**: Overlap buffer (expected) or timestamp not saved
1855
- - **Fix**: Verify newTimestamp saved WITHOUT buffer
1856
-
1857
- **Issue**: "Job status not found"
1858
-
1859
- - **Cause**: JobTracker not initialized or wrong jobId
1860
- - **Fix**: Verify JobTracker initialization and jobId format
1861
-
1862
- ---
1863
-
1864
- **Pattern**: Enterprise incremental extraction with overlap buffer for product catalog - Three-workflow approach
1865
- **⚠️ Versori Sample**: Reference implementation - adapt for your production use case
1866
- **Key Learning**: ALWAYS verify GraphQL schema before using queries
1867
- **Critical**: Apply 60-second overlap buffer to prevent missed records due to clock skew
1868
- **Buffer Pattern**: Query WITH buffer (`updatedOn >= lastRunTime - 60s`), save WITHOUT buffer (`MAX(updatedOn)`)
1869
- **Schema**: Use schema introspection - don't guess field names
1870
- **Three Workflows**: Scheduled incremental, Ad hoc HTTP trigger, Job status monitoring
1871
-
1872
- ---
1873
-
1874
- ## See Also
1875
-
1876
- **Validation & Best Practices:**
1877
-
1878
- - [CLI Validation Workflow](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md) - Validate GraphQL queries and mappings before deployment
1879
- - [Extraction Modes Guide](../extraction-modes-guide.md) - Comparison of incremental vs dateRange vs historical modes
1880
-
1881
- **SDK Documentation:**
1882
-
1883
- - [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Complete field mapping documentation
1884
- - [SDK CLI Tools](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Command-line tools reference
1885
-
1886
- **Related Extraction Patterns:**
1887
-
1888
- - [Products to SFTP XML](./template-extraction-products-to-sftp-xml.md) - Same entity, different format/destination
1889
- - [Virtual Positions to S3 JSON](./template-extraction-virtual-positions-to-s3-json.md) - Different entity, JSON format
1890
-
1891
- ---
1892
-
1893
- ### Pattern 9: Backward Pagination (Optional - Advanced)
1894
-
1895
- **Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
1896
-
1897
- **When to Use**:
1898
-
1899
- - ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
1900
- - ✅ Time-bounded reverse traversal for auditing
1901
- - ✅ Display newest-first in UI/reports
1902
- - ❌ **Don't use for standard incremental sync** - use forward pagination (default)
1903
-
1904
- **GraphQL Query Requirements**:
1905
-
1906
- Your query must support backward pagination by including `$last` and `$before`:
1907
-
1908
- ```graphql
1909
- query GetData(
1910
- $retailerId: ID!
1911
- $first: Int # For forward pagination
1912
- $after: String # For forward pagination
1913
- $last: Int # For backward pagination
1914
- $before: String # For backward pagination
1915
- ) {
1916
- data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
1917
- edges {
1918
- cursor # ✅ REQUIRED
1919
- node {
1920
- id
1921
- createdAt
1922
- # ... other fields
1923
- }
1924
- }
1925
- pageInfo {
1926
- hasNextPage # For forward
1927
- hasPreviousPage # ✅ REQUIRED for backward
1928
- }
1929
- }
1930
- }
1931
- ```
1932
-
1933
- **Implementation**:
1934
-
1935
- ```typescript
1936
- // Backward pagination - newest records first
1937
- const result = await orchestrator.extract({
1938
- query: YOUR_QUERY,
1939
- resultPath: 'data.edges.node',
1940
- variables: {
1941
- retailerId,
1942
- dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
1943
- // ❌ Don't include last/before - orchestrator injects them
1944
- },
1945
- pageSize: 200,
1946
- direction: 'backward', // ✅ Enable reverse pagination
1947
- maxRecords: 10000,
1948
- });
1949
-
1950
- // Records are returned in reverse chronological order
1951
- log.info('Extraction order verification', {
1952
- newest: result.data[0].createdAt,
1953
- oldest: result.data[result.data.length - 1].createdAt
1954
- });
1955
- ```
1956
-
1957
- **Key Differences from Forward Pagination**:
1958
-
1959
- | Aspect | Forward (Default) | Backward |
1960
- | ---------------------- | -------------------------------- | ----------------------- |
1961
- | **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
1962
- | **Variables Injected** | `first`, `after` | `last`, `before` |
1963
- | **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
1964
- | **Cursor Source** | Last edge of page | First edge of page |
1965
- | **Record Order** | Oldest → Newest | Newest → Oldest |
1966
-
1967
- **Important Notes**:
1968
-
1969
- 1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
1970
-
1971
- 2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
1972
-
1973
- 3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
1974
-
1975
- 4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
1976
-
1977
- **Example: Extract Latest 1000 Orders**
1978
-
1979
- ```typescript
1980
- const latestOrders = await orchestrator.extract({
1981
- query: ORDERS_QUERY,
1982
- resultPath: 'orders.edges.node',
1983
- variables: {
1984
- retailerId,
1985
- statuses: ['BOOKED', 'ALLOCATED'],
1986
- },
1987
- direction: 'backward', // Start from newest
1988
- maxRecords: 1000, // Stop after 1000 records
1989
- pageSize: 100, // 100 per page = 10 pages
1990
- });
1991
-
1992
- // latestOrders.data[0] is the newest order
1993
- // latestOrders.data[999] is the 1000th newest order
1994
- ```
1995
-
1996
- **When to Use Forward vs Backward**:
1997
-
1998
- ```typescript
1999
- // ✅ Forward (default) - For incremental sync
2000
- const incrementalData = await orchestrator.extract({
2001
- query: YOUR_QUERY,
2002
- resultPath: 'data.edges.node',
2003
- variables: {
2004
- dateRangeFilter: { from: lastSyncTime, to: now },
2005
- },
2006
- // direction defaults to 'forward'
2007
- // Processes oldest → newest for proper sequencing
2008
- });
2009
-
2010
- // ✅ Backward - For "latest N records" use cases
2011
- const latestData = await orchestrator.extract({
2012
- query: YOUR_QUERY,
2013
- resultPath: 'data.edges.node',
2014
- direction: 'backward',
2015
- maxRecords: 100, // Just get latest 100
2016
- // Gets newest → oldest
2017
- });
2018
- ```
2019
-
2020
- **Pagination Variables Reference**:
2021
-
2022
- | Variable | Forward | Backward | Injected By | Notes |
2023
- | -------- | ----------- | ----------- | ------------ | ------------------------ |
2024
- | `first` | ✅ Used | ❌ Not used | Orchestrator | From `pageSize` |
2025
- | `after` | ✅ Used | ❌ Not used | Orchestrator | From cursor (last edge) |
2026
- | `last` | ❌ Not used | ✅ Used | Orchestrator | From `pageSize` |
2027
- | `before` | ❌ Not used | ✅ Used | Orchestrator | From cursor (first edge) |
2028
-
2029
- **Common Mistakes to Avoid**:
2030
-
2031
- ```typescript
2032
- // ❌ WRONG - Don't pass pagination variables
2033
- const result = await orchestrator.extract({
2034
- variables: {
2035
- last: 200, // ❌ Orchestrator will override this
2036
- before: cursor, // ❌ Orchestrator manages cursor
2037
- },
2038
- direction: 'backward',
2039
- });
2040
-
2041
- // ✅ CORRECT - Let orchestrator inject pagination
2042
- const result = await orchestrator.extract({
2043
- variables: {
2044
- retailerId, // ✅ Your business variables only
2045
- },
2046
- pageSize: 200, // ✅ Orchestrator uses this for last/before
2047
- direction: 'backward',
2048
- });
2049
- ```
2050
-
2051
- #### Optional: Reverse Pagination
2052
-
2053
- - Default: forward ($first/$after) + pageInfo.hasNextPage.
2054
- - Reverse: query must use $last/$before; response must include pageInfo.hasPreviousPage; set direction='backward'.
2055
-
2056
- GraphQL:
2057
-
2058
- ```graphql
2059
- query GetProductsBackward(
2060
- $catalogues: [CatalogueKey]
2061
- $dateRangeFilter: DateRange
2062
- $last: Int!
2063
- $before: String
2064
- ) {
2065
- products(catalogues: $catalogues, updatedOn: $dateRangeFilter, last: $last, before: $before) {
2066
- edges {
2067
- cursor
2068
- node {
2069
- id
2070
- ref
2071
- updatedOn
2072
- }
2073
- }
2074
- pageInfo {
2075
- hasPreviousPage
2076
- }
2077
- }
2078
- }
2079
- ```
2080
-
2081
- SDK:
2082
-
2083
- ```typescript
2084
- await orchestrator.extract({
2085
- query: PRODUCTS_BACKWARD_QUERY,
2086
- resultPath: 'products.edges.node',
2087
- variables: { catalogues, dateRangeFilter },
2088
- pageSize,
2089
- direction: 'backward',
2090
- });
2091
- ```
2092
-
2093
- ---
2094
-
2095
- ## Testing Checklist
2096
-
2097
- **Before production deployment:**
2098
-
2099
- ### 1. Schema Validation
2100
-
2101
- - [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
2102
- - [ ] Run `npx fc-connect validate-schema --mapping ./config/products.export.json --schema ./fluent-schema.json`
2103
- - [ ] Run `npx fc-connect analyze-coverage --mapping ./config/products.export.json --schema ./fluent-schema.json`
2104
- - [ ] Verify all `source` paths in mapping exist in GraphQL schema
2105
- - [ ] Verify query structure matches schema (fields, types, filters)
2106
-
2107
- ### 2. Extraction Testing
2108
-
2109
- - [ ] Test with small dataset first (maxRecords=10)
2110
- - [ ] Verify ExtractionOrchestrator pagination works correctly
2111
- - [ ] Test with multiple pages of data (verify cursor handling)
2112
- - [ ] Verify date range filtering (updatedOn filter)
2113
- - [ ] Test empty result handling (no records in date range)
2114
- - [ ] Verify extraction stops at maxRecords limit
2115
-
2116
- ### 3. Mapping Testing
2117
-
2118
- - [ ] Verify required fields are populated
2119
- - [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
2120
- - [ ] Test custom resolvers with edge cases (if any)
2121
- - [ ] Verify nested field extraction
2122
- - [ ] Test with null/missing fields
2123
- - [ ] Verify mapping error collection works
2124
-
2125
- ### 4. JSON Generation Testing
2126
-
2127
- - [ ] Verify JSON structure matches expected format
2128
- - [ ] Test JSON validation against schema (if applicable)
2129
- - [ ] Verify proper nesting and structure
2130
- - [ ] Test with large datasets (>1000 records)
2131
- - [ ] Verify UTF-8 encoding
2132
- - [ ] Test special character escaping
2133
-
2134
- ### 5. S3 Upload Testing
2135
-
2136
- - [ ] Test S3 connection and authentication
2137
- - [ ] Verify file upload to correct bucket and path
2138
- - [ ] Test file naming convention (timestamp format)
2139
- - [ ] Verify S3 object metadata
2140
- - [ ] Test upload retry logic (simulate network failure)
2141
- - [ ] Verify file permissions and ACLs
2142
-
2143
- ### 6. State Management Testing
2144
-
2145
- - [ ] Verify overlap buffer prevents missed records (60-second default)
2146
- - [ ] Test state recovery after extraction failure
2147
- - [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
2148
- - [ ] Test first run with no previous state (uses fallbackStartDate)
2149
- - [ ] Verify state update only happens on successful upload
2150
- - [ ] Test manual date override (doesn't update state)
2151
-
2152
- ### 7. Job Tracking Testing
2153
-
2154
- - [ ] Test job creation with JobTracker
2155
- - [ ] Verify job status updates at each stage
2156
- - [ ] Test job completion with metadata
2157
- - [ ] Test job failure handling
2158
- - [ ] Query job status via webhook endpoint
2159
- - [ ] Verify job status persists in KV store
2160
-
2161
- ### 8. Error Handling Testing
2162
-
2163
- - [ ] Test with invalid GraphQL query
2164
- - [ ] Test with mapping errors (invalid field paths)
2165
- - [ ] Test with S3 connection failures
2166
- - [ ] Test with authentication failures
2167
- - [ ] Test with network timeouts
2168
- - [ ] Verify error logging includes context (jobId, stage, error details)
2169
- - [ ] Test error threshold logic (if applicable)
2170
-
2171
- ### 9. Staging Environment Testing
2172
-
2173
- - [ ] Run full extraction in staging environment
2174
- - [ ] Verify JSON file format with downstream system
2175
- - [ ] Monitor extraction duration and resource usage
2176
- - [ ] Test with production-like data volumes
2177
- - [ ] Verify no performance degradation over time
2178
-
2179
- ### 10. Integration Testing
2180
-
2181
- - [ ] Test scheduled workflow (cron trigger)
2182
- - [ ] Test ad hoc webhook trigger
2183
- - [ ] Test job status query webhook
2184
- - [ ] Verify activation variables are read correctly
2185
- - [ ] Test with different extraction modes (incremental, date range)
2186
- - [ ] End-to-end test: trigger → extract → transform → upload → verify file
2187
-
2188
- ---
2189
- ## Monitoring & Alerting
2190
-
2191
- ### Success Response Example
2192
-
2193
- ```json
2194
- {
2195
- "success": true,
2196
- "jobId": "SCHEDULED_PRD_20251102_140000_abc123",
2197
- "recordsExtracted": 1523,
2198
- "fileName": "products-2025-11-02T14-00-00-000Z.json",
2199
- "s3Path": "s3://bucket/products/products-2025-11-02T14-00-00-000Z.json",
2200
- "metrics": {
2201
- "extractionDurationMs": 12543,
2202
- "totalPages": 8,
2203
- "pageSize": 200,
2204
- "mappingErrors": 0,
2205
- "fileSizeBytes": 524288,
2206
- "uploadDurationMs": 1234
2207
- },
2208
- "timestamps": {
2209
- "extractionStart": "2025-11-02T14:00:00.000Z",
2210
- "extractionEnd": "2025-11-02T14:00:12.543Z",
2211
- "uploadComplete": "2025-11-02T14:00:13.777Z"
2212
- },
2213
- "state": {
2214
- "previousTimestamp": "2025-11-02T13:00:00.000Z",
2215
- "newTimestamp": "2025-11-02T13:59:58.123Z",
2216
- "stateUpdated": true,
2217
- "overlapBufferSeconds": 60
2218
- }
2219
- }
2220
- ```
2221
-
2222
- ### Error Response Example
2223
-
2224
- ```json
2225
- {
2226
- "success": false,
2227
- "jobId": "ADHOC_PRD_20251102_140500_xyz789",
2228
- "error": "S3 upload failed: Connection timeout",
2229
- "errorCategory": "NETWORK",
2230
- "recordsExtracted": 0,
2231
- "stage": "s3_upload",
2232
- "details": {
2233
- "message": "Failed to upload file after 3 retry attempts",
2234
- "retryAttempts": 3,
2235
- "lastError": "ETIMEDOUT: Connection timed out after 30000ms"
2236
- },
2237
- "state": {
2238
- "stateUpdated": false,
2239
- "willRetryNextRun": true,
2240
- "note": "State not advanced - next extraction will retry same time window"
2241
- }
2242
- }
2243
- ```
2244
-
2245
- ### Key Metrics to Track
2246
-
2247
- ```typescript
2248
- const METRICS = {
2249
- // Extraction Performance
2250
- extractionDurationMs: Date.now() - extractionStart,
2251
- recordCount: records.length,
2252
- pageCount: extractionResult.stats.totalPages,
2253
- avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
2254
-
2255
- // Transformation Performance
2256
- transformedCount: transformedRecords.length,
2257
- failedCount: mappingErrors.length,
2258
- errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
2259
-
2260
- // File Generation
2261
- fileSizeMB: (jsonContent.length / (1024 * 1024)).toFixed(2),
2262
-
2263
- // Upload Performance
2264
- uploadDurationMs: uploadEnd - uploadStart,
2265
- uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
2266
-
2267
- // State Management
2268
- timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
2269
- recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
2270
- };
2271
-
2272
- log.info('Extraction metrics', metrics);
2273
- ```
2274
-
2275
- ### Alert Thresholds
2276
-
2277
- ```typescript
2278
- const ALERT_THRESHOLDS = {
2279
- // Duration Alerts
2280
- EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
2281
- UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
2282
- TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
2283
-
2284
- // Error Rate Alerts
2285
- MAX_ERROR_RATE: 0.05, // 5% mapping errors
2286
- MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
2287
-
2288
- // Volume Alerts
2289
- MAX_RECORDS_PER_RUN: 100000,
2290
- MIN_RECORDS_WARNING: 0, // Alert if no records found
2291
- MAX_FILE_SIZE_MB: 150, // 150MB
2292
-
2293
- // State Alerts
2294
- MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
2295
- MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
2296
- };
2297
-
2298
- // Check thresholds
2299
- if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
2300
- log.warn('Extraction duration exceeded threshold', {
2301
- duration: metrics.extractionDurationMs,
2302
- threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
2303
- recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
2304
- });
2305
- }
2306
- ```
2307
-
2308
- ### Monitoring Dashboard Queries
2309
-
2310
- **Versori Platform Logs Query:**
2311
-
2312
- ```
2313
- # Successful extractions
2314
- log_level:info AND message:"Extraction complete" AND jobId:*
2315
-
2316
- # Failed extractions
2317
- log_level:error AND message:"Extraction workflow failed" AND jobId:*
2318
-
2319
- # Performance issues
2320
- extractionDurationMs:>300000 OR uploadDurationMs:>120000
2321
-
2322
- # High error rates
2323
- errorRate:>5
2324
-
2325
- # State management issues
2326
- stateUpdated:false AND success:true
2327
- ```
2328
-
2329
- ### Common Issues and Solutions
2330
-
2331
- **Issue**: "Extraction timeout after 10 minutes"
2332
-
2333
- - **Cause**: Too many records in single extraction
2334
- - **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
2335
- - **Prevention**: Monitor recordCount trends, set appropriate maxRecords
2336
-
2337
- **Issue**: "Mapping errors for 50% of records"
2338
-
2339
- - **Cause**: Schema mismatch between GraphQL response and mapping config
2340
- - **Fix**: Run schema validation, update mapping config paths
2341
- - **Prevention**: Use `npx fc-connect validate-schema` before deployment
2342
-
2343
- **Issue**: "S3 connection timeout"
2344
-
2345
- - **Cause**: Network issues, firewall, or connection pool exhaustion
2346
- - **Fix**: Check S3 credentials, verify network connectivity
2347
- - **Prevention**: Implement connection health checks, monitor connection status
2348
-
2349
- **Issue**: "State not updating after successful extraction"
2350
-
2351
- - **Cause**: KV write failure or intentional retry logic
2352
- - **Fix**: Check KV logs, verify state update code executed
2353
- - **Prevention**: Add KV write verification, log state updates explicitly
2354
-
2355
- **Issue**: "First run exceeds record limits"
2356
-
2357
- - **Cause**: No previous timestamp, fetches all historical records
2358
- - **Fix**: Set fallbackStartDate close to current date, apply additional filters
2359
- - **Prevention**: Use appropriate fallbackStartDate for initial runs
2360
-
2361
- **Issue**: "Excessive duplicate records in output"
2362
-
2363
- - **Cause**: Overlap buffer (expected) or timestamp not saved correctly
2364
- - **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
2365
- - **Prevention**: Monitor duplicate rates, verify state update logic
2366
-
2367
- ---
2368
-
2369
- ## Troubleshooting Quick Reference
2370
-
2371
- | Error Message | Likely Cause | Solution |
2372
- |--------------|--------------|----------|
2373
- | "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
2374
- | "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
2375
- | "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
2376
- | "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
2377
- | "S3 authentication failed" | Invalid credentials | Verify S3 credentials in activation variables |
2378
- | "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
2379
- | "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
2380
- | "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
2381
- | "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
2382
- | "JSON generation failed" | Format-specific error | Check JSON generation logic, validate output |
2383
-
2384
- ---
1
+ ---
2
+ template_id: tpl-extract-products-graphql-to-s3-json
3
+ canonical_filename: template-extraction-products-to-s3-json.md
4
+ sdk_version: ^0.1.39
5
+ runtime: versori
6
+ direction: extraction
7
+ source: fluent-graphql
8
+ destination: s3-json
9
+ entity: products
10
+ format: json
11
+ logging: versori
12
+ status: stable
13
+ features:
14
+ - memory-management
15
+ - enhanced-logging
16
+ - pagination-progress
17
+ ---
18
+
19
+ # Template: Extraction - Products GraphQL to S3 JSON
20
+
21
+ **SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
22
+ **Last Updated:** 2025-01-24
23
+ **Deployment Target:** Versori Platform
24
+
25
+ ---
26
+
27
+ ## 📋 Implementation Prompt
28
+
29
+ Copy/paste the standardized prompt from `docs/template-loading-matrix.md#prompts`.
30
+
31
+ ---
32
+
33
+ ## 💻 STEP 3: Implementation (Verified Imports)
34
+
35
+ ```ts
36
+ import { Buffer } from 'node:buffer';
37
+ import {
38
+ createClient,
39
+ UniversalMapper,
40
+ S3DataSource,
41
+ JSONBuilder,
42
+ VersoriKVAdapter,
43
+ ExtractionOrchestrator,
44
+ JobTracker,
45
+ } from '@fluentcommerce/fc-connect-sdk';
46
+ ```
47
+
48
+ These are the only SDK imports required for this template. Keep type-only imports out of code samples. Prefer the orchestrator-based flow when you need built-in pagination and stats.
49
+
50
+ ---
51
+
52
+ # Versori Workflows: Product Catalog Extraction to S3 JSON
53
+
54
+ **FC Connect SDK Use Case Guide**
55
+
56
+ > SDK: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
57
+ > Version: ^0.1.39
58
+
59
+ Context: Three Versori workflows for extracting product catalog from Fluent Commerce via GraphQL query with **incremental timestamp tracking**, **ad hoc extraction**, and **job status monitoring**. Transforms with `UniversalMapper`, writes JSON files to S3 for marketplace integration (Amazon, eBay, etc).
60
+
61
+ **Pattern**: EXTRACTION (Fluent → S3 JSON)
62
+ **Complexity**: Medium | Runtime: Versori Platform
63
+ **Format**: JSON with metadata wrapper
64
+
65
+ ---
66
+
67
+ ## ⚠️ IMPORTANT: Production-Ready Base Template
68
+
69
+ > **📋 BASE TEMPLATE - Ready for Production (Customize for Your Needs)**
70
+ >
71
+ > This is a **production-ready base template** demonstrating FC Connect SDK best practices for product extraction workflows with JSON output to S3.
72
+ >
73
+ > **✅ INCLUDED FEATURES:**
74
+ >
75
+ > - ✅ Comprehensive error handling with retry logic
76
+ > - ✅ S3 upload with proper error handling
77
+ > - ✅ State management with overlap buffer (prevents missed records)
78
+ > - ✅ Job tracking with lifecycle management
79
+ > - ✅ Security (credential masking in logs)
80
+ > - ✅ UTC time enforcement (prevents timezone bugs)
81
+ > - ✅ Incremental extraction (safe, efficient, production-ready)
82
+ > - ✅ Natural rate limiting via timestamps
83
+ >
84
+ > **📝 BEFORE DEPLOYING:**
85
+ >
86
+ > 1. Review and customize activation variables for your environment
87
+ > 2. Test with sample data in your Versori workspace
88
+ > 3. Adjust safety limits (pageSize, maxRecords) if needed
89
+ > 4. Configure monitoring alerts for extraction failures
90
+ > 5. Verify S3 bucket credentials and paths
91
+ >
92
+ > **This base template follows SDK best practices - tweak specific to your needs.**
93
+
94
+ ---
95
+
96
+ ## What You'll Build
97
+
98
+ **Three Versori Workflows:**
99
+
100
+ 1. **Scheduled Extraction** - Daily/hourly incremental product updates
101
+ 2. **Ad Hoc Extraction** - On-demand HTTP webhook for manual triggers
102
+ 3. **Job Status Checker** - Monitor extraction job status via JobTracker
103
+
104
+ **Features:**
105
+
106
+ - **Incremental extraction** using `updatedOn > lastRunTime` filter
107
+ - **State management** with VersoriKVAdapter to track last successful run
108
+ - **Job tracking** with JobTracker for status monitoring
109
+ - GraphQL query with auto-pagination
110
+ - UniversalMapper transformation for marketplace schema
111
+ - JSON file generation with product catalog
112
+ - S3 upload to marketplace integration bucket
113
+ - **Failure recovery** with timestamp tracking
114
+ - **Pretty print option** for human-readable JSON
115
+
116
+ ## Business Use Case
117
+
118
+ **Daily product catalog sync to marketplaces:**
119
+
120
+ - Extract only changed products since last run
121
+ - Export as JSON for marketplace API consumption
122
+ - Run daily at midnight for overnight processing
123
+ - Include SKU, title, description, pricing, attributes
124
+ - Enable Amazon/eBay listing updates
125
+ - Support ad hoc manual refresh on demand
126
+ - Monitor job status for workflow integration
127
+
128
+ ## SDK Methods Used
129
+
130
+ ```typescript
131
+ import { Buffer } from 'node:buffer';
132
+ import {
133
+ createClient,
134
+ UniversalMapper,
135
+ S3DataSource,
136
+ JSONBuilder,
137
+ VersoriKVAdapter,
138
+ JobTracker,
139
+ } from '@fluentcommerce/fc-connect-sdk';
140
+
141
+ await createClient(ctx); // Versori-aware client
142
+ await client.graphql({ query, variables, pagination }); // GraphQL with auto-pagination
143
+ new VersoriKVAdapter(ctx.openKv(':project:')); // State management
144
+ new JobTracker(ctx.openKv(':project:'), ctx.log); // Job tracking
145
+ new UniversalMapper(exportMapping); // Field transformation
146
+ const jsonBuilder = new JSONBuilder({ prettyPrint: true, indent: 2 });
147
+ const jsonContent = jsonBuilder.build(dataObject); // JSON generation
148
+ await s3.upload(key, Buffer.from(jsonContent, 'utf8'), options); // S3 upload
149
+ ```
150
+
151
+ ## Activation Variables
152
+
153
+ ```json
154
+ {
155
+ "retailerId": "your-retailer-id",
156
+ "s3BucketName": "marketplace-catalog-exports",
157
+ "awsAccessKeyId": "AKIAXXXXXXXXXXXX",
158
+ "awsSecretAccessKey": "********",
159
+ "awsRegion": "us-east-1",
160
+ "s3Prefix": "products/daily/",
161
+ "pageSize": 200,
162
+ "maxRecords": 50000,
163
+ "fallbackStartDate": "2024-01-01T00:00:00Z",
164
+ "overlapBufferSeconds": "60",
165
+ "prettyPrint": "false",
166
+ "validateConnectionOnStart": "false"
167
+ }
168
+ ```
169
+
170
+ **New Variables (v2.1.0):**
171
+ - `validateConnectionOnStart`: Optional fail-fast connection validation. When `"true"`, validates connection before extraction. Default: `"false"` (validation on first API call)
172
+
173
+ **Configuration Notes:**
174
+ - All variables are required except `validateConnectionOnStart`, `overlapBufferSeconds`, and `prettyPrint`
175
+ - Credentials should be stored securely in Versori activation variables
176
+ - `pageSize` and `maxRecords` can be adjusted based on data volume and performance requirements
177
+
178
+ ## Export Mapping Configuration
179
+
180
+ Create file: `./config/products.export.json`
181
+
182
+ ```json
183
+ {
184
+ "name": "products.export",
185
+ "version": "1.0.0",
186
+ "description": "Fluent Products → Marketplace JSON Export",
187
+ "fields": {
188
+ "sku": { "source": "ref", "required": true, "resolver": "sdk.trim" },
189
+ "title": { "source": "name", "required": true, "resolver": "sdk.trim" },
190
+ "description": { "source": "summary", "required": false, "resolver": "sdk.trim" },
191
+ "gtin": { "source": "gtin", "required": false, "resolver": "sdk.trim" },
192
+ "type": { "source": "type", "required": false, "resolver": "sdk.uppercase" },
193
+ "status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
194
+ "createdOn": { "source": "createdOn", "required": false, "resolver": "sdk.toString" },
195
+ "lastUpdated": { "source": "updatedOn", "required": true, "resolver": "sdk.toString" }
196
+ }
197
+ }
198
+ ```
199
+
200
+ **Note**: Actual GraphQL query structure should be verified against your Fluent schema using introspection. The fields above are examples - adjust based on your actual schema.
201
+
202
+ ## GraphQL Query
203
+
204
+ **Note**: This query is an **example**. Verify against your Fluent Commerce schema using introspection.
205
+
206
+ ```graphql
207
+ query GetProducts($retailerId: ID!, $updatedAfter: DateTime, $first: Int!, $after: String) {
208
+ products(
209
+ retailerId: $retailerId
210
+ updatedOn: { after: $updatedAfter }
211
+ first: $first
212
+ after: $after
213
+ ) {
214
+ edges {
215
+ node {
216
+ id
217
+ ref
218
+ name
219
+ summary
220
+ gtin
221
+ type
222
+ status
223
+ createdOn
224
+ updatedOn
225
+ }
226
+ cursor
227
+ }
228
+ pageInfo {
229
+ hasNextPage
230
+ }
231
+ }
232
+ }
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Versori Workflows Structure
238
+
239
+ **Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
240
+
241
+ **Trigger Types:**
242
+ - **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
243
+ - **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
244
+ - **`workflow()`** → Durable workflows (advanced) - Multi-step processes with state persistence
245
+
246
+ **Execution Steps (chained to triggers):**
247
+ - **`http()`** → External API calls (chained from schedule/webhook)
248
+ - **`fn()`** → Internal processing (chained from schedule/webhook)
249
+
250
+ ### Durable Workflows (Advanced Pattern)
251
+
252
+ **⚠️ Note:** Durable workflows are an advanced pattern not yet used in this template. They're documented here for completeness.
253
+
254
+ **Use Case:** Multi-step processes requiring state persistence, long-running operations (hours/days), human approval workflows, or complex retry/error handling across steps.
255
+
256
+ **Example Pattern:**
257
+ ```typescript
258
+ import { workflow } from '@versori/run';
259
+
260
+ workflow('order-fulfillment', { connection: 'fluent_commerce' }, async (ctx, step) => {
261
+ // Step 1: Validate order
262
+ const order = await step.run('validate-order', async () => {
263
+ return await validateOrder(ctx);
264
+ });
265
+
266
+ // Step 2: Allocate inventory (with retry)
267
+ const allocation = await step.run('allocate-inventory', {
268
+ retry: { max: 3, delay: '1s' }
269
+ }, async () => {
270
+ return await allocateInventory(order);
271
+ });
272
+
273
+ // Step 3: Wait for approval (human-in-loop)
274
+ await step.sleep('wait-for-approval', '1h');
275
+
276
+ // Step 4: Create fulfillment
277
+ return await step.run('create-fulfillment', async () => {
278
+ return await createFulfillment(allocation);
279
+ });
280
+ });
281
+ ```
282
+
283
+ **When to use workflow():**
284
+ - ✅ Multi-step processes requiring state persistence
285
+ - ✅ Long-running operations (hours/days)
286
+ - ✅ Human approval workflows
287
+ - ✅ Complex retry/error handling across steps
288
+ - ❌ Simple CRUD operations (use schedule/webhook + http)
289
+ - ❌ Fire-and-forget patterns (use schedule + http)
290
+
291
+ ### Recommended Project Structure
292
+
293
+ ```
294
+ products-extraction/
295
+ ├── index.ts # Entry point - exports all workflows
296
+ └── src/
297
+ ├── workflows/
298
+ │ ├── scheduled/
299
+ │ │ └── daily-products-extraction.ts # Scheduled: Daily products extraction
300
+ │ │
301
+ │ └── webhook/
302
+ │ ├── adhoc-products-extraction.ts # Webhook: Manual trigger
303
+ │ └── job-status-check.ts # Webhook: Status query
304
+
305
+ ├── services/
306
+ │ └── products-extraction.service.ts # Shared orchestration logic (reusable)
307
+
308
+ └── config/
309
+ └── products.export.json.json # Mapping configuration
310
+ ```
311
+
312
+ ---
313
+
314
+ ```json
315
+ {
316
+ "metadata": {
317
+ "extractedAt": "2025-01-22T00:00:00.000Z",
318
+ "productCount": 2,
319
+ "incrementalFrom": "2025-01-21T00:00:00.000Z",
320
+ "incrementalTo": "2025-01-22T00:00:00.000Z"
321
+ },
322
+ "products": [
323
+ {
324
+ "sku": "SKU-001",
325
+ "title": "Premium Widget",
326
+ "description": "High-quality widget for all purposes",
327
+ "gtin": "012345678901",
328
+ "type": "STANDARD",
329
+ "status": "ACTIVE",
330
+ "createdOn": "2025-01-15T10:00:00Z",
331
+ "lastUpdated": "2025-01-21T10:30:00Z"
332
+ },
333
+ {
334
+ "sku": "SKU-002",
335
+ "title": "Deluxe Gadget",
336
+ "description": "Advanced gadget with premium features",
337
+ "gtin": "012345678902",
338
+ "type": "STANDARD",
339
+ "status": "ACTIVE",
340
+ "createdOn": "2025-01-16T11:00:00Z",
341
+ "lastUpdated": "2025-01-21T14:15:00Z"
342
+ }
343
+ ]
344
+ }
345
+ ```
346
+
347
+ ## Mapping & Resolvers Explained
348
+
349
+ ### SDK Resolvers Used
350
+
351
+ | Field | Resolver | Why? | Example |
352
+ | ------------- | --------------- | ----------------------- | ----------------------------- |
353
+ | `sku` | `sdk.trim` | Clean SKU | " SKU-001 " → "SKU-001" |
354
+ | `title` | `sdk.trim` | Clean names | " Widget " → "Widget" |
355
+ | `description` | `sdk.trim` | Remove extra whitespace | " desc " → "desc" |
356
+ | `gtin` | `sdk.trim` | Normalize GTIN/UPC | " 012345 " → "012345" |
357
+ | `type` | `sdk.uppercase` | Normalize type | "standard" → "STANDARD" |
358
+ | `status` | `sdk.uppercase` | Normalize status | "active" → "ACTIVE" |
359
+ | `createdOn` | `sdk.toString` | Ensure ISO string | Date → "2025-01-15T10:00:00Z" |
360
+ | `lastUpdated` | `sdk.toString` | Ensure ISO string | Date → "2025-01-22T14:30:00Z" |
361
+
362
+ ### Transformation Flow
363
+
364
+ ```typescript
365
+ // 1) GraphQL node (example)
366
+ {
367
+ ref: " SKU-001 ",
368
+ name: " Widget ",
369
+ summary: " High quality ",
370
+ gtin: " 012345678901 ",
371
+ type: "standard",
372
+ status: "active",
373
+ createdOn: "2025-01-15T10:00:00.000Z",
374
+ updatedOn: "2025-01-21T10:30:00.000Z"
375
+ }
376
+
377
+ // 2) Map with UniversalMapper
378
+ const mapper = new UniversalMapper(productsExportMapping);
379
+ const result = await mapper.map(node);
380
+
381
+ // 3) Output
382
+ result.data = {
383
+ sku: "SKU-001",
384
+ title: "Widget",
385
+ description: "High quality",
386
+ gtin: "012345678901",
387
+ type: "STANDARD",
388
+ status: "ACTIVE",
389
+ createdOn: "2025-01-15T10:00:00.000Z",
390
+ lastUpdated: "2025-01-21T10:30:00.000Z"
391
+ };
392
+ ```
393
+
394
+ ### Custom Resolvers for Product-Specific Logic
395
+
396
+ While the mapping above uses built-in SDK resolvers, you can extend with custom business logic for marketplace integration:
397
+
398
+ ```typescript
399
+ const customResolvers = {
400
+ /**
401
+ * Extract category hierarchy for marketplace navigation
402
+ */
403
+ 'custom.formatCategoryPath': (product: any) => {
404
+ const categories = product.categories || [];
405
+ return categories
406
+ .map((c: any) => c.name?.trim())
407
+ .filter(Boolean)
408
+ .join(' > ');
409
+ // Example: "Electronics > Computers > Laptops"
410
+ },
411
+
412
+ /**
413
+ * Generate marketplace-compatible SKU with prefix
414
+ */
415
+ 'custom.generateMarketplaceSKU': (ref: string, retailerId: string) => {
416
+ return `${retailerId}-${ref.trim()}`.toUpperCase();
417
+ // Example: "RETAILER123-SKU-001"
418
+ },
419
+
420
+ /**
421
+ * Format price for marketplace API (convert cents to dollars)
422
+ */
423
+ 'custom.formatPrice': (priceInCents: number) => {
424
+ const dollars = (priceInCents || 0) / 100;
425
+ return dollars.toFixed(2);
426
+ // Example: 2499 → "24.99"
427
+ },
428
+
429
+ /**
430
+ * Extract primary image URL from attributes
431
+ */
432
+ 'custom.getPrimaryImage': (product: any) => {
433
+ const images = product.attributes?.images || [];
434
+ return images.length > 0 ? images[0]?.url : null;
435
+ },
436
+
437
+ /**
438
+ * Generate marketplace title with brand and product name
439
+ */
440
+ 'custom.generateMarketplaceTitle': (product: any) => {
441
+ const brand = product.attributes?.brand?.trim() || '';
442
+ const name = product.name?.trim() || '';
443
+ const maxLength = 200; // Amazon/eBay limit
444
+
445
+ const title = brand ? `${brand} - ${name}` : name;
446
+ return title.length > maxLength ? title.substring(0, maxLength - 3) + '...' : title;
447
+ },
448
+
449
+ /**
450
+ * Extract product attributes for marketplace listing
451
+ */
452
+ 'custom.extractAttributes': (product: any) => {
453
+ const attrs = product.attributes || {};
454
+ return {
455
+ brand: attrs.brand?.trim() || 'Unbranded',
456
+ color: attrs.color?.trim() || null,
457
+ size: attrs.size?.trim() || null,
458
+ weight: attrs.weight ? `${attrs.weight} ${attrs.weightUnit || 'lb'}` : null,
459
+ dimensions: attrs.dimensions || null,
460
+ material: attrs.material?.trim() || null,
461
+ };
462
+ },
463
+
464
+ /**
465
+ * Determine product eligibility for marketplaces
466
+ */
467
+ 'custom.checkMarketplaceEligibility': (product: any) => {
468
+ const status = (product.status || '').toUpperCase();
469
+ const hasGTIN = !!product.gtin;
470
+ const hasImages = (product.attributes?.images || []).length > 0;
471
+ const hasPrice = (product.prices || []).length > 0;
472
+
473
+ return {
474
+ isActive: status === 'ACTIVE',
475
+ hasRequiredFields: hasGTIN && hasImages && hasPrice,
476
+ isEligibleForAmazon: status === 'ACTIVE' && hasGTIN && hasImages && hasPrice,
477
+ isEligibleForEbay: status === 'ACTIVE' && hasImages && hasPrice,
478
+ missingFields: [!hasGTIN && 'GTIN', !hasImages && 'Images', !hasPrice && 'Price'].filter(
479
+ Boolean
480
+ ),
481
+ };
482
+ },
483
+
484
+ /**
485
+ * Format for marketplace API submission
486
+ */
487
+ 'custom.formatForMarketplace': (product: any) => {
488
+ return {
489
+ sku: product.ref?.trim(),
490
+ title: product.name?.trim(),
491
+ description: product.summary?.trim() || product.name?.trim(),
492
+ category: product.categories?.[0]?.name || 'Uncategorized',
493
+ brand: product.attributes?.brand?.trim() || 'Generic',
494
+ gtin: product.gtin?.trim() || null,
495
+ price: (product.prices?.[0]?.value || 0) / 100, // cents to dollars
496
+ currency: product.prices?.[0]?.currency || 'USD',
497
+ images: (product.attributes?.images || []).map((img: any) => img.url),
498
+ status: product.status?.toUpperCase(),
499
+ lastUpdated: product.updatedOn,
500
+ };
501
+ },
502
+ };
503
+
504
+ // Use custom resolvers with UniversalMapper
505
+ const mapper = new UniversalMapper(productsExportMapping, {
506
+ customResolvers,
507
+ });
508
+ ```
509
+
510
+ ### Available SDK Resolvers
511
+
512
+ The SDK provides these built-in resolvers (no custom code needed):
513
+
514
+ **String Transformations:**
515
+
516
+ - `sdk.trim` - Remove leading/trailing whitespace
517
+ - `sdk.uppercase` - Convert to uppercase
518
+ - `sdk.lowercase` - Convert to lowercase
519
+ - `sdk.toString` - Convert to string
520
+
521
+ **Number Parsing:**
522
+
523
+ - `sdk.parseInt` - Parse as integer
524
+ - `sdk.parseFloat` - Parse as decimal (ideal for prices)
525
+ - `sdk.number` - Parse as number (auto-detect int/float)
526
+
527
+ **Date Formatting:**
528
+
529
+ - `sdk.formatDate` - ISO 8601 date only (YYYY-MM-DD)
530
+ - `sdk.formatDateShort` - Short date format
531
+ - `sdk.parseDate` - Parse various date formats
532
+
533
+ **Type Conversions:**
534
+
535
+ - `sdk.boolean` - Convert to boolean
536
+ - `sdk.parseJson` - Parse JSON strings (useful for attributes)
537
+ - `sdk.toJson` - Convert to JSON string
538
+
539
+ **Utilities:**
540
+
541
+ - `sdk.identity` - Return value unchanged
542
+ - `sdk.coalesce` - Return first non-null value
543
+
544
+ See [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) for complete resolver documentation.
545
+
546
+ ## Production Safety & Guardrails
547
+
548
+ ### Overview
549
+
550
+ Even with **incremental-only** extraction, product catalogs need safeguards to prevent runtime failures:
551
+
552
+ - **Memory limits**: Product records can be large (descriptions, attributes, images)
553
+ - **S3 upload limits**: Single files > 5GB require multipart upload
554
+ - **Processing time**: Large catalogs can timeout
555
+ - **JSON parsing**: Massive JSON files stress memory and downstream parsers
556
+
557
+ ### Hard Limits
558
+
559
+ ```typescript
560
+ const SAFETY_LIMITS = {
561
+ MAX_RECORDS_PER_RUN: 100000, // 100k products per run
562
+ MAX_RECORDS_PER_FILE: 25000, // 25k per JSON file
563
+ MAX_FILE_SIZE_MB: 250, // 250MB per file
564
+ MAX_JSON_SIZE_MB: 500, // Total extraction size
565
+ CHUNK_SIZE: 10000, // Process in chunks
566
+ ESTIMATED_BYTES_PER_PRODUCT: 2048, // Conservative estimate (2KB per product)
567
+ };
568
+ ```
569
+
570
+ **Why these limits?**
571
+
572
+ - Products have more fields than orders/fulfillments (descriptions, attributes, variants)
573
+ - JSON is less compact than CSV
574
+ - Marketplace APIs often have file size limits (Amazon: 100MB compressed)
575
+
576
+ ### Runtime Validation Function
577
+
578
+ ```typescript
579
+ /**
580
+ * Validate extraction safety limits before processing
581
+ */
582
+ function validateExtractionLimits(recordCount: number, estimatedSizeMB: number) {
583
+ const MAX_RECORDS_PER_RUN = 100000;
584
+ const MAX_JSON_SIZE_MB = 500;
585
+
586
+ if (recordCount > MAX_RECORDS_PER_RUN) {
587
+ return {
588
+ valid: false,
589
+ error: `Extraction limit exceeded: ${recordCount} records (max: ${MAX_RECORDS_PER_RUN})`,
590
+ recommendation: `Catalog too large for single extraction. Consider:
591
+ 1. Increase extraction frequency to reduce batch size
592
+ 2. Filter by product status (ACTIVE only)
593
+ 3. Split by product type or category
594
+ 4. Use multiple extractions with different filters`,
595
+ recordCount,
596
+ maxAllowed: MAX_RECORDS_PER_RUN,
597
+ };
598
+ }
599
+
600
+ if (estimatedSizeMB > MAX_JSON_SIZE_MB) {
601
+ return {
602
+ valid: false,
603
+ error: `JSON size limit exceeded: ${estimatedSizeMB}MB (max: ${MAX_JSON_SIZE_MB}MB)`,
604
+ recommendation: 'File splitting required. Consider filtering or splitting by category.',
605
+ estimatedSizeMB,
606
+ maxAllowed: MAX_JSON_SIZE_MB,
607
+ };
608
+ }
609
+
610
+ return { valid: true };
611
+ }
612
+ ```
613
+
614
+ ## Complete Workflows Implementation
615
+
616
+ The code examples below demonstrate what goes in each file according to the modular structure shown in the "Recommended Project Structure" section above.
617
+
618
+ ### Workflow 1: Scheduled Incremental Extraction
619
+ **File:** `src/workflows/scheduled/daily-products-extraction.ts`
620
+
621
+ ```typescript
622
+ import { schedule, http } from '@versori/run';
623
+ import { Buffer } from 'node:buffer';
624
+ import {
625
+ createClient,
626
+ UniversalMapper,
627
+ S3DataSource,
628
+ JSONBuilder,
629
+ VersoriKVAdapter,
630
+ JobTracker,
631
+ } from '@fluentcommerce/fc-connect-sdk';
632
+ import productsExportMapping from './config/products.export.json' with { type: 'json' };
633
+
634
+ const PRODUCTS_QUERY = `
635
+ query GetProducts(
636
+ $retailerId: ID!
637
+ $updatedAfter: DateTime
638
+ $first: Int!
639
+ $after: String
640
+ ) {
641
+ products(
642
+ retailerId: $retailerId
643
+ updatedOn: { after: $updatedAfter }
644
+ first: $first
645
+ after: $after
646
+ ) {
647
+ edges {
648
+ node {
649
+ id
650
+ ref
651
+ name
652
+ summary
653
+ gtin
654
+ type
655
+ status
656
+ createdOn
657
+ updatedOn
658
+ }
659
+ cursor
660
+ }
661
+ pageInfo {
662
+ hasNextPage
663
+ }
664
+ }
665
+ }
666
+ `;
667
+
668
+ /**
669
+ * WORKFLOW 1/3: Scheduled Daily Product Extraction (Incremental)
670
+ *
671
+ * Runs daily at midnight to extract changed products
672
+ */
673
+ export const scheduledProductsExtraction = schedule('scheduled-products-extraction', '0 0 * * *').then(
674
+ http('extract-products', { connection: 'fluent_commerce' }, async ctx => {
675
+ const { log, openKv, activation } = ctx;
676
+ const executionStartTime = Date.now();
677
+
678
+ log.info('=== EXECUTION START ===', { timestamp: new Date().toISOString() });
679
+
680
+ // STEP 1/8: Parse activation variables
681
+ const retailerId = activation?.getVariable('retailerId');
682
+ const pageSize = parseInt(activation?.getVariable('pageSize') || '200', 10);
683
+ const maxRecords = parseInt(activation?.getVariable('maxRecords') || '50000', 10);
684
+ const fallbackStartDate =
685
+ activation?.getVariable('fallbackStartDate') || '2024-01-01T00:00:00Z';
686
+ const prettyPrint = activation?.getVariable('prettyPrint') === 'true';
687
+
688
+ const s3Config = {
689
+ bucket: activation?.getVariable('s3BucketName'),
690
+ region: activation?.getVariable('awsRegion') || 'us-east-1',
691
+ accessKeyId: activation?.getVariable('awsAccessKeyId'),
692
+ secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
693
+ };
694
+ const s3Prefix = activation?.getVariable('s3Prefix') || 'products/daily/';
695
+
696
+ // Validate required variables
697
+ const missing: string[] = [];
698
+ if (!retailerId) missing.push('retailerId');
699
+ if (!s3Config.bucket) missing.push('s3BucketName');
700
+ if (!s3Config.accessKeyId) missing.push('awsAccessKeyId');
701
+ if (!s3Config.secretAccessKey) missing.push('awsSecretAccessKey');
702
+ if (missing.length) {
703
+ return { success: false, error: `Missing required variables: ${missing.join(', ')}` };
704
+ }
705
+
706
+ try {
707
+ // STEP 2/8: Initialize JobTracker and start tracking
708
+ const tracker = new JobTracker(openKv(':project:'), log);
709
+ const jobId = `products-extraction-${Date.now()}`;
710
+
711
+ await tracker.createJob(jobId, {
712
+ type: 'extraction',
713
+ entity: 'products',
714
+ mode: 'scheduled',
715
+ retailerId,
716
+ startTime: executionStartTime,
717
+ });
718
+
719
+ // STEP 3/8: Load last successful extraction timestamp with overlap buffer
720
+ const kv = new VersoriKVAdapter(openKv(':project:'));
721
+ const stateKey = ['extraction', 'products', 'lastRunTime'];
722
+ const lastRunState = await kv.get(stateKey);
723
+ const rawLastRunTime = lastRunState?.value?.timestamp || fallbackStartDate;
724
+
725
+ // Overlap buffer configuration (default: 60 seconds)
726
+ const overlapBufferSeconds = parseInt(
727
+ activation?.getVariable('overlapBufferSeconds') || '60',
728
+ 10
729
+ );
730
+ const OVERLAP_BUFFER_MS = overlapBufferSeconds * 1000;
731
+
732
+ // Apply overlap buffer for query (safety window)
733
+ const bufferedLastRunTime = new Date(
734
+ new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
735
+ ).toISOString();
736
+
737
+ const toDate = undefined; // No manual override for scheduled extraction
738
+
739
+ log.info('🔍 Starting incremental products extraction with overlap buffer', {
740
+ jobId,
741
+ rawLastRunTime,
742
+ bufferedLastRunTime,
743
+ effectiveEndTime: toDate || new Date().toISOString(),
744
+ overlapBufferSeconds,
745
+ retailerId,
746
+ maxRecords,
747
+ });
748
+
749
+ // STEP 4/8: Initialize Fluent client + ExtractionOrchestrator
750
+ // ✅ Optional: Validate connection immediately (fail-fast mode)
751
+ // Set activation variable 'validateConnectionOnStart' = 'true' to enable
752
+ // When enabled: Executes query { me { ref } } to verify authentication
753
+ // When disabled: Fast creation, validation happens on first API call (default)
754
+ const validateConnection = activation.getVariable('validateConnectionOnStart') === 'true';
755
+ const client = await createClient(ctx, validateConnection ? { validateConnection: true } : undefined);
756
+
757
+ if (validateConnection) {
758
+ log.info('✅ Connection validated successfully - authentication confirmed', { jobId });
759
+ }
760
+
761
+ const orchestrator = new ExtractionOrchestrator(client, log);
762
+
763
+ // STEP 5/8: Execute extraction with auto-pagination (WITH overlap buffer)
764
+ const effectiveEndTime = toDate || new Date().toISOString();
765
+
766
+ // ? Enhanced: Extract context for progress logging
767
+ const dateRangeInfo = {
768
+ start: bufferedLastRunTime,
769
+ end: effectiveEndTime,
770
+ retailerId
771
+ };
772
+
773
+ // ? Enhanced: Start logging with context
774
+ log.info(`📊 [ExtractionOrchestrator] Starting extraction`, {
775
+ query: 'products',
776
+ pageSize,
777
+ maxRecords,
778
+ dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
779
+ retailerId: dateRangeInfo.retailerId,
780
+ jobId
781
+ });
782
+
783
+ const extractionResult = await orchestrator.extract({
784
+ query: PRODUCTS_QUERY,
785
+ resultPath: 'products.edges.node',
786
+ variables: {
787
+ retailerId,
788
+ dateRangeFilter: {
789
+ after: bufferedLastRunTime,
790
+ before: effectiveEndTime, // End of extraction window
791
+ },
792
+ },
793
+ pageSize,
794
+ maxRecords,
795
+ validateItem: item => !!item.ref,
796
+ });
797
+
798
+ const rawRecords = extractionResult.data;
799
+
800
+ log.info('Products extraction completed', {
801
+ jobId,
802
+ totalRecords: extractionResult.stats.totalRecords,
803
+ totalPages: extractionResult.stats.totalPages,
804
+ validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
805
+ errors: extractionResult.errors ? extractionResult.errors.length : 0,
806
+ });
807
+
808
+ // ? Enhanced: Completion logging with summary
809
+ log.info(`✅ [ExtractionOrchestrator] Extraction completed`, {
810
+ totalRecords: extractionResult.stats.totalRecords,
811
+ totalPages: extractionResult.stats.totalPages,
812
+ validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
813
+ failedValidations: extractionResult.stats.failedValidations,
814
+ truncated: extractionResult.stats.truncated,
815
+ truncationReason: extractionResult.stats.truncationReason,
816
+ dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
817
+ jobId
818
+ });
819
+
820
+ if (extractionResult.errors && extractionResult.errors.length > 0) {
821
+ log.warn('Non-fatal extraction errors encountered', {
822
+ jobId,
823
+ errorCount: extractionResult.errors.length,
824
+ sampleErrors: extractionResult.errors.slice(0, 3),
825
+ });
826
+ }
827
+
828
+ if (rawRecords.length === 0) {
829
+ log.info('No new products to extract');
830
+ await kv.set(stateKey, {
831
+ timestamp: new Date().toISOString(),
832
+ productCount: 0,
833
+ extractedAt: new Date().toISOString(),
834
+ });
835
+ await tracker.markCompleted(jobId, {
836
+ recordsProcessed: 0,
837
+ message: 'No new products to extract',
838
+ });
839
+ return {
840
+ success: true,
841
+ message: 'No new products to extract',
842
+ jobId,
843
+ lastRunTime: rawLastRunTime,
844
+ };
845
+ }
846
+
847
+ log.info('Products retrieved', { jobId, count: rawRecords.length });
848
+
849
+ // STEP 6/8: Validate extraction limits
850
+ const MAX_RECORDS_PER_RUN = 100000;
851
+ const ESTIMATED_BYTES_PER_PRODUCT = 2048;
852
+ const estimatedSizeMB = (rawRecords.length * ESTIMATED_BYTES_PER_PRODUCT) / (1024 * 1024);
853
+ const MAX_JSON_SIZE_MB = 500;
854
+
855
+ if (rawRecords.length > MAX_RECORDS_PER_RUN) {
856
+ await tracker.markFailed(jobId, {
857
+ error: 'Extraction limit exceeded',
858
+ recordCount: rawRecords.length,
859
+ maxAllowed: MAX_RECORDS_PER_RUN,
860
+ });
861
+ log.error('Extraction limit exceeded', {
862
+ recordCount: rawRecords.length,
863
+ maxAllowed: MAX_RECORDS_PER_RUN,
864
+ });
865
+ return {
866
+ success: false,
867
+ error: `Extraction limit exceeded: ${rawRecords.length} records (max: ${MAX_RECORDS_PER_RUN})`,
868
+ recommendation: `Catalog too large for single extraction. Consider:
869
+ 1. Increase extraction frequency to reduce batch size
870
+ 2. Filter by product status (ACTIVE only)
871
+ 3. Split by product type or category
872
+ 4. Use multiple extractions with different filters`,
873
+ recordCount: rawRecords.length,
874
+ maxAllowed: MAX_RECORDS_PER_RUN,
875
+ };
876
+ }
877
+
878
+ if (estimatedSizeMB > MAX_JSON_SIZE_MB) {
879
+ log.warn('JSON size approaching limit', {
880
+ estimatedSizeMB: estimatedSizeMB.toFixed(2),
881
+ maxAllowed: MAX_JSON_SIZE_MB,
882
+ recommendation: 'Consider file splitting or category-based filtering',
883
+ });
884
+ }
885
+
886
+ await tracker.updateJobProgress(jobId, {
887
+ stage: 'validation_complete',
888
+ recordCount: rawRecords.length,
889
+ estimatedSizeMB: estimatedSizeMB.toFixed(2),
890
+ });
891
+
892
+ // STEP 7/8: Transform with UniversalMapper
893
+ const mapper = new UniversalMapper(productsExportMapping);
894
+ const mappingResult = await mapper.map(rawRecords);
895
+
896
+ if (!mappingResult.success) {
897
+ const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
898
+ await tracker.markFailed(
899
+ jobId,
900
+ mappingErrors[0] || 'UniversalMapper returned unsuccessful result',
901
+ {
902
+ failedCount: mappingErrors.length,
903
+ errors: mappingErrors,
904
+ }
905
+ );
906
+ return {
907
+ success: false,
908
+ error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
909
+ errors: mappingErrors,
910
+ };
911
+ }
912
+
913
+ const transformedProducts = Array.isArray(mappingResult.data) ? mappingResult.data : [];
914
+ const mappingErrors = mappingResult.errors || [];
915
+
916
+ if (mappingErrors.length > 0) {
917
+ log.warn('Some products failed transformation', {
918
+ jobId,
919
+ errorCount: mappingErrors.length,
920
+ sampleErrors: mappingErrors.slice(0, 3),
921
+ });
922
+ }
923
+
924
+ if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
925
+ log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
926
+ jobId,
927
+ skippedFields: mappingResult.skippedFields,
928
+ note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
929
+ });
930
+ }
931
+
932
+ if (transformedProducts.length === 0) {
933
+ await tracker.markFailed(jobId, 'All records failed mapping', {
934
+ failedCount: mappingErrors.length,
935
+ errors: mappingErrors,
936
+ });
937
+ return {
938
+ success: false,
939
+ error: 'All records failed mapping',
940
+ errors: mappingErrors,
941
+ };
942
+ }
943
+
944
+ log.info('Records transformed', {
945
+ jobId,
946
+ successful: transformedProducts.length,
947
+ skippedRecords: rawRecords.length - transformedProducts.length,
948
+ });
949
+
950
+ await tracker.updateJobProgress(jobId, {
951
+ stage: 'transformation_complete',
952
+ transformedCount: transformedProducts.length,
953
+ failedCount: mappingErrors.length,
954
+ });
955
+
956
+ // Calculate max updatedOn for next run (WITHOUT buffer)
957
+ const maxUpdatedOn = transformedProducts.reduce((max, product) => {
958
+ const productTime = new Date(product.lastUpdated).getTime();
959
+ return productTime > max ? productTime : max;
960
+ }, new Date(rawLastRunTime).getTime());
961
+
962
+ const newTimestamp = new Date(maxUpdatedOn).toISOString();
963
+
964
+ // Build JSON with metadata
965
+ const jsonOutput = {
966
+ metadata: {
967
+ extractedAt: new Date().toISOString(),
968
+ productCount: transformedProducts.length,
969
+ incrementalFrom: rawLastRunTime,
970
+ incrementalTo: newTimestamp,
971
+ },
972
+ products: transformedProducts,
973
+ };
974
+
975
+ // Use JSONBuilder for consistent JSON generation
976
+ const jsonBuilder = new JSONBuilder({
977
+ prettyPrint,
978
+ indent: 2,
979
+ });
980
+ const jsonContent = jsonBuilder.build(jsonOutput);
981
+
982
+ // Generate timestamped filename
983
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
984
+ const fileName = `products-${timestamp}.json`;
985
+ const s3Key = `${s3Prefix}${fileName}`;
986
+
987
+ log.info('Generated JSON file', {
988
+ jobId,
989
+ fileName,
990
+ size: jsonContent.length,
991
+ productCount: transformedProducts.length,
992
+ });
993
+
994
+ // STEP 8/8: Upload to S3
995
+ const s3 = new S3DataSource(
996
+ {
997
+ type: 'S3_JSON',
998
+ connectionId: 's3-products-export',
999
+ name: 'S3 Products Export',
1000
+ s3Config,
1001
+ },
1002
+ log
1003
+ );
1004
+
1005
+ await s3.upload(s3Key, Buffer.from(jsonContent, 'utf8'), {
1006
+ contentType: 'application/json',
1007
+ metadata: {
1008
+ productCount: String(transformedProducts.length),
1009
+ extractedAt: new Date().toISOString(),
1010
+ incrementalFrom: rawLastRunTime,
1011
+ incrementalTo: newTimestamp,
1012
+ },
1013
+ });
1014
+
1015
+ log.info('JSON file uploaded to S3', { jobId, s3Key });
1016
+
1017
+ // Update state with new timestamp (WITHOUT buffer)
1018
+ await kv.set(stateKey, {
1019
+ timestamp: newTimestamp, // ← NO buffer applied
1020
+ productCount: transformedProducts.length,
1021
+ extractedAt: new Date().toISOString(),
1022
+ overlapBufferSeconds,
1023
+ fileName,
1024
+ s3Key,
1025
+ errors: mappingErrors.length > 0 ? mappingErrors : undefined,
1026
+ });
1027
+
1028
+ log.info('State updated with new timestamp (without buffer)', {
1029
+ jobId,
1030
+ newTimestamp,
1031
+ overlapBufferSeconds,
1032
+ });
1033
+
1034
+ // Complete job tracking
1035
+ const executionDurationMs = Date.now() - executionStartTime;
1036
+ await tracker.markCompleted(jobId, {
1037
+ recordsProcessed: transformedProducts.length,
1038
+ recordsFailed: mappingErrors.length,
1039
+ fileName,
1040
+ s3Key,
1041
+ newTimestamp,
1042
+ executionDurationMs,
1043
+ errors: mappingErrors,
1044
+ });
1045
+
1046
+ log.info('=== EXECUTION END ===', {
1047
+ timestamp: new Date().toISOString(),
1048
+ durationMs: executionDurationMs,
1049
+ success: true,
1050
+ });
1051
+
1052
+ return {
1053
+ success: true,
1054
+ jobId,
1055
+ productsExtracted: transformedProducts.length,
1056
+ recordsFailed: mappingErrors.length,
1057
+ fileName,
1058
+ s3Key,
1059
+ lastRunTime: rawLastRunTime,
1060
+ newTimestamp,
1061
+ executionDurationMs,
1062
+ errors: mappingErrors.length > 0 ? mappingErrors : undefined,
1063
+ };
1064
+ } catch (error: any) {
1065
+ const executionDurationMs = Date.now() - executionStartTime;
1066
+ log.error('Extraction failed', error, {
1067
+ message: error?.message,
1068
+ executionDurationMs,
1069
+ });
1070
+
1071
+ log.info('=== EXECUTION END ===', {
1072
+ timestamp: new Date().toISOString(),
1073
+ durationMs: executionDurationMs,
1074
+ success: false,
1075
+ });
1076
+
1077
+ return {
1078
+ success: false,
1079
+ message: error instanceof Error ? error.message : String(error),
1080
+ stack: error instanceof Error ? error.stack : undefined,
1081
+ errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
1082
+ executionDurationMs,
1083
+ };
1084
+ }
1085
+ })
1086
+ );
1087
+ ```
1088
+
1089
+ ### Workflow 2: Ad Hoc HTTP Extraction
1090
+ **File:** `src/workflows/webhook/adhoc-products-extraction.ts`
1091
+
1092
+ ```typescript
1093
+ /**
1094
+ * WORKFLOW 2/3: Ad Hoc Product Extraction (HTTP Webhook)
1095
+ *
1096
+ * Manual trigger for on-demand product catalog extraction
1097
+ *
1098
+ * Usage (payload examples):
1099
+ * 1) Incremental (use stored state):
1100
+ * {}
1101
+ *
1102
+ * 2) Date range override (preferred keys):
1103
+ * {
1104
+ * "fromDate": "2025-01-01T00:00:00Z",
1105
+ * "toDate": "2025-01-22T00:00:00Z",
1106
+ * "updateState": false
1107
+ * }
1108
+ *
1109
+ * 3) Backward-compatible keys (startDate/endDate) are also accepted and mapped:
1110
+ * {
1111
+ * "startDate": "2025-01-01T00:00:00Z",
1112
+ * "endDate": "2025-01-22T00:00:00Z"
1113
+ * }
1114
+ */
1115
+ export const adHocProductsExtraction = webhook('extract-products-adhoc', {
1116
+ connection: 'products-adhoc',
1117
+ }).then(
1118
+ http('execute-adhoc-extraction', { connection: 'fluent_commerce' }, async ctx => {
1119
+ const { log, openKv, activation, data } = ctx;
1120
+ const executionStartTime = Date.now();
1121
+
1122
+ log.info('=== EXECUTION START ===', { timestamp: new Date().toISOString() });
1123
+
1124
+ // Parse request body and support both new (fromDate/toDate) and alternative (startDate/endDate) keys
1125
+ const requestData = typeof data === 'string' ? JSON.parse(data) : data;
1126
+ const fromDate = requestData?.fromDate || requestData?.startDate;
1127
+ const toDate = requestData?.toDate || requestData?.endDate;
1128
+ const updateState = requestData?.updateState === true; // default: false
1129
+ const maxRecordsOverride = requestData?.maxRecords;
1130
+
1131
+ const retailerId = activation?.getVariable('retailerId');
1132
+ const pageSize = parseInt(activation?.getVariable('pageSize') || '200', 10);
1133
+ const maxRecords =
1134
+ maxRecordsOverride || parseInt(activation?.getVariable('maxRecords') || '50000', 10);
1135
+ const prettyPrint = activation?.getVariable('prettyPrint') === 'true';
1136
+
1137
+ const s3Config = {
1138
+ bucket: activation?.getVariable('s3BucketName'),
1139
+ region: activation?.getVariable('awsRegion') || 'us-east-1',
1140
+ accessKeyId: activation?.getVariable('awsAccessKeyId'),
1141
+ secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
1142
+ };
1143
+ const s3Prefix = activation?.getVariable('s3Prefix') || 'products/adhoc/';
1144
+
1145
+ // Validate required variables
1146
+ const missing: string[] = [];
1147
+ if (!retailerId) missing.push('retailerId');
1148
+ if (!s3Config.bucket) missing.push('s3BucketName');
1149
+ if (!s3Config.accessKeyId) missing.push('awsAccessKeyId');
1150
+ if (!s3Config.secretAccessKey) missing.push('awsSecretAccessKey');
1151
+ if (missing.length) {
1152
+ return { success: false, error: `Missing required variables: ${missing.join(', ')}` };
1153
+ }
1154
+
1155
+ try {
1156
+ // Initialize JobTracker
1157
+ const tracker = new JobTracker(openKv(':project:'), log);
1158
+ const jobId = `products-adhoc-${Date.now()}`;
1159
+
1160
+ await tracker.createJob(jobId, {
1161
+ type: 'extraction',
1162
+ entity: 'products',
1163
+ mode: 'adhoc',
1164
+ retailerId,
1165
+ fromDate,
1166
+ toDate,
1167
+ updateState,
1168
+ startTime: executionStartTime,
1169
+ });
1170
+
1171
+ log.info('Starting ad hoc products extraction', {
1172
+ jobId,
1173
+ retailerId,
1174
+ fromDate: fromDate || 'not specified',
1175
+ toDate: toDate || 'not specified',
1176
+ maxRecords,
1177
+ updateState,
1178
+ });
1179
+
1180
+ // Initialize Fluent client + Orchestrator
1181
+ // ✅ Optional: Validate connection immediately (fail-fast mode)
1182
+ // Set activation variable 'validateConnectionOnStart' = 'true' to enable
1183
+ // When enabled: Executes query { me { ref } } to verify authentication
1184
+ // When disabled: Fast creation, validation happens on first API call (default)
1185
+ const validateConnection = activation.getVariable('validateConnectionOnStart') === 'true';
1186
+ const client = await createClient(ctx, validateConnection ? { validateConnection: true } : undefined);
1187
+
1188
+ if (validateConnection) {
1189
+ log.info('✅ Connection validated successfully - authentication confirmed', { jobId });
1190
+ }
1191
+
1192
+ const orchestrator = new ExtractionOrchestrator(client, log);
1193
+
1194
+ // Execute extraction with auto-pagination
1195
+ // ? Enhanced: Extract context for progress logging
1196
+ const dateRangeInfo = {
1197
+ start: fromDate || 'all',
1198
+ end: toDate || 'now',
1199
+ retailerId
1200
+ };
1201
+
1202
+ // ? Enhanced: Start logging with context
1203
+ log.info(`📊 [ExtractionOrchestrator] Starting extraction`, {
1204
+ query: 'products',
1205
+ pageSize,
1206
+ maxRecords,
1207
+ dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
1208
+ retailerId: dateRangeInfo.retailerId,
1209
+ jobId
1210
+ });
1211
+
1212
+ const extraction = await orchestrator.extract({
1213
+ query: PRODUCTS_QUERY,
1214
+ resultPath: 'products.edges.node',
1215
+ variables: {
1216
+ retailerId,
1217
+ ...(fromDate ? { updatedAfter: fromDate } : {}),
1218
+ },
1219
+ pageSize,
1220
+ maxRecords,
1221
+ validateItem: (item: any) => !!item.ref,
1222
+ });
1223
+
1224
+ const nodes = extraction.data || [];
1225
+
1226
+ // ? Enhanced: Completion logging with summary
1227
+ log.info(`✅ [ExtractionOrchestrator] Extraction completed`, {
1228
+ totalRecords: extraction.stats.totalRecords,
1229
+ totalPages: extraction.stats.totalPages,
1230
+ validRecords: extraction.stats.validRecords ?? nodes.length,
1231
+ failedValidations: extraction.stats.failedValidations,
1232
+ truncated: extraction.stats.truncated,
1233
+ truncationReason: extraction.stats.truncationReason,
1234
+ dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
1235
+ jobId
1236
+ });
1237
+
1238
+ if (nodes.length === 0) {
1239
+ await tracker.markCompleted(jobId, {
1240
+ recordsProcessed: 0,
1241
+ message: 'No products found',
1242
+ });
1243
+ return {
1244
+ success: true,
1245
+ message: 'No products found',
1246
+ jobId,
1247
+ fromDate,
1248
+ toDate,
1249
+ stateUpdated: false,
1250
+ };
1251
+ }
1252
+
1253
+ // Transform with UniversalMapper (bulk mapping)
1254
+ const mapper = new UniversalMapper(productsExportMapping);
1255
+ const mappingResult = await mapper.map(nodes);
1256
+
1257
+ if (!mappingResult.success) {
1258
+ const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
1259
+ log.error('Mapping failed - terminating job', {
1260
+ jobId,
1261
+ errorCount: mappingErrors.length,
1262
+ sampleErrors: mappingErrors.slice(0, 3),
1263
+ });
1264
+
1265
+ await tracker.markFailed(
1266
+ jobId,
1267
+ mappingErrors[0] || 'UniversalMapper returned unsuccessful result',
1268
+ {
1269
+ errors: mappingErrors,
1270
+ failedCount: mappingErrors.length,
1271
+ }
1272
+ );
1273
+ return {
1274
+ success: false,
1275
+ error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
1276
+ jobId,
1277
+ errors: mappingErrors,
1278
+ };
1279
+ }
1280
+
1281
+ const transformedProducts = mappingResult.data || [];
1282
+ const mappingErrors = mappingResult.errors || [];
1283
+
1284
+ if (mappingErrors.length > 0) {
1285
+ log.warn('Some records failed transformation', {
1286
+ jobId,
1287
+ errorCount: mappingErrors.length,
1288
+ sampleErrors: mappingErrors.slice(0, 3),
1289
+ });
1290
+ }
1291
+
1292
+ if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
1293
+ log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
1294
+ jobId,
1295
+ skippedFields: mappingResult.skippedFields,
1296
+ note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
1297
+ });
1298
+ }
1299
+
1300
+ if (transformedProducts.length === 0) {
1301
+ await tracker.markFailed(jobId, 'All records failed mapping', {
1302
+ failedCount: mappingErrors.length,
1303
+ errors: mappingErrors,
1304
+ });
1305
+ return {
1306
+ success: false,
1307
+ error: 'All records failed mapping',
1308
+ jobId,
1309
+ errors: mappingErrors,
1310
+ };
1311
+ }
1312
+
1313
+ // Compute newTimestamp from transformed records (WITHOUT buffer)
1314
+ const newTimestamp = new Date(
1315
+ transformedProducts.reduce(
1316
+ (max, product) => {
1317
+ const t = new Date(product.lastUpdated).getTime();
1318
+ return t > max ? t : max;
1319
+ },
1320
+ fromDate ? new Date(fromDate).getTime() : 0
1321
+ )
1322
+ ).toISOString();
1323
+
1324
+ // Build JSON output
1325
+ const jsonOutput = {
1326
+ metadata: {
1327
+ extractedAt: new Date().toISOString(),
1328
+ productCount: transformedProducts.length,
1329
+ mode: 'adhoc',
1330
+ ...(fromDate && { fromDate }),
1331
+ ...(toDate && { toDate }),
1332
+ stateUpdated: updateState,
1333
+ },
1334
+ products: transformedProducts,
1335
+ };
1336
+
1337
+ // Use JSONBuilder for consistent JSON generation
1338
+ const jsonBuilder = new JSONBuilder({
1339
+ prettyPrint,
1340
+ indent: 2,
1341
+ });
1342
+ const jsonContent = jsonBuilder.build(jsonOutput);
1343
+
1344
+ // Upload to S3
1345
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1346
+ const fileName = `products-adhoc-${timestamp}.json`;
1347
+ const s3Key = `${s3Prefix}${fileName}`;
1348
+
1349
+ const s3 = new S3DataSource(
1350
+ {
1351
+ type: 'S3_JSON',
1352
+ connectionId: 's3-products-export',
1353
+ name: 'S3 Products Export',
1354
+ s3Config,
1355
+ },
1356
+ log
1357
+ );
1358
+
1359
+ await s3.upload(s3Key, Buffer.from(jsonContent, 'utf8'), {
1360
+ contentType: 'application/json',
1361
+ metadata: {
1362
+ productCount: String(transformedProducts.length),
1363
+ extractedAt: new Date().toISOString(),
1364
+ mode: 'adhoc',
1365
+ },
1366
+ });
1367
+
1368
+ // Optionally update state (adhoc): respects updateState flag
1369
+ let stateUpdated = false;
1370
+ if (updateState) {
1371
+ const kv = new VersoriKVAdapter(openKv(':project:'));
1372
+ const stateKey = ['extraction', 'products', 'lastRunTime'];
1373
+ await kv.set(stateKey, {
1374
+ timestamp: newTimestamp, // WITHOUT buffer
1375
+ productCount: transformedProducts.length,
1376
+ extractedAt: new Date().toISOString(),
1377
+ fileName,
1378
+ s3Key,
1379
+ errors: mappingErrors.length > 0 ? mappingErrors : undefined,
1380
+ });
1381
+ stateUpdated = true;
1382
+ }
1383
+
1384
+ // Complete job tracking
1385
+ const executionDurationMs = Date.now() - executionStartTime;
1386
+ await tracker.markCompleted(jobId, {
1387
+ recordsProcessed: transformedProducts.length,
1388
+ recordsFailed: mappingErrors.length,
1389
+ fileName,
1390
+ s3Key,
1391
+ newTimestamp,
1392
+ stateUpdated,
1393
+ executionDurationMs,
1394
+ errors: mappingErrors,
1395
+ });
1396
+
1397
+ log.info('=== EXECUTION END ===', {
1398
+ timestamp: new Date().toISOString(),
1399
+ durationMs: executionDurationMs,
1400
+ success: true,
1401
+ });
1402
+
1403
+ return {
1404
+ success: true,
1405
+ jobId,
1406
+ productsExtracted: transformedProducts.length,
1407
+ recordsFailed: mappingErrors.length,
1408
+ fileName,
1409
+ s3Key,
1410
+ newTimestamp,
1411
+ stateUpdated,
1412
+ executionDurationMs,
1413
+ errors: mappingErrors.length > 0 ? mappingErrors : undefined,
1414
+ };
1415
+ } catch (error: any) {
1416
+ const executionDurationMs = Date.now() - executionStartTime;
1417
+ log.error('Ad hoc extraction failed', error, { executionDurationMs });
1418
+
1419
+ log.info('=== EXECUTION END ===', {
1420
+ timestamp: new Date().toISOString(),
1421
+ durationMs: executionDurationMs,
1422
+ success: false,
1423
+ });
1424
+
1425
+ return {
1426
+ success: false,
1427
+ message: error instanceof Error ? error.message : String(error),
1428
+ stack: error instanceof Error ? error.stack : undefined,
1429
+ errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
1430
+ executionDurationMs,
1431
+ };
1432
+ }
1433
+ })
1434
+ );
1435
+ ```
1436
+
1437
+ ### Workflow 3: Job Status Checker
1438
+ **File:** `src/workflows/webhook/job-status-check.ts`
1439
+
1440
+ ```typescript
1441
+ /**
1442
+ * WORKFLOW 3/3: Job Status Checker
1443
+ *
1444
+ * Check status of extraction job
1445
+ *
1446
+ * Usage:
1447
+ * POST /job-status
1448
+ * {
1449
+ * "jobId": "products-extraction-1234567890"
1450
+ * }
1451
+ */
1452
+ export const checkJobStatus = webhook('products-job-status', {
1453
+ connection: 'products-job-status',
1454
+ }).then(
1455
+ http('query-job-status', {}, async ctx => {
1456
+ const { log, openKv, data } = ctx;
1457
+
1458
+ const requestData = typeof data === 'string' ? JSON.parse(data) : data;
1459
+ const jobId = requestData?.jobId;
1460
+
1461
+ if (!jobId) {
1462
+ return {
1463
+ success: false,
1464
+ error: 'Missing required field: jobId',
1465
+ };
1466
+ }
1467
+
1468
+ try {
1469
+ const tracker = new JobTracker(openKv(':project:'), log);
1470
+ const job = await tracker.getJob(jobId);
1471
+
1472
+ if (!job) {
1473
+ return {
1474
+ success: false,
1475
+ error: `Job not found: ${jobId}`,
1476
+ };
1477
+ }
1478
+
1479
+ return {
1480
+ success: true,
1481
+ job,
1482
+ };
1483
+ } catch (error: any) {
1484
+ log.error('Failed to get job status', error);
1485
+ return {
1486
+ success: false,
1487
+ message: error instanceof Error ? error.message : String(error),
1488
+
1489
+ stack: error instanceof Error ? error.stack : undefined,
1490
+
1491
+ errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
1492
+ };
1493
+ }
1494
+ })
1495
+ );
1496
+ ```
1497
+
1498
+ ### Entry Point (Workflow Registration)
1499
+ **File:** `index.ts`
1500
+
1501
+ ```typescript
1502
+ /**
1503
+ * Entry point - Export all workflows for Versori platform
1504
+ *
1505
+ * This file exports all workflows to be registered with Versori.
1506
+ * Each workflow is defined in its own file for better organization.
1507
+ */
1508
+
1509
+ // Scheduled workflows
1510
+ export { scheduledProductsExtraction } from './src/workflows/scheduled/daily-products-extraction';
1511
+
1512
+ // Webhook workflows
1513
+ export { adHocProductsExtraction } from './src/workflows/webhook/adhoc-products-extraction';
1514
+ export { checkJobStatus } from './src/workflows/webhook/job-status-check';
1515
+ ```
1516
+
1517
+ ---
1518
+
1519
+ ## Important: Schema Verification
1520
+
1521
+ **Before using this template**, verify the GraphQL query structure against your actual Fluent Commerce schema:
1522
+
1523
+ ```bash
1524
+ # Introspect your schema
1525
+ cd fc-connect-sdk
1526
+ npx fc-connect introspect-schema --url https://your-fluent-api.com/graphql
1527
+
1528
+ # Check available fields on Product type
1529
+ npx fc-connect introspect-schema --url <url> --output schema.json
1530
+ # Then search for "Product" type in schema.json
1531
+ ```
1532
+
1533
+ The query structure shown above is an **example**. Adjust field names to match your actual schema.
1534
+
1535
+ ## Use Cases
1536
+
1537
+ **1. Amazon Seller Central Integration:**
1538
+
1539
+ - Daily product feed for listing updates
1540
+ - Include GTIN/UPC for catalog matching
1541
+ - Export pricing/inventory separately
1542
+
1543
+ **2. eBay Marketplace Sync:**
1544
+
1545
+ - Catalog updates for active listings
1546
+ - Include product descriptions and attributes
1547
+ - Handle variation products
1548
+
1549
+ **3. Internal Product Master:**
1550
+
1551
+ - Central product catalog for all systems
1552
+ - Include extended attributes
1553
+ - Version control for catalog changes
1554
+
1555
+ **4. Ad Hoc Refresh:**
1556
+
1557
+ - Manual catalog refresh for urgent updates
1558
+ - Integration testing with sample data
1559
+ - Troubleshooting specific product changes
1560
+
1561
+ ## Production Checklist
1562
+
1563
+ - [ ] **Verify GraphQL query against actual schema** (use introspection)
1564
+ - [ ] Set appropriate extraction frequency (daily for catalog)
1565
+ - [ ] Configure correct product status filter (ACTIVE only?)
1566
+ - [ ] Test with real product data changes
1567
+ - [ ] Verify S3 bucket permissions
1568
+ - [ ] Set up monitoring/alerts for failed extractions
1569
+ - [ ] Document JSON schema for downstream consumers
1570
+ - [ ] Configure S3 lifecycle policy for old files
1571
+ - [ ] Test incremental extraction (only changed products)
1572
+ - [ ] Test ad hoc extraction workflow
1573
+ - [ ] Test job status monitoring
1574
+ - [ ] Verify overlap buffer prevents missed records
1575
+
1576
+ ---
1577
+
1578
+ ### Pattern 7: State Management & Date Overrides
1579
+
1580
+ **Use Case**: Understand how state management works with scheduled and ad-hoc extractions.
1581
+
1582
+ **How it works**:
1583
+
1584
+ VersoriKV stores the last successful extraction timestamp to enable incremental sync:
1585
+
1586
+ ```typescript
1587
+ interface ExtractionState {
1588
+ timestamp: string; // Last run timestamp (WITHOUT overlap buffer)
1589
+ recordCount: number; // Number of records extracted
1590
+ extractedAt: string; // When extraction completed
1591
+ fileName?: string; // Generated filename
1592
+ s3Key?: string; // S3 upload path
1593
+ overlapBufferSeconds?: number; // Buffer configuration
1594
+ }
1595
+ ```
1596
+
1597
+ **State Priority Chain** (highest to lowest):
1598
+
1599
+ 1. **`fromDate` override** (manual date in webhook payload) - Highest priority
1600
+ 2. **Stored state** (`await kv.get(stateKey)`) - Normal incremental mode
1601
+ 3. **`fallbackStartDate`** (activation variable) - First run fallback
1602
+
1603
+ **Three Scenarios**:
1604
+
1605
+ #### Scenario 1: Normal Scheduled Runs (Incremental)
1606
+
1607
+ ```typescript
1608
+ // Payload: {} (empty - no overrides)
1609
+
1610
+ // Behavior:
1611
+ // 1. Load last timestamp from KV: "2025-01-22T10:00:00Z"
1612
+ // 2. Apply overlap buffer: "2025-01-22T09:59:00Z" (query WITH buffer)
1613
+ // 3. Extract records updated since buffered time
1614
+ // 4. Calculate MAX(updatedOn) from results: "2025-01-22T14:30:00Z"
1615
+ // 5. Save new timestamp WITHOUT buffer: "2025-01-22T14:30:00Z"
1616
+ // 6. Next run starts from "2025-01-22T14:29:00Z" (with buffer)
1617
+ ```
1618
+
1619
+ **Test**:
1620
+
1621
+ ```bash
1622
+ # Trigger scheduled run (no payload needed)
1623
+ # State advances automatically
1624
+ curl -X POST https://workspace.versori.run/products-extract-daily
1625
+ ```
1626
+
1627
+ #### Scenario 2: Ad-hoc Extraction WITH State Update
1628
+
1629
+ ```typescript
1630
+ // Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": true }
1631
+
1632
+ // Behavior:
1633
+ // 1. Ignore stored state
1634
+ // 2. Use fromDate: "2025-01-01T00:00:00Z" (no buffer applied to manual dates)
1635
+ // 3. Extract all records since 2025-01-01
1636
+ // 4. Calculate MAX(updatedOn): "2025-01-22T14:30:00Z"
1637
+ // 5. Save new timestamp: "2025-01-22T14:30:00Z" (updates state!)
1638
+ // 6. Next scheduled run starts from this new timestamp
1639
+ ```
1640
+
1641
+ **Use Case**: One-time catch-up extraction that advances the state pointer.
1642
+
1643
+ **Test**:
1644
+
1645
+ ```bash
1646
+ curl -X POST https://workspace.versori.run/products-extract-webhook \
1647
+ -H "x-api-key: your-api-key" \
1648
+ -H "Content-Type: application/json" \
1649
+ -d '{
1650
+ "fromDate": "2025-01-01T00:00:00Z",
1651
+ "updateState": true
1652
+ }'
1653
+ ```
1654
+
1655
+ #### Scenario 3: Ad-hoc Extraction WITHOUT State Update
1656
+
1657
+ ```typescript
1658
+ // Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": false }
1659
+
1660
+ // Behavior:
1661
+ // 1. Ignore stored state
1662
+ // 2. Use fromDate: "2025-01-01T00:00:00Z"
1663
+ // 3. Extract all records since 2025-01-01
1664
+ // 4. DO NOT update state
1665
+ // 5. Next scheduled run uses previous timestamp (unaffected)
1666
+ ```
1667
+
1668
+ **Use Case**: Historical backfill or testing without affecting incremental sync.
1669
+
1670
+ **Test**:
1671
+
1672
+ ```bash
1673
+ curl -X POST https://workspace.versori.run/products-extract-webhook \
1674
+ -H "x-api-key: your-api-key" \
1675
+ -H "Content-Type: application/json" \
1676
+ -d '{
1677
+ "fromDate": "2025-01-01T00:00:00Z",
1678
+ "toDate": "2025-01-31T23:59:59Z",
1679
+ "updateState": false
1680
+ }'
1681
+ ```
1682
+
1683
+ **Why this matters**:
1684
+
1685
+ - **Incremental sync** relies on state continuity
1686
+ - **Manual overrides** allow catch-up without breaking incremental flow
1687
+ - **Overlap buffer** prevents missed records at time boundaries
1688
+ - **State isolation** lets you test/backfill without affecting production sync
1689
+
1690
+ ---
1691
+
1692
+ ### Pattern 8: Optional GraphQL Query Logging
1693
+
1694
+ **Use Case**: Debug extraction issues by logging the exact GraphQL query sent to Fluent Commerce API.
1695
+
1696
+ **When to use**:
1697
+
1698
+ - ✅ Debugging pagination issues
1699
+ - ✅ Verifying query variables (dates, filters, limits)
1700
+ - ✅ Development and testing
1701
+ - ❌ Production (verbose logs, potential secrets in variables)
1702
+
1703
+ **How to enable**:
1704
+
1705
+ Set `DEBUG_GRAPHQL=true` environment variable in Versori activation settings.
1706
+
1707
+ **Implementation**:
1708
+
1709
+ ```typescript
1710
+ // In your extraction workflow
1711
+ const DEBUG_GRAPHQL = activation?.getVariable('DEBUG_GRAPHQL') === 'true';
1712
+
1713
+ if (DEBUG_GRAPHQL) {
1714
+ log.info('GraphQL Query Debug', {
1715
+ query: PRODUCTS_QUERY,
1716
+ variables: {
1717
+ catalogues,
1718
+ dateRangeFilter,
1719
+ first: pageSize,
1720
+ after: null, // First page
1721
+ },
1722
+ pagination: {
1723
+ pageSize,
1724
+ maxRecords,
1725
+ currentPage: 1,
1726
+ },
1727
+ });
1728
+ }
1729
+
1730
+ const extractionResult = await orchestrator.extract({
1731
+ query: PRODUCTS_QUERY,
1732
+ resultPath: 'products.edges.node',
1733
+ variables: {
1734
+ catalogues,
1735
+ dateRangeFilter,
1736
+ },
1737
+ pageSize,
1738
+ maxRecords,
1739
+ });
1740
+
1741
+ if (DEBUG_GRAPHQL) {
1742
+ log.info('GraphQL Response Debug', {
1743
+ totalRecords: extractionResult.stats.totalRecords,
1744
+ totalPages: extractionResult.stats.totalPages,
1745
+ truncated: extractionResult.stats.truncated,
1746
+ truncationReason: extractionResult.stats.truncationReason,
1747
+ validRecords: extractionResult.stats.validRecords ?? extractionResult.data.length,
1748
+ firstRecordId: extractionResult.data[0]?.id,
1749
+ lastRecordId: extractionResult.data[extractionResult.data.length - 1]?.id,
1750
+ });
1751
+ }
1752
+ ```
1753
+
1754
+ **What gets logged**:
1755
+
1756
+ ```json
1757
+ {
1758
+ "level": "info",
1759
+ "message": "GraphQL Query Debug",
1760
+ "query": "query GetProducts($catalogues: [ProductCatalogueKey], $dateRangeFilter: DateRange, ...)",
1761
+ "variables": {
1762
+ "catalogues": [{ "ref": "DEFAULT_CATALOGUE" }],
1763
+ "dateRangeFilter": "2025-01-22T09:59:00Z",
1764
+ "first": 200,
1765
+ "after": null
1766
+ },
1767
+ "pagination": {
1768
+ "pageSize": 200,
1769
+ "maxRecords": 50000,
1770
+ "currentPage": 1
1771
+ }
1772
+ }
1773
+ ```
1774
+
1775
+ **Versori Environment Variables**:
1776
+
1777
+ Add to activation settings:
1778
+
1779
+ ```json
1780
+ {
1781
+ "DEBUG_GRAPHQL": "true"
1782
+ }
1783
+ ```
1784
+
1785
+ **Testing**:
1786
+
1787
+ ```bash
1788
+ # Enable debug logging
1789
+ curl -X POST https://workspace.versori.run/products-extract-daily
1790
+
1791
+ # Check Versori logs for "GraphQL Query Debug" entries
1792
+ # Verify query structure and variables are correct
1793
+ ```
1794
+
1795
+ **Sample Debug Output**:
1796
+
1797
+ ```
1798
+ [INFO] GraphQL Query Debug
1799
+ query: "query GetProducts($catalogues: [ProductCatalogueKey], $dateRangeFilter: DateRange, ...)"
1800
+ variables: { catalogues: [{ ref: "DEFAULT_CATALOGUE" }], dateRangeFilter: "2025-01-22T09:59:00Z", first: 200, after: null }
1801
+ pagination: { pageSize: 200, maxRecords: 50000, currentPage: 1 }
1802
+
1803
+ [INFO] Extraction complete
1804
+ totalRecords: 1250
1805
+ totalPages: 7
1806
+ truncated: false
1807
+ validRecords: 1250
1808
+
1809
+ [INFO] GraphQL Response Debug
1810
+ totalRecords: 1250
1811
+ totalPages: 7
1812
+ truncated: false
1813
+ truncationReason: null
1814
+ validRecords: 1250
1815
+ firstRecordId: "product_abc"
1816
+ lastRecordId: "product_xyz"
1817
+ ```
1818
+
1819
+ **Key Benefits**:
1820
+
1821
+ - Quickly identify pagination configuration issues
1822
+ - Verify date filters are applied correctly
1823
+ - Debug "no records found" scenarios
1824
+ - Validate ExtractionOrchestrator variable injection
1825
+
1826
+ **Production Best Practice**: Disable `DEBUG_GRAPHQL` in production to reduce log volume and avoid logging sensitive data.
1827
+
1828
+ ---
1829
+
1830
+ ## Troubleshooting Guide
1831
+
1832
+ **Issue**: "Extraction timeout after 10 minutes"
1833
+
1834
+ - **Cause**: Too many records
1835
+ - **Fix**: Reduce maxRecords, increase frequency
1836
+
1837
+ **Issue**: "Mapping errors for 50% of records"
1838
+
1839
+ - **Cause**: Schema mismatch
1840
+ - **Fix**: Run schema validation, check field names
1841
+
1842
+ **Issue**: "State not updating"
1843
+
1844
+ - **Cause**: KV write failure or intentional retry
1845
+ - **Fix**: Check KV logs, verify state update code
1846
+
1847
+ **Issue**: "First run exceeds limits"
1848
+
1849
+ - **Cause**: No previous timestamp, fetches all
1850
+ - **Fix**: Set fallbackStartDate close to current, apply filters
1851
+
1852
+ **Issue**: "Excessive duplicates"
1853
+
1854
+ - **Cause**: Overlap buffer (expected) or timestamp not saved
1855
+ - **Fix**: Verify newTimestamp saved WITHOUT buffer
1856
+
1857
+ **Issue**: "Job status not found"
1858
+
1859
+ - **Cause**: JobTracker not initialized or wrong jobId
1860
+ - **Fix**: Verify JobTracker initialization and jobId format
1861
+
1862
+ ---
1863
+
1864
+ **Pattern**: Enterprise incremental extraction with overlap buffer for product catalog - Three-workflow approach
1865
+ **⚠️ Versori Sample**: Reference implementation - adapt for your production use case
1866
+ **Key Learning**: ALWAYS verify GraphQL schema before using queries
1867
+ **Critical**: Apply 60-second overlap buffer to prevent missed records due to clock skew
1868
+ **Buffer Pattern**: Query WITH buffer (`updatedOn >= lastRunTime - 60s`), save WITHOUT buffer (`MAX(updatedOn)`)
1869
+ **Schema**: Use schema introspection - don't guess field names
1870
+ **Three Workflows**: Scheduled incremental, Ad hoc HTTP trigger, Job status monitoring
1871
+
1872
+ ---
1873
+
1874
+ ## See Also
1875
+
1876
+ **Validation & Best Practices:**
1877
+
1878
+ - [CLI Validation Workflow](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md) - Validate GraphQL queries and mappings before deployment
1879
+ - [Extraction Modes Guide](../extraction-modes-guide.md) - Comparison of incremental vs dateRange vs historical modes
1880
+
1881
+ **SDK Documentation:**
1882
+
1883
+ - [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Complete field mapping documentation
1884
+ - [SDK CLI Tools](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Command-line tools reference
1885
+
1886
+ **Related Extraction Patterns:**
1887
+
1888
+ - [Products to SFTP XML](./template-extraction-products-to-sftp-xml.md) - Same entity, different format/destination
1889
+ - [Virtual Positions to S3 JSON](./template-extraction-virtual-positions-to-s3-json.md) - Different entity, JSON format
1890
+
1891
+ ---
1892
+
1893
+ ### Pattern 9: Backward Pagination (Optional - Advanced)
1894
+
1895
+ **Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
1896
+
1897
+ **When to Use**:
1898
+
1899
+ - ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
1900
+ - ✅ Time-bounded reverse traversal for auditing
1901
+ - ✅ Display newest-first in UI/reports
1902
+ - ❌ **Don't use for standard incremental sync** - use forward pagination (default)
1903
+
1904
+ **GraphQL Query Requirements**:
1905
+
1906
+ Your query must support backward pagination by including `$last` and `$before`:
1907
+
1908
+ ```graphql
1909
+ query GetData(
1910
+ $retailerId: ID!
1911
+ $first: Int # For forward pagination
1912
+ $after: String # For forward pagination
1913
+ $last: Int # For backward pagination
1914
+ $before: String # For backward pagination
1915
+ ) {
1916
+ data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
1917
+ edges {
1918
+ cursor # ✅ REQUIRED
1919
+ node {
1920
+ id
1921
+ createdAt
1922
+ # ... other fields
1923
+ }
1924
+ }
1925
+ pageInfo {
1926
+ hasNextPage # For forward
1927
+ hasPreviousPage # ✅ REQUIRED for backward
1928
+ }
1929
+ }
1930
+ }
1931
+ ```
1932
+
1933
+ **Implementation**:
1934
+
1935
+ ```typescript
1936
+ // Backward pagination - newest records first
1937
+ const result = await orchestrator.extract({
1938
+ query: YOUR_QUERY,
1939
+ resultPath: 'data.edges.node',
1940
+ variables: {
1941
+ retailerId,
1942
+ dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
1943
+ // ❌ Don't include last/before - orchestrator injects them
1944
+ },
1945
+ pageSize: 200,
1946
+ direction: 'backward', // ✅ Enable reverse pagination
1947
+ maxRecords: 10000,
1948
+ });
1949
+
1950
+ // Records are returned in reverse chronological order
1951
+ log.info('Extraction order verification', {
1952
+ newest: result.data[0].createdAt,
1953
+ oldest: result.data[result.data.length - 1].createdAt
1954
+ });
1955
+ ```
1956
+
1957
+ **Key Differences from Forward Pagination**:
1958
+
1959
+ | Aspect | Forward (Default) | Backward |
1960
+ | ---------------------- | -------------------------------- | ----------------------- |
1961
+ | **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
1962
+ | **Variables Injected** | `first`, `after` | `last`, `before` |
1963
+ | **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
1964
+ | **Cursor Source** | Last edge of page | First edge of page |
1965
+ | **Record Order** | Oldest → Newest | Newest → Oldest |
1966
+
1967
+ **Important Notes**:
1968
+
1969
+ 1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
1970
+
1971
+ 2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
1972
+
1973
+ 3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
1974
+
1975
+ 4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
1976
+
1977
+ **Example: Extract Latest 1000 Orders**
1978
+
1979
+ ```typescript
1980
+ const latestOrders = await orchestrator.extract({
1981
+ query: ORDERS_QUERY,
1982
+ resultPath: 'orders.edges.node',
1983
+ variables: {
1984
+ retailerId,
1985
+ statuses: ['BOOKED', 'ALLOCATED'],
1986
+ },
1987
+ direction: 'backward', // Start from newest
1988
+ maxRecords: 1000, // Stop after 1000 records
1989
+ pageSize: 100, // 100 per page = 10 pages
1990
+ });
1991
+
1992
+ // latestOrders.data[0] is the newest order
1993
+ // latestOrders.data[999] is the 1000th newest order
1994
+ ```
1995
+
1996
+ **When to Use Forward vs Backward**:
1997
+
1998
+ ```typescript
1999
+ // ✅ Forward (default) - For incremental sync
2000
+ const incrementalData = await orchestrator.extract({
2001
+ query: YOUR_QUERY,
2002
+ resultPath: 'data.edges.node',
2003
+ variables: {
2004
+ dateRangeFilter: { from: lastSyncTime, to: now },
2005
+ },
2006
+ // direction defaults to 'forward'
2007
+ // Processes oldest → newest for proper sequencing
2008
+ });
2009
+
2010
+ // ✅ Backward - For "latest N records" use cases
2011
+ const latestData = await orchestrator.extract({
2012
+ query: YOUR_QUERY,
2013
+ resultPath: 'data.edges.node',
2014
+ direction: 'backward',
2015
+ maxRecords: 100, // Just get latest 100
2016
+ // Gets newest → oldest
2017
+ });
2018
+ ```
2019
+
2020
+ **Pagination Variables Reference**:
2021
+
2022
+ | Variable | Forward | Backward | Injected By | Notes |
2023
+ | -------- | ----------- | ----------- | ------------ | ------------------------ |
2024
+ | `first` | ✅ Used | ❌ Not used | Orchestrator | From `pageSize` |
2025
+ | `after` | ✅ Used | ❌ Not used | Orchestrator | From cursor (last edge) |
2026
+ | `last` | ❌ Not used | ✅ Used | Orchestrator | From `pageSize` |
2027
+ | `before` | ❌ Not used | ✅ Used | Orchestrator | From cursor (first edge) |
2028
+
2029
+ **Common Mistakes to Avoid**:
2030
+
2031
+ ```typescript
2032
+ // ❌ WRONG - Don't pass pagination variables
2033
+ const result = await orchestrator.extract({
2034
+ variables: {
2035
+ last: 200, // ❌ Orchestrator will override this
2036
+ before: cursor, // ❌ Orchestrator manages cursor
2037
+ },
2038
+ direction: 'backward',
2039
+ });
2040
+
2041
+ // ✅ CORRECT - Let orchestrator inject pagination
2042
+ const result = await orchestrator.extract({
2043
+ variables: {
2044
+ retailerId, // ✅ Your business variables only
2045
+ },
2046
+ pageSize: 200, // ✅ Orchestrator uses this for last/before
2047
+ direction: 'backward',
2048
+ });
2049
+ ```
2050
+
2051
+ #### Optional: Reverse Pagination
2052
+
2053
+ - Default: forward ($first/$after) + pageInfo.hasNextPage.
2054
+ - Reverse: query must use $last/$before; response must include pageInfo.hasPreviousPage; set direction='backward'.
2055
+
2056
+ GraphQL:
2057
+
2058
+ ```graphql
2059
+ query GetProductsBackward(
2060
+ $catalogues: [CatalogueKey]
2061
+ $dateRangeFilter: DateRange
2062
+ $last: Int!
2063
+ $before: String
2064
+ ) {
2065
+ products(catalogues: $catalogues, updatedOn: $dateRangeFilter, last: $last, before: $before) {
2066
+ edges {
2067
+ cursor
2068
+ node {
2069
+ id
2070
+ ref
2071
+ updatedOn
2072
+ }
2073
+ }
2074
+ pageInfo {
2075
+ hasPreviousPage
2076
+ }
2077
+ }
2078
+ }
2079
+ ```
2080
+
2081
+ SDK:
2082
+
2083
+ ```typescript
2084
+ await orchestrator.extract({
2085
+ query: PRODUCTS_BACKWARD_QUERY,
2086
+ resultPath: 'products.edges.node',
2087
+ variables: { catalogues, dateRangeFilter },
2088
+ pageSize,
2089
+ direction: 'backward',
2090
+ });
2091
+ ```
2092
+
2093
+ ---
2094
+
2095
+ ## Testing Checklist
2096
+
2097
+ **Before production deployment:**
2098
+
2099
+ ### 1. Schema Validation
2100
+
2101
+ - [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
2102
+ - [ ] Run `npx fc-connect validate-schema --mapping ./config/products.export.json --schema ./fluent-schema.json`
2103
+ - [ ] Run `npx fc-connect analyze-coverage --mapping ./config/products.export.json --schema ./fluent-schema.json`
2104
+ - [ ] Verify all `source` paths in mapping exist in GraphQL schema
2105
+ - [ ] Verify query structure matches schema (fields, types, filters)
2106
+
2107
+ ### 2. Extraction Testing
2108
+
2109
+ - [ ] Test with small dataset first (maxRecords=10)
2110
+ - [ ] Verify ExtractionOrchestrator pagination works correctly
2111
+ - [ ] Test with multiple pages of data (verify cursor handling)
2112
+ - [ ] Verify date range filtering (updatedOn filter)
2113
+ - [ ] Test empty result handling (no records in date range)
2114
+ - [ ] Verify extraction stops at maxRecords limit
2115
+
2116
+ ### 3. Mapping Testing
2117
+
2118
+ - [ ] Verify required fields are populated
2119
+ - [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
2120
+ - [ ] Test custom resolvers with edge cases (if any)
2121
+ - [ ] Verify nested field extraction
2122
+ - [ ] Test with null/missing fields
2123
+ - [ ] Verify mapping error collection works
2124
+
2125
+ ### 4. JSON Generation Testing
2126
+
2127
+ - [ ] Verify JSON structure matches expected format
2128
+ - [ ] Test JSON validation against schema (if applicable)
2129
+ - [ ] Verify proper nesting and structure
2130
+ - [ ] Test with large datasets (>1000 records)
2131
+ - [ ] Verify UTF-8 encoding
2132
+ - [ ] Test special character escaping
2133
+
2134
+ ### 5. S3 Upload Testing
2135
+
2136
+ - [ ] Test S3 connection and authentication
2137
+ - [ ] Verify file upload to correct bucket and path
2138
+ - [ ] Test file naming convention (timestamp format)
2139
+ - [ ] Verify S3 object metadata
2140
+ - [ ] Test upload retry logic (simulate network failure)
2141
+ - [ ] Verify file permissions and ACLs
2142
+
2143
+ ### 6. State Management Testing
2144
+
2145
+ - [ ] Verify overlap buffer prevents missed records (60-second default)
2146
+ - [ ] Test state recovery after extraction failure
2147
+ - [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
2148
+ - [ ] Test first run with no previous state (uses fallbackStartDate)
2149
+ - [ ] Verify state update only happens on successful upload
2150
+ - [ ] Test manual date override (doesn't update state)
2151
+
2152
+ ### 7. Job Tracking Testing
2153
+
2154
+ - [ ] Test job creation with JobTracker
2155
+ - [ ] Verify job status updates at each stage
2156
+ - [ ] Test job completion with metadata
2157
+ - [ ] Test job failure handling
2158
+ - [ ] Query job status via webhook endpoint
2159
+ - [ ] Verify job status persists in KV store
2160
+
2161
+ ### 8. Error Handling Testing
2162
+
2163
+ - [ ] Test with invalid GraphQL query
2164
+ - [ ] Test with mapping errors (invalid field paths)
2165
+ - [ ] Test with S3 connection failures
2166
+ - [ ] Test with authentication failures
2167
+ - [ ] Test with network timeouts
2168
+ - [ ] Verify error logging includes context (jobId, stage, error details)
2169
+ - [ ] Test error threshold logic (if applicable)
2170
+
2171
+ ### 9. Staging Environment Testing
2172
+
2173
+ - [ ] Run full extraction in staging environment
2174
+ - [ ] Verify JSON file format with downstream system
2175
+ - [ ] Monitor extraction duration and resource usage
2176
+ - [ ] Test with production-like data volumes
2177
+ - [ ] Verify no performance degradation over time
2178
+
2179
+ ### 10. Integration Testing
2180
+
2181
+ - [ ] Test scheduled workflow (cron trigger)
2182
+ - [ ] Test ad hoc webhook trigger
2183
+ - [ ] Test job status query webhook
2184
+ - [ ] Verify activation variables are read correctly
2185
+ - [ ] Test with different extraction modes (incremental, date range)
2186
+ - [ ] End-to-end test: trigger → extract → transform → upload → verify file
2187
+
2188
+ ---
2189
+ ## Monitoring & Alerting
2190
+
2191
+ ### Success Response Example
2192
+
2193
+ ```json
2194
+ {
2195
+ "success": true,
2196
+ "jobId": "SCHEDULED_PRD_20251102_140000_abc123",
2197
+ "recordsExtracted": 1523,
2198
+ "fileName": "products-2025-11-02T14-00-00-000Z.json",
2199
+ "s3Path": "s3://bucket/products/products-2025-11-02T14-00-00-000Z.json",
2200
+ "metrics": {
2201
+ "extractionDurationMs": 12543,
2202
+ "totalPages": 8,
2203
+ "pageSize": 200,
2204
+ "mappingErrors": 0,
2205
+ "fileSizeBytes": 524288,
2206
+ "uploadDurationMs": 1234
2207
+ },
2208
+ "timestamps": {
2209
+ "extractionStart": "2025-11-02T14:00:00.000Z",
2210
+ "extractionEnd": "2025-11-02T14:00:12.543Z",
2211
+ "uploadComplete": "2025-11-02T14:00:13.777Z"
2212
+ },
2213
+ "state": {
2214
+ "previousTimestamp": "2025-11-02T13:00:00.000Z",
2215
+ "newTimestamp": "2025-11-02T13:59:58.123Z",
2216
+ "stateUpdated": true,
2217
+ "overlapBufferSeconds": 60
2218
+ }
2219
+ }
2220
+ ```
2221
+
2222
+ ### Error Response Example
2223
+
2224
+ ```json
2225
+ {
2226
+ "success": false,
2227
+ "jobId": "ADHOC_PRD_20251102_140500_xyz789",
2228
+ "error": "S3 upload failed: Connection timeout",
2229
+ "errorCategory": "NETWORK",
2230
+ "recordsExtracted": 0,
2231
+ "stage": "s3_upload",
2232
+ "details": {
2233
+ "message": "Failed to upload file after 3 retry attempts",
2234
+ "retryAttempts": 3,
2235
+ "lastError": "ETIMEDOUT: Connection timed out after 30000ms"
2236
+ },
2237
+ "state": {
2238
+ "stateUpdated": false,
2239
+ "willRetryNextRun": true,
2240
+ "note": "State not advanced - next extraction will retry same time window"
2241
+ }
2242
+ }
2243
+ ```
2244
+
2245
+ ### Key Metrics to Track
2246
+
2247
+ ```typescript
2248
+ const METRICS = {
2249
+ // Extraction Performance
2250
+ extractionDurationMs: Date.now() - extractionStart,
2251
+ recordCount: records.length,
2252
+ pageCount: extractionResult.stats.totalPages,
2253
+ avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
2254
+
2255
+ // Transformation Performance
2256
+ transformedCount: transformedRecords.length,
2257
+ failedCount: mappingErrors.length,
2258
+ errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
2259
+
2260
+ // File Generation
2261
+ fileSizeMB: (jsonContent.length / (1024 * 1024)).toFixed(2),
2262
+
2263
+ // Upload Performance
2264
+ uploadDurationMs: uploadEnd - uploadStart,
2265
+ uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
2266
+
2267
+ // State Management
2268
+ timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
2269
+ recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
2270
+ };
2271
+
2272
+ log.info('Extraction metrics', metrics);
2273
+ ```
2274
+
2275
+ ### Alert Thresholds
2276
+
2277
+ ```typescript
2278
+ const ALERT_THRESHOLDS = {
2279
+ // Duration Alerts
2280
+ EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
2281
+ UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
2282
+ TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
2283
+
2284
+ // Error Rate Alerts
2285
+ MAX_ERROR_RATE: 0.05, // 5% mapping errors
2286
+ MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
2287
+
2288
+ // Volume Alerts
2289
+ MAX_RECORDS_PER_RUN: 100000,
2290
+ MIN_RECORDS_WARNING: 0, // Alert if no records found
2291
+ MAX_FILE_SIZE_MB: 150, // 150MB
2292
+
2293
+ // State Alerts
2294
+ MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
2295
+ MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
2296
+ };
2297
+
2298
+ // Check thresholds
2299
+ if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
2300
+ log.warn('Extraction duration exceeded threshold', {
2301
+ duration: metrics.extractionDurationMs,
2302
+ threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
2303
+ recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
2304
+ });
2305
+ }
2306
+ ```
2307
+
2308
+ ### Monitoring Dashboard Queries
2309
+
2310
+ **Versori Platform Logs Query:**
2311
+
2312
+ ```
2313
+ # Successful extractions
2314
+ log_level:info AND message:"Extraction complete" AND jobId:*
2315
+
2316
+ # Failed extractions
2317
+ log_level:error AND message:"Extraction workflow failed" AND jobId:*
2318
+
2319
+ # Performance issues
2320
+ extractionDurationMs:>300000 OR uploadDurationMs:>120000
2321
+
2322
+ # High error rates
2323
+ errorRate:>5
2324
+
2325
+ # State management issues
2326
+ stateUpdated:false AND success:true
2327
+ ```
2328
+
2329
+ ### Common Issues and Solutions
2330
+
2331
+ **Issue**: "Extraction timeout after 10 minutes"
2332
+
2333
+ - **Cause**: Too many records in single extraction
2334
+ - **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
2335
+ - **Prevention**: Monitor recordCount trends, set appropriate maxRecords
2336
+
2337
+ **Issue**: "Mapping errors for 50% of records"
2338
+
2339
+ - **Cause**: Schema mismatch between GraphQL response and mapping config
2340
+ - **Fix**: Run schema validation, update mapping config paths
2341
+ - **Prevention**: Use `npx fc-connect validate-schema` before deployment
2342
+
2343
+ **Issue**: "S3 connection timeout"
2344
+
2345
+ - **Cause**: Network issues, firewall, or connection pool exhaustion
2346
+ - **Fix**: Check S3 credentials, verify network connectivity
2347
+ - **Prevention**: Implement connection health checks, monitor connection status
2348
+
2349
+ **Issue**: "State not updating after successful extraction"
2350
+
2351
+ - **Cause**: KV write failure or intentional retry logic
2352
+ - **Fix**: Check KV logs, verify state update code executed
2353
+ - **Prevention**: Add KV write verification, log state updates explicitly
2354
+
2355
+ **Issue**: "First run exceeds record limits"
2356
+
2357
+ - **Cause**: No previous timestamp, fetches all historical records
2358
+ - **Fix**: Set fallbackStartDate close to current date, apply additional filters
2359
+ - **Prevention**: Use appropriate fallbackStartDate for initial runs
2360
+
2361
+ **Issue**: "Excessive duplicate records in output"
2362
+
2363
+ - **Cause**: Overlap buffer (expected) or timestamp not saved correctly
2364
+ - **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
2365
+ - **Prevention**: Monitor duplicate rates, verify state update logic
2366
+
2367
+ ---
2368
+
2369
+ ## Troubleshooting Quick Reference
2370
+
2371
+ | Error Message | Likely Cause | Solution |
2372
+ |--------------|--------------|----------|
2373
+ | "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
2374
+ | "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
2375
+ | "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
2376
+ | "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
2377
+ | "S3 authentication failed" | Invalid credentials | Verify S3 credentials in activation variables |
2378
+ | "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
2379
+ | "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
2380
+ | "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
2381
+ | "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
2382
+ | "JSON generation failed" | Format-specific error | Check JSON generation logic, validate output |
2383
+
2384
+ ---