@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55

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