@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.56

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