@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.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
|
@@ -1,2062 +1,2062 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-extract-fulfillments-to-sftp-csv
|
|
3
|
-
canonical_filename: template-extraction-fulfillments-to-sftp-csv.md
|
|
4
|
-
version: 2.0.0
|
|
5
|
-
sdk_version: ^0.1.39
|
|
6
|
-
runtime: versori
|
|
7
|
-
direction: extraction
|
|
8
|
-
source: fluent-graphql
|
|
9
|
-
destination: sftp-csv
|
|
10
|
-
entity: fulfillments
|
|
11
|
-
format: csv
|
|
12
|
-
logging: versori
|
|
13
|
-
status: stable
|
|
14
|
-
features:
|
|
15
|
-
- memory-management
|
|
16
|
-
- enhanced-logging
|
|
17
|
-
- pagination-progress
|
|
18
|
-
- dispose-finally
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
# Template: Extraction - Fulfillments to SFTP CSV
|
|
22
|
-
|
|
23
|
-
**Template Version:** 2.0.0
|
|
24
|
-
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
25
|
-
**Last Updated:** 2025-01-24
|
|
26
|
-
**Deployment Target:** Versori Platform
|
|
27
|
-
|
|
28
|
-
**🆕 Version 2.0.0 Enhancements:**
|
|
29
|
-
- ✅ **Memory Management** - Clear large result sets after processing batches
|
|
30
|
-
- ✅ **Enhanced Logging** - Pagination progress tracking with emoji indicators (📊, 📥, ✅)
|
|
31
|
-
- ✅ **Pagination Progress** - Real-time page-by-page progress logging with metrics
|
|
32
|
-
- ✅ **Resource Cleanup** - SFTP dispose in finally blocks prevents connection leaks
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## 📚 STEP 1: Load These Docs (Human Checklist)
|
|
37
|
-
|
|
38
|
-
1. REQUIRED (load all)
|
|
39
|
-
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
40
|
-
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
41
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
42
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
43
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
44
|
-
- [ ] fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
45
|
-
|
|
46
|
-
Copy-paste list (open these):
|
|
47
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
48
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
49
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
50
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
51
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
52
|
-
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## 📋 STEP 2: Tell Your AI (Prompt)
|
|
57
|
-
|
|
58
|
-
Copy/paste this prompt into your AI tool after loading the documentation above:
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
I need a Versori scheduled extractor that:
|
|
62
|
-
|
|
63
|
-
1) Queries Fluent Commerce GraphQL for Fulfillments with auto-pagination
|
|
64
|
-
2) Supports incremental runs via KV state (with an overlap buffer)
|
|
65
|
-
3) Transforms results using UniversalMapper per mapping JSON
|
|
66
|
-
4) Generates CSV and uploads to SFTP
|
|
67
|
-
5) Tracks progress with JobTracker and exposes a job-status webhook
|
|
68
|
-
6) Uses native Versori log (LoggingService removed - use native log)
|
|
69
|
-
|
|
70
|
-
Use the loaded docs to fill in SDK specifics and best practices.
|
|
71
|
-
Keep the structure identical to the template; only adapt where needed.
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## 📋 Template Overview
|
|
77
|
-
|
|
78
|
-
This connector runs on the Versori platform. Most operational settings (Fluent account/connection, SFTP credentials, schedule, page size/limits) are configured via activation variables. Data shape and logic (mapping JSON, CSV structure, GraphQL selection set/filters, validators/resolvers) are adjusted in code as needed. It extracts fulfillments from Fluent Commerce via GraphQL, transforms the data into CSV, and uploads the result to SFTP.
|
|
79
|
-
|
|
80
|
-
### What This Template Does
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
┌─────────────────────────────────────────────────────────────────┐
|
|
84
|
-
│ EXTRACTION WORKFLOW │
|
|
85
|
-
└─────────────────────────────────────────────────────────────────┘
|
|
86
|
-
|
|
87
|
-
1. TRIGGER
|
|
88
|
-
├─ Scheduled (Cron): Runs automatically every 4 hours
|
|
89
|
-
├─ Ad hoc (Webhook): Manual trigger with optional date override
|
|
90
|
-
└─ Status Query (Webhook): Check job progress
|
|
91
|
-
|
|
92
|
-
2. EXTRACT (ExtractionOrchestrator)
|
|
93
|
-
├─ Query Fluent GraphQL API for fulfillments
|
|
94
|
-
├─ Auto-pagination (handles large datasets)
|
|
95
|
-
├─ Apply date filters (incremental or manual range)
|
|
96
|
-
└─ Validate each record (optional)
|
|
97
|
-
|
|
98
|
-
3. TRANSFORM (UniversalMapper)
|
|
99
|
-
├─ Map GraphQL fields to CSV schema
|
|
100
|
-
├─ Apply SDK resolvers (trim, uppercase, formatDateShort, etc.)
|
|
101
|
-
├─ Extract nested data (order.customer.email)
|
|
102
|
-
└─ Handle transformation errors
|
|
103
|
-
|
|
104
|
-
4. GENERATE CSV (CSVParserService)
|
|
105
|
-
├─ Convert transformed records to CSV
|
|
106
|
-
├─ Include headers
|
|
107
|
-
├─ Handle special characters
|
|
108
|
-
└─ Generate timestamped filename
|
|
109
|
-
|
|
110
|
-
5. UPLOAD (SftpDataSource)
|
|
111
|
-
├─ Connect to SFTP server
|
|
112
|
-
├─ Upload CSV file with retry logic
|
|
113
|
-
├─ Verify upload success
|
|
114
|
-
└─ CRITICAL: Call dispose() in finally block
|
|
115
|
-
|
|
116
|
-
6. TRACK JOB (JobTracker)
|
|
117
|
-
├─ Create job with unique ID
|
|
118
|
-
├─ Update status at each step
|
|
119
|
-
├─ Store job result in KV
|
|
120
|
-
└─ Enable status queries via webhook
|
|
121
|
-
|
|
122
|
-
7. UPDATE STATE (VersoriKVAdapter)
|
|
123
|
-
├─ Calculate max updatedOn from records
|
|
124
|
-
├─ Store timestamp for next incremental run
|
|
125
|
-
├─ Apply overlap buffer (prevent missed records)
|
|
126
|
-
└─ Skip update if manual date override
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Key Features
|
|
130
|
-
|
|
131
|
-
- Job tracking with status queries
|
|
132
|
-
- Execution modes: scheduled, ad hoc, status query
|
|
133
|
-
- Uses ExtractionOrchestrator, UniversalMapper, JobTracker, CSVParserService
|
|
134
|
-
- Error handling, retry logic
|
|
135
|
-
- Reusable services suitable for similar use cases
|
|
136
|
-
|
|
137
|
-
Note: JobTracker persists stage/status to Versori KV for visibility, job-status webhooks, and auditing. Recommended for production multi-step flows; can be skipped for trivial single-step utilities.
|
|
138
|
-
|
|
139
|
-
### 📦 Package Information
|
|
140
|
-
|
|
141
|
-
**SDK:** [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
npm install @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
---
|
|
148
|
-
|
|
149
|
-
**Templates are designed for direct deployment; customize via activation variables.**
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## 📦 SDK Imports (Verified - Versori Optimized)
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
import { Buffer } from 'node:buffer';
|
|
157
|
-
import {
|
|
158
|
-
createClient,
|
|
159
|
-
ExtractionOrchestrator,
|
|
160
|
-
JobTracker,
|
|
161
|
-
UniversalMapper,
|
|
162
|
-
CSVParserService,
|
|
163
|
-
SftpDataSource,
|
|
164
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
165
|
-
|
|
166
|
-
import { schedule, webhook, http, fn } from '@versori/run';
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
**Note:** All imports are from actual SDK exports - this code compiles and runs as-is.
|
|
170
|
-
|
|
171
|
-
**? VERSORI PLATFORM - Use Native Logs:**
|
|
172
|
-
|
|
173
|
-
- Use `log` from context: `const { log } = ctx;`
|
|
174
|
-
- Don't import or use LoggingService for Versori connectors
|
|
175
|
-
- Native Versori logs are simpler and automatically integrated with platform monitoring
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## ⚙️ Configuration
|
|
180
|
-
|
|
181
|
-
### SFTP Connection Setup (Recommended)
|
|
182
|
-
|
|
183
|
-
**? BEST PRACTICE:** Store SFTP credentials in a Versori connection object with Basic Auth:
|
|
184
|
-
|
|
185
|
-
**Connection Configuration:**
|
|
186
|
-
|
|
187
|
-
1. In Versori platform, create a connection named `versori_ftp_server`
|
|
188
|
-
2. Set **Authentication Type**: `Basic Auth`
|
|
189
|
-
3. Enter **Username**: Your SFTP username
|
|
190
|
-
4. Enter **Password**: Your SFTP password
|
|
191
|
-
5. The SDK will automatically decode the credentials using:
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
// Get SFTP credentials from Versori connection (Basic Auth)
|
|
195
|
-
// RECOMMENDED: Use activation.connections (already decoded)
|
|
196
|
-
const allConnections = ctx.activation.connections || [];
|
|
197
|
-
const sftpConn = allConnections.find(c => c.name === 'versori_ftp_server');
|
|
198
|
-
|
|
199
|
-
if (!sftpConn) {
|
|
200
|
-
throw new Error('SFTP connection "versori_ftp_server" not found');
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const credential = sftpConn.credentials[0]?.credential;
|
|
204
|
-
if (!credential?.data?.basicAuth) {
|
|
205
|
-
throw new Error('SFTP connection not configured with Basic Authentication');
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const { username, password } = credential.data.basicAuth;
|
|
209
|
-
// ? Already decoded - no Buffer.from() needed!
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
**Why use connections instead of activation variables?**
|
|
213
|
-
|
|
214
|
-
- ✅ Credentials stored securely in Versori vault
|
|
215
|
-
- ✅ Connection can be reused across workflows
|
|
216
|
-
- ✅ No need to manage sensitive data in activation variables
|
|
217
|
-
- ✅ Easier credential rotation
|
|
218
|
-
- ✅ Better separation of concerns (config vs secrets)
|
|
219
|
-
- ✅ Follows industry security best practices
|
|
220
|
-
|
|
221
|
-
### Activation Variables
|
|
222
|
-
|
|
223
|
-
**Configuration is driven by activation variables - modify these instead of code:**
|
|
224
|
-
|
|
225
|
-
```json
|
|
226
|
-
{
|
|
227
|
-
"retailerId": "your-retailer-id",
|
|
228
|
-
"sftpHost": "sftp.partner.com",
|
|
229
|
-
"sftpPort": 22,
|
|
230
|
-
"sftpPrivateKey": "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----",
|
|
231
|
-
"sftpRemotePath": "/incoming/fulfillments/",
|
|
232
|
-
"fileNamePrefix": "fulfillments",
|
|
233
|
-
"pageSize": 200,
|
|
234
|
-
"maxRecords": 10000,
|
|
235
|
-
"overlapBufferSeconds": 60,
|
|
236
|
-
"fulfillmentStatuses": "SHIPPED,DELIVERED",
|
|
237
|
-
"validateConnectionOnStart": "false"
|
|
238
|
-
}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
> **Note:** `sftpUsername` and `sftpPassword` are fetched from the `versori_ftp_server` connection (see above).
|
|
242
|
-
|
|
243
|
-
Note: Webhook security is enforced via Versori connections. Configure auth on the connection and reference it in `webhook({ connection: '...' })`.
|
|
244
|
-
|
|
245
|
-
### Variable Explanations
|
|
246
|
-
|
|
247
|
-
| Variable | Purpose | Default | Customization Hints |
|
|
248
|
-
| ---------------------------- | --------------------------- | ------------------------- | -------------------------------- |
|
|
249
|
-
| `retailerId` | Fluent retailer ID | - | Required - your retailer ID |
|
|
250
|
-
| **SFTP Credentials** | _From Connection_ | | _See connection setup above_ |
|
|
251
|
-
| `sftpHost` | SFTP server hostname | - | Required - partner SFTP server |
|
|
252
|
-
| `sftpPort` | SFTP server port | `22` | Standard SFTP port |
|
|
253
|
-
| `sftpPrivateKey` | SFTP private key (optional) | - | Alternative to password auth |
|
|
254
|
-
| `sftpRemotePath` | SFTP upload directory | `/incoming/fulfillments/` | Customize folder structure |
|
|
255
|
-
| `fileNamePrefix` | CSV filename prefix | `fulfillments` | Customize naming convention |
|
|
256
|
-
| `pageSize` | Records per GraphQL page | `200` | Increase for fewer API calls |
|
|
257
|
-
| `maxRecords` | Total extraction limit | `50000` | Safety limit - adjust for volume |
|
|
258
|
-
| `overlapBufferSeconds` | Incremental safety window | `60` | Prevents missed records |
|
|
259
|
-
| `fulfillmentStatuses` | Status filter (comma-sep) | `SHIPPED,DELIVERED` | Filter by fulfillment status |
|
|
260
|
-
| `validateConnectionOnStart` | Validate auth on startup | `false` | `true` = fail-fast, `false` = fast startup |
|
|
261
|
-
|
|
262
|
-
---
|
|
263
|
-
|
|
264
|
-
### 📋 State Management & Incremental Sync
|
|
265
|
-
|
|
266
|
-
**How incremental sync works:**
|
|
267
|
-
|
|
268
|
-
1. **First Run:** Uses `DEFAULT_FALLBACK` date (2024-01-01T00:00:00Z)
|
|
269
|
-
2. **Subsequent Runs:** Uses `lastFulfillmentSync` timestamp from KV store
|
|
270
|
-
3. **Overlap Buffer:** Subtracts 60 seconds to catch late-arriving records
|
|
271
|
-
4. **State Update:** After successful upload, stores max `updatedOn` for next run
|
|
272
|
-
|
|
273
|
-
**Incremental vs Manual Modes:**
|
|
274
|
-
|
|
275
|
-
| Mode | When to Use | State Update | Payload Example |
|
|
276
|
-
| --------------------------- | -------------------- | ------------ | -------------------------------------------------------------- |
|
|
277
|
-
| **Incremental** | Daily scheduled sync | ? Yes | `{}` (empty - uses last sync) |
|
|
278
|
-
| **Manual Range** | Historical backfill | ❌ No | `{ "fromDate": "2024-01-01T00:00:00Z", "updateState": false }` |
|
|
279
|
-
| **Manual Range with State** | One-time catch-up | ? Yes | `{ "fromDate": "2024-01-01T00:00:00Z", "updateState": true }` |
|
|
280
|
-
|
|
281
|
-
**Why overlap buffer?**
|
|
282
|
-
|
|
283
|
-
Records updated near the sync time might not appear in the query due to:
|
|
284
|
-
|
|
285
|
-
- Clock drift between systems
|
|
286
|
-
- Transaction timing in the database
|
|
287
|
-
- GraphQL query execution timing
|
|
288
|
-
|
|
289
|
-
The 60-second buffer ensures these edge-case records are captured in the next run, preventing data loss.
|
|
290
|
-
|
|
291
|
-
**When to skip state update (`updateState: false`):**
|
|
292
|
-
|
|
293
|
-
- Historical backfills (don't affect ongoing incremental sync)
|
|
294
|
-
- Testing/debugging specific date ranges
|
|
295
|
-
- Reprocessing old data without changing the sync pointer
|
|
296
|
-
|
|
297
|
-
---
|
|
298
|
-
|
|
299
|
-
### 📁 SFTP Configuration
|
|
300
|
-
|
|
301
|
-
**Note:** SFTP credentials and paths are configured separately.
|
|
302
|
-
|
|
303
|
-
- **SFTP Host:** Configured via `sftpHost` activation variable
|
|
304
|
-
- **SFTP Path:** Configured via `sftpRemotePath` activation variable (e.g., `/incoming/fulfillments/`)
|
|
305
|
-
- **Filename Pattern:** Configured via `fileNamePrefix` activation variable (e.g., `fulfillments`)
|
|
306
|
-
|
|
307
|
-
**The workflow generates files like:** `{sftpRemotePath}{fileNamePrefix}-{timestamp}.csv`
|
|
308
|
-
|
|
309
|
-
**Example:** With `sftpRemotePath="/incoming/fulfillments/"` and `fileNamePrefix="fulfillments"`, a generated file will look like:
|
|
310
|
-
|
|
311
|
-
```
|
|
312
|
-
/incoming/fulfillments/fulfillments-2025-10-27T18-30-45Z.csv
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
**Note:** To change the upload folder, modify the `sftpRemotePath` activation variable (not in code).
|
|
316
|
-
|
|
317
|
-
**CRITICAL:** Always call `sftp.dispose()` in a finally block to close connections properly.
|
|
318
|
-
|
|
319
|
-
---
|
|
320
|
-
|
|
321
|
-
### Auto-pagination and limits (ExtractionOrchestrator)
|
|
322
|
-
|
|
323
|
-
**What:** ExtractionOrchestrator handles GraphQL Relay cursor-based pagination automatically.
|
|
324
|
-
|
|
325
|
-
**Why:** Prevents manual pagination loop code, handles large datasets efficiently.
|
|
326
|
-
|
|
327
|
-
**How:** You configure `pageSize` and `maxRecords`; the orchestrator injects `$first` and `$after` variables automatically and loops until `pageInfo.hasNextPage === false` or `maxRecords` is reached.
|
|
328
|
-
|
|
329
|
-
**Critical:** Your query MUST include `edges { cursor }` and `pageInfo { hasNextPage }` fields, or pagination will fail.
|
|
330
|
-
|
|
331
|
-
#### Configuration Parameters
|
|
332
|
-
|
|
333
|
-
| Parameter | Purpose | Example | Effect |
|
|
334
|
-
| ---------------- | ------------------------------ | --------------------------- | -------------------------- |
|
|
335
|
-
| `pageSize` | Records per GraphQL request | `200` | Controls `first` variable |
|
|
336
|
-
| `maxRecords` | Total extraction limit | `50000` | Hard stop across all pages |
|
|
337
|
-
| `resultPath` | Where records live in response | `"fulfillments.edges.node"` | Flattening path |
|
|
338
|
-
| `validateItem()` | Optional record filter | `(item) => !!item.ref` | Skips invalid records |
|
|
339
|
-
|
|
340
|
-
#### GraphQL Query Requirements
|
|
341
|
-
|
|
342
|
-
**Your query MUST include these pagination fields:**
|
|
343
|
-
|
|
344
|
-
```graphql
|
|
345
|
-
query GetFulfillments(
|
|
346
|
-
$retailerId: ID!
|
|
347
|
-
$dateRangeFilter: DateRange
|
|
348
|
-
$statuses: [String!]
|
|
349
|
-
$first: Int! # ← Orchestrator injects this
|
|
350
|
-
$after: String # ← Orchestrator injects this
|
|
351
|
-
) {
|
|
352
|
-
fulfillments(
|
|
353
|
-
retailerId: $retailerId
|
|
354
|
-
updatedOn: $dateRangeFilter
|
|
355
|
-
status: $statuses
|
|
356
|
-
first: $first # ← Pagination page size
|
|
357
|
-
after: $after # ← Pagination cursor
|
|
358
|
-
) {
|
|
359
|
-
edges {
|
|
360
|
-
# ← REQUIRED: Relay connection structure
|
|
361
|
-
node {
|
|
362
|
-
# ← REQUIRED: Actual records here
|
|
363
|
-
id
|
|
364
|
-
ref
|
|
365
|
-
# ... your fields
|
|
366
|
-
}
|
|
367
|
-
cursor # ← REQUIRED: Orchestrator uses this for next page
|
|
368
|
-
}
|
|
369
|
-
pageInfo {
|
|
370
|
-
# ← REQUIRED: Orchestrator checks this
|
|
371
|
-
hasNextPage # ← REQUIRED: Tells orchestrator to continue
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
---
|
|
378
|
-
|
|
379
|
-
### Pattern: Backward Pagination (Optional - Advanced)
|
|
380
|
-
|
|
381
|
-
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
382
|
-
|
|
383
|
-
**When to Use**:
|
|
384
|
-
|
|
385
|
-
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
386
|
-
- ✅ Time-bounded reverse traversal for auditing
|
|
387
|
-
- ✅ Display newest-first in UI/reports
|
|
388
|
-
- ❌ **Don't use for standard incremental sync** - use forward pagination (default)
|
|
389
|
-
|
|
390
|
-
**GraphQL Query Requirements**:
|
|
391
|
-
|
|
392
|
-
Your query must support backward pagination by including `$last` and `$before`:
|
|
393
|
-
|
|
394
|
-
```graphql
|
|
395
|
-
query GetData(
|
|
396
|
-
$retailerId: ID!
|
|
397
|
-
$first: Int # For forward pagination
|
|
398
|
-
$after: String # For forward pagination
|
|
399
|
-
$last: Int # For backward pagination
|
|
400
|
-
$before: String # For backward pagination
|
|
401
|
-
) {
|
|
402
|
-
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
403
|
-
edges {
|
|
404
|
-
cursor # ? REQUIRED
|
|
405
|
-
node {
|
|
406
|
-
id
|
|
407
|
-
createdAt
|
|
408
|
-
# ... other fields
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
pageInfo {
|
|
412
|
-
hasNextPage # For forward
|
|
413
|
-
hasPreviousPage # ? REQUIRED for backward
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
**Implementation**:
|
|
420
|
-
|
|
421
|
-
```typescript
|
|
422
|
-
// Backward pagination - newest records first
|
|
423
|
-
const result = await orchestrator.extract({
|
|
424
|
-
query: YOUR_QUERY,
|
|
425
|
-
resultPath: 'data.edges.node',
|
|
426
|
-
variables: {
|
|
427
|
-
retailerId,
|
|
428
|
-
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
429
|
-
// ❌ Don't include last/before - orchestrator injects them
|
|
430
|
-
},
|
|
431
|
-
pageSize: 200,
|
|
432
|
-
direction: 'backward', // ? Enable reverse pagination
|
|
433
|
-
maxRecords: 10000,
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
// Records are returned in reverse chronological order
|
|
437
|
-
console.log(result.data[0].createdAt); // Newest
|
|
438
|
-
console.log(result.data[result.data.length - 1].createdAt); // Oldest (within range)
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
**Key Differences from Forward Pagination**:
|
|
442
|
-
|
|
443
|
-
| Aspect | Forward (Default) | Backward |
|
|
444
|
-
| ---------------------- | -------------------------------- | ----------------------- |
|
|
445
|
-
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
446
|
-
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
447
|
-
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
448
|
-
| **Cursor Source** | Last edge of page | First edge of page |
|
|
449
|
-
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
450
|
-
|
|
451
|
-
**Important Notes**:
|
|
452
|
-
|
|
453
|
-
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
454
|
-
|
|
455
|
-
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
456
|
-
|
|
457
|
-
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
458
|
-
|
|
459
|
-
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
460
|
-
|
|
461
|
-
**Example: Extract Latest 1000 Orders**
|
|
462
|
-
|
|
463
|
-
```typescript
|
|
464
|
-
const latestOrders = await orchestrator.extract({
|
|
465
|
-
query: ORDERS_QUERY,
|
|
466
|
-
resultPath: 'orders.edges.node',
|
|
467
|
-
variables: {
|
|
468
|
-
retailerId,
|
|
469
|
-
statuses: ['BOOKED', 'ALLOCATED'],
|
|
470
|
-
},
|
|
471
|
-
direction: 'backward', // Start from newest
|
|
472
|
-
maxRecords: 1000, // Stop after 1000 records
|
|
473
|
-
pageSize: 100, // 100 per page = 10 pages
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// latestOrders.data[0] is the newest order
|
|
477
|
-
// latestOrders.data[999] is the 1000th newest order
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
**When to Use Forward vs Backward**:
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
// ? Forward (default) - For incremental sync
|
|
484
|
-
const incrementalData = await orchestrator.extract({
|
|
485
|
-
query: YOUR_QUERY,
|
|
486
|
-
resultPath: 'data.edges.node',
|
|
487
|
-
variables: {
|
|
488
|
-
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
489
|
-
},
|
|
490
|
-
// direction defaults to 'forward'
|
|
491
|
-
// Processes oldest → newest for proper sequencing
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
// ? Backward - For "latest N records" use cases
|
|
495
|
-
const latestData = await orchestrator.extract({
|
|
496
|
-
query: YOUR_QUERY,
|
|
497
|
-
resultPath: 'data.edges.node',
|
|
498
|
-
direction: 'backward',
|
|
499
|
-
maxRecords: 100, // Just get latest 100
|
|
500
|
-
// Gets newest → oldest
|
|
501
|
-
});
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
**Pagination Variables Reference**:
|
|
505
|
-
|
|
506
|
-
| Variable | Forward | Backward | Injected By | Notes |
|
|
507
|
-
| -------- | ----------- | ----------- | ------------ | ------------------------ |
|
|
508
|
-
| `first` | ? Used | ❌ Not used | Orchestrator | From `pageSize` |
|
|
509
|
-
| `after` | ? Used | ❌ Not used | Orchestrator | From cursor (last edge) |
|
|
510
|
-
| `last` | ❌ Not used | ? Used | Orchestrator | From `pageSize` |
|
|
511
|
-
| `before` | ❌ Not used | ? Used | Orchestrator | From cursor (first edge) |
|
|
512
|
-
|
|
513
|
-
**Common Mistakes to Avoid**:
|
|
514
|
-
|
|
515
|
-
```typescript
|
|
516
|
-
// ❌ WRONG - Don't pass pagination variables
|
|
517
|
-
const result = await orchestrator.extract({
|
|
518
|
-
variables: {
|
|
519
|
-
last: 200, // ❌ Orchestrator will override this
|
|
520
|
-
before: cursor, // ❌ Orchestrator manages cursor
|
|
521
|
-
},
|
|
522
|
-
direction: 'backward',
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// ? CORRECT - Let orchestrator inject pagination
|
|
526
|
-
const result = await orchestrator.extract({
|
|
527
|
-
variables: {
|
|
528
|
-
retailerId, // ? Your business variables only
|
|
529
|
-
},
|
|
530
|
-
pageSize: 200, // ? Orchestrator uses this for last/before
|
|
531
|
-
direction: 'backward',
|
|
532
|
-
});
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
---
|
|
536
|
-
|
|
537
|
-
## 📄 Mapping Configuration
|
|
538
|
-
|
|
539
|
-
**File:** `config/fulfillments.export.csv.json`
|
|
540
|
-
|
|
541
|
-
```json
|
|
542
|
-
{
|
|
543
|
-
"name": "fulfillments.export.csv",
|
|
544
|
-
"version": "1.0.0",
|
|
545
|
-
"description": "Fulfillments → CSV Export Mapping",
|
|
546
|
-
"fields": {
|
|
547
|
-
"order_number": {
|
|
548
|
-
"source": "orderRef",
|
|
549
|
-
"required": true,
|
|
550
|
-
"resolver": "sdk.trim"
|
|
551
|
-
},
|
|
552
|
-
"fulfillment_id": {
|
|
553
|
-
"source": "ref",
|
|
554
|
-
"required": true,
|
|
555
|
-
"resolver": "sdk.trim"
|
|
556
|
-
},
|
|
557
|
-
"tracking_number": {
|
|
558
|
-
"source": "trackingNumber",
|
|
559
|
-
"required": false,
|
|
560
|
-
"resolver": "sdk.trim"
|
|
561
|
-
},
|
|
562
|
-
"carrier": {
|
|
563
|
-
"source": "carrier",
|
|
564
|
-
"required": false,
|
|
565
|
-
"resolver": "sdk.uppercase"
|
|
566
|
-
},
|
|
567
|
-
"service_level": {
|
|
568
|
-
"source": "serviceLevel",
|
|
569
|
-
"required": false,
|
|
570
|
-
"resolver": "sdk.trim"
|
|
571
|
-
},
|
|
572
|
-
"status": {
|
|
573
|
-
"source": "status",
|
|
574
|
-
"required": true,
|
|
575
|
-
"resolver": "sdk.uppercase"
|
|
576
|
-
},
|
|
577
|
-
"ship_date": {
|
|
578
|
-
"source": "shippedOn",
|
|
579
|
-
"required": false,
|
|
580
|
-
"resolver": "sdk.formatDateShort"
|
|
581
|
-
},
|
|
582
|
-
"delivery_date": {
|
|
583
|
-
"source": "deliveredOn",
|
|
584
|
-
"required": false,
|
|
585
|
-
"resolver": "sdk.formatDateShort"
|
|
586
|
-
},
|
|
587
|
-
"customer_email": {
|
|
588
|
-
"source": "order.customer.email",
|
|
589
|
-
"required": false,
|
|
590
|
-
"resolver": "sdk.trim"
|
|
591
|
-
},
|
|
592
|
-
"last_updated": {
|
|
593
|
-
"source": "updatedOn",
|
|
594
|
-
"required": true,
|
|
595
|
-
"resolver": "sdk.toString"
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
**AI Customization Hints:**
|
|
602
|
-
|
|
603
|
-
- Add fields: Copy existing field config, change `source` path
|
|
604
|
-
- Remove fields: Delete field from config
|
|
605
|
-
- Change resolvers: Replace `sdk.trim` with `sdk.uppercase`, etc.
|
|
606
|
-
- Nested fields: Use dot notation like `order.customer.email`
|
|
607
|
-
|
|
608
|
-
Note: Customize mapping by editing the JSON above; prefer built-in resolvers. See SDK Universal Mapping guide for advanced usage.
|
|
609
|
-
|
|
610
|
-
---
|
|
611
|
-
|
|
612
|
-
## Versori Workflows Structure
|
|
613
|
-
|
|
614
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
615
|
-
|
|
616
|
-
**Trigger Types:**
|
|
617
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
618
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
619
|
-
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
620
|
-
|
|
621
|
-
**Execution Steps (chained to triggers):**
|
|
622
|
-
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
623
|
-
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
624
|
-
|
|
625
|
-
### Recommended Project Structure
|
|
626
|
-
|
|
627
|
-
```
|
|
628
|
-
fulfillments-extraction/
|
|
629
|
-
├── index.ts # Entry point - exports all workflows
|
|
630
|
-
└── src/
|
|
631
|
-
├── workflows/
|
|
632
|
-
│ ├── scheduled/
|
|
633
|
-
│ │ └── daily-fulfillments-extraction.ts # Scheduled: Daily extraction
|
|
634
|
-
│ │
|
|
635
|
-
│ └── webhook/
|
|
636
|
-
│ ├── adhoc-fulfillments-extraction.ts # Webhook: Manual trigger
|
|
637
|
-
│ └── job-status-check.ts # Webhook: Status query
|
|
638
|
-
│
|
|
639
|
-
├── services/
|
|
640
|
-
│ └── fulfillments-extraction.service.ts # Shared orchestration logic (reusable)
|
|
641
|
-
│
|
|
642
|
-
└── config/
|
|
643
|
-
└── fulfillments.export.csv.json # Mapping configuration
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
### 1. Entry Point (`index.ts`)
|
|
648
|
-
|
|
649
|
-
**Pattern:** MemoryInterpreter (export all workflows from single entry point)
|
|
650
|
-
|
|
651
|
-
**Benefits:**
|
|
652
|
-
- ✅ Versori platform discovers all workflows automatically
|
|
653
|
-
- ✅ Clean separation of concerns (entry point vs workflow logic)
|
|
654
|
-
- ✅ Easy to add/remove workflows (just update exports)
|
|
655
|
-
- ✅ Single source of truth for workflow registration
|
|
656
|
-
|
|
657
|
-
```typescript
|
|
658
|
-
// ═══════════════════════════════════════════════════════════
|
|
659
|
-
// 🚀 VERSORI FULFILLMENTS EXTRACTION
|
|
660
|
-
// ═══════════════════════════════════════════════════════════
|
|
661
|
-
/**
|
|
662
|
-
* Entry Point - Registers all workflows with Versori platform
|
|
663
|
-
*
|
|
664
|
-
* This file is the entry point for the Versori deployment.
|
|
665
|
-
* It registers three workflows:
|
|
666
|
-
* 1. Scheduled extraction (runs automatically)
|
|
667
|
-
* 2. Ad hoc webhook (manual trigger)
|
|
668
|
-
* 3. Job status webhook (query progress)
|
|
669
|
-
*
|
|
670
|
-
* AI CUSTOMIZATION:
|
|
671
|
-
* - Add new workflows by importing and registering them
|
|
672
|
-
* - Remove workflows by commenting out registration
|
|
673
|
-
* - Change workflow names in import statements
|
|
674
|
-
*/
|
|
675
|
-
// ═══════════════════════════════════════════════════════════
|
|
676
|
-
|
|
677
|
-
import {
|
|
678
|
-
scheduledFulfillmentsExtraction,
|
|
679
|
-
adhocFulfillmentsExtraction,
|
|
680
|
-
fulfillmentsJobStatus,
|
|
681
|
-
} from './workflows/fulfillments-extraction';
|
|
682
|
-
|
|
683
|
-
// Register workflows with Versori platform
|
|
684
|
-
// The platform will expose these as executable endpoints
|
|
685
|
-
|
|
686
|
-
export {
|
|
687
|
-
scheduledFulfillmentsExtraction, // Cron-based auto-run
|
|
688
|
-
adhocFulfillmentsExtraction, // Manual webhook trigger
|
|
689
|
-
fulfillmentsJobStatus, // Job status query
|
|
690
|
-
};
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
---
|
|
694
|
-
|
|
695
|
-
### 2. Workflows (src/workflows/fulfillments-extraction.ts)
|
|
696
|
-
|
|
697
|
-
```typescript
|
|
698
|
-
/**
|
|
699
|
-
* Workflows - Defines 3 execution patterns for fulfillments extraction
|
|
700
|
-
*
|
|
701
|
-
* WORKFLOW 1: Scheduled (Cron) - Runs automatically every 4 hours
|
|
702
|
-
* WORKFLOW 2: Ad hoc (Webhook) - Manual trigger with optional date override
|
|
703
|
-
* WORKFLOW 3: Job Status (Webhook) - Query job progress
|
|
704
|
-
*
|
|
705
|
-
* AI CUSTOMIZATION HINTS:
|
|
706
|
-
* - Change schedule: Modify cron expression in schedule()
|
|
707
|
-
* - Add filtering: Pass additional params to executeFulfillmentExtraction()
|
|
708
|
-
* - Change response format: Modify return object structure
|
|
709
|
-
*/
|
|
710
|
-
|
|
711
|
-
import { schedule, webhook, http, fn } from '@versori/run';
|
|
712
|
-
import { executeFulfillmentExtraction, getJobStatus } from '../services/extraction-orchestration';
|
|
713
|
-
import { generateJobId } from '../utils/job-id-generator';
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* WORKFLOW 1: Scheduled Extraction
|
|
717
|
-
*
|
|
718
|
-
* Purpose: Automated extraction every 4 hours for incremental sync
|
|
719
|
-
* Trigger: Cron schedule (every 4 hours at minute 0)
|
|
720
|
-
* State Update: Always updates lastSync timestamp
|
|
721
|
-
*
|
|
722
|
-
* AI CUSTOMIZATION:
|
|
723
|
-
* - Change schedule: Modify the cron expression string
|
|
724
|
-
* Examples:
|
|
725
|
-
* - Every hour: '0 * * * *'
|
|
726
|
-
* - Every 30 min: '*/30 * * * *'
|
|
727
|
-
* - Daily at 2 AM: '0 2 * * *'
|
|
728
|
-
*/
|
|
729
|
-
export const scheduledFulfillmentsExtraction = schedule(
|
|
730
|
-
'fulfillments-scheduled',
|
|
731
|
-
'0 */4 * * *' // ← CUSTOMIZE: Cron expression
|
|
732
|
-
)
|
|
733
|
-
.then(
|
|
734
|
-
http('execute-scheduled-extraction', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
735
|
-
const { log } = ctx;
|
|
736
|
-
|
|
737
|
-
// Generate unique job ID for tracking
|
|
738
|
-
// Format: SCHEDULED_FULFILLMENTS_YYYYMMDD_HHMMSS_random
|
|
739
|
-
const jobId = generateJobId('SCHEDULED', 'FULFILLMENTS');
|
|
740
|
-
|
|
741
|
-
log.info('Scheduled extraction triggered', { jobId });
|
|
742
|
-
|
|
743
|
-
try {
|
|
744
|
-
// Execute main workflow (extraction → transform → upload)
|
|
745
|
-
const result = await executeFulfillmentExtraction(ctx, {
|
|
746
|
-
jobId,
|
|
747
|
-
triggeredBy: 'schedule',
|
|
748
|
-
updateState: true, // Always update state for scheduled runs
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
log.info('Scheduled extraction completed', {
|
|
752
|
-
jobId,
|
|
753
|
-
recordCount: result.recordsExtracted,
|
|
754
|
-
fileName: result.fileName
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
return result;
|
|
758
|
-
|
|
759
|
-
} catch (error: any) {
|
|
760
|
-
log.error('Scheduled extraction failed', {
|
|
761
|
-
jobId,
|
|
762
|
-
message: error instanceof Error ? error.message : String(error),
|
|
763
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
764
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
765
|
-
});
|
|
766
|
-
throw error;
|
|
767
|
-
}
|
|
768
|
-
})
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* WORKFLOW 2: Ad hoc Extraction (Manual Trigger)
|
|
773
|
-
*
|
|
774
|
-
* Purpose: Manual extraction with optional date range override
|
|
775
|
-
* Trigger: Webhook POST to /webhooks/fulfillments-adhoc
|
|
776
|
-
* State Update: Optional (controlled by request payload)
|
|
777
|
-
*
|
|
778
|
-
* WEBHOOK PAYLOAD EXAMPLES:
|
|
779
|
-
*
|
|
780
|
-
* 1. Incremental (use last sync timestamp):
|
|
781
|
-
* {}
|
|
782
|
-
*
|
|
783
|
-
* 2. Date range (manual override):
|
|
784
|
-
* {
|
|
785
|
-
* "fromDate": "2025-01-01T00:00:00Z",
|
|
786
|
-
* "toDate": "2025-01-31T23:59:59Z",
|
|
787
|
-
* "updateState": false
|
|
788
|
-
* }
|
|
789
|
-
*
|
|
790
|
-
* AI CUSTOMIZATION:
|
|
791
|
-
* - Add request validation
|
|
792
|
-
* - Add authentication check
|
|
793
|
-
* - Add custom filters from payload
|
|
794
|
-
*/
|
|
795
|
-
export const adhocFulfillmentsExtraction = webhook(
|
|
796
|
-
'fulfillments-adhoc',
|
|
797
|
-
{ connection: 'fulfillments-adhoc', response: { mode: 'sync' } }
|
|
798
|
-
)
|
|
799
|
-
.then(
|
|
800
|
-
http('execute-adhoc-extraction', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
801
|
-
const { data, log } = ctx;
|
|
802
|
-
|
|
803
|
-
// Generate unique job ID
|
|
804
|
-
const jobId = generateJobId('ADHOC', 'FULFILLMENTS');
|
|
805
|
-
|
|
806
|
-
// SECURITY: Authentication is enforced by Versori connection configuration
|
|
807
|
-
// Configure auth on the connection and reference it in webhook({ connection: '...' })
|
|
808
|
-
|
|
809
|
-
// Extract optional date override from webhook payload
|
|
810
|
-
const fromDate = data.fromDate as string | undefined;
|
|
811
|
-
const toDate = data.toDate as string | undefined;
|
|
812
|
-
const updateState = data.updateState === true; // Default false; advance state only if explicitly true
|
|
813
|
-
|
|
814
|
-
log.info('Ad hoc extraction triggered via webhook', {
|
|
815
|
-
jobId,
|
|
816
|
-
hasDateOverride: !!fromDate,
|
|
817
|
-
fromDate: fromDate || 'not specified',
|
|
818
|
-
toDate: toDate || 'not specified',
|
|
819
|
-
updateState
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
try {
|
|
823
|
-
// Execute main workflow with optional overrides
|
|
824
|
-
const result = await executeFulfillmentExtraction(ctx, {
|
|
825
|
-
jobId,
|
|
826
|
-
triggeredBy: 'webhook',
|
|
827
|
-
fromDate, // Optional: override start date
|
|
828
|
-
toDate, // Optional: override end date
|
|
829
|
-
updateState, // Optional: skip state update for historical queries
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
log.info('Ad hoc extraction completed', {
|
|
833
|
-
jobId,
|
|
834
|
-
recordCount: result.recordsExtracted,
|
|
835
|
-
fileName: result.fileName,
|
|
836
|
-
isManualOverride: !!fromDate,
|
|
837
|
-
stateUpdated: result.stateUpdated
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
// Return success with job details
|
|
841
|
-
return {
|
|
842
|
-
success: true,
|
|
843
|
-
jobId,
|
|
844
|
-
recordsExtracted: result.recordsExtracted,
|
|
845
|
-
fileName: result.fileName,
|
|
846
|
-
sftpPath: result.sftpPath,
|
|
847
|
-
statusUrl: `/webhooks/fulfillments-job-status?jobId=${jobId}`,
|
|
848
|
-
dateRange: fromDate ? {
|
|
849
|
-
from: fromDate,
|
|
850
|
-
to: toDate || 'not specified',
|
|
851
|
-
updateState
|
|
852
|
-
} : undefined
|
|
853
|
-
};
|
|
854
|
-
|
|
855
|
-
} catch (error: any) {
|
|
856
|
-
log.error('Ad hoc extraction failed', {
|
|
857
|
-
jobId,
|
|
858
|
-
message: error instanceof Error ? error.message : String(error),
|
|
859
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
860
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
return {
|
|
864
|
-
success: false,
|
|
865
|
-
jobId,
|
|
866
|
-
error: error instanceof Error ? error.message : String(error)
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
})
|
|
870
|
-
);
|
|
871
|
-
|
|
872
|
-
/**
|
|
873
|
-
* WORKFLOW 3: Job Status Query
|
|
874
|
-
*
|
|
875
|
-
* Purpose: Check job progress and status
|
|
876
|
-
* Trigger: Webhook GET/POST to /webhooks/fulfillments-job-status?jobId=xxx
|
|
877
|
-
* Returns: Current job status, stage, progress
|
|
878
|
-
*
|
|
879
|
-
* QUERY EXAMPLES:
|
|
880
|
-
*
|
|
881
|
-
* 1. HTTP GET:
|
|
882
|
-
* GET /webhooks/fulfillments-job-status?jobId=ADHOC_FULFILLMENTS_20251027_183045_abc123
|
|
883
|
-
*
|
|
884
|
-
* 2. HTTP POST:
|
|
885
|
-
* POST /webhooks/fulfillments-job-status
|
|
886
|
-
* { "jobId": "ADHOC_FULFILLMENTS_20251027_183045_abc123" }
|
|
887
|
-
*/
|
|
888
|
-
export const fulfillmentsJobStatus = webhook(
|
|
889
|
-
'fulfillments-job-status',
|
|
890
|
-
{ connection: 'fulfillments-job-status', response: { mode: 'sync' } }
|
|
891
|
-
)
|
|
892
|
-
.then(
|
|
893
|
-
fn('query-job-status', async (ctx) => {
|
|
894
|
-
const { data, log, openKv, activation } = ctx;
|
|
895
|
-
const req = ctx.request();
|
|
896
|
-
|
|
897
|
-
// SECURITY: Authentication is enforced by Versori connection configuration
|
|
898
|
-
// Configure auth on the connection and reference it in webhook({ connection: '...' })
|
|
899
|
-
|
|
900
|
-
// Get jobId from query param or POST body
|
|
901
|
-
const jobId = data.jobId as string;
|
|
902
|
-
|
|
903
|
-
if (!jobId) {
|
|
904
|
-
log.error('Job ID not provided in request');
|
|
905
|
-
return {
|
|
906
|
-
success: false,
|
|
907
|
-
error: 'Job ID is required. Provide jobId in query param or request body.'
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
log.info('Querying job status', { jobId });
|
|
912
|
-
|
|
913
|
-
try {
|
|
914
|
-
// Query job status from KV store
|
|
915
|
-
const status = await getJobStatus(openKv(':project:'), jobId, log);
|
|
916
|
-
|
|
917
|
-
if (!status) {
|
|
918
|
-
log.info('Job not found', { jobId });
|
|
919
|
-
return {
|
|
920
|
-
success: false,
|
|
921
|
-
error: 'Job not found',
|
|
922
|
-
jobId
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
log.info('Job status retrieved', { jobId, status: status.status });
|
|
927
|
-
|
|
928
|
-
return {
|
|
929
|
-
success: true,
|
|
930
|
-
jobId,
|
|
931
|
-
...status
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
} catch (error: any) {
|
|
935
|
-
log.error('Failed to query job status', {
|
|
936
|
-
jobId,
|
|
937
|
-
message: error instanceof Error ? error.message : String(error),
|
|
938
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
939
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
return {
|
|
943
|
-
success: false,
|
|
944
|
-
jobId,
|
|
945
|
-
error: error instanceof Error ? error.message : String(error)
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
|
-
})
|
|
949
|
-
);
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
---
|
|
953
|
-
|
|
954
|
-
## 3. Main Orchestration Service (src/services/extraction-orchestration.ts)
|
|
955
|
-
|
|
956
|
-
```typescript
|
|
957
|
-
/**
|
|
958
|
-
* MAIN ORCHESTRATION SERVICE
|
|
959
|
-
*
|
|
960
|
-
* This is the heart of the extraction workflow. It coordinates all steps:
|
|
961
|
-
* 1. Initialize clients and services
|
|
962
|
-
* 2. Determine date range (incremental vs manual)
|
|
963
|
-
* 3. Extract data using ExtractionOrchestrator
|
|
964
|
-
* 4. Transform using UniversalMapper
|
|
965
|
-
* 5. Generate CSV using CSVParserService
|
|
966
|
-
* 6. Upload to SFTP
|
|
967
|
-
* 7. Track job progress with JobTracker
|
|
968
|
-
* 8. Update state for next run
|
|
969
|
-
*
|
|
970
|
-
* NAMING PATTERN (consistent across all use cases):
|
|
971
|
-
* - Interface: {Entity}ExtractionParams (e.g., FulfillmentExtractionParams)
|
|
972
|
-
* - Result: {Entity}ExtractionResult (e.g., FulfillmentExtractionResult)
|
|
973
|
-
* - Main function: execute{Entity}Extraction (e.g., executeFulfillmentExtraction)
|
|
974
|
-
*
|
|
975
|
-
* AI CUSTOMIZATION HINTS:
|
|
976
|
-
* - Change entity: Replace "Fulfillment" with "Order", "Product", etc.
|
|
977
|
-
* - Change output: Replace CSVParserService with XMLBuilder
|
|
978
|
-
* - Change destination: Replace SftpDataSource with S3DataSource
|
|
979
|
-
* - Add steps: Insert new service calls between existing steps
|
|
980
|
-
*/
|
|
981
|
-
|
|
982
|
-
import { Buffer } from 'node:buffer';
|
|
983
|
-
import {
|
|
984
|
-
createClient,
|
|
985
|
-
ExtractionOrchestrator,
|
|
986
|
-
JobTracker,
|
|
987
|
-
UniversalMapper,
|
|
988
|
-
CSVParserService,
|
|
989
|
-
SftpDataSource,
|
|
990
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
991
|
-
|
|
992
|
-
import mappingConfig from '../../config/fulfillments.export.csv.json' with { type: 'json' };
|
|
993
|
-
|
|
994
|
-
// ? VERSORI PLATFORM: Use native log from context, not LoggingService
|
|
995
|
-
|
|
996
|
-
/**
|
|
997
|
-
* Parameters for extraction workflow
|
|
998
|
-
*
|
|
999
|
-
* NAMING: {Entity}ExtractionParams
|
|
1000
|
-
*/
|
|
1001
|
-
export interface FulfillmentExtractionParams {
|
|
1002
|
-
jobId: string;
|
|
1003
|
-
triggeredBy: 'schedule' | 'webhook';
|
|
1004
|
-
fromDate?: string; // Optional: manual date override
|
|
1005
|
-
toDate?: string; // Optional: manual date override
|
|
1006
|
-
updateState: boolean; // Whether to update rawLastRunTime timestamp
|
|
1007
|
-
|
|
1008
|
-
// AI CUSTOMIZATION: Add filters specific to entity
|
|
1009
|
-
fulfillmentStatuses?: string[]; // e.g., ['SHIPPED', 'DELIVERED']
|
|
1010
|
-
retailerId?: string; // e.g., 'YOUR_RETAILER_ID'
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
/**
|
|
1014
|
-
* Result from extraction workflow
|
|
1015
|
-
*
|
|
1016
|
-
* NAMING: {Entity}ExtractionResult
|
|
1017
|
-
*/
|
|
1018
|
-
export interface FulfillmentExtractionResult {
|
|
1019
|
-
success: boolean;
|
|
1020
|
-
jobId: string;
|
|
1021
|
-
recordsExtracted: number;
|
|
1022
|
-
fileName?: string;
|
|
1023
|
-
sftpPath?: string;
|
|
1024
|
-
error?: string;
|
|
1025
|
-
errors?: any[];
|
|
1026
|
-
isManualOverride?: boolean;
|
|
1027
|
-
stateUpdated?: boolean;
|
|
1028
|
-
newTimestamp?: string;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
/**
|
|
1032
|
-
* GraphQL Query for Fulfillments
|
|
1033
|
-
*
|
|
1034
|
-
* NAMING: {ENTITY}_EXTRACTION_QUERY (uppercase constant)
|
|
1035
|
-
*/
|
|
1036
|
-
const FULFILLMENTS_EXTRACTION_QUERY = `
|
|
1037
|
-
query GetFulfillments(
|
|
1038
|
-
$updatedAfter: DateTime!
|
|
1039
|
-
$statuses: [String!]
|
|
1040
|
-
$first: Int!
|
|
1041
|
-
$after: String
|
|
1042
|
-
) {
|
|
1043
|
-
fulfillments(
|
|
1044
|
-
updatedOn: { after: $updatedAfter }
|
|
1045
|
-
status: $statuses
|
|
1046
|
-
first: $first
|
|
1047
|
-
after: $after
|
|
1048
|
-
) {
|
|
1049
|
-
edges {
|
|
1050
|
-
node {
|
|
1051
|
-
id
|
|
1052
|
-
ref
|
|
1053
|
-
orderRef
|
|
1054
|
-
status
|
|
1055
|
-
trackingNumber
|
|
1056
|
-
carrier
|
|
1057
|
-
serviceLevel
|
|
1058
|
-
shippedOn
|
|
1059
|
-
deliveredOn
|
|
1060
|
-
updatedOn
|
|
1061
|
-
order {
|
|
1062
|
-
customer {
|
|
1063
|
-
email
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
cursor
|
|
1068
|
-
}
|
|
1069
|
-
pageInfo {
|
|
1070
|
-
hasNextPage
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
`;
|
|
1075
|
-
|
|
1076
|
-
/**
|
|
1077
|
-
* Query job status from KV store
|
|
1078
|
-
*
|
|
1079
|
-
* ? VERSORI PLATFORM: Uses native log from context (passed as parameter)
|
|
1080
|
-
*/
|
|
1081
|
-
export async function getJobStatus(
|
|
1082
|
-
kv: any, // ? Versori KV (compatible with JobTracker's KVAdapter interface)
|
|
1083
|
-
jobId: string,
|
|
1084
|
-
log: any // ? Native Versori log from context
|
|
1085
|
-
): Promise<any | undefined> {
|
|
1086
|
-
try {
|
|
1087
|
-
const tracker = new JobTracker(kv, log);
|
|
1088
|
-
return await tracker.getJob(jobId);
|
|
1089
|
-
} catch (error: any) {
|
|
1090
|
-
log.error('Failed to get job status', {
|
|
1091
|
-
jobId,
|
|
1092
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1093
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1094
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1095
|
-
});
|
|
1096
|
-
return undefined;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/**
|
|
1101
|
-
* MAIN ORCHESTRATION FUNCTION
|
|
1102
|
-
*
|
|
1103
|
-
* NAMING: execute{Entity}Extraction (e.g., executeFulfillmentExtraction)
|
|
1104
|
-
*
|
|
1105
|
-
* This function implements the complete workflow in steps.
|
|
1106
|
-
* Each step is clearly commented for AI understanding.
|
|
1107
|
-
*/
|
|
1108
|
-
export async function executeFulfillmentExtraction(
|
|
1109
|
-
ctx: any,
|
|
1110
|
-
params: FulfillmentExtractionParams
|
|
1111
|
-
): Promise<FulfillmentExtractionResult> {
|
|
1112
|
-
// ? VERSORI PLATFORM: Extract native log from context
|
|
1113
|
-
const { log, openKv, activation } = ctx;
|
|
1114
|
-
const { jobId, triggeredBy, fromDate, toDate, updateState } = params;
|
|
1115
|
-
|
|
1116
|
-
// Open KV store for state management and job tracking
|
|
1117
|
-
// ? Pass raw Versori KV directly - it matches KVAdapter interface
|
|
1118
|
-
// ? Pass native log to JobTracker
|
|
1119
|
-
const kv = openKv(':project:');
|
|
1120
|
-
const tracker = new JobTracker(kv, log);
|
|
1121
|
-
|
|
1122
|
-
try {
|
|
1123
|
-
// ═══════════════════════════════════════════════════════════
|
|
1124
|
-
// STEP 1/8: Initialize Job Tracking
|
|
1125
|
-
// ═══════════════════════════════════════════════════════════
|
|
1126
|
-
log.info('🚀 [STEP 1/8] Initializing job tracking', { jobId });
|
|
1127
|
-
|
|
1128
|
-
await tracker.createJob(jobId, {
|
|
1129
|
-
triggeredBy,
|
|
1130
|
-
hasDateOverride: !!fromDate,
|
|
1131
|
-
fromDate,
|
|
1132
|
-
toDate,
|
|
1133
|
-
updateStateAfterRun: updateState,
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
// ═══════════════════════════════════════════════════════════
|
|
1137
|
-
// STEP 2/8: Initialize Fluent Client
|
|
1138
|
-
// ═══════════════════════════════════════════════════════════
|
|
1139
|
-
log.info('🚀 [STEP 2/8] Initializing Fluent Commerce client', { jobId });
|
|
1140
|
-
|
|
1141
|
-
// ✅ Optional: Validate connection immediately (fail-fast mode)
|
|
1142
|
-
// Set activation variable 'validateConnectionOnStart' = 'true' to enable
|
|
1143
|
-
// When enabled: Executes query { me { ref } } to verify authentication
|
|
1144
|
-
// When disabled: Fast creation, validation happens on first API call (default)
|
|
1145
|
-
const validateConnection = activation.getVariable('validateConnectionOnStart') === 'true';
|
|
1146
|
-
const client = await createClient(ctx, validateConnection ? { validateConnection: true } : undefined);
|
|
1147
|
-
|
|
1148
|
-
if (!client) {
|
|
1149
|
-
throw new Error('Failed to create Fluent Commerce client');
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
if (validateConnection) {
|
|
1153
|
-
log.info('✅ Connection validated successfully - authentication confirmed', { jobId });
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
// ═══════════════════════════════════════════════════════════
|
|
1157
|
-
// STEP 3/8: Determine Date Range
|
|
1158
|
-
// ═══════════════════════════════════════════════════════════
|
|
1159
|
-
log.info('🔍 [STEP 3/8] Determining date range for extraction', { jobId });
|
|
1160
|
-
|
|
1161
|
-
// State key for incremental sync tracking
|
|
1162
|
-
// NAMING: last{Entity}Sync (e.g., lastFulfillmentSync)
|
|
1163
|
-
const STATE_KEY = 'lastFulfillmentSync';
|
|
1164
|
-
const DEFAULT_FALLBACK = '2024-01-01T00:00:00Z';
|
|
1165
|
-
const OVERLAP_BUFFER_SECONDS = parseInt(
|
|
1166
|
-
activation.getVariable('overlapBufferSeconds') || '60',
|
|
1167
|
-
10
|
|
1168
|
-
);
|
|
1169
|
-
|
|
1170
|
-
let bufferedLastRunTime: string;
|
|
1171
|
-
const isManualOverride = !!fromDate;
|
|
1172
|
-
|
|
1173
|
-
if (isManualOverride) {
|
|
1174
|
-
// Manual date override from webhook
|
|
1175
|
-
bufferedLastRunTime = fromDate!;
|
|
1176
|
-
log.info('Using manual date override', { fromDate, toDate });
|
|
1177
|
-
} else {
|
|
1178
|
-
// Incremental sync - get last sync timestamp
|
|
1179
|
-
const rawLastRunTime = (await kv.get<string>(STATE_KEY)) || DEFAULT_FALLBACK;
|
|
1180
|
-
|
|
1181
|
-
// Apply overlap buffer (prevents missed records)
|
|
1182
|
-
bufferedLastRunTime = new Date(
|
|
1183
|
-
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_SECONDS * 1000
|
|
1184
|
-
).toISOString();
|
|
1185
|
-
|
|
1186
|
-
log.info('Using incremental sync with overlap buffer', {
|
|
1187
|
-
rawLastRunTime,
|
|
1188
|
-
bufferedLastRunTime,
|
|
1189
|
-
overlapBufferSeconds: OVERLAP_BUFFER_SECONDS,
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
// ═══════════════════════════════════════════════════════════
|
|
1194
|
-
// STEP 4/8: Extract Data (ExtractionOrchestrator)
|
|
1195
|
-
// ═══════════════════════════════════════════════════════════
|
|
1196
|
-
log.info('📥 [STEP 4/8] Extracting data from Fluent Commerce', { jobId });
|
|
1197
|
-
|
|
1198
|
-
await tracker.updateJob(jobId, {
|
|
1199
|
-
status: 'processing',
|
|
1200
|
-
stage: 'extraction',
|
|
1201
|
-
message: 'Extracting data with auto-pagination',
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
// Configure extraction
|
|
1205
|
-
const pageSize = parseInt(activation.getVariable('pageSize') || '200', 10);
|
|
1206
|
-
const maxRecords = parseInt(activation.getVariable('maxRecords') || '10000', 10);
|
|
1207
|
-
|
|
1208
|
-
// Parse status filter
|
|
1209
|
-
const fulfillmentStatuses =
|
|
1210
|
-
params.fulfillmentStatuses ||
|
|
1211
|
-
(activation.getVariable('fulfillmentStatuses') || 'SHIPPED,DELIVERED').split(',');
|
|
1212
|
-
|
|
1213
|
-
// Initialize ExtractionOrchestrator
|
|
1214
|
-
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
1215
|
-
|
|
1216
|
-
// ? Enhanced: Extract context for progress logging
|
|
1217
|
-
const dateRangeInfo = {
|
|
1218
|
-
start: bufferedLastRunTime || 'N/A',
|
|
1219
|
-
end: new Date().toISOString(),
|
|
1220
|
-
statuses: fulfillmentStatuses.join(', ') || 'all'
|
|
1221
|
-
};
|
|
1222
|
-
|
|
1223
|
-
// Duration tracking
|
|
1224
|
-
const extractionStartTime = Date.now();
|
|
1225
|
-
|
|
1226
|
-
// ? Enhanced: Start logging with context
|
|
1227
|
-
log.info(`🔍 [ExtractionOrchestrator] Starting extraction`, {
|
|
1228
|
-
query: 'fulfillments',
|
|
1229
|
-
pageSize,
|
|
1230
|
-
maxRecords,
|
|
1231
|
-
dateRange: `from ${dateRangeInfo.start}`,
|
|
1232
|
-
statuses: dateRangeInfo.statuses,
|
|
1233
|
-
jobId
|
|
1234
|
-
});
|
|
1235
|
-
|
|
1236
|
-
// Execute extraction with auto-pagination
|
|
1237
|
-
const extractionResult = await orchestrator.extract({
|
|
1238
|
-
query: FULFILLMENTS_EXTRACTION_QUERY,
|
|
1239
|
-
resultPath: 'fulfillments.edges.node',
|
|
1240
|
-
variables: {
|
|
1241
|
-
updatedAfter: bufferedLastRunTime,
|
|
1242
|
-
statuses: fulfillmentStatuses,
|
|
1243
|
-
// Note: Don't include 'first' or 'after' here; orchestrator injects them
|
|
1244
|
-
},
|
|
1245
|
-
pageSize,
|
|
1246
|
-
maxRecords,
|
|
1247
|
-
// Optional: validate each record
|
|
1248
|
-
validateItem: (item: any) => {
|
|
1249
|
-
return !!(item.ref && item.orderRef);
|
|
1250
|
-
},
|
|
1251
|
-
});
|
|
1252
|
-
|
|
1253
|
-
const rawRecords = extractionResult.data;
|
|
1254
|
-
|
|
1255
|
-
// Calculate extraction duration
|
|
1256
|
-
const extractionDuration = Date.now() - extractionStartTime;
|
|
1257
|
-
|
|
1258
|
-
log.info('✅ [STEP 4/8] Extraction completed', {
|
|
1259
|
-
jobId,
|
|
1260
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1261
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1262
|
-
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1263
|
-
errors: extractionResult.errors ? extractionResult.errors.length : 0,
|
|
1264
|
-
duration: `${extractionDuration}ms`,
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
// ? Enhanced: Completion logging with summary
|
|
1268
|
-
log.info(`✅ [ExtractionOrchestrator] Extraction completed`, {
|
|
1269
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1270
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1271
|
-
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1272
|
-
failedValidations: extractionResult.stats.failedValidations,
|
|
1273
|
-
truncated: extractionResult.stats.truncated,
|
|
1274
|
-
truncationReason: extractionResult.stats.truncationReason,
|
|
1275
|
-
dateRange: `from ${dateRangeInfo.start}`,
|
|
1276
|
-
duration: `${extractionDuration}ms`,
|
|
1277
|
-
jobId
|
|
1278
|
-
});
|
|
1279
|
-
|
|
1280
|
-
if (extractionResult.errors && extractionResult.errors.length > 0) {
|
|
1281
|
-
log.warn('Non-fatal extraction errors encountered', {
|
|
1282
|
-
jobId,
|
|
1283
|
-
errorCount: extractionResult.errors.length,
|
|
1284
|
-
sampleErrors: extractionResult.errors.slice(0, 3),
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
// Handle empty result
|
|
1289
|
-
if (rawRecords.length === 0) {
|
|
1290
|
-
log.info('No records to process');
|
|
1291
|
-
|
|
1292
|
-
// Update state even with no records (prevents re-querying empty window)
|
|
1293
|
-
if (updateState) {
|
|
1294
|
-
await kv.set(STATE_KEY, new Date().toISOString());
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
await tracker.markCompleted(jobId, {
|
|
1298
|
-
recordCount: 0,
|
|
1299
|
-
message: 'No records to extract',
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
return {
|
|
1303
|
-
success: true,
|
|
1304
|
-
jobId,
|
|
1305
|
-
recordsExtracted: 0,
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
// ═══════════════════════════════════════════════════════════
|
|
1310
|
-
// STEP 5/8: Transform Data (UniversalMapper)
|
|
1311
|
-
// ═══════════════════════════════════════════════════════════
|
|
1312
|
-
log.info('🔄 [STEP 5/8] Transforming data with UniversalMapper', {
|
|
1313
|
-
jobId,
|
|
1314
|
-
recordCount: rawRecords.length,
|
|
1315
|
-
});
|
|
1316
|
-
|
|
1317
|
-
await tracker.updateJob(jobId, {
|
|
1318
|
-
status: 'processing',
|
|
1319
|
-
stage: 'transformation',
|
|
1320
|
-
message: `Transforming ${rawRecords.length} records`,
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
|
-
const mapper = new UniversalMapper(mappingConfig);
|
|
1324
|
-
const mappingResult = await mapper.map(rawRecords);
|
|
1325
|
-
|
|
1326
|
-
if (!mappingResult.success) {
|
|
1327
|
-
const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
|
|
1328
|
-
log.error('[STEP 5/8] Mapping failed - terminating job', {
|
|
1329
|
-
jobId,
|
|
1330
|
-
errorCount: mappingErrors.length,
|
|
1331
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
1332
|
-
});
|
|
1333
|
-
|
|
1334
|
-
await tracker.markFailed(
|
|
1335
|
-
jobId,
|
|
1336
|
-
new Error(mappingErrors[0] || 'UniversalMapper returned unsuccessful result')
|
|
1337
|
-
);
|
|
1338
|
-
|
|
1339
|
-
return {
|
|
1340
|
-
success: false,
|
|
1341
|
-
jobId,
|
|
1342
|
-
recordsExtracted: 0,
|
|
1343
|
-
error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
const transformedRecords = Array.isArray(mappingResult.data) ? mappingResult.data : [];
|
|
1348
|
-
const mappingErrors = mappingResult.errors || [];
|
|
1349
|
-
|
|
1350
|
-
if (mappingErrors.length > 0) {
|
|
1351
|
-
log.warn('[STEP 5/8] Some records failed transformation', {
|
|
1352
|
-
jobId,
|
|
1353
|
-
errorCount: mappingErrors.length,
|
|
1354
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
1359
|
-
log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
1360
|
-
jobId,
|
|
1361
|
-
skippedFields: mappingResult.skippedFields,
|
|
1362
|
-
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
if (transformedRecords.length === 0) {
|
|
1367
|
-
throw new Error('All records failed transformation');
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
log.info('Transformation complete', {
|
|
1371
|
-
jobId,
|
|
1372
|
-
successful: transformedRecords.length,
|
|
1373
|
-
skippedRecords: rawRecords.length - transformedRecords.length,
|
|
1374
|
-
});
|
|
1375
|
-
|
|
1376
|
-
// ═══════════════════════════════════════════════════════════
|
|
1377
|
-
// STEP 6/8: Generate CSV (CSVParserService)
|
|
1378
|
-
// ═══════════════════════════════════════════════════════════
|
|
1379
|
-
log.info('📄 [STEP 6/8] Generating CSV file', { jobId });
|
|
1380
|
-
|
|
1381
|
-
await tracker.updateJob(jobId, {
|
|
1382
|
-
status: 'processing',
|
|
1383
|
-
stage: 'csv_generation',
|
|
1384
|
-
message: `Generating CSV for ${transformedRecords.length} records`,
|
|
1385
|
-
});
|
|
1386
|
-
|
|
1387
|
-
// Initialize CSVParserService
|
|
1388
|
-
const csvParser = new CSVParserService();
|
|
1389
|
-
|
|
1390
|
-
// Generate CSV content
|
|
1391
|
-
const csvContent = csvParser.stringify(transformedRecords, { headers: true });
|
|
1392
|
-
|
|
1393
|
-
// Generate filename using helper function
|
|
1394
|
-
const fileNamePrefix = activation.getVariable('fileNamePrefix') || 'fulfillments';
|
|
1395
|
-
const extractFileName = (prefix: string) => {
|
|
1396
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1397
|
-
return `${prefix}-${timestamp}.csv`;
|
|
1398
|
-
};
|
|
1399
|
-
const fileName = extractFileName(fileNamePrefix);
|
|
1400
|
-
|
|
1401
|
-
log.info('✅ CSV file generated', {
|
|
1402
|
-
fileName,
|
|
1403
|
-
sizeBytes: csvContent.length,
|
|
1404
|
-
recordCount: transformedRecords.length,
|
|
1405
|
-
});
|
|
1406
|
-
|
|
1407
|
-
// ═══════════════════════════════════════════════════════════
|
|
1408
|
-
// STEP 7/8: Upload to SFTP (SftpDataSource)
|
|
1409
|
-
// ═══════════════════════════════════════════════════════════
|
|
1410
|
-
log.info('📤 [STEP 7/8] Uploading to SFTP', { jobId, fileName });
|
|
1411
|
-
|
|
1412
|
-
await tracker.updateJob(jobId, {
|
|
1413
|
-
status: 'processing',
|
|
1414
|
-
stage: 'sftp_upload',
|
|
1415
|
-
message: `Uploading ${fileName} to SFTP`,
|
|
1416
|
-
});
|
|
1417
|
-
|
|
1418
|
-
// Get SFTP credentials from Versori connection (Basic Auth)
|
|
1419
|
-
// RECOMMENDED: Use activation.connections (already decoded)
|
|
1420
|
-
const allConnections = ctx.activation.connections || [];
|
|
1421
|
-
const sftpConn = allConnections.find(c => c.name === 'versori_ftp_server');
|
|
1422
|
-
|
|
1423
|
-
if (!sftpConn) {
|
|
1424
|
-
throw new Error('SFTP connection "versori_ftp_server" not found');
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
const credential = sftpConn.credentials[0]?.credential;
|
|
1428
|
-
if (!credential?.data?.basicAuth) {
|
|
1429
|
-
throw new Error('SFTP connection not configured with Basic Authentication');
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
const { username, password } = credential.data.basicAuth;
|
|
1433
|
-
// ? Already decoded - no Buffer.from() needed!
|
|
1434
|
-
|
|
1435
|
-
// Get other SFTP config from activation variables
|
|
1436
|
-
const sftpConfig = {
|
|
1437
|
-
host: activation.getVariable('sftpHost'),
|
|
1438
|
-
port: parseInt(activation.getVariable('sftpPort') || '22', 10),
|
|
1439
|
-
username, // From connection
|
|
1440
|
-
password, // From connection
|
|
1441
|
-
privateKey: activation.getVariable('sftpPrivateKey'), // Optional: if using key-based auth
|
|
1442
|
-
};
|
|
1443
|
-
const sftpRemotePath = activation.getVariable('sftpRemotePath') || '/incoming/fulfillments/';
|
|
1444
|
-
|
|
1445
|
-
// Validate SFTP config
|
|
1446
|
-
if (!sftpConfig.host || !sftpConfig.username) {
|
|
1447
|
-
throw new Error('SFTP configuration incomplete: missing host or username from connection');
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
if (!sftpConfig.password && !sftpConfig.privateKey) {
|
|
1451
|
-
throw new Error(
|
|
1452
|
-
'SFTP configuration incomplete: missing password from connection or privateKey from activation'
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
// Initialize SFTP data source
|
|
1457
|
-
// ? VERSORI PLATFORM: Pass native log from context
|
|
1458
|
-
// ? Declare as let for proper disposal in finally block
|
|
1459
|
-
let sftp: SftpDataSource | undefined;
|
|
1460
|
-
|
|
1461
|
-
// Construct SFTP path
|
|
1462
|
-
const sftpPath = `${sftpRemotePath}${fileName}`;
|
|
1463
|
-
|
|
1464
|
-
try {
|
|
1465
|
-
sftp = new SftpDataSource(
|
|
1466
|
-
{
|
|
1467
|
-
type: 'SFTP_CSV',
|
|
1468
|
-
connectionId: 'fulfillments-sftp',
|
|
1469
|
-
name: 'Fulfillments SFTP Upload',
|
|
1470
|
-
settings: {
|
|
1471
|
-
host: sftpConfig.host!,
|
|
1472
|
-
port: sftpConfig.port,
|
|
1473
|
-
username: sftpConfig.username!,
|
|
1474
|
-
password: sftpConfig.password,
|
|
1475
|
-
privateKey: sftpConfig.privateKey,
|
|
1476
|
-
remotePath: sftpRemotePath,
|
|
1477
|
-
filePattern: '*.csv',
|
|
1478
|
-
},
|
|
1479
|
-
},
|
|
1480
|
-
log
|
|
1481
|
-
);
|
|
1482
|
-
|
|
1483
|
-
// Create remote directory if needed
|
|
1484
|
-
await sftp.createDirectory(sftpRemotePath, true);
|
|
1485
|
-
|
|
1486
|
-
// Upload with retry logic (built into SftpDataSource)
|
|
1487
|
-
await sftp.uploadFile(sftpPath, Buffer.from(csvContent, 'utf-8'), {
|
|
1488
|
-
createDirectories: true,
|
|
1489
|
-
});
|
|
1490
|
-
|
|
1491
|
-
log.info('✅ SFTP upload successful', { fileName, remotePath: sftpPath });
|
|
1492
|
-
} finally {
|
|
1493
|
-
// ?? CRITICAL: Always dispose SFTP connection
|
|
1494
|
-
if (sftp) {
|
|
1495
|
-
await sftp.dispose();
|
|
1496
|
-
log.info('🔌 SFTP connection disposed');
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
// ═══════════════════════════════════════════════════════════
|
|
1501
|
-
// STEP 8/8: Update State & Complete Job
|
|
1502
|
-
// ═══════════════════════════════════════════════════════════
|
|
1503
|
-
log.info('💾 [STEP 8/8] Updating state and completing job', { jobId });
|
|
1504
|
-
|
|
1505
|
-
// Calculate new timestamp for next incremental run
|
|
1506
|
-
let newTimestamp: string | undefined;
|
|
1507
|
-
|
|
1508
|
-
if (updateState) {
|
|
1509
|
-
// Find max updatedOn from extracted records
|
|
1510
|
-
const maxUpdatedOn = rawRecords.reduce((max, record) => {
|
|
1511
|
-
const recordTime = new Date(record.updatedOn).getTime();
|
|
1512
|
-
return recordTime > max ? recordTime : max;
|
|
1513
|
-
}, new Date(bufferedLastRunTime).getTime());
|
|
1514
|
-
|
|
1515
|
-
newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
1516
|
-
|
|
1517
|
-
// Store new timestamp (WITHOUT buffer - buffer only applied on read)
|
|
1518
|
-
await kv.set(STATE_KEY, newTimestamp);
|
|
1519
|
-
|
|
1520
|
-
log.info('✅ State updated', {
|
|
1521
|
-
oldTimestamp: bufferedLastRunTime,
|
|
1522
|
-
newTimestamp,
|
|
1523
|
-
});
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
// Mark job as completed
|
|
1527
|
-
await tracker.markCompleted(jobId, {
|
|
1528
|
-
recordCount: transformedRecords.length,
|
|
1529
|
-
fileName,
|
|
1530
|
-
sftpPath,
|
|
1531
|
-
errorCount: mappingErrors.length,
|
|
1532
|
-
isManualOverride,
|
|
1533
|
-
stateUpdated: updateState,
|
|
1534
|
-
newTimestamp,
|
|
1535
|
-
});
|
|
1536
|
-
|
|
1537
|
-
log.info('✅ [COMPLETE] Extraction workflow completed successfully', { jobId });
|
|
1538
|
-
|
|
1539
|
-
return {
|
|
1540
|
-
success: true,
|
|
1541
|
-
jobId,
|
|
1542
|
-
recordsExtracted: transformedRecords.length,
|
|
1543
|
-
fileName,
|
|
1544
|
-
sftpPath,
|
|
1545
|
-
isManualOverride,
|
|
1546
|
-
stateUpdated: updateState,
|
|
1547
|
-
newTimestamp,
|
|
1548
|
-
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1549
|
-
};
|
|
1550
|
-
} catch (error: any) {
|
|
1551
|
-
log.error('❌ [FATAL] Extraction workflow failed', {
|
|
1552
|
-
jobId,
|
|
1553
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1554
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1555
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1556
|
-
recommendation: 'Check logs for detailed error information. Verify activation variables, connection credentials, and SFTP configuration.',
|
|
1557
|
-
});
|
|
1558
|
-
|
|
1559
|
-
// Mark job as failed
|
|
1560
|
-
await tracker.markFailed(jobId, error);
|
|
1561
|
-
|
|
1562
|
-
return {
|
|
1563
|
-
success: false,
|
|
1564
|
-
jobId,
|
|
1565
|
-
recordsExtracted: 0,
|
|
1566
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
```
|
|
1571
|
-
|
|
1572
|
-
---
|
|
1573
|
-
|
|
1574
|
-
## 4. Utility Functions (src/utils/job-id-generator.ts)
|
|
1575
|
-
|
|
1576
|
-
```typescript
|
|
1577
|
-
/**
|
|
1578
|
-
* Job ID Generator
|
|
1579
|
-
*
|
|
1580
|
-
* Generates unique job IDs for tracking extraction workflows
|
|
1581
|
-
*
|
|
1582
|
-
* FORMAT: {TYPE}_{ENTITY}_{YYYYMMDD}_{HHMMSS}_{RANDOM}
|
|
1583
|
-
* Example: SCHEDULED_FULFILLMENTS_20251027_183045_a1b2c3
|
|
1584
|
-
*/
|
|
1585
|
-
|
|
1586
|
-
/**
|
|
1587
|
-
* Generate unique job ID
|
|
1588
|
-
*
|
|
1589
|
-
* @param type - Job trigger type (SCHEDULED, ADHOC, MANUAL)
|
|
1590
|
-
* @param entity - Entity abbreviation (FULFILLMENTS, VP, ORD, PRD)
|
|
1591
|
-
* @returns Unique job ID string
|
|
1592
|
-
*/
|
|
1593
|
-
export function generateJobId(type: string, entity: string): string {
|
|
1594
|
-
const now = new Date();
|
|
1595
|
-
|
|
1596
|
-
// Format: YYYYMMDD
|
|
1597
|
-
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
1598
|
-
|
|
1599
|
-
// Format: HHMMSS
|
|
1600
|
-
const timeStr = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
1601
|
-
|
|
1602
|
-
// Random suffix (6 chars)
|
|
1603
|
-
const randomStr = Math.random().toString(36).substring(2, 8);
|
|
1604
|
-
|
|
1605
|
-
return `${type}_${entity}_${dateStr}_${timeStr}_${randomStr}`;
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
/**
|
|
1609
|
-
* Parse job ID components
|
|
1610
|
-
*/
|
|
1611
|
-
export function parseJobId(jobId: string): {
|
|
1612
|
-
type: string;
|
|
1613
|
-
entity: string;
|
|
1614
|
-
date: string;
|
|
1615
|
-
time: string;
|
|
1616
|
-
random: string;
|
|
1617
|
-
} | null {
|
|
1618
|
-
const parts = jobId.split('_');
|
|
1619
|
-
|
|
1620
|
-
if (parts.length !== 5) {
|
|
1621
|
-
return null;
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
return {
|
|
1625
|
-
type: parts[0],
|
|
1626
|
-
entity: parts[1],
|
|
1627
|
-
date: parts[2],
|
|
1628
|
-
time: parts[3],
|
|
1629
|
-
random: parts[4],
|
|
1630
|
-
};
|
|
1631
|
-
}
|
|
1632
|
-
```
|
|
1633
|
-
|
|
1634
|
-
---
|
|
1635
|
-
|
|
1636
|
-
## 5. Package Configuration
|
|
1637
|
-
|
|
1638
|
-
### package.json
|
|
1639
|
-
|
|
1640
|
-
```json
|
|
1641
|
-
{
|
|
1642
|
-
"name": "fulfillments-to-sftp-csv",
|
|
1643
|
-
"version": "1.0.0",
|
|
1644
|
-
"description": "Extract fulfillments from Fluent Commerce and export to SFTP as CSV",
|
|
1645
|
-
"type": "module",
|
|
1646
|
-
"main": "src/index.ts",
|
|
1647
|
-
"scripts": {
|
|
1648
|
-
"dev": "versori dev",
|
|
1649
|
-
"build": "versori build",
|
|
1650
|
-
"deploy": "versori deploy"
|
|
1651
|
-
},
|
|
1652
|
-
"dependencies": {
|
|
1653
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
1654
|
-
"@versori/run": "latest"
|
|
1655
|
-
},
|
|
1656
|
-
"devDependencies": {
|
|
1657
|
-
"@types/node": "^20.0.0",
|
|
1658
|
-
"typescript": "^5.0.0"
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
```
|
|
1662
|
-
|
|
1663
|
-
### tsconfig.json
|
|
1664
|
-
|
|
1665
|
-
```json
|
|
1666
|
-
{
|
|
1667
|
-
"compilerOptions": {
|
|
1668
|
-
"module": "ES2022",
|
|
1669
|
-
"target": "ES2024",
|
|
1670
|
-
"moduleResolution": "node"
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
```
|
|
1674
|
-
|
|
1675
|
-
---
|
|
1676
|
-
|
|
1677
|
-
## 6. Deployment Instructions
|
|
1678
|
-
|
|
1679
|
-
### Deploy to Versori
|
|
1680
|
-
|
|
1681
|
-
```bash
|
|
1682
|
-
# 1. Install dependencies
|
|
1683
|
-
npm install
|
|
1684
|
-
|
|
1685
|
-
# 2. Test locally (if using Versori CLI)
|
|
1686
|
-
npm run dev
|
|
1687
|
-
|
|
1688
|
-
# 3. Deploy to Versori platform
|
|
1689
|
-
npm run deploy
|
|
1690
|
-
```
|
|
1691
|
-
|
|
1692
|
-
### Configure SFTP Connection
|
|
1693
|
-
|
|
1694
|
-
1. Create a Versori connection named `versori_ftp_server`
|
|
1695
|
-
2. Set **Authentication Type**: `Basic Auth`
|
|
1696
|
-
3. Enter your SFTP **Username** and **Password**
|
|
1697
|
-
|
|
1698
|
-
### Configure Activation Variables
|
|
1699
|
-
|
|
1700
|
-
In Versori platform settings, configure:
|
|
1701
|
-
|
|
1702
|
-
```json
|
|
1703
|
-
{
|
|
1704
|
-
"retailerId": "your-retailer-id",
|
|
1705
|
-
"sftpHost": "sftp.partner.com",
|
|
1706
|
-
"sftpPort": 22,
|
|
1707
|
-
"sftpRemotePath": "/incoming/fulfillments/",
|
|
1708
|
-
"fileNamePrefix": "fulfillments",
|
|
1709
|
-
"pageSize": 200,
|
|
1710
|
-
"maxRecords": 50000,
|
|
1711
|
-
"overlapBufferSeconds": 60,
|
|
1712
|
-
"fulfillmentStatuses": "SHIPPED,DELIVERED",
|
|
1713
|
-
"webhookApiKey": "your-secure-api-key-here"
|
|
1714
|
-
}
|
|
1715
|
-
```
|
|
1716
|
-
|
|
1717
|
-
> **Note:** `sftpUsername` and `sftpPassword` are now stored in the `versori_ftp_server` connection.
|
|
1718
|
-
|
|
1719
|
-
---
|
|
1720
|
-
|
|
1721
|
-
## 7. Testing
|
|
1722
|
-
|
|
1723
|
-
### Test Scheduled Extraction
|
|
1724
|
-
|
|
1725
|
-
The scheduled workflow runs automatically based on cron schedule.
|
|
1726
|
-
|
|
1727
|
-
**Check logs:**
|
|
1728
|
-
|
|
1729
|
-
```
|
|
1730
|
-
[STEP 1/8] Initializing job tracking
|
|
1731
|
-
[STEP 2/8] Initializing Fluent Commerce client
|
|
1732
|
-
[STEP 3/8] Determining date range for extraction
|
|
1733
|
-
[STEP 4/8] Extracting data from Fluent Commerce
|
|
1734
|
-
[STEP 5/8] Transforming data with UniversalMapper
|
|
1735
|
-
[STEP 6/8] Generating CSV file
|
|
1736
|
-
[STEP 7/8] Uploading to SFTP
|
|
1737
|
-
[STEP 8/8] Updating state and completing job
|
|
1738
|
-
```
|
|
1739
|
-
|
|
1740
|
-
### Test Ad hoc Extraction
|
|
1741
|
-
|
|
1742
|
-
```bash
|
|
1743
|
-
# Incremental (uses last sync timestamp)
|
|
1744
|
-
curl -X POST https://api.versori.com/webhooks/fulfillments-adhoc \
|
|
1745
|
-
-H "X-API-Key: your-api-key" \
|
|
1746
|
-
-H "Content-Type: application/json" \
|
|
1747
|
-
-d '{}'
|
|
1748
|
-
|
|
1749
|
-
# Date range override
|
|
1750
|
-
curl -X POST https://api.versori.com/webhooks/fulfillments-adhoc \
|
|
1751
|
-
-H "X-API-Key: your-api-key" \
|
|
1752
|
-
-H "Content-Type: application/json" \
|
|
1753
|
-
-d '{
|
|
1754
|
-
"fromDate": "2025-01-01T00:00:00Z",
|
|
1755
|
-
"toDate": "2025-01-31T23:59:59Z",
|
|
1756
|
-
"updateState": false
|
|
1757
|
-
}'
|
|
1758
|
-
```
|
|
1759
|
-
|
|
1760
|
-
### Test Job Status Query
|
|
1761
|
-
|
|
1762
|
-
```bash
|
|
1763
|
-
curl -X POST https://api.versori.com/webhooks/fulfillments-job-status \
|
|
1764
|
-
-H "X-API-Key: your-api-key" \
|
|
1765
|
-
-H "Content-Type: application/json" \
|
|
1766
|
-
-d '{
|
|
1767
|
-
"jobId": "ADHOC_FULFILLMENTS_20251027_183045_abc123"
|
|
1768
|
-
}'
|
|
1769
|
-
```
|
|
1770
|
-
|
|
1771
|
-
---
|
|
1772
|
-
|
|
1773
|
-
## Testing Checklist
|
|
1774
|
-
|
|
1775
|
-
**Before production deployment:**
|
|
1776
|
-
|
|
1777
|
-
### 1. Schema Validation
|
|
1778
|
-
|
|
1779
|
-
- [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
|
|
1780
|
-
- [ ] Run `npx fc-connect validate-schema --mapping ./config/fulfillments.export.csv.json --schema ./fluent-schema.json`
|
|
1781
|
-
- [ ] Run `npx fc-connect analyze-coverage --mapping ./config/fulfillments.export.csv.json --schema ./fluent-schema.json`
|
|
1782
|
-
- [ ] Verify all `source` paths in mapping exist in GraphQL schema
|
|
1783
|
-
- [ ] Verify query structure matches schema (fields, types, filters)
|
|
1784
|
-
|
|
1785
|
-
### 2. Extraction Testing
|
|
1786
|
-
|
|
1787
|
-
- [ ] Test with small dataset first (maxRecords=10)
|
|
1788
|
-
- [ ] Verify ExtractionOrchestrator pagination works correctly
|
|
1789
|
-
- [ ] Test with multiple pages of data (verify cursor handling)
|
|
1790
|
-
- [ ] Verify date range filtering (updatedOn filter)
|
|
1791
|
-
- [ ] Test empty result handling (no records in date range)
|
|
1792
|
-
- [ ] Verify extraction stops at maxRecords limit
|
|
1793
|
-
|
|
1794
|
-
### 3. Mapping Testing
|
|
1795
|
-
|
|
1796
|
-
- [ ] Verify required fields are populated
|
|
1797
|
-
- [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
|
|
1798
|
-
- [ ] Test custom resolvers with edge cases (if any)
|
|
1799
|
-
- [ ] Verify nested field extraction
|
|
1800
|
-
- [ ] Test with null/missing fields
|
|
1801
|
-
- [ ] Verify mapping error collection works
|
|
1802
|
-
|
|
1803
|
-
### 4. CSV Generation Testing
|
|
1804
|
-
|
|
1805
|
-
- [ ] Verify CSV structure matches expected format
|
|
1806
|
-
- [ ] Test CSV validation against schema (if applicable)
|
|
1807
|
-
- [ ] Verify header row is present and correct
|
|
1808
|
-
- [ ] Test with large datasets (>1000 records)
|
|
1809
|
-
- [ ] Verify UTF-8 encoding
|
|
1810
|
-
- [ ] Test special character handling (commas, quotes, newlines)
|
|
1811
|
-
|
|
1812
|
-
### 5. SFTP Upload Testing
|
|
1813
|
-
|
|
1814
|
-
- [ ] Test SFTP connection and authentication
|
|
1815
|
-
- [ ] Verify file upload to correct path
|
|
1816
|
-
- [ ] Test file naming convention (timestamp format)
|
|
1817
|
-
- [ ] Verify file permissions on SFTP server
|
|
1818
|
-
- [ ] Test upload retry logic (simulate network failure)
|
|
1819
|
-
- [ ] Verify SFTP connection disposal (no connection leaks)
|
|
1820
|
-
|
|
1821
|
-
### 6. State Management Testing
|
|
1822
|
-
|
|
1823
|
-
- [ ] Verify overlap buffer prevents missed records (60-second default)
|
|
1824
|
-
- [ ] Test state recovery after extraction failure
|
|
1825
|
-
- [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
|
|
1826
|
-
- [ ] Test first run with no previous state (uses fallbackStartDate)
|
|
1827
|
-
- [ ] Verify state update only happens on successful upload
|
|
1828
|
-
- [ ] Test manual date override (doesn't update state)
|
|
1829
|
-
|
|
1830
|
-
### 7. Job Tracking Testing
|
|
1831
|
-
|
|
1832
|
-
- [ ] Test job creation with JobTracker
|
|
1833
|
-
- [ ] Verify job status updates at each stage
|
|
1834
|
-
- [ ] Test job completion with metadata
|
|
1835
|
-
- [ ] Test job failure handling
|
|
1836
|
-
- [ ] Query job status via webhook endpoint
|
|
1837
|
-
- [ ] Verify job status persists in KV store
|
|
1838
|
-
|
|
1839
|
-
### 8. Error Handling Testing
|
|
1840
|
-
|
|
1841
|
-
- [ ] Test with invalid GraphQL query
|
|
1842
|
-
- [ ] Test with mapping errors (invalid field paths)
|
|
1843
|
-
- [ ] Test with SFTP connection failures
|
|
1844
|
-
- [ ] Test with authentication failures
|
|
1845
|
-
- [ ] Test with network timeouts
|
|
1846
|
-
- [ ] Verify error logging includes context (jobId, stage, error details)
|
|
1847
|
-
- [ ] Test error threshold logic (if applicable)
|
|
1848
|
-
|
|
1849
|
-
### 9. Staging Environment Testing
|
|
1850
|
-
|
|
1851
|
-
- [ ] Run full extraction in staging environment
|
|
1852
|
-
- [ ] Verify CSV file format with downstream system
|
|
1853
|
-
- [ ] Monitor extraction duration and resource usage
|
|
1854
|
-
- [ ] Test with production-like data volumes
|
|
1855
|
-
- [ ] Verify no performance degradation over time
|
|
1856
|
-
|
|
1857
|
-
### 10. Integration Testing
|
|
1858
|
-
|
|
1859
|
-
- [ ] Test scheduled workflow (cron trigger)
|
|
1860
|
-
- [ ] Test ad hoc webhook trigger
|
|
1861
|
-
- [ ] Test job status query webhook
|
|
1862
|
-
- [ ] Verify activation variables are read correctly
|
|
1863
|
-
- [ ] Test with different extraction modes (incremental, date range)
|
|
1864
|
-
- [ ] End-to-end test: trigger ? extract ? transform ? upload ? verify file
|
|
1865
|
-
|
|
1866
|
-
---
|
|
1867
|
-
## Monitoring & Alerting
|
|
1868
|
-
|
|
1869
|
-
### Success Response Example
|
|
1870
|
-
|
|
1871
|
-
```json
|
|
1872
|
-
{
|
|
1873
|
-
"success": true,
|
|
1874
|
-
"jobId": "SCHEDULED_FUL_20251102_140000_abc123",
|
|
1875
|
-
"recordsExtracted": 1523,
|
|
1876
|
-
"fileName": "fulfillments-2025-11-02T14-00-00-000Z.csv",
|
|
1877
|
-
"sftpPath": "/outbound/fulfillments/fulfillments-2025-11-02T14-00-00-000Z.csv",
|
|
1878
|
-
"metrics": {
|
|
1879
|
-
"extractionDurationMs": 12543,
|
|
1880
|
-
"totalPages": 8,
|
|
1881
|
-
"pageSize": 200,
|
|
1882
|
-
"mappingErrors": 0,
|
|
1883
|
-
"fileSizeBytes": 524288,
|
|
1884
|
-
"uploadDurationMs": 1234
|
|
1885
|
-
},
|
|
1886
|
-
"timestamps": {
|
|
1887
|
-
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
1888
|
-
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
1889
|
-
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
1890
|
-
},
|
|
1891
|
-
"state": {
|
|
1892
|
-
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
1893
|
-
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
1894
|
-
"stateUpdated": true,
|
|
1895
|
-
"overlapBufferSeconds": 60
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
```
|
|
1899
|
-
|
|
1900
|
-
### Error Response Example
|
|
1901
|
-
|
|
1902
|
-
```json
|
|
1903
|
-
{
|
|
1904
|
-
"success": false,
|
|
1905
|
-
"jobId": "ADHOC_FUL_20251102_140500_xyz789",
|
|
1906
|
-
"error": "SFTP upload failed: Connection timeout",
|
|
1907
|
-
"errorCategory": "NETWORK",
|
|
1908
|
-
"recordsExtracted": 0,
|
|
1909
|
-
"stage": "sftp_upload",
|
|
1910
|
-
"details": {
|
|
1911
|
-
"message": "Failed to upload file after 3 retry attempts",
|
|
1912
|
-
"retryAttempts": 3,
|
|
1913
|
-
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
1914
|
-
},
|
|
1915
|
-
"state": {
|
|
1916
|
-
"stateUpdated": false,
|
|
1917
|
-
"willRetryNextRun": true,
|
|
1918
|
-
"note": "State not advanced - next extraction will retry same time window"
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
```
|
|
1922
|
-
|
|
1923
|
-
### Key Metrics to Track
|
|
1924
|
-
|
|
1925
|
-
```typescript
|
|
1926
|
-
const METRICS = {
|
|
1927
|
-
// Extraction Performance
|
|
1928
|
-
extractionDurationMs: Date.now() - extractionStart,
|
|
1929
|
-
recordCount: records.length,
|
|
1930
|
-
pageCount: extractionResult.stats.totalPages,
|
|
1931
|
-
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
1932
|
-
|
|
1933
|
-
// Transformation Performance
|
|
1934
|
-
transformedCount: transformedRecords.length,
|
|
1935
|
-
failedCount: mappingErrors.length,
|
|
1936
|
-
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
1937
|
-
|
|
1938
|
-
// File Generation
|
|
1939
|
-
fileSizeMB: (csvContent.length / (1024 * 1024)).toFixed(2),
|
|
1940
|
-
|
|
1941
|
-
// Upload Performance
|
|
1942
|
-
uploadDurationMs: uploadEnd - uploadStart,
|
|
1943
|
-
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
1944
|
-
|
|
1945
|
-
// State Management
|
|
1946
|
-
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
1947
|
-
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
1948
|
-
};
|
|
1949
|
-
|
|
1950
|
-
log.info('Extraction metrics', metrics);
|
|
1951
|
-
```
|
|
1952
|
-
|
|
1953
|
-
### Alert Thresholds
|
|
1954
|
-
|
|
1955
|
-
```typescript
|
|
1956
|
-
const ALERT_THRESHOLDS = {
|
|
1957
|
-
// Duration Alerts
|
|
1958
|
-
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
1959
|
-
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
1960
|
-
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
1961
|
-
|
|
1962
|
-
// Error Rate Alerts
|
|
1963
|
-
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
1964
|
-
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
1965
|
-
|
|
1966
|
-
// Volume Alerts
|
|
1967
|
-
MAX_RECORDS_PER_RUN: 100000,
|
|
1968
|
-
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
1969
|
-
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
1970
|
-
|
|
1971
|
-
// State Alerts
|
|
1972
|
-
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
1973
|
-
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
1974
|
-
};
|
|
1975
|
-
|
|
1976
|
-
// Check thresholds
|
|
1977
|
-
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
1978
|
-
log.warn('Extraction duration exceeded threshold', {
|
|
1979
|
-
duration: metrics.extractionDurationMs,
|
|
1980
|
-
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
1981
|
-
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
1982
|
-
});
|
|
1983
|
-
}
|
|
1984
|
-
```
|
|
1985
|
-
|
|
1986
|
-
### Monitoring Dashboard Queries
|
|
1987
|
-
|
|
1988
|
-
**Versori Platform Logs Query:**
|
|
1989
|
-
|
|
1990
|
-
```
|
|
1991
|
-
# Successful extractions
|
|
1992
|
-
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
1993
|
-
|
|
1994
|
-
# Failed extractions
|
|
1995
|
-
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
1996
|
-
|
|
1997
|
-
# Performance issues
|
|
1998
|
-
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
1999
|
-
|
|
2000
|
-
# High error rates
|
|
2001
|
-
errorRate:>5
|
|
2002
|
-
|
|
2003
|
-
# State management issues
|
|
2004
|
-
stateUpdated:false AND success:true
|
|
2005
|
-
```
|
|
2006
|
-
|
|
2007
|
-
### Common Issues and Solutions
|
|
2008
|
-
|
|
2009
|
-
**Issue**: "Extraction timeout after 10 minutes"
|
|
2010
|
-
|
|
2011
|
-
- **Cause**: Too many records in single extraction
|
|
2012
|
-
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
2013
|
-
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
2014
|
-
|
|
2015
|
-
**Issue**: "Mapping errors for 50% of records"
|
|
2016
|
-
|
|
2017
|
-
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
2018
|
-
- **Fix**: Run schema validation, update mapping config paths
|
|
2019
|
-
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
2020
|
-
|
|
2021
|
-
**Issue**: "SFTP connection timeout"
|
|
2022
|
-
|
|
2023
|
-
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
2024
|
-
- **Fix**: Check SFTP credentials, verify network connectivity
|
|
2025
|
-
- **Prevention**: Implement connection health checks, monitor connection status
|
|
2026
|
-
|
|
2027
|
-
**Issue**: "State not updating after successful extraction"
|
|
2028
|
-
|
|
2029
|
-
- **Cause**: KV write failure or intentional retry logic
|
|
2030
|
-
- **Fix**: Check KV logs, verify state update code executed
|
|
2031
|
-
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
2032
|
-
|
|
2033
|
-
**Issue**: "First run exceeds record limits"
|
|
2034
|
-
|
|
2035
|
-
- **Cause**: No previous timestamp, fetches all historical records
|
|
2036
|
-
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
2037
|
-
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
2038
|
-
|
|
2039
|
-
**Issue**: "Excessive duplicate records in output"
|
|
2040
|
-
|
|
2041
|
-
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
2042
|
-
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
2043
|
-
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
2044
|
-
|
|
2045
|
-
---
|
|
2046
|
-
|
|
2047
|
-
## Troubleshooting Quick Reference
|
|
2048
|
-
|
|
2049
|
-
| Error Message | Likely Cause | Solution |
|
|
2050
|
-
|--------------|--------------|----------|
|
|
2051
|
-
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
2052
|
-
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
2053
|
-
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
2054
|
-
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
2055
|
-
| "SFTP authentication failed" | Invalid credentials | Verify SFTP credentials in activation variables |
|
|
2056
|
-
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
2057
|
-
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
2058
|
-
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
2059
|
-
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
2060
|
-
| "CSV generation failed" | Format-specific error | Check CSV generation logic, validate output |
|
|
2061
|
-
|
|
2062
|
-
---
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-extract-fulfillments-to-sftp-csv
|
|
3
|
+
canonical_filename: template-extraction-fulfillments-to-sftp-csv.md
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
sdk_version: ^0.1.39
|
|
6
|
+
runtime: versori
|
|
7
|
+
direction: extraction
|
|
8
|
+
source: fluent-graphql
|
|
9
|
+
destination: sftp-csv
|
|
10
|
+
entity: fulfillments
|
|
11
|
+
format: csv
|
|
12
|
+
logging: versori
|
|
13
|
+
status: stable
|
|
14
|
+
features:
|
|
15
|
+
- memory-management
|
|
16
|
+
- enhanced-logging
|
|
17
|
+
- pagination-progress
|
|
18
|
+
- dispose-finally
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Template: Extraction - Fulfillments to SFTP CSV
|
|
22
|
+
|
|
23
|
+
**Template Version:** 2.0.0
|
|
24
|
+
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
25
|
+
**Last Updated:** 2025-01-24
|
|
26
|
+
**Deployment Target:** Versori Platform
|
|
27
|
+
|
|
28
|
+
**🆕 Version 2.0.0 Enhancements:**
|
|
29
|
+
- ✅ **Memory Management** - Clear large result sets after processing batches
|
|
30
|
+
- ✅ **Enhanced Logging** - Pagination progress tracking with emoji indicators (📊, 📥, ✅)
|
|
31
|
+
- ✅ **Pagination Progress** - Real-time page-by-page progress logging with metrics
|
|
32
|
+
- ✅ **Resource Cleanup** - SFTP dispose in finally blocks prevents connection leaks
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 📚 STEP 1: Load These Docs (Human Checklist)
|
|
37
|
+
|
|
38
|
+
1. REQUIRED (load all)
|
|
39
|
+
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
40
|
+
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
41
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
42
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
43
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
44
|
+
- [ ] fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
45
|
+
|
|
46
|
+
Copy-paste list (open these):
|
|
47
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
48
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
49
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
50
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
51
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
52
|
+
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 📋 STEP 2: Tell Your AI (Prompt)
|
|
57
|
+
|
|
58
|
+
Copy/paste this prompt into your AI tool after loading the documentation above:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
I need a Versori scheduled extractor that:
|
|
62
|
+
|
|
63
|
+
1) Queries Fluent Commerce GraphQL for Fulfillments with auto-pagination
|
|
64
|
+
2) Supports incremental runs via KV state (with an overlap buffer)
|
|
65
|
+
3) Transforms results using UniversalMapper per mapping JSON
|
|
66
|
+
4) Generates CSV and uploads to SFTP
|
|
67
|
+
5) Tracks progress with JobTracker and exposes a job-status webhook
|
|
68
|
+
6) Uses native Versori log (LoggingService removed - use native log)
|
|
69
|
+
|
|
70
|
+
Use the loaded docs to fill in SDK specifics and best practices.
|
|
71
|
+
Keep the structure identical to the template; only adapt where needed.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 📋 Template Overview
|
|
77
|
+
|
|
78
|
+
This connector runs on the Versori platform. Most operational settings (Fluent account/connection, SFTP credentials, schedule, page size/limits) are configured via activation variables. Data shape and logic (mapping JSON, CSV structure, GraphQL selection set/filters, validators/resolvers) are adjusted in code as needed. It extracts fulfillments from Fluent Commerce via GraphQL, transforms the data into CSV, and uploads the result to SFTP.
|
|
79
|
+
|
|
80
|
+
### What This Template Does
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
84
|
+
│ EXTRACTION WORKFLOW │
|
|
85
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
86
|
+
|
|
87
|
+
1. TRIGGER
|
|
88
|
+
├─ Scheduled (Cron): Runs automatically every 4 hours
|
|
89
|
+
├─ Ad hoc (Webhook): Manual trigger with optional date override
|
|
90
|
+
└─ Status Query (Webhook): Check job progress
|
|
91
|
+
|
|
92
|
+
2. EXTRACT (ExtractionOrchestrator)
|
|
93
|
+
├─ Query Fluent GraphQL API for fulfillments
|
|
94
|
+
├─ Auto-pagination (handles large datasets)
|
|
95
|
+
├─ Apply date filters (incremental or manual range)
|
|
96
|
+
└─ Validate each record (optional)
|
|
97
|
+
|
|
98
|
+
3. TRANSFORM (UniversalMapper)
|
|
99
|
+
├─ Map GraphQL fields to CSV schema
|
|
100
|
+
├─ Apply SDK resolvers (trim, uppercase, formatDateShort, etc.)
|
|
101
|
+
├─ Extract nested data (order.customer.email)
|
|
102
|
+
└─ Handle transformation errors
|
|
103
|
+
|
|
104
|
+
4. GENERATE CSV (CSVParserService)
|
|
105
|
+
├─ Convert transformed records to CSV
|
|
106
|
+
├─ Include headers
|
|
107
|
+
├─ Handle special characters
|
|
108
|
+
└─ Generate timestamped filename
|
|
109
|
+
|
|
110
|
+
5. UPLOAD (SftpDataSource)
|
|
111
|
+
├─ Connect to SFTP server
|
|
112
|
+
├─ Upload CSV file with retry logic
|
|
113
|
+
├─ Verify upload success
|
|
114
|
+
└─ CRITICAL: Call dispose() in finally block
|
|
115
|
+
|
|
116
|
+
6. TRACK JOB (JobTracker)
|
|
117
|
+
├─ Create job with unique ID
|
|
118
|
+
├─ Update status at each step
|
|
119
|
+
├─ Store job result in KV
|
|
120
|
+
└─ Enable status queries via webhook
|
|
121
|
+
|
|
122
|
+
7. UPDATE STATE (VersoriKVAdapter)
|
|
123
|
+
├─ Calculate max updatedOn from records
|
|
124
|
+
├─ Store timestamp for next incremental run
|
|
125
|
+
├─ Apply overlap buffer (prevent missed records)
|
|
126
|
+
└─ Skip update if manual date override
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Key Features
|
|
130
|
+
|
|
131
|
+
- Job tracking with status queries
|
|
132
|
+
- Execution modes: scheduled, ad hoc, status query
|
|
133
|
+
- Uses ExtractionOrchestrator, UniversalMapper, JobTracker, CSVParserService
|
|
134
|
+
- Error handling, retry logic
|
|
135
|
+
- Reusable services suitable for similar use cases
|
|
136
|
+
|
|
137
|
+
Note: JobTracker persists stage/status to Versori KV for visibility, job-status webhooks, and auditing. Recommended for production multi-step flows; can be skipped for trivial single-step utilities.
|
|
138
|
+
|
|
139
|
+
### 📦 Package Information
|
|
140
|
+
|
|
141
|
+
**SDK:** [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm install @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
**Templates are designed for direct deployment; customize via activation variables.**
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 📦 SDK Imports (Verified - Versori Optimized)
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { Buffer } from 'node:buffer';
|
|
157
|
+
import {
|
|
158
|
+
createClient,
|
|
159
|
+
ExtractionOrchestrator,
|
|
160
|
+
JobTracker,
|
|
161
|
+
UniversalMapper,
|
|
162
|
+
CSVParserService,
|
|
163
|
+
SftpDataSource,
|
|
164
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
165
|
+
|
|
166
|
+
import { schedule, webhook, http, fn } from '@versori/run';
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Note:** All imports are from actual SDK exports - this code compiles and runs as-is.
|
|
170
|
+
|
|
171
|
+
**? VERSORI PLATFORM - Use Native Logs:**
|
|
172
|
+
|
|
173
|
+
- Use `log` from context: `const { log } = ctx;`
|
|
174
|
+
- Don't import or use LoggingService for Versori connectors
|
|
175
|
+
- Native Versori logs are simpler and automatically integrated with platform monitoring
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## ⚙️ Configuration
|
|
180
|
+
|
|
181
|
+
### SFTP Connection Setup (Recommended)
|
|
182
|
+
|
|
183
|
+
**? BEST PRACTICE:** Store SFTP credentials in a Versori connection object with Basic Auth:
|
|
184
|
+
|
|
185
|
+
**Connection Configuration:**
|
|
186
|
+
|
|
187
|
+
1. In Versori platform, create a connection named `versori_ftp_server`
|
|
188
|
+
2. Set **Authentication Type**: `Basic Auth`
|
|
189
|
+
3. Enter **Username**: Your SFTP username
|
|
190
|
+
4. Enter **Password**: Your SFTP password
|
|
191
|
+
5. The SDK will automatically decode the credentials using:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// Get SFTP credentials from Versori connection (Basic Auth)
|
|
195
|
+
// RECOMMENDED: Use activation.connections (already decoded)
|
|
196
|
+
const allConnections = ctx.activation.connections || [];
|
|
197
|
+
const sftpConn = allConnections.find(c => c.name === 'versori_ftp_server');
|
|
198
|
+
|
|
199
|
+
if (!sftpConn) {
|
|
200
|
+
throw new Error('SFTP connection "versori_ftp_server" not found');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const credential = sftpConn.credentials[0]?.credential;
|
|
204
|
+
if (!credential?.data?.basicAuth) {
|
|
205
|
+
throw new Error('SFTP connection not configured with Basic Authentication');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { username, password } = credential.data.basicAuth;
|
|
209
|
+
// ? Already decoded - no Buffer.from() needed!
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Why use connections instead of activation variables?**
|
|
213
|
+
|
|
214
|
+
- ✅ Credentials stored securely in Versori vault
|
|
215
|
+
- ✅ Connection can be reused across workflows
|
|
216
|
+
- ✅ No need to manage sensitive data in activation variables
|
|
217
|
+
- ✅ Easier credential rotation
|
|
218
|
+
- ✅ Better separation of concerns (config vs secrets)
|
|
219
|
+
- ✅ Follows industry security best practices
|
|
220
|
+
|
|
221
|
+
### Activation Variables
|
|
222
|
+
|
|
223
|
+
**Configuration is driven by activation variables - modify these instead of code:**
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"retailerId": "your-retailer-id",
|
|
228
|
+
"sftpHost": "sftp.partner.com",
|
|
229
|
+
"sftpPort": 22,
|
|
230
|
+
"sftpPrivateKey": "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----",
|
|
231
|
+
"sftpRemotePath": "/incoming/fulfillments/",
|
|
232
|
+
"fileNamePrefix": "fulfillments",
|
|
233
|
+
"pageSize": 200,
|
|
234
|
+
"maxRecords": 10000,
|
|
235
|
+
"overlapBufferSeconds": 60,
|
|
236
|
+
"fulfillmentStatuses": "SHIPPED,DELIVERED",
|
|
237
|
+
"validateConnectionOnStart": "false"
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
> **Note:** `sftpUsername` and `sftpPassword` are fetched from the `versori_ftp_server` connection (see above).
|
|
242
|
+
|
|
243
|
+
Note: Webhook security is enforced via Versori connections. Configure auth on the connection and reference it in `webhook({ connection: '...' })`.
|
|
244
|
+
|
|
245
|
+
### Variable Explanations
|
|
246
|
+
|
|
247
|
+
| Variable | Purpose | Default | Customization Hints |
|
|
248
|
+
| ---------------------------- | --------------------------- | ------------------------- | -------------------------------- |
|
|
249
|
+
| `retailerId` | Fluent retailer ID | - | Required - your retailer ID |
|
|
250
|
+
| **SFTP Credentials** | _From Connection_ | | _See connection setup above_ |
|
|
251
|
+
| `sftpHost` | SFTP server hostname | - | Required - partner SFTP server |
|
|
252
|
+
| `sftpPort` | SFTP server port | `22` | Standard SFTP port |
|
|
253
|
+
| `sftpPrivateKey` | SFTP private key (optional) | - | Alternative to password auth |
|
|
254
|
+
| `sftpRemotePath` | SFTP upload directory | `/incoming/fulfillments/` | Customize folder structure |
|
|
255
|
+
| `fileNamePrefix` | CSV filename prefix | `fulfillments` | Customize naming convention |
|
|
256
|
+
| `pageSize` | Records per GraphQL page | `200` | Increase for fewer API calls |
|
|
257
|
+
| `maxRecords` | Total extraction limit | `50000` | Safety limit - adjust for volume |
|
|
258
|
+
| `overlapBufferSeconds` | Incremental safety window | `60` | Prevents missed records |
|
|
259
|
+
| `fulfillmentStatuses` | Status filter (comma-sep) | `SHIPPED,DELIVERED` | Filter by fulfillment status |
|
|
260
|
+
| `validateConnectionOnStart` | Validate auth on startup | `false` | `true` = fail-fast, `false` = fast startup |
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### 📋 State Management & Incremental Sync
|
|
265
|
+
|
|
266
|
+
**How incremental sync works:**
|
|
267
|
+
|
|
268
|
+
1. **First Run:** Uses `DEFAULT_FALLBACK` date (2024-01-01T00:00:00Z)
|
|
269
|
+
2. **Subsequent Runs:** Uses `lastFulfillmentSync` timestamp from KV store
|
|
270
|
+
3. **Overlap Buffer:** Subtracts 60 seconds to catch late-arriving records
|
|
271
|
+
4. **State Update:** After successful upload, stores max `updatedOn` for next run
|
|
272
|
+
|
|
273
|
+
**Incremental vs Manual Modes:**
|
|
274
|
+
|
|
275
|
+
| Mode | When to Use | State Update | Payload Example |
|
|
276
|
+
| --------------------------- | -------------------- | ------------ | -------------------------------------------------------------- |
|
|
277
|
+
| **Incremental** | Daily scheduled sync | ? Yes | `{}` (empty - uses last sync) |
|
|
278
|
+
| **Manual Range** | Historical backfill | ❌ No | `{ "fromDate": "2024-01-01T00:00:00Z", "updateState": false }` |
|
|
279
|
+
| **Manual Range with State** | One-time catch-up | ? Yes | `{ "fromDate": "2024-01-01T00:00:00Z", "updateState": true }` |
|
|
280
|
+
|
|
281
|
+
**Why overlap buffer?**
|
|
282
|
+
|
|
283
|
+
Records updated near the sync time might not appear in the query due to:
|
|
284
|
+
|
|
285
|
+
- Clock drift between systems
|
|
286
|
+
- Transaction timing in the database
|
|
287
|
+
- GraphQL query execution timing
|
|
288
|
+
|
|
289
|
+
The 60-second buffer ensures these edge-case records are captured in the next run, preventing data loss.
|
|
290
|
+
|
|
291
|
+
**When to skip state update (`updateState: false`):**
|
|
292
|
+
|
|
293
|
+
- Historical backfills (don't affect ongoing incremental sync)
|
|
294
|
+
- Testing/debugging specific date ranges
|
|
295
|
+
- Reprocessing old data without changing the sync pointer
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
### 📁 SFTP Configuration
|
|
300
|
+
|
|
301
|
+
**Note:** SFTP credentials and paths are configured separately.
|
|
302
|
+
|
|
303
|
+
- **SFTP Host:** Configured via `sftpHost` activation variable
|
|
304
|
+
- **SFTP Path:** Configured via `sftpRemotePath` activation variable (e.g., `/incoming/fulfillments/`)
|
|
305
|
+
- **Filename Pattern:** Configured via `fileNamePrefix` activation variable (e.g., `fulfillments`)
|
|
306
|
+
|
|
307
|
+
**The workflow generates files like:** `{sftpRemotePath}{fileNamePrefix}-{timestamp}.csv`
|
|
308
|
+
|
|
309
|
+
**Example:** With `sftpRemotePath="/incoming/fulfillments/"` and `fileNamePrefix="fulfillments"`, a generated file will look like:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
/incoming/fulfillments/fulfillments-2025-10-27T18-30-45Z.csv
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Note:** To change the upload folder, modify the `sftpRemotePath` activation variable (not in code).
|
|
316
|
+
|
|
317
|
+
**CRITICAL:** Always call `sftp.dispose()` in a finally block to close connections properly.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
### Auto-pagination and limits (ExtractionOrchestrator)
|
|
322
|
+
|
|
323
|
+
**What:** ExtractionOrchestrator handles GraphQL Relay cursor-based pagination automatically.
|
|
324
|
+
|
|
325
|
+
**Why:** Prevents manual pagination loop code, handles large datasets efficiently.
|
|
326
|
+
|
|
327
|
+
**How:** You configure `pageSize` and `maxRecords`; the orchestrator injects `$first` and `$after` variables automatically and loops until `pageInfo.hasNextPage === false` or `maxRecords` is reached.
|
|
328
|
+
|
|
329
|
+
**Critical:** Your query MUST include `edges { cursor }` and `pageInfo { hasNextPage }` fields, or pagination will fail.
|
|
330
|
+
|
|
331
|
+
#### Configuration Parameters
|
|
332
|
+
|
|
333
|
+
| Parameter | Purpose | Example | Effect |
|
|
334
|
+
| ---------------- | ------------------------------ | --------------------------- | -------------------------- |
|
|
335
|
+
| `pageSize` | Records per GraphQL request | `200` | Controls `first` variable |
|
|
336
|
+
| `maxRecords` | Total extraction limit | `50000` | Hard stop across all pages |
|
|
337
|
+
| `resultPath` | Where records live in response | `"fulfillments.edges.node"` | Flattening path |
|
|
338
|
+
| `validateItem()` | Optional record filter | `(item) => !!item.ref` | Skips invalid records |
|
|
339
|
+
|
|
340
|
+
#### GraphQL Query Requirements
|
|
341
|
+
|
|
342
|
+
**Your query MUST include these pagination fields:**
|
|
343
|
+
|
|
344
|
+
```graphql
|
|
345
|
+
query GetFulfillments(
|
|
346
|
+
$retailerId: ID!
|
|
347
|
+
$dateRangeFilter: DateRange
|
|
348
|
+
$statuses: [String!]
|
|
349
|
+
$first: Int! # ← Orchestrator injects this
|
|
350
|
+
$after: String # ← Orchestrator injects this
|
|
351
|
+
) {
|
|
352
|
+
fulfillments(
|
|
353
|
+
retailerId: $retailerId
|
|
354
|
+
updatedOn: $dateRangeFilter
|
|
355
|
+
status: $statuses
|
|
356
|
+
first: $first # ← Pagination page size
|
|
357
|
+
after: $after # ← Pagination cursor
|
|
358
|
+
) {
|
|
359
|
+
edges {
|
|
360
|
+
# ← REQUIRED: Relay connection structure
|
|
361
|
+
node {
|
|
362
|
+
# ← REQUIRED: Actual records here
|
|
363
|
+
id
|
|
364
|
+
ref
|
|
365
|
+
# ... your fields
|
|
366
|
+
}
|
|
367
|
+
cursor # ← REQUIRED: Orchestrator uses this for next page
|
|
368
|
+
}
|
|
369
|
+
pageInfo {
|
|
370
|
+
# ← REQUIRED: Orchestrator checks this
|
|
371
|
+
hasNextPage # ← REQUIRED: Tells orchestrator to continue
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
### Pattern: Backward Pagination (Optional - Advanced)
|
|
380
|
+
|
|
381
|
+
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
382
|
+
|
|
383
|
+
**When to Use**:
|
|
384
|
+
|
|
385
|
+
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
386
|
+
- ✅ Time-bounded reverse traversal for auditing
|
|
387
|
+
- ✅ Display newest-first in UI/reports
|
|
388
|
+
- ❌ **Don't use for standard incremental sync** - use forward pagination (default)
|
|
389
|
+
|
|
390
|
+
**GraphQL Query Requirements**:
|
|
391
|
+
|
|
392
|
+
Your query must support backward pagination by including `$last` and `$before`:
|
|
393
|
+
|
|
394
|
+
```graphql
|
|
395
|
+
query GetData(
|
|
396
|
+
$retailerId: ID!
|
|
397
|
+
$first: Int # For forward pagination
|
|
398
|
+
$after: String # For forward pagination
|
|
399
|
+
$last: Int # For backward pagination
|
|
400
|
+
$before: String # For backward pagination
|
|
401
|
+
) {
|
|
402
|
+
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
403
|
+
edges {
|
|
404
|
+
cursor # ? REQUIRED
|
|
405
|
+
node {
|
|
406
|
+
id
|
|
407
|
+
createdAt
|
|
408
|
+
# ... other fields
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
pageInfo {
|
|
412
|
+
hasNextPage # For forward
|
|
413
|
+
hasPreviousPage # ? REQUIRED for backward
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Implementation**:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
// Backward pagination - newest records first
|
|
423
|
+
const result = await orchestrator.extract({
|
|
424
|
+
query: YOUR_QUERY,
|
|
425
|
+
resultPath: 'data.edges.node',
|
|
426
|
+
variables: {
|
|
427
|
+
retailerId,
|
|
428
|
+
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
429
|
+
// ❌ Don't include last/before - orchestrator injects them
|
|
430
|
+
},
|
|
431
|
+
pageSize: 200,
|
|
432
|
+
direction: 'backward', // ? Enable reverse pagination
|
|
433
|
+
maxRecords: 10000,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Records are returned in reverse chronological order
|
|
437
|
+
console.log(result.data[0].createdAt); // Newest
|
|
438
|
+
console.log(result.data[result.data.length - 1].createdAt); // Oldest (within range)
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Key Differences from Forward Pagination**:
|
|
442
|
+
|
|
443
|
+
| Aspect | Forward (Default) | Backward |
|
|
444
|
+
| ---------------------- | -------------------------------- | ----------------------- |
|
|
445
|
+
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
446
|
+
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
447
|
+
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
448
|
+
| **Cursor Source** | Last edge of page | First edge of page |
|
|
449
|
+
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
450
|
+
|
|
451
|
+
**Important Notes**:
|
|
452
|
+
|
|
453
|
+
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
454
|
+
|
|
455
|
+
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
456
|
+
|
|
457
|
+
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
458
|
+
|
|
459
|
+
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
460
|
+
|
|
461
|
+
**Example: Extract Latest 1000 Orders**
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
const latestOrders = await orchestrator.extract({
|
|
465
|
+
query: ORDERS_QUERY,
|
|
466
|
+
resultPath: 'orders.edges.node',
|
|
467
|
+
variables: {
|
|
468
|
+
retailerId,
|
|
469
|
+
statuses: ['BOOKED', 'ALLOCATED'],
|
|
470
|
+
},
|
|
471
|
+
direction: 'backward', // Start from newest
|
|
472
|
+
maxRecords: 1000, // Stop after 1000 records
|
|
473
|
+
pageSize: 100, // 100 per page = 10 pages
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// latestOrders.data[0] is the newest order
|
|
477
|
+
// latestOrders.data[999] is the 1000th newest order
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**When to Use Forward vs Backward**:
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
// ? Forward (default) - For incremental sync
|
|
484
|
+
const incrementalData = await orchestrator.extract({
|
|
485
|
+
query: YOUR_QUERY,
|
|
486
|
+
resultPath: 'data.edges.node',
|
|
487
|
+
variables: {
|
|
488
|
+
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
489
|
+
},
|
|
490
|
+
// direction defaults to 'forward'
|
|
491
|
+
// Processes oldest → newest for proper sequencing
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// ? Backward - For "latest N records" use cases
|
|
495
|
+
const latestData = await orchestrator.extract({
|
|
496
|
+
query: YOUR_QUERY,
|
|
497
|
+
resultPath: 'data.edges.node',
|
|
498
|
+
direction: 'backward',
|
|
499
|
+
maxRecords: 100, // Just get latest 100
|
|
500
|
+
// Gets newest → oldest
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Pagination Variables Reference**:
|
|
505
|
+
|
|
506
|
+
| Variable | Forward | Backward | Injected By | Notes |
|
|
507
|
+
| -------- | ----------- | ----------- | ------------ | ------------------------ |
|
|
508
|
+
| `first` | ? Used | ❌ Not used | Orchestrator | From `pageSize` |
|
|
509
|
+
| `after` | ? Used | ❌ Not used | Orchestrator | From cursor (last edge) |
|
|
510
|
+
| `last` | ❌ Not used | ? Used | Orchestrator | From `pageSize` |
|
|
511
|
+
| `before` | ❌ Not used | ? Used | Orchestrator | From cursor (first edge) |
|
|
512
|
+
|
|
513
|
+
**Common Mistakes to Avoid**:
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
// ❌ WRONG - Don't pass pagination variables
|
|
517
|
+
const result = await orchestrator.extract({
|
|
518
|
+
variables: {
|
|
519
|
+
last: 200, // ❌ Orchestrator will override this
|
|
520
|
+
before: cursor, // ❌ Orchestrator manages cursor
|
|
521
|
+
},
|
|
522
|
+
direction: 'backward',
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// ? CORRECT - Let orchestrator inject pagination
|
|
526
|
+
const result = await orchestrator.extract({
|
|
527
|
+
variables: {
|
|
528
|
+
retailerId, // ? Your business variables only
|
|
529
|
+
},
|
|
530
|
+
pageSize: 200, // ? Orchestrator uses this for last/before
|
|
531
|
+
direction: 'backward',
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## 📄 Mapping Configuration
|
|
538
|
+
|
|
539
|
+
**File:** `config/fulfillments.export.csv.json`
|
|
540
|
+
|
|
541
|
+
```json
|
|
542
|
+
{
|
|
543
|
+
"name": "fulfillments.export.csv",
|
|
544
|
+
"version": "1.0.0",
|
|
545
|
+
"description": "Fulfillments → CSV Export Mapping",
|
|
546
|
+
"fields": {
|
|
547
|
+
"order_number": {
|
|
548
|
+
"source": "orderRef",
|
|
549
|
+
"required": true,
|
|
550
|
+
"resolver": "sdk.trim"
|
|
551
|
+
},
|
|
552
|
+
"fulfillment_id": {
|
|
553
|
+
"source": "ref",
|
|
554
|
+
"required": true,
|
|
555
|
+
"resolver": "sdk.trim"
|
|
556
|
+
},
|
|
557
|
+
"tracking_number": {
|
|
558
|
+
"source": "trackingNumber",
|
|
559
|
+
"required": false,
|
|
560
|
+
"resolver": "sdk.trim"
|
|
561
|
+
},
|
|
562
|
+
"carrier": {
|
|
563
|
+
"source": "carrier",
|
|
564
|
+
"required": false,
|
|
565
|
+
"resolver": "sdk.uppercase"
|
|
566
|
+
},
|
|
567
|
+
"service_level": {
|
|
568
|
+
"source": "serviceLevel",
|
|
569
|
+
"required": false,
|
|
570
|
+
"resolver": "sdk.trim"
|
|
571
|
+
},
|
|
572
|
+
"status": {
|
|
573
|
+
"source": "status",
|
|
574
|
+
"required": true,
|
|
575
|
+
"resolver": "sdk.uppercase"
|
|
576
|
+
},
|
|
577
|
+
"ship_date": {
|
|
578
|
+
"source": "shippedOn",
|
|
579
|
+
"required": false,
|
|
580
|
+
"resolver": "sdk.formatDateShort"
|
|
581
|
+
},
|
|
582
|
+
"delivery_date": {
|
|
583
|
+
"source": "deliveredOn",
|
|
584
|
+
"required": false,
|
|
585
|
+
"resolver": "sdk.formatDateShort"
|
|
586
|
+
},
|
|
587
|
+
"customer_email": {
|
|
588
|
+
"source": "order.customer.email",
|
|
589
|
+
"required": false,
|
|
590
|
+
"resolver": "sdk.trim"
|
|
591
|
+
},
|
|
592
|
+
"last_updated": {
|
|
593
|
+
"source": "updatedOn",
|
|
594
|
+
"required": true,
|
|
595
|
+
"resolver": "sdk.toString"
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**AI Customization Hints:**
|
|
602
|
+
|
|
603
|
+
- Add fields: Copy existing field config, change `source` path
|
|
604
|
+
- Remove fields: Delete field from config
|
|
605
|
+
- Change resolvers: Replace `sdk.trim` with `sdk.uppercase`, etc.
|
|
606
|
+
- Nested fields: Use dot notation like `order.customer.email`
|
|
607
|
+
|
|
608
|
+
Note: Customize mapping by editing the JSON above; prefer built-in resolvers. See SDK Universal Mapping guide for advanced usage.
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Versori Workflows Structure
|
|
613
|
+
|
|
614
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
615
|
+
|
|
616
|
+
**Trigger Types:**
|
|
617
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
618
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
619
|
+
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
620
|
+
|
|
621
|
+
**Execution Steps (chained to triggers):**
|
|
622
|
+
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
623
|
+
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
624
|
+
|
|
625
|
+
### Recommended Project Structure
|
|
626
|
+
|
|
627
|
+
```
|
|
628
|
+
fulfillments-extraction/
|
|
629
|
+
├── index.ts # Entry point - exports all workflows
|
|
630
|
+
└── src/
|
|
631
|
+
├── workflows/
|
|
632
|
+
│ ├── scheduled/
|
|
633
|
+
│ │ └── daily-fulfillments-extraction.ts # Scheduled: Daily extraction
|
|
634
|
+
│ │
|
|
635
|
+
│ └── webhook/
|
|
636
|
+
│ ├── adhoc-fulfillments-extraction.ts # Webhook: Manual trigger
|
|
637
|
+
│ └── job-status-check.ts # Webhook: Status query
|
|
638
|
+
│
|
|
639
|
+
├── services/
|
|
640
|
+
│ └── fulfillments-extraction.service.ts # Shared orchestration logic (reusable)
|
|
641
|
+
│
|
|
642
|
+
└── config/
|
|
643
|
+
└── fulfillments.export.csv.json # Mapping configuration
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
### 1. Entry Point (`index.ts`)
|
|
648
|
+
|
|
649
|
+
**Pattern:** MemoryInterpreter (export all workflows from single entry point)
|
|
650
|
+
|
|
651
|
+
**Benefits:**
|
|
652
|
+
- ✅ Versori platform discovers all workflows automatically
|
|
653
|
+
- ✅ Clean separation of concerns (entry point vs workflow logic)
|
|
654
|
+
- ✅ Easy to add/remove workflows (just update exports)
|
|
655
|
+
- ✅ Single source of truth for workflow registration
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
// ═══════════════════════════════════════════════════════════
|
|
659
|
+
// 🚀 VERSORI FULFILLMENTS EXTRACTION
|
|
660
|
+
// ═══════════════════════════════════════════════════════════
|
|
661
|
+
/**
|
|
662
|
+
* Entry Point - Registers all workflows with Versori platform
|
|
663
|
+
*
|
|
664
|
+
* This file is the entry point for the Versori deployment.
|
|
665
|
+
* It registers three workflows:
|
|
666
|
+
* 1. Scheduled extraction (runs automatically)
|
|
667
|
+
* 2. Ad hoc webhook (manual trigger)
|
|
668
|
+
* 3. Job status webhook (query progress)
|
|
669
|
+
*
|
|
670
|
+
* AI CUSTOMIZATION:
|
|
671
|
+
* - Add new workflows by importing and registering them
|
|
672
|
+
* - Remove workflows by commenting out registration
|
|
673
|
+
* - Change workflow names in import statements
|
|
674
|
+
*/
|
|
675
|
+
// ═══════════════════════════════════════════════════════════
|
|
676
|
+
|
|
677
|
+
import {
|
|
678
|
+
scheduledFulfillmentsExtraction,
|
|
679
|
+
adhocFulfillmentsExtraction,
|
|
680
|
+
fulfillmentsJobStatus,
|
|
681
|
+
} from './workflows/fulfillments-extraction';
|
|
682
|
+
|
|
683
|
+
// Register workflows with Versori platform
|
|
684
|
+
// The platform will expose these as executable endpoints
|
|
685
|
+
|
|
686
|
+
export {
|
|
687
|
+
scheduledFulfillmentsExtraction, // Cron-based auto-run
|
|
688
|
+
adhocFulfillmentsExtraction, // Manual webhook trigger
|
|
689
|
+
fulfillmentsJobStatus, // Job status query
|
|
690
|
+
};
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
### 2. Workflows (src/workflows/fulfillments-extraction.ts)
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
/**
|
|
699
|
+
* Workflows - Defines 3 execution patterns for fulfillments extraction
|
|
700
|
+
*
|
|
701
|
+
* WORKFLOW 1: Scheduled (Cron) - Runs automatically every 4 hours
|
|
702
|
+
* WORKFLOW 2: Ad hoc (Webhook) - Manual trigger with optional date override
|
|
703
|
+
* WORKFLOW 3: Job Status (Webhook) - Query job progress
|
|
704
|
+
*
|
|
705
|
+
* AI CUSTOMIZATION HINTS:
|
|
706
|
+
* - Change schedule: Modify cron expression in schedule()
|
|
707
|
+
* - Add filtering: Pass additional params to executeFulfillmentExtraction()
|
|
708
|
+
* - Change response format: Modify return object structure
|
|
709
|
+
*/
|
|
710
|
+
|
|
711
|
+
import { schedule, webhook, http, fn } from '@versori/run';
|
|
712
|
+
import { executeFulfillmentExtraction, getJobStatus } from '../services/extraction-orchestration';
|
|
713
|
+
import { generateJobId } from '../utils/job-id-generator';
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* WORKFLOW 1: Scheduled Extraction
|
|
717
|
+
*
|
|
718
|
+
* Purpose: Automated extraction every 4 hours for incremental sync
|
|
719
|
+
* Trigger: Cron schedule (every 4 hours at minute 0)
|
|
720
|
+
* State Update: Always updates lastSync timestamp
|
|
721
|
+
*
|
|
722
|
+
* AI CUSTOMIZATION:
|
|
723
|
+
* - Change schedule: Modify the cron expression string
|
|
724
|
+
* Examples:
|
|
725
|
+
* - Every hour: '0 * * * *'
|
|
726
|
+
* - Every 30 min: '*/30 * * * *'
|
|
727
|
+
* - Daily at 2 AM: '0 2 * * *'
|
|
728
|
+
*/
|
|
729
|
+
export const scheduledFulfillmentsExtraction = schedule(
|
|
730
|
+
'fulfillments-scheduled',
|
|
731
|
+
'0 */4 * * *' // ← CUSTOMIZE: Cron expression
|
|
732
|
+
)
|
|
733
|
+
.then(
|
|
734
|
+
http('execute-scheduled-extraction', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
735
|
+
const { log } = ctx;
|
|
736
|
+
|
|
737
|
+
// Generate unique job ID for tracking
|
|
738
|
+
// Format: SCHEDULED_FULFILLMENTS_YYYYMMDD_HHMMSS_random
|
|
739
|
+
const jobId = generateJobId('SCHEDULED', 'FULFILLMENTS');
|
|
740
|
+
|
|
741
|
+
log.info('Scheduled extraction triggered', { jobId });
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
// Execute main workflow (extraction → transform → upload)
|
|
745
|
+
const result = await executeFulfillmentExtraction(ctx, {
|
|
746
|
+
jobId,
|
|
747
|
+
triggeredBy: 'schedule',
|
|
748
|
+
updateState: true, // Always update state for scheduled runs
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
log.info('Scheduled extraction completed', {
|
|
752
|
+
jobId,
|
|
753
|
+
recordCount: result.recordsExtracted,
|
|
754
|
+
fileName: result.fileName
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
return result;
|
|
758
|
+
|
|
759
|
+
} catch (error: any) {
|
|
760
|
+
log.error('Scheduled extraction failed', {
|
|
761
|
+
jobId,
|
|
762
|
+
message: error instanceof Error ? error.message : String(error),
|
|
763
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
764
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
765
|
+
});
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
768
|
+
})
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* WORKFLOW 2: Ad hoc Extraction (Manual Trigger)
|
|
773
|
+
*
|
|
774
|
+
* Purpose: Manual extraction with optional date range override
|
|
775
|
+
* Trigger: Webhook POST to /webhooks/fulfillments-adhoc
|
|
776
|
+
* State Update: Optional (controlled by request payload)
|
|
777
|
+
*
|
|
778
|
+
* WEBHOOK PAYLOAD EXAMPLES:
|
|
779
|
+
*
|
|
780
|
+
* 1. Incremental (use last sync timestamp):
|
|
781
|
+
* {}
|
|
782
|
+
*
|
|
783
|
+
* 2. Date range (manual override):
|
|
784
|
+
* {
|
|
785
|
+
* "fromDate": "2025-01-01T00:00:00Z",
|
|
786
|
+
* "toDate": "2025-01-31T23:59:59Z",
|
|
787
|
+
* "updateState": false
|
|
788
|
+
* }
|
|
789
|
+
*
|
|
790
|
+
* AI CUSTOMIZATION:
|
|
791
|
+
* - Add request validation
|
|
792
|
+
* - Add authentication check
|
|
793
|
+
* - Add custom filters from payload
|
|
794
|
+
*/
|
|
795
|
+
export const adhocFulfillmentsExtraction = webhook(
|
|
796
|
+
'fulfillments-adhoc',
|
|
797
|
+
{ connection: 'fulfillments-adhoc', response: { mode: 'sync' } }
|
|
798
|
+
)
|
|
799
|
+
.then(
|
|
800
|
+
http('execute-adhoc-extraction', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
801
|
+
const { data, log } = ctx;
|
|
802
|
+
|
|
803
|
+
// Generate unique job ID
|
|
804
|
+
const jobId = generateJobId('ADHOC', 'FULFILLMENTS');
|
|
805
|
+
|
|
806
|
+
// SECURITY: Authentication is enforced by Versori connection configuration
|
|
807
|
+
// Configure auth on the connection and reference it in webhook({ connection: '...' })
|
|
808
|
+
|
|
809
|
+
// Extract optional date override from webhook payload
|
|
810
|
+
const fromDate = data.fromDate as string | undefined;
|
|
811
|
+
const toDate = data.toDate as string | undefined;
|
|
812
|
+
const updateState = data.updateState === true; // Default false; advance state only if explicitly true
|
|
813
|
+
|
|
814
|
+
log.info('Ad hoc extraction triggered via webhook', {
|
|
815
|
+
jobId,
|
|
816
|
+
hasDateOverride: !!fromDate,
|
|
817
|
+
fromDate: fromDate || 'not specified',
|
|
818
|
+
toDate: toDate || 'not specified',
|
|
819
|
+
updateState
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
try {
|
|
823
|
+
// Execute main workflow with optional overrides
|
|
824
|
+
const result = await executeFulfillmentExtraction(ctx, {
|
|
825
|
+
jobId,
|
|
826
|
+
triggeredBy: 'webhook',
|
|
827
|
+
fromDate, // Optional: override start date
|
|
828
|
+
toDate, // Optional: override end date
|
|
829
|
+
updateState, // Optional: skip state update for historical queries
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
log.info('Ad hoc extraction completed', {
|
|
833
|
+
jobId,
|
|
834
|
+
recordCount: result.recordsExtracted,
|
|
835
|
+
fileName: result.fileName,
|
|
836
|
+
isManualOverride: !!fromDate,
|
|
837
|
+
stateUpdated: result.stateUpdated
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
// Return success with job details
|
|
841
|
+
return {
|
|
842
|
+
success: true,
|
|
843
|
+
jobId,
|
|
844
|
+
recordsExtracted: result.recordsExtracted,
|
|
845
|
+
fileName: result.fileName,
|
|
846
|
+
sftpPath: result.sftpPath,
|
|
847
|
+
statusUrl: `/webhooks/fulfillments-job-status?jobId=${jobId}`,
|
|
848
|
+
dateRange: fromDate ? {
|
|
849
|
+
from: fromDate,
|
|
850
|
+
to: toDate || 'not specified',
|
|
851
|
+
updateState
|
|
852
|
+
} : undefined
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
} catch (error: any) {
|
|
856
|
+
log.error('Ad hoc extraction failed', {
|
|
857
|
+
jobId,
|
|
858
|
+
message: error instanceof Error ? error.message : String(error),
|
|
859
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
860
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
return {
|
|
864
|
+
success: false,
|
|
865
|
+
jobId,
|
|
866
|
+
error: error instanceof Error ? error.message : String(error)
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
})
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* WORKFLOW 3: Job Status Query
|
|
874
|
+
*
|
|
875
|
+
* Purpose: Check job progress and status
|
|
876
|
+
* Trigger: Webhook GET/POST to /webhooks/fulfillments-job-status?jobId=xxx
|
|
877
|
+
* Returns: Current job status, stage, progress
|
|
878
|
+
*
|
|
879
|
+
* QUERY EXAMPLES:
|
|
880
|
+
*
|
|
881
|
+
* 1. HTTP GET:
|
|
882
|
+
* GET /webhooks/fulfillments-job-status?jobId=ADHOC_FULFILLMENTS_20251027_183045_abc123
|
|
883
|
+
*
|
|
884
|
+
* 2. HTTP POST:
|
|
885
|
+
* POST /webhooks/fulfillments-job-status
|
|
886
|
+
* { "jobId": "ADHOC_FULFILLMENTS_20251027_183045_abc123" }
|
|
887
|
+
*/
|
|
888
|
+
export const fulfillmentsJobStatus = webhook(
|
|
889
|
+
'fulfillments-job-status',
|
|
890
|
+
{ connection: 'fulfillments-job-status', response: { mode: 'sync' } }
|
|
891
|
+
)
|
|
892
|
+
.then(
|
|
893
|
+
fn('query-job-status', async (ctx) => {
|
|
894
|
+
const { data, log, openKv, activation } = ctx;
|
|
895
|
+
const req = ctx.request();
|
|
896
|
+
|
|
897
|
+
// SECURITY: Authentication is enforced by Versori connection configuration
|
|
898
|
+
// Configure auth on the connection and reference it in webhook({ connection: '...' })
|
|
899
|
+
|
|
900
|
+
// Get jobId from query param or POST body
|
|
901
|
+
const jobId = data.jobId as string;
|
|
902
|
+
|
|
903
|
+
if (!jobId) {
|
|
904
|
+
log.error('Job ID not provided in request');
|
|
905
|
+
return {
|
|
906
|
+
success: false,
|
|
907
|
+
error: 'Job ID is required. Provide jobId in query param or request body.'
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
log.info('Querying job status', { jobId });
|
|
912
|
+
|
|
913
|
+
try {
|
|
914
|
+
// Query job status from KV store
|
|
915
|
+
const status = await getJobStatus(openKv(':project:'), jobId, log);
|
|
916
|
+
|
|
917
|
+
if (!status) {
|
|
918
|
+
log.info('Job not found', { jobId });
|
|
919
|
+
return {
|
|
920
|
+
success: false,
|
|
921
|
+
error: 'Job not found',
|
|
922
|
+
jobId
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
log.info('Job status retrieved', { jobId, status: status.status });
|
|
927
|
+
|
|
928
|
+
return {
|
|
929
|
+
success: true,
|
|
930
|
+
jobId,
|
|
931
|
+
...status
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
} catch (error: any) {
|
|
935
|
+
log.error('Failed to query job status', {
|
|
936
|
+
jobId,
|
|
937
|
+
message: error instanceof Error ? error.message : String(error),
|
|
938
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
939
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
return {
|
|
943
|
+
success: false,
|
|
944
|
+
jobId,
|
|
945
|
+
error: error instanceof Error ? error.message : String(error)
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
})
|
|
949
|
+
);
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
---
|
|
953
|
+
|
|
954
|
+
## 3. Main Orchestration Service (src/services/extraction-orchestration.ts)
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
/**
|
|
958
|
+
* MAIN ORCHESTRATION SERVICE
|
|
959
|
+
*
|
|
960
|
+
* This is the heart of the extraction workflow. It coordinates all steps:
|
|
961
|
+
* 1. Initialize clients and services
|
|
962
|
+
* 2. Determine date range (incremental vs manual)
|
|
963
|
+
* 3. Extract data using ExtractionOrchestrator
|
|
964
|
+
* 4. Transform using UniversalMapper
|
|
965
|
+
* 5. Generate CSV using CSVParserService
|
|
966
|
+
* 6. Upload to SFTP
|
|
967
|
+
* 7. Track job progress with JobTracker
|
|
968
|
+
* 8. Update state for next run
|
|
969
|
+
*
|
|
970
|
+
* NAMING PATTERN (consistent across all use cases):
|
|
971
|
+
* - Interface: {Entity}ExtractionParams (e.g., FulfillmentExtractionParams)
|
|
972
|
+
* - Result: {Entity}ExtractionResult (e.g., FulfillmentExtractionResult)
|
|
973
|
+
* - Main function: execute{Entity}Extraction (e.g., executeFulfillmentExtraction)
|
|
974
|
+
*
|
|
975
|
+
* AI CUSTOMIZATION HINTS:
|
|
976
|
+
* - Change entity: Replace "Fulfillment" with "Order", "Product", etc.
|
|
977
|
+
* - Change output: Replace CSVParserService with XMLBuilder
|
|
978
|
+
* - Change destination: Replace SftpDataSource with S3DataSource
|
|
979
|
+
* - Add steps: Insert new service calls between existing steps
|
|
980
|
+
*/
|
|
981
|
+
|
|
982
|
+
import { Buffer } from 'node:buffer';
|
|
983
|
+
import {
|
|
984
|
+
createClient,
|
|
985
|
+
ExtractionOrchestrator,
|
|
986
|
+
JobTracker,
|
|
987
|
+
UniversalMapper,
|
|
988
|
+
CSVParserService,
|
|
989
|
+
SftpDataSource,
|
|
990
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
991
|
+
|
|
992
|
+
import mappingConfig from '../../config/fulfillments.export.csv.json' with { type: 'json' };
|
|
993
|
+
|
|
994
|
+
// ? VERSORI PLATFORM: Use native log from context, not LoggingService
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Parameters for extraction workflow
|
|
998
|
+
*
|
|
999
|
+
* NAMING: {Entity}ExtractionParams
|
|
1000
|
+
*/
|
|
1001
|
+
export interface FulfillmentExtractionParams {
|
|
1002
|
+
jobId: string;
|
|
1003
|
+
triggeredBy: 'schedule' | 'webhook';
|
|
1004
|
+
fromDate?: string; // Optional: manual date override
|
|
1005
|
+
toDate?: string; // Optional: manual date override
|
|
1006
|
+
updateState: boolean; // Whether to update rawLastRunTime timestamp
|
|
1007
|
+
|
|
1008
|
+
// AI CUSTOMIZATION: Add filters specific to entity
|
|
1009
|
+
fulfillmentStatuses?: string[]; // e.g., ['SHIPPED', 'DELIVERED']
|
|
1010
|
+
retailerId?: string; // e.g., 'YOUR_RETAILER_ID'
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Result from extraction workflow
|
|
1015
|
+
*
|
|
1016
|
+
* NAMING: {Entity}ExtractionResult
|
|
1017
|
+
*/
|
|
1018
|
+
export interface FulfillmentExtractionResult {
|
|
1019
|
+
success: boolean;
|
|
1020
|
+
jobId: string;
|
|
1021
|
+
recordsExtracted: number;
|
|
1022
|
+
fileName?: string;
|
|
1023
|
+
sftpPath?: string;
|
|
1024
|
+
error?: string;
|
|
1025
|
+
errors?: any[];
|
|
1026
|
+
isManualOverride?: boolean;
|
|
1027
|
+
stateUpdated?: boolean;
|
|
1028
|
+
newTimestamp?: string;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* GraphQL Query for Fulfillments
|
|
1033
|
+
*
|
|
1034
|
+
* NAMING: {ENTITY}_EXTRACTION_QUERY (uppercase constant)
|
|
1035
|
+
*/
|
|
1036
|
+
const FULFILLMENTS_EXTRACTION_QUERY = `
|
|
1037
|
+
query GetFulfillments(
|
|
1038
|
+
$updatedAfter: DateTime!
|
|
1039
|
+
$statuses: [String!]
|
|
1040
|
+
$first: Int!
|
|
1041
|
+
$after: String
|
|
1042
|
+
) {
|
|
1043
|
+
fulfillments(
|
|
1044
|
+
updatedOn: { after: $updatedAfter }
|
|
1045
|
+
status: $statuses
|
|
1046
|
+
first: $first
|
|
1047
|
+
after: $after
|
|
1048
|
+
) {
|
|
1049
|
+
edges {
|
|
1050
|
+
node {
|
|
1051
|
+
id
|
|
1052
|
+
ref
|
|
1053
|
+
orderRef
|
|
1054
|
+
status
|
|
1055
|
+
trackingNumber
|
|
1056
|
+
carrier
|
|
1057
|
+
serviceLevel
|
|
1058
|
+
shippedOn
|
|
1059
|
+
deliveredOn
|
|
1060
|
+
updatedOn
|
|
1061
|
+
order {
|
|
1062
|
+
customer {
|
|
1063
|
+
email
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
cursor
|
|
1068
|
+
}
|
|
1069
|
+
pageInfo {
|
|
1070
|
+
hasNextPage
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
`;
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Query job status from KV store
|
|
1078
|
+
*
|
|
1079
|
+
* ? VERSORI PLATFORM: Uses native log from context (passed as parameter)
|
|
1080
|
+
*/
|
|
1081
|
+
export async function getJobStatus(
|
|
1082
|
+
kv: any, // ? Versori KV (compatible with JobTracker's KVAdapter interface)
|
|
1083
|
+
jobId: string,
|
|
1084
|
+
log: any // ? Native Versori log from context
|
|
1085
|
+
): Promise<any | undefined> {
|
|
1086
|
+
try {
|
|
1087
|
+
const tracker = new JobTracker(kv, log);
|
|
1088
|
+
return await tracker.getJob(jobId);
|
|
1089
|
+
} catch (error: any) {
|
|
1090
|
+
log.error('Failed to get job status', {
|
|
1091
|
+
jobId,
|
|
1092
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1093
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1094
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1095
|
+
});
|
|
1096
|
+
return undefined;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* MAIN ORCHESTRATION FUNCTION
|
|
1102
|
+
*
|
|
1103
|
+
* NAMING: execute{Entity}Extraction (e.g., executeFulfillmentExtraction)
|
|
1104
|
+
*
|
|
1105
|
+
* This function implements the complete workflow in steps.
|
|
1106
|
+
* Each step is clearly commented for AI understanding.
|
|
1107
|
+
*/
|
|
1108
|
+
export async function executeFulfillmentExtraction(
|
|
1109
|
+
ctx: any,
|
|
1110
|
+
params: FulfillmentExtractionParams
|
|
1111
|
+
): Promise<FulfillmentExtractionResult> {
|
|
1112
|
+
// ? VERSORI PLATFORM: Extract native log from context
|
|
1113
|
+
const { log, openKv, activation } = ctx;
|
|
1114
|
+
const { jobId, triggeredBy, fromDate, toDate, updateState } = params;
|
|
1115
|
+
|
|
1116
|
+
// Open KV store for state management and job tracking
|
|
1117
|
+
// ? Pass raw Versori KV directly - it matches KVAdapter interface
|
|
1118
|
+
// ? Pass native log to JobTracker
|
|
1119
|
+
const kv = openKv(':project:');
|
|
1120
|
+
const tracker = new JobTracker(kv, log);
|
|
1121
|
+
|
|
1122
|
+
try {
|
|
1123
|
+
// ═══════════════════════════════════════════════════════════
|
|
1124
|
+
// STEP 1/8: Initialize Job Tracking
|
|
1125
|
+
// ═══════════════════════════════════════════════════════════
|
|
1126
|
+
log.info('🚀 [STEP 1/8] Initializing job tracking', { jobId });
|
|
1127
|
+
|
|
1128
|
+
await tracker.createJob(jobId, {
|
|
1129
|
+
triggeredBy,
|
|
1130
|
+
hasDateOverride: !!fromDate,
|
|
1131
|
+
fromDate,
|
|
1132
|
+
toDate,
|
|
1133
|
+
updateStateAfterRun: updateState,
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
// ═══════════════════════════════════════════════════════════
|
|
1137
|
+
// STEP 2/8: Initialize Fluent Client
|
|
1138
|
+
// ═══════════════════════════════════════════════════════════
|
|
1139
|
+
log.info('🚀 [STEP 2/8] Initializing Fluent Commerce client', { jobId });
|
|
1140
|
+
|
|
1141
|
+
// ✅ Optional: Validate connection immediately (fail-fast mode)
|
|
1142
|
+
// Set activation variable 'validateConnectionOnStart' = 'true' to enable
|
|
1143
|
+
// When enabled: Executes query { me { ref } } to verify authentication
|
|
1144
|
+
// When disabled: Fast creation, validation happens on first API call (default)
|
|
1145
|
+
const validateConnection = activation.getVariable('validateConnectionOnStart') === 'true';
|
|
1146
|
+
const client = await createClient(ctx, validateConnection ? { validateConnection: true } : undefined);
|
|
1147
|
+
|
|
1148
|
+
if (!client) {
|
|
1149
|
+
throw new Error('Failed to create Fluent Commerce client');
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (validateConnection) {
|
|
1153
|
+
log.info('✅ Connection validated successfully - authentication confirmed', { jobId });
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// ═══════════════════════════════════════════════════════════
|
|
1157
|
+
// STEP 3/8: Determine Date Range
|
|
1158
|
+
// ═══════════════════════════════════════════════════════════
|
|
1159
|
+
log.info('🔍 [STEP 3/8] Determining date range for extraction', { jobId });
|
|
1160
|
+
|
|
1161
|
+
// State key for incremental sync tracking
|
|
1162
|
+
// NAMING: last{Entity}Sync (e.g., lastFulfillmentSync)
|
|
1163
|
+
const STATE_KEY = 'lastFulfillmentSync';
|
|
1164
|
+
const DEFAULT_FALLBACK = '2024-01-01T00:00:00Z';
|
|
1165
|
+
const OVERLAP_BUFFER_SECONDS = parseInt(
|
|
1166
|
+
activation.getVariable('overlapBufferSeconds') || '60',
|
|
1167
|
+
10
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
let bufferedLastRunTime: string;
|
|
1171
|
+
const isManualOverride = !!fromDate;
|
|
1172
|
+
|
|
1173
|
+
if (isManualOverride) {
|
|
1174
|
+
// Manual date override from webhook
|
|
1175
|
+
bufferedLastRunTime = fromDate!;
|
|
1176
|
+
log.info('Using manual date override', { fromDate, toDate });
|
|
1177
|
+
} else {
|
|
1178
|
+
// Incremental sync - get last sync timestamp
|
|
1179
|
+
const rawLastRunTime = (await kv.get<string>(STATE_KEY)) || DEFAULT_FALLBACK;
|
|
1180
|
+
|
|
1181
|
+
// Apply overlap buffer (prevents missed records)
|
|
1182
|
+
bufferedLastRunTime = new Date(
|
|
1183
|
+
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_SECONDS * 1000
|
|
1184
|
+
).toISOString();
|
|
1185
|
+
|
|
1186
|
+
log.info('Using incremental sync with overlap buffer', {
|
|
1187
|
+
rawLastRunTime,
|
|
1188
|
+
bufferedLastRunTime,
|
|
1189
|
+
overlapBufferSeconds: OVERLAP_BUFFER_SECONDS,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// ═══════════════════════════════════════════════════════════
|
|
1194
|
+
// STEP 4/8: Extract Data (ExtractionOrchestrator)
|
|
1195
|
+
// ═══════════════════════════════════════════════════════════
|
|
1196
|
+
log.info('📥 [STEP 4/8] Extracting data from Fluent Commerce', { jobId });
|
|
1197
|
+
|
|
1198
|
+
await tracker.updateJob(jobId, {
|
|
1199
|
+
status: 'processing',
|
|
1200
|
+
stage: 'extraction',
|
|
1201
|
+
message: 'Extracting data with auto-pagination',
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
// Configure extraction
|
|
1205
|
+
const pageSize = parseInt(activation.getVariable('pageSize') || '200', 10);
|
|
1206
|
+
const maxRecords = parseInt(activation.getVariable('maxRecords') || '10000', 10);
|
|
1207
|
+
|
|
1208
|
+
// Parse status filter
|
|
1209
|
+
const fulfillmentStatuses =
|
|
1210
|
+
params.fulfillmentStatuses ||
|
|
1211
|
+
(activation.getVariable('fulfillmentStatuses') || 'SHIPPED,DELIVERED').split(',');
|
|
1212
|
+
|
|
1213
|
+
// Initialize ExtractionOrchestrator
|
|
1214
|
+
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
1215
|
+
|
|
1216
|
+
// ? Enhanced: Extract context for progress logging
|
|
1217
|
+
const dateRangeInfo = {
|
|
1218
|
+
start: bufferedLastRunTime || 'N/A',
|
|
1219
|
+
end: new Date().toISOString(),
|
|
1220
|
+
statuses: fulfillmentStatuses.join(', ') || 'all'
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
// Duration tracking
|
|
1224
|
+
const extractionStartTime = Date.now();
|
|
1225
|
+
|
|
1226
|
+
// ? Enhanced: Start logging with context
|
|
1227
|
+
log.info(`🔍 [ExtractionOrchestrator] Starting extraction`, {
|
|
1228
|
+
query: 'fulfillments',
|
|
1229
|
+
pageSize,
|
|
1230
|
+
maxRecords,
|
|
1231
|
+
dateRange: `from ${dateRangeInfo.start}`,
|
|
1232
|
+
statuses: dateRangeInfo.statuses,
|
|
1233
|
+
jobId
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
// Execute extraction with auto-pagination
|
|
1237
|
+
const extractionResult = await orchestrator.extract({
|
|
1238
|
+
query: FULFILLMENTS_EXTRACTION_QUERY,
|
|
1239
|
+
resultPath: 'fulfillments.edges.node',
|
|
1240
|
+
variables: {
|
|
1241
|
+
updatedAfter: bufferedLastRunTime,
|
|
1242
|
+
statuses: fulfillmentStatuses,
|
|
1243
|
+
// Note: Don't include 'first' or 'after' here; orchestrator injects them
|
|
1244
|
+
},
|
|
1245
|
+
pageSize,
|
|
1246
|
+
maxRecords,
|
|
1247
|
+
// Optional: validate each record
|
|
1248
|
+
validateItem: (item: any) => {
|
|
1249
|
+
return !!(item.ref && item.orderRef);
|
|
1250
|
+
},
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
const rawRecords = extractionResult.data;
|
|
1254
|
+
|
|
1255
|
+
// Calculate extraction duration
|
|
1256
|
+
const extractionDuration = Date.now() - extractionStartTime;
|
|
1257
|
+
|
|
1258
|
+
log.info('✅ [STEP 4/8] Extraction completed', {
|
|
1259
|
+
jobId,
|
|
1260
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1261
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1262
|
+
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1263
|
+
errors: extractionResult.errors ? extractionResult.errors.length : 0,
|
|
1264
|
+
duration: `${extractionDuration}ms`,
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
// ? Enhanced: Completion logging with summary
|
|
1268
|
+
log.info(`✅ [ExtractionOrchestrator] Extraction completed`, {
|
|
1269
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1270
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1271
|
+
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1272
|
+
failedValidations: extractionResult.stats.failedValidations,
|
|
1273
|
+
truncated: extractionResult.stats.truncated,
|
|
1274
|
+
truncationReason: extractionResult.stats.truncationReason,
|
|
1275
|
+
dateRange: `from ${dateRangeInfo.start}`,
|
|
1276
|
+
duration: `${extractionDuration}ms`,
|
|
1277
|
+
jobId
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
if (extractionResult.errors && extractionResult.errors.length > 0) {
|
|
1281
|
+
log.warn('Non-fatal extraction errors encountered', {
|
|
1282
|
+
jobId,
|
|
1283
|
+
errorCount: extractionResult.errors.length,
|
|
1284
|
+
sampleErrors: extractionResult.errors.slice(0, 3),
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Handle empty result
|
|
1289
|
+
if (rawRecords.length === 0) {
|
|
1290
|
+
log.info('No records to process');
|
|
1291
|
+
|
|
1292
|
+
// Update state even with no records (prevents re-querying empty window)
|
|
1293
|
+
if (updateState) {
|
|
1294
|
+
await kv.set(STATE_KEY, new Date().toISOString());
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
await tracker.markCompleted(jobId, {
|
|
1298
|
+
recordCount: 0,
|
|
1299
|
+
message: 'No records to extract',
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
return {
|
|
1303
|
+
success: true,
|
|
1304
|
+
jobId,
|
|
1305
|
+
recordsExtracted: 0,
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// ═══════════════════════════════════════════════════════════
|
|
1310
|
+
// STEP 5/8: Transform Data (UniversalMapper)
|
|
1311
|
+
// ═══════════════════════════════════════════════════════════
|
|
1312
|
+
log.info('🔄 [STEP 5/8] Transforming data with UniversalMapper', {
|
|
1313
|
+
jobId,
|
|
1314
|
+
recordCount: rawRecords.length,
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
await tracker.updateJob(jobId, {
|
|
1318
|
+
status: 'processing',
|
|
1319
|
+
stage: 'transformation',
|
|
1320
|
+
message: `Transforming ${rawRecords.length} records`,
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
const mapper = new UniversalMapper(mappingConfig);
|
|
1324
|
+
const mappingResult = await mapper.map(rawRecords);
|
|
1325
|
+
|
|
1326
|
+
if (!mappingResult.success) {
|
|
1327
|
+
const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
|
|
1328
|
+
log.error('[STEP 5/8] Mapping failed - terminating job', {
|
|
1329
|
+
jobId,
|
|
1330
|
+
errorCount: mappingErrors.length,
|
|
1331
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
await tracker.markFailed(
|
|
1335
|
+
jobId,
|
|
1336
|
+
new Error(mappingErrors[0] || 'UniversalMapper returned unsuccessful result')
|
|
1337
|
+
);
|
|
1338
|
+
|
|
1339
|
+
return {
|
|
1340
|
+
success: false,
|
|
1341
|
+
jobId,
|
|
1342
|
+
recordsExtracted: 0,
|
|
1343
|
+
error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const transformedRecords = Array.isArray(mappingResult.data) ? mappingResult.data : [];
|
|
1348
|
+
const mappingErrors = mappingResult.errors || [];
|
|
1349
|
+
|
|
1350
|
+
if (mappingErrors.length > 0) {
|
|
1351
|
+
log.warn('[STEP 5/8] Some records failed transformation', {
|
|
1352
|
+
jobId,
|
|
1353
|
+
errorCount: mappingErrors.length,
|
|
1354
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
1359
|
+
log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
1360
|
+
jobId,
|
|
1361
|
+
skippedFields: mappingResult.skippedFields,
|
|
1362
|
+
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
if (transformedRecords.length === 0) {
|
|
1367
|
+
throw new Error('All records failed transformation');
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
log.info('Transformation complete', {
|
|
1371
|
+
jobId,
|
|
1372
|
+
successful: transformedRecords.length,
|
|
1373
|
+
skippedRecords: rawRecords.length - transformedRecords.length,
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
// ═══════════════════════════════════════════════════════════
|
|
1377
|
+
// STEP 6/8: Generate CSV (CSVParserService)
|
|
1378
|
+
// ═══════════════════════════════════════════════════════════
|
|
1379
|
+
log.info('📄 [STEP 6/8] Generating CSV file', { jobId });
|
|
1380
|
+
|
|
1381
|
+
await tracker.updateJob(jobId, {
|
|
1382
|
+
status: 'processing',
|
|
1383
|
+
stage: 'csv_generation',
|
|
1384
|
+
message: `Generating CSV for ${transformedRecords.length} records`,
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
// Initialize CSVParserService
|
|
1388
|
+
const csvParser = new CSVParserService();
|
|
1389
|
+
|
|
1390
|
+
// Generate CSV content
|
|
1391
|
+
const csvContent = csvParser.stringify(transformedRecords, { headers: true });
|
|
1392
|
+
|
|
1393
|
+
// Generate filename using helper function
|
|
1394
|
+
const fileNamePrefix = activation.getVariable('fileNamePrefix') || 'fulfillments';
|
|
1395
|
+
const extractFileName = (prefix: string) => {
|
|
1396
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1397
|
+
return `${prefix}-${timestamp}.csv`;
|
|
1398
|
+
};
|
|
1399
|
+
const fileName = extractFileName(fileNamePrefix);
|
|
1400
|
+
|
|
1401
|
+
log.info('✅ CSV file generated', {
|
|
1402
|
+
fileName,
|
|
1403
|
+
sizeBytes: csvContent.length,
|
|
1404
|
+
recordCount: transformedRecords.length,
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
// ═══════════════════════════════════════════════════════════
|
|
1408
|
+
// STEP 7/8: Upload to SFTP (SftpDataSource)
|
|
1409
|
+
// ═══════════════════════════════════════════════════════════
|
|
1410
|
+
log.info('📤 [STEP 7/8] Uploading to SFTP', { jobId, fileName });
|
|
1411
|
+
|
|
1412
|
+
await tracker.updateJob(jobId, {
|
|
1413
|
+
status: 'processing',
|
|
1414
|
+
stage: 'sftp_upload',
|
|
1415
|
+
message: `Uploading ${fileName} to SFTP`,
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// Get SFTP credentials from Versori connection (Basic Auth)
|
|
1419
|
+
// RECOMMENDED: Use activation.connections (already decoded)
|
|
1420
|
+
const allConnections = ctx.activation.connections || [];
|
|
1421
|
+
const sftpConn = allConnections.find(c => c.name === 'versori_ftp_server');
|
|
1422
|
+
|
|
1423
|
+
if (!sftpConn) {
|
|
1424
|
+
throw new Error('SFTP connection "versori_ftp_server" not found');
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const credential = sftpConn.credentials[0]?.credential;
|
|
1428
|
+
if (!credential?.data?.basicAuth) {
|
|
1429
|
+
throw new Error('SFTP connection not configured with Basic Authentication');
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const { username, password } = credential.data.basicAuth;
|
|
1433
|
+
// ? Already decoded - no Buffer.from() needed!
|
|
1434
|
+
|
|
1435
|
+
// Get other SFTP config from activation variables
|
|
1436
|
+
const sftpConfig = {
|
|
1437
|
+
host: activation.getVariable('sftpHost'),
|
|
1438
|
+
port: parseInt(activation.getVariable('sftpPort') || '22', 10),
|
|
1439
|
+
username, // From connection
|
|
1440
|
+
password, // From connection
|
|
1441
|
+
privateKey: activation.getVariable('sftpPrivateKey'), // Optional: if using key-based auth
|
|
1442
|
+
};
|
|
1443
|
+
const sftpRemotePath = activation.getVariable('sftpRemotePath') || '/incoming/fulfillments/';
|
|
1444
|
+
|
|
1445
|
+
// Validate SFTP config
|
|
1446
|
+
if (!sftpConfig.host || !sftpConfig.username) {
|
|
1447
|
+
throw new Error('SFTP configuration incomplete: missing host or username from connection');
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (!sftpConfig.password && !sftpConfig.privateKey) {
|
|
1451
|
+
throw new Error(
|
|
1452
|
+
'SFTP configuration incomplete: missing password from connection or privateKey from activation'
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Initialize SFTP data source
|
|
1457
|
+
// ? VERSORI PLATFORM: Pass native log from context
|
|
1458
|
+
// ? Declare as let for proper disposal in finally block
|
|
1459
|
+
let sftp: SftpDataSource | undefined;
|
|
1460
|
+
|
|
1461
|
+
// Construct SFTP path
|
|
1462
|
+
const sftpPath = `${sftpRemotePath}${fileName}`;
|
|
1463
|
+
|
|
1464
|
+
try {
|
|
1465
|
+
sftp = new SftpDataSource(
|
|
1466
|
+
{
|
|
1467
|
+
type: 'SFTP_CSV',
|
|
1468
|
+
connectionId: 'fulfillments-sftp',
|
|
1469
|
+
name: 'Fulfillments SFTP Upload',
|
|
1470
|
+
settings: {
|
|
1471
|
+
host: sftpConfig.host!,
|
|
1472
|
+
port: sftpConfig.port,
|
|
1473
|
+
username: sftpConfig.username!,
|
|
1474
|
+
password: sftpConfig.password,
|
|
1475
|
+
privateKey: sftpConfig.privateKey,
|
|
1476
|
+
remotePath: sftpRemotePath,
|
|
1477
|
+
filePattern: '*.csv',
|
|
1478
|
+
},
|
|
1479
|
+
},
|
|
1480
|
+
log
|
|
1481
|
+
);
|
|
1482
|
+
|
|
1483
|
+
// Create remote directory if needed
|
|
1484
|
+
await sftp.createDirectory(sftpRemotePath, true);
|
|
1485
|
+
|
|
1486
|
+
// Upload with retry logic (built into SftpDataSource)
|
|
1487
|
+
await sftp.uploadFile(sftpPath, Buffer.from(csvContent, 'utf-8'), {
|
|
1488
|
+
createDirectories: true,
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
log.info('✅ SFTP upload successful', { fileName, remotePath: sftpPath });
|
|
1492
|
+
} finally {
|
|
1493
|
+
// ?? CRITICAL: Always dispose SFTP connection
|
|
1494
|
+
if (sftp) {
|
|
1495
|
+
await sftp.dispose();
|
|
1496
|
+
log.info('🔌 SFTP connection disposed');
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// ═══════════════════════════════════════════════════════════
|
|
1501
|
+
// STEP 8/8: Update State & Complete Job
|
|
1502
|
+
// ═══════════════════════════════════════════════════════════
|
|
1503
|
+
log.info('💾 [STEP 8/8] Updating state and completing job', { jobId });
|
|
1504
|
+
|
|
1505
|
+
// Calculate new timestamp for next incremental run
|
|
1506
|
+
let newTimestamp: string | undefined;
|
|
1507
|
+
|
|
1508
|
+
if (updateState) {
|
|
1509
|
+
// Find max updatedOn from extracted records
|
|
1510
|
+
const maxUpdatedOn = rawRecords.reduce((max, record) => {
|
|
1511
|
+
const recordTime = new Date(record.updatedOn).getTime();
|
|
1512
|
+
return recordTime > max ? recordTime : max;
|
|
1513
|
+
}, new Date(bufferedLastRunTime).getTime());
|
|
1514
|
+
|
|
1515
|
+
newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
1516
|
+
|
|
1517
|
+
// Store new timestamp (WITHOUT buffer - buffer only applied on read)
|
|
1518
|
+
await kv.set(STATE_KEY, newTimestamp);
|
|
1519
|
+
|
|
1520
|
+
log.info('✅ State updated', {
|
|
1521
|
+
oldTimestamp: bufferedLastRunTime,
|
|
1522
|
+
newTimestamp,
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Mark job as completed
|
|
1527
|
+
await tracker.markCompleted(jobId, {
|
|
1528
|
+
recordCount: transformedRecords.length,
|
|
1529
|
+
fileName,
|
|
1530
|
+
sftpPath,
|
|
1531
|
+
errorCount: mappingErrors.length,
|
|
1532
|
+
isManualOverride,
|
|
1533
|
+
stateUpdated: updateState,
|
|
1534
|
+
newTimestamp,
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
log.info('✅ [COMPLETE] Extraction workflow completed successfully', { jobId });
|
|
1538
|
+
|
|
1539
|
+
return {
|
|
1540
|
+
success: true,
|
|
1541
|
+
jobId,
|
|
1542
|
+
recordsExtracted: transformedRecords.length,
|
|
1543
|
+
fileName,
|
|
1544
|
+
sftpPath,
|
|
1545
|
+
isManualOverride,
|
|
1546
|
+
stateUpdated: updateState,
|
|
1547
|
+
newTimestamp,
|
|
1548
|
+
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1549
|
+
};
|
|
1550
|
+
} catch (error: any) {
|
|
1551
|
+
log.error('❌ [FATAL] Extraction workflow failed', {
|
|
1552
|
+
jobId,
|
|
1553
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1554
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1555
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1556
|
+
recommendation: 'Check logs for detailed error information. Verify activation variables, connection credentials, and SFTP configuration.',
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
// Mark job as failed
|
|
1560
|
+
await tracker.markFailed(jobId, error);
|
|
1561
|
+
|
|
1562
|
+
return {
|
|
1563
|
+
success: false,
|
|
1564
|
+
jobId,
|
|
1565
|
+
recordsExtracted: 0,
|
|
1566
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
---
|
|
1573
|
+
|
|
1574
|
+
## 4. Utility Functions (src/utils/job-id-generator.ts)
|
|
1575
|
+
|
|
1576
|
+
```typescript
|
|
1577
|
+
/**
|
|
1578
|
+
* Job ID Generator
|
|
1579
|
+
*
|
|
1580
|
+
* Generates unique job IDs for tracking extraction workflows
|
|
1581
|
+
*
|
|
1582
|
+
* FORMAT: {TYPE}_{ENTITY}_{YYYYMMDD}_{HHMMSS}_{RANDOM}
|
|
1583
|
+
* Example: SCHEDULED_FULFILLMENTS_20251027_183045_a1b2c3
|
|
1584
|
+
*/
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* Generate unique job ID
|
|
1588
|
+
*
|
|
1589
|
+
* @param type - Job trigger type (SCHEDULED, ADHOC, MANUAL)
|
|
1590
|
+
* @param entity - Entity abbreviation (FULFILLMENTS, VP, ORD, PRD)
|
|
1591
|
+
* @returns Unique job ID string
|
|
1592
|
+
*/
|
|
1593
|
+
export function generateJobId(type: string, entity: string): string {
|
|
1594
|
+
const now = new Date();
|
|
1595
|
+
|
|
1596
|
+
// Format: YYYYMMDD
|
|
1597
|
+
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
1598
|
+
|
|
1599
|
+
// Format: HHMMSS
|
|
1600
|
+
const timeStr = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
1601
|
+
|
|
1602
|
+
// Random suffix (6 chars)
|
|
1603
|
+
const randomStr = Math.random().toString(36).substring(2, 8);
|
|
1604
|
+
|
|
1605
|
+
return `${type}_${entity}_${dateStr}_${timeStr}_${randomStr}`;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Parse job ID components
|
|
1610
|
+
*/
|
|
1611
|
+
export function parseJobId(jobId: string): {
|
|
1612
|
+
type: string;
|
|
1613
|
+
entity: string;
|
|
1614
|
+
date: string;
|
|
1615
|
+
time: string;
|
|
1616
|
+
random: string;
|
|
1617
|
+
} | null {
|
|
1618
|
+
const parts = jobId.split('_');
|
|
1619
|
+
|
|
1620
|
+
if (parts.length !== 5) {
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
return {
|
|
1625
|
+
type: parts[0],
|
|
1626
|
+
entity: parts[1],
|
|
1627
|
+
date: parts[2],
|
|
1628
|
+
time: parts[3],
|
|
1629
|
+
random: parts[4],
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
---
|
|
1635
|
+
|
|
1636
|
+
## 5. Package Configuration
|
|
1637
|
+
|
|
1638
|
+
### package.json
|
|
1639
|
+
|
|
1640
|
+
```json
|
|
1641
|
+
{
|
|
1642
|
+
"name": "fulfillments-to-sftp-csv",
|
|
1643
|
+
"version": "1.0.0",
|
|
1644
|
+
"description": "Extract fulfillments from Fluent Commerce and export to SFTP as CSV",
|
|
1645
|
+
"type": "module",
|
|
1646
|
+
"main": "src/index.ts",
|
|
1647
|
+
"scripts": {
|
|
1648
|
+
"dev": "versori dev",
|
|
1649
|
+
"build": "versori build",
|
|
1650
|
+
"deploy": "versori deploy"
|
|
1651
|
+
},
|
|
1652
|
+
"dependencies": {
|
|
1653
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
1654
|
+
"@versori/run": "latest"
|
|
1655
|
+
},
|
|
1656
|
+
"devDependencies": {
|
|
1657
|
+
"@types/node": "^20.0.0",
|
|
1658
|
+
"typescript": "^5.0.0"
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
```
|
|
1662
|
+
|
|
1663
|
+
### tsconfig.json
|
|
1664
|
+
|
|
1665
|
+
```json
|
|
1666
|
+
{
|
|
1667
|
+
"compilerOptions": {
|
|
1668
|
+
"module": "ES2022",
|
|
1669
|
+
"target": "ES2024",
|
|
1670
|
+
"moduleResolution": "node"
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
```
|
|
1674
|
+
|
|
1675
|
+
---
|
|
1676
|
+
|
|
1677
|
+
## 6. Deployment Instructions
|
|
1678
|
+
|
|
1679
|
+
### Deploy to Versori
|
|
1680
|
+
|
|
1681
|
+
```bash
|
|
1682
|
+
# 1. Install dependencies
|
|
1683
|
+
npm install
|
|
1684
|
+
|
|
1685
|
+
# 2. Test locally (if using Versori CLI)
|
|
1686
|
+
npm run dev
|
|
1687
|
+
|
|
1688
|
+
# 3. Deploy to Versori platform
|
|
1689
|
+
npm run deploy
|
|
1690
|
+
```
|
|
1691
|
+
|
|
1692
|
+
### Configure SFTP Connection
|
|
1693
|
+
|
|
1694
|
+
1. Create a Versori connection named `versori_ftp_server`
|
|
1695
|
+
2. Set **Authentication Type**: `Basic Auth`
|
|
1696
|
+
3. Enter your SFTP **Username** and **Password**
|
|
1697
|
+
|
|
1698
|
+
### Configure Activation Variables
|
|
1699
|
+
|
|
1700
|
+
In Versori platform settings, configure:
|
|
1701
|
+
|
|
1702
|
+
```json
|
|
1703
|
+
{
|
|
1704
|
+
"retailerId": "your-retailer-id",
|
|
1705
|
+
"sftpHost": "sftp.partner.com",
|
|
1706
|
+
"sftpPort": 22,
|
|
1707
|
+
"sftpRemotePath": "/incoming/fulfillments/",
|
|
1708
|
+
"fileNamePrefix": "fulfillments",
|
|
1709
|
+
"pageSize": 200,
|
|
1710
|
+
"maxRecords": 50000,
|
|
1711
|
+
"overlapBufferSeconds": 60,
|
|
1712
|
+
"fulfillmentStatuses": "SHIPPED,DELIVERED",
|
|
1713
|
+
"webhookApiKey": "your-secure-api-key-here"
|
|
1714
|
+
}
|
|
1715
|
+
```
|
|
1716
|
+
|
|
1717
|
+
> **Note:** `sftpUsername` and `sftpPassword` are now stored in the `versori_ftp_server` connection.
|
|
1718
|
+
|
|
1719
|
+
---
|
|
1720
|
+
|
|
1721
|
+
## 7. Testing
|
|
1722
|
+
|
|
1723
|
+
### Test Scheduled Extraction
|
|
1724
|
+
|
|
1725
|
+
The scheduled workflow runs automatically based on cron schedule.
|
|
1726
|
+
|
|
1727
|
+
**Check logs:**
|
|
1728
|
+
|
|
1729
|
+
```
|
|
1730
|
+
[STEP 1/8] Initializing job tracking
|
|
1731
|
+
[STEP 2/8] Initializing Fluent Commerce client
|
|
1732
|
+
[STEP 3/8] Determining date range for extraction
|
|
1733
|
+
[STEP 4/8] Extracting data from Fluent Commerce
|
|
1734
|
+
[STEP 5/8] Transforming data with UniversalMapper
|
|
1735
|
+
[STEP 6/8] Generating CSV file
|
|
1736
|
+
[STEP 7/8] Uploading to SFTP
|
|
1737
|
+
[STEP 8/8] Updating state and completing job
|
|
1738
|
+
```
|
|
1739
|
+
|
|
1740
|
+
### Test Ad hoc Extraction
|
|
1741
|
+
|
|
1742
|
+
```bash
|
|
1743
|
+
# Incremental (uses last sync timestamp)
|
|
1744
|
+
curl -X POST https://api.versori.com/webhooks/fulfillments-adhoc \
|
|
1745
|
+
-H "X-API-Key: your-api-key" \
|
|
1746
|
+
-H "Content-Type: application/json" \
|
|
1747
|
+
-d '{}'
|
|
1748
|
+
|
|
1749
|
+
# Date range override
|
|
1750
|
+
curl -X POST https://api.versori.com/webhooks/fulfillments-adhoc \
|
|
1751
|
+
-H "X-API-Key: your-api-key" \
|
|
1752
|
+
-H "Content-Type: application/json" \
|
|
1753
|
+
-d '{
|
|
1754
|
+
"fromDate": "2025-01-01T00:00:00Z",
|
|
1755
|
+
"toDate": "2025-01-31T23:59:59Z",
|
|
1756
|
+
"updateState": false
|
|
1757
|
+
}'
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1760
|
+
### Test Job Status Query
|
|
1761
|
+
|
|
1762
|
+
```bash
|
|
1763
|
+
curl -X POST https://api.versori.com/webhooks/fulfillments-job-status \
|
|
1764
|
+
-H "X-API-Key: your-api-key" \
|
|
1765
|
+
-H "Content-Type: application/json" \
|
|
1766
|
+
-d '{
|
|
1767
|
+
"jobId": "ADHOC_FULFILLMENTS_20251027_183045_abc123"
|
|
1768
|
+
}'
|
|
1769
|
+
```
|
|
1770
|
+
|
|
1771
|
+
---
|
|
1772
|
+
|
|
1773
|
+
## Testing Checklist
|
|
1774
|
+
|
|
1775
|
+
**Before production deployment:**
|
|
1776
|
+
|
|
1777
|
+
### 1. Schema Validation
|
|
1778
|
+
|
|
1779
|
+
- [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
|
|
1780
|
+
- [ ] Run `npx fc-connect validate-schema --mapping ./config/fulfillments.export.csv.json --schema ./fluent-schema.json`
|
|
1781
|
+
- [ ] Run `npx fc-connect analyze-coverage --mapping ./config/fulfillments.export.csv.json --schema ./fluent-schema.json`
|
|
1782
|
+
- [ ] Verify all `source` paths in mapping exist in GraphQL schema
|
|
1783
|
+
- [ ] Verify query structure matches schema (fields, types, filters)
|
|
1784
|
+
|
|
1785
|
+
### 2. Extraction Testing
|
|
1786
|
+
|
|
1787
|
+
- [ ] Test with small dataset first (maxRecords=10)
|
|
1788
|
+
- [ ] Verify ExtractionOrchestrator pagination works correctly
|
|
1789
|
+
- [ ] Test with multiple pages of data (verify cursor handling)
|
|
1790
|
+
- [ ] Verify date range filtering (updatedOn filter)
|
|
1791
|
+
- [ ] Test empty result handling (no records in date range)
|
|
1792
|
+
- [ ] Verify extraction stops at maxRecords limit
|
|
1793
|
+
|
|
1794
|
+
### 3. Mapping Testing
|
|
1795
|
+
|
|
1796
|
+
- [ ] Verify required fields are populated
|
|
1797
|
+
- [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
|
|
1798
|
+
- [ ] Test custom resolvers with edge cases (if any)
|
|
1799
|
+
- [ ] Verify nested field extraction
|
|
1800
|
+
- [ ] Test with null/missing fields
|
|
1801
|
+
- [ ] Verify mapping error collection works
|
|
1802
|
+
|
|
1803
|
+
### 4. CSV Generation Testing
|
|
1804
|
+
|
|
1805
|
+
- [ ] Verify CSV structure matches expected format
|
|
1806
|
+
- [ ] Test CSV validation against schema (if applicable)
|
|
1807
|
+
- [ ] Verify header row is present and correct
|
|
1808
|
+
- [ ] Test with large datasets (>1000 records)
|
|
1809
|
+
- [ ] Verify UTF-8 encoding
|
|
1810
|
+
- [ ] Test special character handling (commas, quotes, newlines)
|
|
1811
|
+
|
|
1812
|
+
### 5. SFTP Upload Testing
|
|
1813
|
+
|
|
1814
|
+
- [ ] Test SFTP connection and authentication
|
|
1815
|
+
- [ ] Verify file upload to correct path
|
|
1816
|
+
- [ ] Test file naming convention (timestamp format)
|
|
1817
|
+
- [ ] Verify file permissions on SFTP server
|
|
1818
|
+
- [ ] Test upload retry logic (simulate network failure)
|
|
1819
|
+
- [ ] Verify SFTP connection disposal (no connection leaks)
|
|
1820
|
+
|
|
1821
|
+
### 6. State Management Testing
|
|
1822
|
+
|
|
1823
|
+
- [ ] Verify overlap buffer prevents missed records (60-second default)
|
|
1824
|
+
- [ ] Test state recovery after extraction failure
|
|
1825
|
+
- [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
|
|
1826
|
+
- [ ] Test first run with no previous state (uses fallbackStartDate)
|
|
1827
|
+
- [ ] Verify state update only happens on successful upload
|
|
1828
|
+
- [ ] Test manual date override (doesn't update state)
|
|
1829
|
+
|
|
1830
|
+
### 7. Job Tracking Testing
|
|
1831
|
+
|
|
1832
|
+
- [ ] Test job creation with JobTracker
|
|
1833
|
+
- [ ] Verify job status updates at each stage
|
|
1834
|
+
- [ ] Test job completion with metadata
|
|
1835
|
+
- [ ] Test job failure handling
|
|
1836
|
+
- [ ] Query job status via webhook endpoint
|
|
1837
|
+
- [ ] Verify job status persists in KV store
|
|
1838
|
+
|
|
1839
|
+
### 8. Error Handling Testing
|
|
1840
|
+
|
|
1841
|
+
- [ ] Test with invalid GraphQL query
|
|
1842
|
+
- [ ] Test with mapping errors (invalid field paths)
|
|
1843
|
+
- [ ] Test with SFTP connection failures
|
|
1844
|
+
- [ ] Test with authentication failures
|
|
1845
|
+
- [ ] Test with network timeouts
|
|
1846
|
+
- [ ] Verify error logging includes context (jobId, stage, error details)
|
|
1847
|
+
- [ ] Test error threshold logic (if applicable)
|
|
1848
|
+
|
|
1849
|
+
### 9. Staging Environment Testing
|
|
1850
|
+
|
|
1851
|
+
- [ ] Run full extraction in staging environment
|
|
1852
|
+
- [ ] Verify CSV file format with downstream system
|
|
1853
|
+
- [ ] Monitor extraction duration and resource usage
|
|
1854
|
+
- [ ] Test with production-like data volumes
|
|
1855
|
+
- [ ] Verify no performance degradation over time
|
|
1856
|
+
|
|
1857
|
+
### 10. Integration Testing
|
|
1858
|
+
|
|
1859
|
+
- [ ] Test scheduled workflow (cron trigger)
|
|
1860
|
+
- [ ] Test ad hoc webhook trigger
|
|
1861
|
+
- [ ] Test job status query webhook
|
|
1862
|
+
- [ ] Verify activation variables are read correctly
|
|
1863
|
+
- [ ] Test with different extraction modes (incremental, date range)
|
|
1864
|
+
- [ ] End-to-end test: trigger ? extract ? transform ? upload ? verify file
|
|
1865
|
+
|
|
1866
|
+
---
|
|
1867
|
+
## Monitoring & Alerting
|
|
1868
|
+
|
|
1869
|
+
### Success Response Example
|
|
1870
|
+
|
|
1871
|
+
```json
|
|
1872
|
+
{
|
|
1873
|
+
"success": true,
|
|
1874
|
+
"jobId": "SCHEDULED_FUL_20251102_140000_abc123",
|
|
1875
|
+
"recordsExtracted": 1523,
|
|
1876
|
+
"fileName": "fulfillments-2025-11-02T14-00-00-000Z.csv",
|
|
1877
|
+
"sftpPath": "/outbound/fulfillments/fulfillments-2025-11-02T14-00-00-000Z.csv",
|
|
1878
|
+
"metrics": {
|
|
1879
|
+
"extractionDurationMs": 12543,
|
|
1880
|
+
"totalPages": 8,
|
|
1881
|
+
"pageSize": 200,
|
|
1882
|
+
"mappingErrors": 0,
|
|
1883
|
+
"fileSizeBytes": 524288,
|
|
1884
|
+
"uploadDurationMs": 1234
|
|
1885
|
+
},
|
|
1886
|
+
"timestamps": {
|
|
1887
|
+
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
1888
|
+
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
1889
|
+
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
1890
|
+
},
|
|
1891
|
+
"state": {
|
|
1892
|
+
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
1893
|
+
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
1894
|
+
"stateUpdated": true,
|
|
1895
|
+
"overlapBufferSeconds": 60
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
```
|
|
1899
|
+
|
|
1900
|
+
### Error Response Example
|
|
1901
|
+
|
|
1902
|
+
```json
|
|
1903
|
+
{
|
|
1904
|
+
"success": false,
|
|
1905
|
+
"jobId": "ADHOC_FUL_20251102_140500_xyz789",
|
|
1906
|
+
"error": "SFTP upload failed: Connection timeout",
|
|
1907
|
+
"errorCategory": "NETWORK",
|
|
1908
|
+
"recordsExtracted": 0,
|
|
1909
|
+
"stage": "sftp_upload",
|
|
1910
|
+
"details": {
|
|
1911
|
+
"message": "Failed to upload file after 3 retry attempts",
|
|
1912
|
+
"retryAttempts": 3,
|
|
1913
|
+
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
1914
|
+
},
|
|
1915
|
+
"state": {
|
|
1916
|
+
"stateUpdated": false,
|
|
1917
|
+
"willRetryNextRun": true,
|
|
1918
|
+
"note": "State not advanced - next extraction will retry same time window"
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
```
|
|
1922
|
+
|
|
1923
|
+
### Key Metrics to Track
|
|
1924
|
+
|
|
1925
|
+
```typescript
|
|
1926
|
+
const METRICS = {
|
|
1927
|
+
// Extraction Performance
|
|
1928
|
+
extractionDurationMs: Date.now() - extractionStart,
|
|
1929
|
+
recordCount: records.length,
|
|
1930
|
+
pageCount: extractionResult.stats.totalPages,
|
|
1931
|
+
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
1932
|
+
|
|
1933
|
+
// Transformation Performance
|
|
1934
|
+
transformedCount: transformedRecords.length,
|
|
1935
|
+
failedCount: mappingErrors.length,
|
|
1936
|
+
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
1937
|
+
|
|
1938
|
+
// File Generation
|
|
1939
|
+
fileSizeMB: (csvContent.length / (1024 * 1024)).toFixed(2),
|
|
1940
|
+
|
|
1941
|
+
// Upload Performance
|
|
1942
|
+
uploadDurationMs: uploadEnd - uploadStart,
|
|
1943
|
+
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
1944
|
+
|
|
1945
|
+
// State Management
|
|
1946
|
+
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
1947
|
+
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
1948
|
+
};
|
|
1949
|
+
|
|
1950
|
+
log.info('Extraction metrics', metrics);
|
|
1951
|
+
```
|
|
1952
|
+
|
|
1953
|
+
### Alert Thresholds
|
|
1954
|
+
|
|
1955
|
+
```typescript
|
|
1956
|
+
const ALERT_THRESHOLDS = {
|
|
1957
|
+
// Duration Alerts
|
|
1958
|
+
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
1959
|
+
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
1960
|
+
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
1961
|
+
|
|
1962
|
+
// Error Rate Alerts
|
|
1963
|
+
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
1964
|
+
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
1965
|
+
|
|
1966
|
+
// Volume Alerts
|
|
1967
|
+
MAX_RECORDS_PER_RUN: 100000,
|
|
1968
|
+
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
1969
|
+
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
1970
|
+
|
|
1971
|
+
// State Alerts
|
|
1972
|
+
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
1973
|
+
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
1974
|
+
};
|
|
1975
|
+
|
|
1976
|
+
// Check thresholds
|
|
1977
|
+
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
1978
|
+
log.warn('Extraction duration exceeded threshold', {
|
|
1979
|
+
duration: metrics.extractionDurationMs,
|
|
1980
|
+
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
1981
|
+
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
```
|
|
1985
|
+
|
|
1986
|
+
### Monitoring Dashboard Queries
|
|
1987
|
+
|
|
1988
|
+
**Versori Platform Logs Query:**
|
|
1989
|
+
|
|
1990
|
+
```
|
|
1991
|
+
# Successful extractions
|
|
1992
|
+
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
1993
|
+
|
|
1994
|
+
# Failed extractions
|
|
1995
|
+
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
1996
|
+
|
|
1997
|
+
# Performance issues
|
|
1998
|
+
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
1999
|
+
|
|
2000
|
+
# High error rates
|
|
2001
|
+
errorRate:>5
|
|
2002
|
+
|
|
2003
|
+
# State management issues
|
|
2004
|
+
stateUpdated:false AND success:true
|
|
2005
|
+
```
|
|
2006
|
+
|
|
2007
|
+
### Common Issues and Solutions
|
|
2008
|
+
|
|
2009
|
+
**Issue**: "Extraction timeout after 10 minutes"
|
|
2010
|
+
|
|
2011
|
+
- **Cause**: Too many records in single extraction
|
|
2012
|
+
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
2013
|
+
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
2014
|
+
|
|
2015
|
+
**Issue**: "Mapping errors for 50% of records"
|
|
2016
|
+
|
|
2017
|
+
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
2018
|
+
- **Fix**: Run schema validation, update mapping config paths
|
|
2019
|
+
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
2020
|
+
|
|
2021
|
+
**Issue**: "SFTP connection timeout"
|
|
2022
|
+
|
|
2023
|
+
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
2024
|
+
- **Fix**: Check SFTP credentials, verify network connectivity
|
|
2025
|
+
- **Prevention**: Implement connection health checks, monitor connection status
|
|
2026
|
+
|
|
2027
|
+
**Issue**: "State not updating after successful extraction"
|
|
2028
|
+
|
|
2029
|
+
- **Cause**: KV write failure or intentional retry logic
|
|
2030
|
+
- **Fix**: Check KV logs, verify state update code executed
|
|
2031
|
+
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
2032
|
+
|
|
2033
|
+
**Issue**: "First run exceeds record limits"
|
|
2034
|
+
|
|
2035
|
+
- **Cause**: No previous timestamp, fetches all historical records
|
|
2036
|
+
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
2037
|
+
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
2038
|
+
|
|
2039
|
+
**Issue**: "Excessive duplicate records in output"
|
|
2040
|
+
|
|
2041
|
+
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
2042
|
+
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
2043
|
+
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
2044
|
+
|
|
2045
|
+
---
|
|
2046
|
+
|
|
2047
|
+
## Troubleshooting Quick Reference
|
|
2048
|
+
|
|
2049
|
+
| Error Message | Likely Cause | Solution |
|
|
2050
|
+
|--------------|--------------|----------|
|
|
2051
|
+
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
2052
|
+
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
2053
|
+
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
2054
|
+
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
2055
|
+
| "SFTP authentication failed" | Invalid credentials | Verify SFTP credentials in activation variables |
|
|
2056
|
+
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
2057
|
+
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
2058
|
+
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
2059
|
+
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
2060
|
+
| "CSV generation failed" | Format-specific error | Check CSV generation logic, validate output |
|
|
2061
|
+
|
|
2062
|
+
---
|