@fluentcommerce/fc-connect-sdk 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -2
- package/README.md +39 -0
- package/dist/cjs/auth/index.d.ts +3 -0
- package/dist/cjs/auth/index.js +13 -0
- package/dist/cjs/auth/profile-loader.d.ts +18 -0
- package/dist/cjs/auth/profile-loader.js +208 -0
- package/dist/cjs/client-factory.d.ts +4 -0
- package/dist/cjs/client-factory.js +10 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/auth/index.d.ts +3 -0
- package/dist/esm/auth/index.js +2 -0
- package/dist/esm/auth/profile-loader.d.ts +18 -0
- package/dist/esm/auth/profile-loader.js +169 -0
- package/dist/esm/client-factory.d.ts +4 -0
- package/dist/esm/client-factory.js +9 -0
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +2 -1
- 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/dist/types/auth/index.d.ts +3 -0
- package/dist/types/auth/profile-loader.d.ts +18 -0
- package/dist/types/client-factory.d.ts +4 -0
- package/dist/types/index.d.ts +3 -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 -482
- 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
|
@@ -1,2541 +1,2541 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-extract-orders-to-sftp-xml
|
|
3
|
-
canonical_filename: template-extraction-orders-to-sftp-xml.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-xml
|
|
10
|
-
entity: orders
|
|
11
|
-
format: xml
|
|
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 - Orders to SFTP XML
|
|
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
|
-
## Installation
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npm install @fluentcommerce/fc-connect-sdk
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Always check [npm registry](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk) for the latest version.
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## 📚 STEP 1: Load These Docs (Human Checklist)
|
|
45
|
-
|
|
46
|
-
1. REQUIRED (load all)
|
|
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
|
-
Copy-paste list (open these):
|
|
55
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
56
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
57
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
58
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
59
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
60
|
-
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## 📋 STEP 2: Tell Your AI (Prompt)
|
|
65
|
-
|
|
66
|
-
Copy/paste this prompt after loading the documentation above:
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
Create a Versori scheduled extractor for orders that uses ExtractionOrchestrator + JobTracker, incremental updatedOn with a 60s overlap buffer, transforms via UniversalMapper, generates XML with SDK's XMLBuilder, uploads to SFTP using SftpDataSource with dispose(). Include 3 workflows: scheduled, ad-hoc webhook, and job-status query with native Versori logging.
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
## 📦 SDK Imports (Verified - Versori Optimized)
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
import { Buffer } from 'node:buffer';
|
|
78
|
-
import {
|
|
79
|
-
createClient,
|
|
80
|
-
ExtractionOrchestrator,
|
|
81
|
-
JobTracker,
|
|
82
|
-
UniversalMapper,
|
|
83
|
-
XMLBuilder,
|
|
84
|
-
SftpDataSource,
|
|
85
|
-
VersoriKVAdapter,
|
|
86
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
87
|
-
|
|
88
|
-
import { schedule, webhook, http, fn } from '@versori/run';
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
# Versori Scheduled: Orders Extraction to SFTP XML (Incremental)
|
|
94
|
-
|
|
95
|
-
**FC Connect SDK Use Case Guide**
|
|
96
|
-
|
|
97
|
-
> SDK: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
98
|
-
> Version: Check [npm](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk) for latest
|
|
99
|
-
|
|
100
|
-
Context: Scheduled Versori workflow that extracts new/updated orders from Fluent Commerce via GraphQL query with **ExtractionOrchestrator**, **JobTracker**, and **incremental timestamp tracking**, transforms with `UniversalMapper`, and writes **XML files** to partner SFTP server for 3PL/WMS/fulfillment center integration.
|
|
101
|
-
|
|
102
|
-
**Pattern**: EXTRACTION (Fluent → SFTP XML)
|
|
103
|
-
**Complexity**: High | Runtime: Versori Platform (Scheduled)
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## ⚠️ IMPORTANT: Production-Ready Base Template
|
|
108
|
-
|
|
109
|
-
> **📋 BASE TEMPLATE - Ready for Production (Customize for Your Needs)**
|
|
110
|
-
>
|
|
111
|
-
> This is a **production-ready base template** demonstrating FC Connect SDK best practices for order extraction workflows with XML output.
|
|
112
|
-
>
|
|
113
|
-
> **✅ INCLUDED FEATURES:**
|
|
114
|
-
>
|
|
115
|
-
> - ✅ Comprehensive error handling with retry logic
|
|
116
|
-
> - ✅ SFTP upload with exponential backoff (3 attempts)
|
|
117
|
-
> - ✅ State management with overlap buffer (prevents missed records)
|
|
118
|
-
> - ✅ Job tracking with lifecycle management
|
|
119
|
-
> - ✅ Security (credential masking in logs)
|
|
120
|
-
> - ✅ UTC time enforcement (prevents timezone bugs)
|
|
121
|
-
> - ✅ Incremental extraction (safe, efficient, production-ready)
|
|
122
|
-
> - ✅ Natural rate limiting via timestamps
|
|
123
|
-
>
|
|
124
|
-
> **📝 BEFORE DEPLOYING:**
|
|
125
|
-
>
|
|
126
|
-
> 1. Review and customize activation variables for your environment
|
|
127
|
-
> 2. Test with sample data in your Versori workspace
|
|
128
|
-
> 3. Adjust safety limits (pageSize, maxRecords) if needed
|
|
129
|
-
> 4. Configure monitoring alerts for extraction failures
|
|
130
|
-
> 5. Verify SFTP credentials and paths
|
|
131
|
-
>
|
|
132
|
-
> **This base template follows SDK best practices - tweak specific to your needs.**
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## What You'll Build
|
|
137
|
-
|
|
138
|
-
- **Incremental extraction** using `updatedOn >= (lastRunTime - buffer)` with **overlap buffer**
|
|
139
|
-
- **ExtractionOrchestrator** for auto-pagination and path-based extraction
|
|
140
|
-
- **JobTracker** for lifecycle management and status tracking
|
|
141
|
-
- **State management** with VersoriKV to track last successful run
|
|
142
|
-
- **Safety buffer** (60 seconds) to handle clock skew and race conditions
|
|
143
|
-
- GraphQL query with nested order lines
|
|
144
|
-
- UniversalMapper transformation with line item data
|
|
145
|
-
- **XML file generation** with proper structure for 3PL/WMS systems
|
|
146
|
-
- **SFTP upload** to partner server (with `dispose()` cleanup)
|
|
147
|
-
- **3 workflow patterns**: scheduled, ad-hoc webhook, job status query
|
|
148
|
-
- **Failure recovery** with timestamp tracking
|
|
149
|
-
|
|
150
|
-
## Business Use Case
|
|
151
|
-
|
|
152
|
-
**Hourly order feed to 3PL/fulfillment center:**
|
|
153
|
-
|
|
154
|
-
- Extract new and updated orders since last run
|
|
155
|
-
- Generate XML file with order header + line items
|
|
156
|
-
- Upload to 3PL SFTP server for warehouse management system
|
|
157
|
-
- Run every hour to enable real-time fulfillment
|
|
158
|
-
- Support order updates (address changes, item modifications)
|
|
159
|
-
- Standard XML format for EDI/ERP integration
|
|
160
|
-
|
|
161
|
-
## SDK Methods Used
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
import { Buffer } from 'node:buffer';
|
|
165
|
-
import {
|
|
166
|
-
createClient,
|
|
167
|
-
ExtractionOrchestrator,
|
|
168
|
-
JobTracker,
|
|
169
|
-
UniversalMapper,
|
|
170
|
-
XMLBuilder,
|
|
171
|
-
SftpDataSource,
|
|
172
|
-
VersoriKVAdapter,
|
|
173
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
174
|
-
|
|
175
|
-
await createClient(ctx); // Versori-aware client
|
|
176
|
-
const orchestrator = new ExtractionOrchestrator(client, log); // Auto-pagination
|
|
177
|
-
const tracker = new JobTracker(kv, log); // Job lifecycle tracking
|
|
178
|
-
await orchestrator.extract({ query, resultPath, variables, pageSize, maxRecords }); // Extract
|
|
179
|
-
new VersoriKVAdapter(ctx.openKv(':project:')); // State management
|
|
180
|
-
new UniversalMapper(exportMapping); // Field transformation
|
|
181
|
-
new XMLBuilder(options); // XML generation with auto-escaping
|
|
182
|
-
await sftp.uploadFile(remotePath, buffer); // SFTP upload (no options param)
|
|
183
|
-
await sftp.dispose(); // CRITICAL: Connection cleanup
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
## SFTP Connection Setup (Recommended)
|
|
187
|
-
|
|
188
|
-
**✅ BEST PRACTICE:** Store SFTP credentials in a Versori connection object with Basic Auth:
|
|
189
|
-
|
|
190
|
-
**Connection Configuration:**
|
|
191
|
-
|
|
192
|
-
1. In Versori platform, create a connection named `versori_ftp_server`
|
|
193
|
-
2. Set **Authentication Type**: `Basic Auth`
|
|
194
|
-
3. Enter **Username**: Your SFTP username
|
|
195
|
-
4. Enter **Password**: Your SFTP password
|
|
196
|
-
5. The SDK will automatically decode the credentials using:
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
// Get SFTP credentials from Versori connection (Basic Auth)
|
|
200
|
-
// RECOMMENDED: Use activation.connections (already decoded)
|
|
201
|
-
const allConnections = ctx.activation.connections || [];
|
|
202
|
-
const sftpConn = allConnections.find(c => c.name === 'versori_ftp_server');
|
|
203
|
-
|
|
204
|
-
if (!sftpConn) {
|
|
205
|
-
throw new Error('SFTP connection "versori_ftp_server" not found');
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const credential = sftpConn.credentials[0]?.credential;
|
|
209
|
-
if (!credential?.data?.basicAuth) {
|
|
210
|
-
throw new Error('SFTP connection not configured with Basic Authentication');
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const { username, password } = credential.data.basicAuth;
|
|
214
|
-
// ✅ Already decoded - no Buffer.from() needed!
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
**Why use connections instead of activation variables?**
|
|
218
|
-
|
|
219
|
-
- ✅ Credentials stored securely in Versori vault
|
|
220
|
-
- ✅ Connection can be reused across workflows
|
|
221
|
-
- ✅ No need to manage sensitive data in activation variables
|
|
222
|
-
- ✅ Easier credential rotation
|
|
223
|
-
|
|
224
|
-
### SFTP Credential Access Methods
|
|
225
|
-
|
|
226
|
-
**You have TWO methods to access SFTP credentials from Versori connections:**
|
|
227
|
-
|
|
228
|
-
#### Method 1: activation.connections (✅ RECOMMENDED)
|
|
229
|
-
|
|
230
|
-
**Best for:** All scenarios - cleanest, no decoding needed
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
import { Buffer } from 'node:buffer'; // Required for SFTP upload
|
|
234
|
-
|
|
235
|
-
const { activation, log } = ctx;
|
|
236
|
-
|
|
237
|
-
// Get the connection (credentials ALREADY DECODED!)
|
|
238
|
-
const allConnections = activation.connections || [];
|
|
239
|
-
const sftpConnection = allConnections.find(c => c.name === 'versori_ftp_server');
|
|
240
|
-
|
|
241
|
-
if (!sftpConnection) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
`SFTP connection not found. Available: ${allConnections.map(c => c.name).join(', ')}`
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const sftpCred = sftpConnection.credentials[0]?.credential;
|
|
248
|
-
|
|
249
|
-
if (!sftpCred?.data?.basicAuth) {
|
|
250
|
-
throw new Error('SFTP connection not configured with Basic Auth');
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ✅ Already decoded - no Buffer.from() needed!
|
|
254
|
-
const sftpUsername = sftpCred.data.basicAuth.username;
|
|
255
|
-
const sftpPassword = sftpCred.data.basicAuth.password;
|
|
256
|
-
const sftpHost = sftpConnection.baseUrl || ctx.activation.getVariable('sftpHost');
|
|
257
|
-
|
|
258
|
-
log.info('SFTP credentials retrieved', {
|
|
259
|
-
username: sftpUsername,
|
|
260
|
-
host: sftpHost,
|
|
261
|
-
hasPassword: !!sftpPassword,
|
|
262
|
-
});
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
**Why this is best:**
|
|
266
|
-
|
|
267
|
-
- ✅ No base64 decoding required
|
|
268
|
-
- ✅ Type-safe access to credential structure
|
|
269
|
-
- ✅ Works in all task types (fn, http, webhook)
|
|
270
|
-
- ✅ Cleaner error handling
|
|
271
|
-
|
|
272
|
-
#### Method 2: credentials().get() (Alternative)
|
|
273
|
-
|
|
274
|
-
**Use when:** Working in fn() tasks where activation.connections not available
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
import { Buffer } from 'node:buffer'; // Required for both decoding AND SFTP upload
|
|
278
|
-
|
|
279
|
-
// Retrieve credentials (returns base64-encoded accessToken)
|
|
280
|
-
const sftpCred = await ctx.credentials().getAccessToken('versori_ftp_server');
|
|
281
|
-
|
|
282
|
-
if (!sftpCred?.accessToken) {
|
|
283
|
-
throw new Error('No SFTP credentials found');
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// ⚠️ Manual base64 decoding required
|
|
287
|
-
const rawBasicAuth = Buffer.from(sftpCred.accessToken, 'base64').toString('utf-8');
|
|
288
|
-
const [sftpUsername, sftpPassword] = rawBasicAuth.split(':');
|
|
289
|
-
|
|
290
|
-
if (!sftpUsername || !sftpPassword) {
|
|
291
|
-
throw new Error('Invalid credential format');
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
log.info('SFTP credentials decoded', {
|
|
295
|
-
hasUsername: !!sftpUsername,
|
|
296
|
-
hasPassword: !!sftpPassword,
|
|
297
|
-
});
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
**Limitations:**
|
|
301
|
-
|
|
302
|
-
- ⚠️ Requires manual base64 decoding
|
|
303
|
-
- ⚠️ More error-prone (string splitting)
|
|
304
|
-
- ⚠️ Only works in fn() tasks
|
|
305
|
-
|
|
306
|
-
### Buffer Import Reminder (CRITICAL!)
|
|
307
|
-
|
|
308
|
-
**For both methods, you MUST import Buffer in Versori/Deno runtime:**
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
import { Buffer } from 'node:buffer'; // ⚠️ ALWAYS REQUIRED
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
**Why:**
|
|
315
|
-
|
|
316
|
-
- SFTP uploads require Buffer: `Buffer.from(xmlContent, 'utf8')`
|
|
317
|
-
- Method 2 decoding requires Buffer: `Buffer.from(accessToken, 'base64')`
|
|
318
|
-
- Deno runtime doesn't have global Buffer (unlike Node.js)
|
|
319
|
-
|
|
320
|
-
**See:** [SFTP Credential Access & Security](../../../../../02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md) for complete documentation with troubleshooting and security best practices
|
|
321
|
-
|
|
322
|
-
## Activation Variables
|
|
323
|
-
|
|
324
|
-
**Configuration is driven by activation variables - modify these instead of code:**
|
|
325
|
-
|
|
326
|
-
```json
|
|
327
|
-
{
|
|
328
|
-
"retailerId": "your-retailer-id",
|
|
329
|
-
"sftpHost": "sftp.3pl-partner.com",
|
|
330
|
-
"sftpPort": 22,
|
|
331
|
-
"sftpPrivateKey": "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----",
|
|
332
|
-
"sftpRemotePath": "/incoming/orders/",
|
|
333
|
-
"pageSize": 200,
|
|
334
|
-
"maxRecords": 10000,
|
|
335
|
-
"fallbackStartDate": "2024-01-01T00:00:00Z",
|
|
336
|
-
"overlapBufferSeconds": "60"
|
|
337
|
-
}
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
> **Note:** `sftpUsername` and `sftpPassword` are fetched from the `versori_ftp_server` Basic Auth connection (see SFTP Connection Setup above).
|
|
341
|
-
|
|
342
|
-
## Export Mapping Configuration
|
|
343
|
-
|
|
344
|
-
**IMPORTANT**: Fields match CSV version exactly for consistency.
|
|
345
|
-
|
|
346
|
-
Create file: `./config/orders.export.xml.json`
|
|
347
|
-
|
|
348
|
-
```json
|
|
349
|
-
{
|
|
350
|
-
"name": "orders.export.xml",
|
|
351
|
-
"version": "1.0.0",
|
|
352
|
-
"description": "Fluent Orders → 3PL XML Export",
|
|
353
|
-
"fields": {
|
|
354
|
-
"order_id": { "source": "ref", "required": true, "resolver": "sdk.trim" },
|
|
355
|
-
"order_date": { "source": "createdOn", "required": true, "resolver": "sdk.formatDateShort" },
|
|
356
|
-
"order_status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
|
|
357
|
-
"customer_name": { "source": "customer.firstName", "required": false, "resolver": "sdk.trim" },
|
|
358
|
-
"customer_email": { "source": "customer.email", "required": false, "resolver": "sdk.trim" },
|
|
359
|
-
"ship_to_name": { "source": "deliveryAddress.name", "required": true, "resolver": "sdk.trim" },
|
|
360
|
-
"ship_to_address1": {
|
|
361
|
-
"source": "deliveryAddress.street1",
|
|
362
|
-
"required": true,
|
|
363
|
-
"resolver": "sdk.trim"
|
|
364
|
-
},
|
|
365
|
-
"ship_to_city": { "source": "deliveryAddress.city", "required": true, "resolver": "sdk.trim" },
|
|
366
|
-
"ship_to_state": {
|
|
367
|
-
"source": "deliveryAddress.state",
|
|
368
|
-
"required": true,
|
|
369
|
-
"resolver": "sdk.uppercase"
|
|
370
|
-
},
|
|
371
|
-
"ship_to_zip": {
|
|
372
|
-
"source": "deliveryAddress.postcode",
|
|
373
|
-
"required": true,
|
|
374
|
-
"resolver": "sdk.trim"
|
|
375
|
-
},
|
|
376
|
-
"ship_to_country": {
|
|
377
|
-
"source": "deliveryAddress.country",
|
|
378
|
-
"required": true,
|
|
379
|
-
"resolver": "sdk.uppercase"
|
|
380
|
-
},
|
|
381
|
-
"line_item_sku": { "source": "product.ref", "required": true, "resolver": "sdk.trim" },
|
|
382
|
-
"line_item_qty": { "source": "quantity", "required": true, "resolver": "sdk.parseInt" },
|
|
383
|
-
"line_item_price": { "source": "price", "required": true, "resolver": "sdk.parseFloat" }
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
## Mapping & Resolvers Explained
|
|
389
|
-
|
|
390
|
-
### SDK Resolvers Used
|
|
391
|
-
|
|
392
|
-
The export mapping uses **SDK resolvers** to transform order data into 3PL-ready XML format:
|
|
393
|
-
|
|
394
|
-
| Field | Resolver | Why? | Example Transformation |
|
|
395
|
-
| ----------------- | --------------------- | ---------------------------- | ------------------------------------------------ |
|
|
396
|
-
| `order_id` | `sdk.trim` | Clean order references | `" ORD-123 "` → `"ORD-123"` |
|
|
397
|
-
| `order_date` | `sdk.formatDateShort` | 3PL-friendly date format | `"2025-01-22T14:30:00Z"` → `"2025-01-22"` |
|
|
398
|
-
| `order_status` | `sdk.uppercase` | Normalize status codes | `"created"` → `"CREATED"` |
|
|
399
|
-
| `customer_name` | `sdk.trim` | Clean customer data | `"John "` → `"John"` |
|
|
400
|
-
| `customer_email` | `sdk.trim` | Clean email addresses | `" user@example.com "` → `"user@example.com"` |
|
|
401
|
-
| `ship_to_name` | `sdk.trim` | Clean shipping contact | `"Jane Doe "` → `"Jane Doe"` |
|
|
402
|
-
| `ship_to_state` | `sdk.uppercase` | Normalize state codes | `"ny"` → `"NY"` |
|
|
403
|
-
| `ship_to_country` | `sdk.uppercase` | Normalize country codes | `"us"` → `"US"` |
|
|
404
|
-
| `line_item_sku` | `sdk.trim` | Clean SKU references | `" SKU-001 "` → `"SKU-001"` |
|
|
405
|
-
| `line_item_qty` | `sdk.parseInt` | Parse quantities as integers | `"5"` → `5` |
|
|
406
|
-
| `line_item_price` | `sdk.parseFloat` | Parse prices as decimals | `"19.99"` → `19.99` |
|
|
407
|
-
|
|
408
|
-
### Transformation Flow
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
// 1. GraphQL Response (from Fluent API)
|
|
412
|
-
{
|
|
413
|
-
ref: " ORD-12345 ",
|
|
414
|
-
createdOn: "2025-01-22T14:30:00.000Z",
|
|
415
|
-
status: "created",
|
|
416
|
-
customer: {
|
|
417
|
-
firstName: "John ",
|
|
418
|
-
email: " john@example.com "
|
|
419
|
-
},
|
|
420
|
-
deliveryAddress: {
|
|
421
|
-
name: "Jane Doe ",
|
|
422
|
-
street1: "123 Main St",
|
|
423
|
-
city: "New York",
|
|
424
|
-
state: "ny",
|
|
425
|
-
postcode: "10001",
|
|
426
|
-
country: "us"
|
|
427
|
-
},
|
|
428
|
-
items: [
|
|
429
|
-
{
|
|
430
|
-
quantity: "2",
|
|
431
|
-
price: "29.99",
|
|
432
|
-
product: { ref: " SKU-001 ", name: "Widget" }
|
|
433
|
-
}
|
|
434
|
-
]
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// 2. UniversalMapper applies resolvers
|
|
438
|
-
const mapper = new UniversalMapper(ordersExportMapping);
|
|
439
|
-
const result = await mapper.map(order);
|
|
440
|
-
|
|
441
|
-
// 3. Transformed Output (clean, normalized)
|
|
442
|
-
result.data = {
|
|
443
|
-
order_id: "ORD-12345", // ✅ Trimmed
|
|
444
|
-
order_date: "2025-01-22", // ✅ Short date format
|
|
445
|
-
order_status: "CREATED", // ✅ Uppercased
|
|
446
|
-
customer_name: "John", // ✅ Trimmed
|
|
447
|
-
customer_email: "john@example.com", // ✅ Trimmed
|
|
448
|
-
ship_to_name: "Jane Doe", // ✅ Trimmed
|
|
449
|
-
ship_to_address1: "123 Main St",
|
|
450
|
-
ship_to_city: "New York",
|
|
451
|
-
ship_to_state: "NY", // ✅ Uppercased
|
|
452
|
-
ship_to_zip: "10001",
|
|
453
|
-
ship_to_country: "US", // ✅ Uppercased
|
|
454
|
-
items: [
|
|
455
|
-
{
|
|
456
|
-
line_item_sku: "SKU-001", // ✅ Trimmed
|
|
457
|
-
line_item_qty: 2, // ✅ Integer
|
|
458
|
-
line_item_price: 29.99 // ✅ Float
|
|
459
|
-
}
|
|
460
|
-
]
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// 4. Generate XML with proper escaping
|
|
464
|
-
const xml = buildOrdersXML([result.data]);
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
### Custom Resolvers for Order-Specific Logic
|
|
468
|
-
|
|
469
|
-
You can add **custom resolvers** for 3PL-specific transformations:
|
|
470
|
-
|
|
471
|
-
```typescript
|
|
472
|
-
const ordersExportMapping = {
|
|
473
|
-
name: 'orders.export.xml',
|
|
474
|
-
version: '1.0.0',
|
|
475
|
-
fields: {
|
|
476
|
-
order_id: { source: 'ref', required: true, resolver: 'sdk.trim' },
|
|
477
|
-
order_date: { source: 'createdOn', required: true, resolver: 'sdk.formatDateShort' },
|
|
478
|
-
|
|
479
|
-
// Custom resolver: Generate 3PL reference number
|
|
480
|
-
partner_order_ref: {
|
|
481
|
-
source: 'ref',
|
|
482
|
-
resolver: 'custom.generate3PLReference',
|
|
483
|
-
},
|
|
484
|
-
|
|
485
|
-
// Custom resolver: Calculate order total
|
|
486
|
-
order_total: {
|
|
487
|
-
source: 'items',
|
|
488
|
-
resolver: 'custom.calculateOrderTotal',
|
|
489
|
-
},
|
|
490
|
-
|
|
491
|
-
// Custom resolver: Map shipping service level
|
|
492
|
-
shipping_service_code: {
|
|
493
|
-
source: 'shippingMethod',
|
|
494
|
-
resolver: 'custom.mapShippingService',
|
|
495
|
-
},
|
|
496
|
-
|
|
497
|
-
// Custom resolver: Format phone number for 3PL
|
|
498
|
-
ship_to_phone: {
|
|
499
|
-
source: 'deliveryAddress.phone',
|
|
500
|
-
resolver: 'custom.formatPhone',
|
|
501
|
-
},
|
|
502
|
-
},
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
// Custom resolver implementations
|
|
506
|
-
const customResolvers = {
|
|
507
|
-
'custom.generate3PLReference': (orderRef: string) => {
|
|
508
|
-
// Format: 3PL-YYYYMMDD-ORDERREF
|
|
509
|
-
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
510
|
-
return `3PL-${today}-${orderRef}`;
|
|
511
|
-
},
|
|
512
|
-
|
|
513
|
-
'custom.calculateOrderTotal': (items: any[]) => {
|
|
514
|
-
const total = items.reduce((sum, item) => {
|
|
515
|
-
const qty = parseFloat(item.quantity) || 0;
|
|
516
|
-
const price = parseFloat(item.price) || 0;
|
|
517
|
-
return sum + qty * price;
|
|
518
|
-
}, 0);
|
|
519
|
-
return total.toFixed(2);
|
|
520
|
-
},
|
|
521
|
-
|
|
522
|
-
'custom.mapShippingService': (method: string) => {
|
|
523
|
-
const serviceMap: Record<string, string> = {
|
|
524
|
-
STANDARD: 'GROUND',
|
|
525
|
-
EXPRESS: '2DAY',
|
|
526
|
-
OVERNIGHT: 'NEXTDAY',
|
|
527
|
-
INTERNATIONAL: 'INTL',
|
|
528
|
-
};
|
|
529
|
-
return serviceMap[method] || 'GROUND';
|
|
530
|
-
},
|
|
531
|
-
|
|
532
|
-
'custom.formatPhone': (phone: string) => {
|
|
533
|
-
if (!phone) return '';
|
|
534
|
-
// Remove all non-digits
|
|
535
|
-
const digits = phone.replace(/\D/g, '');
|
|
536
|
-
// Format as (XXX) XXX-XXXX
|
|
537
|
-
if (digits.length === 10) {
|
|
538
|
-
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
539
|
-
}
|
|
540
|
-
return phone;
|
|
541
|
-
},
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
// Use with UniversalMapper
|
|
545
|
-
const mapper = new UniversalMapper(ordersExportMapping, { customResolvers });
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
### Available SDK Resolvers
|
|
549
|
-
|
|
550
|
-
**String Transformations:**
|
|
551
|
-
|
|
552
|
-
- `sdk.trim` - Remove whitespace
|
|
553
|
-
- `sdk.uppercase` - Convert to uppercase
|
|
554
|
-
- `sdk.lowercase` - Convert to lowercase
|
|
555
|
-
- `sdk.toString` - Convert to string
|
|
556
|
-
|
|
557
|
-
**Number Transformations:**
|
|
558
|
-
|
|
559
|
-
- `sdk.parseInt` - Parse integer
|
|
560
|
-
- `sdk.parseFloat` - Parse decimal
|
|
561
|
-
- `sdk.number` - Generic number conversion
|
|
562
|
-
|
|
563
|
-
**Date Transformations:**
|
|
564
|
-
|
|
565
|
-
- `sdk.formatDate` - ISO 8601 format (`2025-01-22T14:30:00Z`)
|
|
566
|
-
- `sdk.formatDateShort` - Short date format (`2025-01-22`)
|
|
567
|
-
- `sdk.parseDate` - Parse date string
|
|
568
|
-
|
|
569
|
-
**Type Conversions:**
|
|
570
|
-
|
|
571
|
-
- `sdk.boolean` - Convert to boolean
|
|
572
|
-
- `sdk.parseJson` - Parse JSON string
|
|
573
|
-
- `sdk.toJson` - Convert to JSON string
|
|
574
|
-
|
|
575
|
-
**Utility:**
|
|
576
|
-
|
|
577
|
-
- `sdk.identity` - Pass through unchanged
|
|
578
|
-
- `sdk.coalesce` - Return first non-null value
|
|
579
|
-
|
|
580
|
-
See [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) for complete resolver documentation.
|
|
581
|
-
|
|
582
|
-
## GraphQL Query
|
|
583
|
-
|
|
584
|
-
```graphql
|
|
585
|
-
query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, $first: Int!, $after: String) {
|
|
586
|
-
orders(
|
|
587
|
-
retailerId: $retailerId
|
|
588
|
-
updatedOn: { after: $updatedAfter }
|
|
589
|
-
first: $first
|
|
590
|
-
after: $after
|
|
591
|
-
) {
|
|
592
|
-
edges {
|
|
593
|
-
node {
|
|
594
|
-
id
|
|
595
|
-
ref
|
|
596
|
-
status
|
|
597
|
-
createdOn
|
|
598
|
-
updatedOn
|
|
599
|
-
customer {
|
|
600
|
-
firstName
|
|
601
|
-
lastName
|
|
602
|
-
email
|
|
603
|
-
}
|
|
604
|
-
deliveryAddress {
|
|
605
|
-
name
|
|
606
|
-
street1
|
|
607
|
-
street2
|
|
608
|
-
city
|
|
609
|
-
state
|
|
610
|
-
postcode
|
|
611
|
-
country
|
|
612
|
-
}
|
|
613
|
-
items {
|
|
614
|
-
id
|
|
615
|
-
quantity
|
|
616
|
-
price
|
|
617
|
-
product {
|
|
618
|
-
ref
|
|
619
|
-
name
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
cursor
|
|
624
|
-
}
|
|
625
|
-
pageInfo {
|
|
626
|
-
hasNextPage
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
## Expected XML Output
|
|
633
|
-
|
|
634
|
-
**IMPORTANT**: XML structure with same fields as CSV version for consistency.
|
|
635
|
-
|
|
636
|
-
```xml
|
|
637
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
638
|
-
<Orders>
|
|
639
|
-
<Order>
|
|
640
|
-
<OrderHeader>
|
|
641
|
-
<order_id>ORD-001</order_id>
|
|
642
|
-
<order_date>2025-01-22</order_date>
|
|
643
|
-
<order_status>CREATED</order_status>
|
|
644
|
-
<customer_name>John</customer_name>
|
|
645
|
-
<customer_email>john@example.com</customer_email>
|
|
646
|
-
</OrderHeader>
|
|
647
|
-
<ShipTo>
|
|
648
|
-
<ship_to_name>John Smith</ship_to_name>
|
|
649
|
-
<ship_to_address1>123 Main St</ship_to_address1>
|
|
650
|
-
<ship_to_city>New York</ship_to_city>
|
|
651
|
-
<ship_to_state>NY</ship_to_state>
|
|
652
|
-
<ship_to_zip>10001</ship_to_zip>
|
|
653
|
-
<ship_to_country>US</ship_to_country>
|
|
654
|
-
</ShipTo>
|
|
655
|
-
<LineItems>
|
|
656
|
-
<LineItem>
|
|
657
|
-
<line_item_sku>SKU-001</line_item_sku>
|
|
658
|
-
<line_item_qty>2</line_item_qty>
|
|
659
|
-
<line_item_price>29.99</line_item_price>
|
|
660
|
-
</LineItem>
|
|
661
|
-
<LineItem>
|
|
662
|
-
<line_item_sku>SKU-002</line_item_sku>
|
|
663
|
-
<line_item_qty>1</line_item_qty>
|
|
664
|
-
<line_item_price>49.99</line_item_price>
|
|
665
|
-
</LineItem>
|
|
666
|
-
</LineItems>
|
|
667
|
-
</Order>
|
|
668
|
-
<Order>
|
|
669
|
-
<OrderHeader>
|
|
670
|
-
<order_id>ORD-002</order_id>
|
|
671
|
-
<order_date>2025-01-22</order_date>
|
|
672
|
-
<order_status>PAID</order_status>
|
|
673
|
-
<customer_name>Jane</customer_name>
|
|
674
|
-
<customer_email>jane@example.com</customer_email>
|
|
675
|
-
</OrderHeader>
|
|
676
|
-
<ShipTo>
|
|
677
|
-
<ship_to_name>Jane Doe</ship_to_name>
|
|
678
|
-
<ship_to_address1>456 Oak Ave</ship_to_address1>
|
|
679
|
-
<ship_to_city>Los Angeles</ship_to_city>
|
|
680
|
-
<ship_to_state>CA</ship_to_state>
|
|
681
|
-
<ship_to_zip>90001</ship_to_zip>
|
|
682
|
-
<ship_to_country>US</ship_to_country>
|
|
683
|
-
</ShipTo>
|
|
684
|
-
<LineItems>
|
|
685
|
-
<LineItem>
|
|
686
|
-
<line_item_sku>SKU-003</line_item_sku>
|
|
687
|
-
<line_item_qty>1</line_item_qty>
|
|
688
|
-
<line_item_price>19.99</line_item_price>
|
|
689
|
-
</LineItem>
|
|
690
|
-
</LineItems>
|
|
691
|
-
</Order>
|
|
692
|
-
</Orders>
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
**Note**: XML preserves hierarchical structure (Order → LineItems) unlike CSV which flattens to rows.
|
|
696
|
-
|
|
697
|
-
---
|
|
698
|
-
|
|
699
|
-
## Versori Workflows Structure
|
|
700
|
-
|
|
701
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
702
|
-
|
|
703
|
-
**Trigger Types:**
|
|
704
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
705
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
706
|
-
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
707
|
-
|
|
708
|
-
**Execution Steps (chained to triggers):**
|
|
709
|
-
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
710
|
-
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
711
|
-
|
|
712
|
-
### Recommended Project Structure
|
|
713
|
-
|
|
714
|
-
```
|
|
715
|
-
orders-extraction/
|
|
716
|
-
├── index.ts # Entry point - exports all workflows
|
|
717
|
-
└── src/
|
|
718
|
-
├── workflows/
|
|
719
|
-
│ ├── scheduled/
|
|
720
|
-
│ │ └── daily-orders-extraction.ts # Scheduled: Daily orders extraction
|
|
721
|
-
│ │
|
|
722
|
-
│ └── webhook/
|
|
723
|
-
│ ├── adhoc-orders-extraction.ts # Webhook: Manual trigger
|
|
724
|
-
│ └── job-status-check.ts # Webhook: Status query
|
|
725
|
-
│
|
|
726
|
-
├── services/
|
|
727
|
-
│ └── orders-extraction.service.ts # Shared orchestration logic (reusable)
|
|
728
|
-
│
|
|
729
|
-
└── config/
|
|
730
|
-
└── orders.export.xml.json # Mapping configuration
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
**Why This Structure:**
|
|
734
|
-
- ✅ Easier to maintain (each workflow in its own file)
|
|
735
|
-
- ✅ Clear separation of concerns (scheduled vs webhook)
|
|
736
|
-
- ✅ Better for team collaboration (fewer merge conflicts)
|
|
737
|
-
- ✅ Follows Versori best practices
|
|
738
|
-
|
|
739
|
-
---
|
|
740
|
-
|
|
741
|
-
### Overview
|
|
742
|
-
|
|
743
|
-
Even with **incremental-only** extraction, order data needs safeguards to prevent runtime failures:
|
|
744
|
-
|
|
745
|
-
- **Nested data**: Orders contain line items, addresses, customers (1 order → 5-20 line items)
|
|
746
|
-
- **XML generation**: More memory-intensive than CSV generation
|
|
747
|
-
- **Time-critical**: 3PL/fulfillment systems need reliable, timely feeds
|
|
748
|
-
- **High priority**: Order processing delays directly impact customers
|
|
749
|
-
|
|
750
|
-
### Hard Limits
|
|
751
|
-
|
|
752
|
-
```typescript
|
|
753
|
-
const SAFETY_LIMITS = {
|
|
754
|
-
MAX_ORDERS_PER_RUN: 50000, // 50k orders per run
|
|
755
|
-
MAX_XML_ELEMENTS: 500000, // 500k XML elements total
|
|
756
|
-
MAX_RECORDS_PER_FILE: 10000, // 10k orders per XML file (SFTP-friendly)
|
|
757
|
-
MAX_FILE_SIZE_MB: 100, // 100MB per file
|
|
758
|
-
MAX_XML_SIZE_MB: 200, // Total extraction size
|
|
759
|
-
CHUNK_SIZE: 5000, // Process in chunks
|
|
760
|
-
AVG_LINE_ITEMS_PER_ORDER: 3, // Conservative estimate
|
|
761
|
-
ESTIMATED_BYTES_PER_ORDER_XML: 2000, // XML with full structure
|
|
762
|
-
};
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
**Why XML needs special consideration?**
|
|
766
|
-
|
|
767
|
-
- **XML overhead**: Tags and structure add 2-3x size vs CSV
|
|
768
|
-
- **Memory during generation**: Building XML tree in memory
|
|
769
|
-
- **SFTP partners**: Often have stricter file size limits than S3
|
|
770
|
-
- **Validation**: 3PL systems validate XML against schemas
|
|
771
|
-
|
|
772
|
-
### Runtime Validation Function
|
|
773
|
-
|
|
774
|
-
```typescript
|
|
775
|
-
/**
|
|
776
|
-
* Validate extraction safety limits before processing
|
|
777
|
-
* CRITICAL: Account for XML size overhead vs CSV
|
|
778
|
-
*/
|
|
779
|
-
function validateExtractionLimits(orderCount: number, totalLineItems: number) {
|
|
780
|
-
const MAX_ORDERS_PER_RUN = 50000;
|
|
781
|
-
const MAX_XML_ELEMENTS = 500000; // orders + line items + structure
|
|
782
|
-
const ESTIMATED_BYTES_PER_ORDER_XML = 2000; // Full XML order element
|
|
783
|
-
const estimatedSizeMB = (orderCount * ESTIMATED_BYTES_PER_ORDER_XML) / (1024 * 1024);
|
|
784
|
-
const MAX_XML_SIZE_MB = 200;
|
|
785
|
-
|
|
786
|
-
if (orderCount > MAX_ORDERS_PER_RUN) {
|
|
787
|
-
return {
|
|
788
|
-
valid: false,
|
|
789
|
-
error: `Extraction limit exceeded: ${orderCount} orders (max: ${MAX_ORDERS_PER_RUN})`,
|
|
790
|
-
recommendation: `Too many orders for single extraction. Consider:
|
|
791
|
-
1. Increase extraction frequency (daily → hourly)
|
|
792
|
-
2. Add order status filters (NEW, PAID only)
|
|
793
|
-
3. Split by fulfillment location
|
|
794
|
-
4. Contact support if consistently exceeding limits`,
|
|
795
|
-
orderCount,
|
|
796
|
-
maxAllowed: MAX_ORDERS_PER_RUN,
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const totalElements = orderCount + totalLineItems + orderCount * 2; // headers + shipping
|
|
801
|
-
if (totalElements > MAX_XML_ELEMENTS) {
|
|
802
|
-
return {
|
|
803
|
-
valid: false,
|
|
804
|
-
error: `XML element limit exceeded: ${totalElements} elements (max: ${MAX_XML_ELEMENTS})`,
|
|
805
|
-
recommendation: `XML structure too large. Orders: ${orderCount}, Line items: ${totalLineItems}. Increase extraction frequency.`,
|
|
806
|
-
totalElements,
|
|
807
|
-
maxAllowed: MAX_XML_ELEMENTS,
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
if (estimatedSizeMB > MAX_XML_SIZE_MB) {
|
|
812
|
-
return {
|
|
813
|
-
valid: false,
|
|
814
|
-
error: `XML size limit exceeded: ${estimatedSizeMB}MB (max: ${MAX_XML_SIZE_MB}MB)`,
|
|
815
|
-
recommendation:
|
|
816
|
-
'File splitting required. Increase extraction frequency to reduce batch size.',
|
|
817
|
-
estimatedSizeMB,
|
|
818
|
-
maxAllowed: MAX_XML_SIZE_MB,
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
return { valid: true };
|
|
823
|
-
}
|
|
824
|
-
```
|
|
825
|
-
|
|
826
|
-
### Memory-Safe XML Generation with XMLBuilder
|
|
827
|
-
|
|
828
|
-
The SDK's `XMLBuilder` handles XML generation efficiently with automatic escaping:
|
|
829
|
-
|
|
830
|
-
```typescript
|
|
831
|
-
import { Buffer } from 'node:buffer';
|
|
832
|
-
import { XMLBuilder } from '@fluentcommerce/fc-connect-sdk';
|
|
833
|
-
|
|
834
|
-
// Initialize XMLBuilder (reusable)
|
|
835
|
-
const xmlBuilder = new XMLBuilder({
|
|
836
|
-
rootElement: 'Orders',
|
|
837
|
-
prettyPrint: true,
|
|
838
|
-
indent: ' ',
|
|
839
|
-
xmlDeclaration: true,
|
|
840
|
-
encoding: 'UTF-8',
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* Generate XML from orders using XMLBuilder
|
|
845
|
-
* Handles nested structures (OrderHeader, ShipTo, LineItems) automatically
|
|
846
|
-
*/
|
|
847
|
-
function buildOrdersXML(orders: any[]): string {
|
|
848
|
-
// Transform to XMLBuilder format with nested structures
|
|
849
|
-
const ordersForXml = orders.map(order => ({
|
|
850
|
-
OrderHeader: {
|
|
851
|
-
order_id: order.order_id,
|
|
852
|
-
order_date: order.order_date,
|
|
853
|
-
order_status: order.order_status,
|
|
854
|
-
customer_name: order.customer_name || '',
|
|
855
|
-
customer_email: order.customer_email || '',
|
|
856
|
-
},
|
|
857
|
-
ShipTo: {
|
|
858
|
-
ship_to_name: order.ship_to_name,
|
|
859
|
-
ship_to_address1: order.ship_to_address1,
|
|
860
|
-
ship_to_city: order.ship_to_city,
|
|
861
|
-
ship_to_state: order.ship_to_state,
|
|
862
|
-
ship_to_zip: order.ship_to_zip,
|
|
863
|
-
ship_to_country: order.ship_to_country,
|
|
864
|
-
},
|
|
865
|
-
LineItems: {
|
|
866
|
-
LineItem: order.line_items.map(item => ({
|
|
867
|
-
line_item_sku: item.line_item_sku,
|
|
868
|
-
line_item_qty: item.line_item_qty,
|
|
869
|
-
line_item_price: item.line_item_price,
|
|
870
|
-
})),
|
|
871
|
-
},
|
|
872
|
-
}));
|
|
873
|
-
|
|
874
|
-
// XMLBuilder handles all escaping and structure automatically
|
|
875
|
-
return xmlBuilder.build({ Order: ordersForXml });
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// Example output:
|
|
879
|
-
// <Orders>
|
|
880
|
-
// <Order>
|
|
881
|
-
// <OrderHeader>
|
|
882
|
-
// <order_id>ORD-001</order_id>
|
|
883
|
-
// <customer_name>Smith & Jones</customer_name>
|
|
884
|
-
// </OrderHeader>
|
|
885
|
-
// <ShipTo>
|
|
886
|
-
// <ship_to_name>John Smith</ship_to_name>
|
|
887
|
-
// <ship_to_address1>123 Main St</ship_to_address1>
|
|
888
|
-
// </ShipTo>
|
|
889
|
-
// <LineItems>
|
|
890
|
-
// <LineItem>
|
|
891
|
-
// <line_item_sku>SKU-001</line_item_sku>
|
|
892
|
-
// <line_item_qty>2</line_item_qty>
|
|
893
|
-
// </LineItem>
|
|
894
|
-
// </LineItems>
|
|
895
|
-
// </Order>
|
|
896
|
-
// </Orders>
|
|
897
|
-
```
|
|
898
|
-
|
|
899
|
-
**Benefits:**
|
|
900
|
-
|
|
901
|
-
- ✅ Automatic XML escaping (handles &, <, >, ", ', etc.)
|
|
902
|
-
- ✅ Nested structure support (OrderHeader, ShipTo, LineItems)
|
|
903
|
-
- ✅ Array handling (multiple LineItem elements)
|
|
904
|
-
- ✅ Memory-efficient streaming
|
|
905
|
-
- ✅ Pretty printing with proper indentation
|
|
906
|
-
|
|
907
|
-
## Complete Workflow Code
|
|
908
|
-
|
|
909
|
-
The following code examples demonstrate the implementation across separate files following the recommended modular structure.
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
### 1. Entry Point (src/index.ts)
|
|
913
|
-
|
|
914
|
-
```typescript
|
|
915
|
-
/**
|
|
916
|
-
* Entry point - Export all workflows for Versori platform
|
|
917
|
-
*
|
|
918
|
-
* This file exports all workflows to be registered with Versori.
|
|
919
|
-
* Each workflow is defined in its own file for better organization.
|
|
920
|
-
*/
|
|
921
|
-
|
|
922
|
-
// Scheduled workflows
|
|
923
|
-
export { scheduledOrdersExtraction } from './workflows/scheduled/daily-orders-extraction';
|
|
924
|
-
|
|
925
|
-
// Webhook workflows
|
|
926
|
-
export { adhocOrdersExtraction } from './workflows/webhook/adhoc-orders-extraction';
|
|
927
|
-
export { ordersJobStatus } from './workflows/webhook/job-status-check';
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
### 2. Scheduled Workflow (src/workflows/scheduled/daily-orders-extraction.ts)
|
|
931
|
-
|
|
932
|
-
```typescript
|
|
933
|
-
import { schedule, http } from '@versori/run';
|
|
934
|
-
import {
|
|
935
|
-
executeOrderExtraction,
|
|
936
|
-
generateJobId,
|
|
937
|
-
} from '../../services/extraction-orchestration';
|
|
938
|
-
|
|
939
|
-
/**
|
|
940
|
-
* Scheduled workflow: Daily orders extraction to SFTP XML
|
|
941
|
-
* Runs at 2:00 AM daily
|
|
942
|
-
*
|
|
943
|
-
* DELEGATION PATTERN:
|
|
944
|
-
* - Workflow receives ctx from Versori
|
|
945
|
-
* - Passes entire ctx to service function
|
|
946
|
-
* - Service handles all business logic
|
|
947
|
-
*/
|
|
948
|
-
export const scheduledOrdersExtraction = schedule('orders-extract-xml-daily', '0 2 * * *').then(
|
|
949
|
-
http('execute-scheduled-extraction', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
950
|
-
const { log } = ctx;
|
|
951
|
-
const executionStartTime = Date.now();
|
|
952
|
-
const jobId = generateJobId('SCHED', 'ORDERS');
|
|
953
|
-
|
|
954
|
-
log.info('🚀 [WORKFLOW] Starting scheduled extraction', { jobId });
|
|
955
|
-
|
|
956
|
-
const result = await executeOrderExtraction(ctx, {
|
|
957
|
-
jobId,
|
|
958
|
-
triggeredBy: 'schedule',
|
|
959
|
-
updateState: true, // Always update state for scheduled runs
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
const duration = Date.now() - executionStartTime;
|
|
963
|
-
|
|
964
|
-
if (result.success) {
|
|
965
|
-
log.info('✅ [WORKFLOW] Extraction completed successfully', {
|
|
966
|
-
jobId,
|
|
967
|
-
ordersExtracted: result.ordersExtracted,
|
|
968
|
-
duration: `${duration}ms`,
|
|
969
|
-
});
|
|
970
|
-
} else {
|
|
971
|
-
log.error('❌ [WORKFLOW] Extraction failed', {
|
|
972
|
-
jobId,
|
|
973
|
-
error: result.error,
|
|
974
|
-
duration: `${duration}ms`,
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
return { ...result, duration };
|
|
979
|
-
})
|
|
980
|
-
);
|
|
981
|
-
```
|
|
982
|
-
|
|
983
|
-
### 3. Ad-hoc Webhook (src/workflows/webhook/adhoc-orders-extraction.ts)
|
|
984
|
-
|
|
985
|
-
```typescript
|
|
986
|
-
import { webhook, http } from '@versori/run';
|
|
987
|
-
import {
|
|
988
|
-
executeOrderExtraction,
|
|
989
|
-
generateJobId,
|
|
990
|
-
} from '../../services/extraction-orchestration';
|
|
991
|
-
|
|
992
|
-
/**
|
|
993
|
-
* Webhook workflow: Ad-hoc orders extraction
|
|
994
|
-
* Allows manual/on-demand extraction with date range overrides
|
|
995
|
-
*/
|
|
996
|
-
export const adhocOrdersExtraction = webhook('orders-adhoc', {
|
|
997
|
-
connection: 'orders-adhoc',
|
|
998
|
-
response: { mode: 'sync' },
|
|
999
|
-
}).then(
|
|
1000
|
-
http('execute-adhoc-extraction', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
1001
|
-
const { log } = ctx;
|
|
1002
|
-
const executionStartTime = Date.now();
|
|
1003
|
-
const jobId = generateJobId('ADHOC', 'ORDERS');
|
|
1004
|
-
|
|
1005
|
-
log.info('🔧 [WEBHOOK] Starting ad-hoc extraction', {
|
|
1006
|
-
jobId,
|
|
1007
|
-
fromDate: ctx.data.fromDate,
|
|
1008
|
-
toDate: ctx.data.toDate
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
const result = await executeOrderExtraction(ctx, {
|
|
1012
|
-
jobId,
|
|
1013
|
-
triggeredBy: 'webhook',
|
|
1014
|
-
fromDate: ctx.data.fromDate,
|
|
1015
|
-
toDate: ctx.data.toDate,
|
|
1016
|
-
updateState: ctx.data.updateState !== false,
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
const duration = Date.now() - executionStartTime;
|
|
1020
|
-
|
|
1021
|
-
if (result.success) {
|
|
1022
|
-
log.info('✅ [WEBHOOK] Ad-hoc extraction completed', {
|
|
1023
|
-
jobId,
|
|
1024
|
-
ordersExtracted: result.ordersExtracted,
|
|
1025
|
-
duration: `${duration}ms`,
|
|
1026
|
-
});
|
|
1027
|
-
} else {
|
|
1028
|
-
log.error('❌ [WEBHOOK] Ad-hoc extraction failed', {
|
|
1029
|
-
jobId,
|
|
1030
|
-
error: result.error,
|
|
1031
|
-
duration: `${duration}ms`,
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
return { ...result, duration };
|
|
1036
|
-
})
|
|
1037
|
-
);
|
|
1038
|
-
```
|
|
1039
|
-
|
|
1040
|
-
### 4. Job Status Query (src/workflows/webhook/job-status-check.ts)
|
|
1041
|
-
|
|
1042
|
-
```typescript
|
|
1043
|
-
import { webhook, fn } from '@versori/run';
|
|
1044
|
-
import { getJobStatus } from '../../services/extraction-orchestration';
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* Webhook workflow: Job status query
|
|
1048
|
-
* Allows checking the status of extraction jobs by ID
|
|
1049
|
-
*/
|
|
1050
|
-
export const ordersJobStatus = webhook('orders-job-status', {
|
|
1051
|
-
connection: 'orders-job-status',
|
|
1052
|
-
response: { mode: 'sync' },
|
|
1053
|
-
}).then(
|
|
1054
|
-
fn('query-job-status', async (ctx: any) => {
|
|
1055
|
-
const { data, log, openKv } = ctx;
|
|
1056
|
-
// Security is enforced by the 'orders-job-status' connection
|
|
1057
|
-
|
|
1058
|
-
log.info('🔍 [STATUS] Querying job status', { jobId: data.jobId });
|
|
1059
|
-
|
|
1060
|
-
const jobId = data.jobId;
|
|
1061
|
-
if (!jobId) {
|
|
1062
|
-
log.error('❌ [STATUS] Missing job ID');
|
|
1063
|
-
return { success: false, error: 'Job ID required' };
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
const status = await getJobStatus(openKv(':project:'), jobId, log);
|
|
1067
|
-
|
|
1068
|
-
if (status) {
|
|
1069
|
-
log.info('✅ [STATUS] Job found', { jobId, status: status.status });
|
|
1070
|
-
return { success: true, jobId, ...status };
|
|
1071
|
-
} else {
|
|
1072
|
-
log.warn('⚠️ [STATUS] Job not found', { jobId });
|
|
1073
|
-
return { success: false, error: 'Job not found', jobId };
|
|
1074
|
-
}
|
|
1075
|
-
})
|
|
1076
|
-
);
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
### 5. Main Orchestration Service (src/services/extraction-orchestration.ts)
|
|
1080
|
-
|
|
1081
|
-
```typescript
|
|
1082
|
-
import { Buffer } from 'node:buffer';
|
|
1083
|
-
import {
|
|
1084
|
-
createClient,
|
|
1085
|
-
ExtractionOrchestrator,
|
|
1086
|
-
JobTracker,
|
|
1087
|
-
UniversalMapper,
|
|
1088
|
-
XMLBuilder,
|
|
1089
|
-
SftpDataSource,
|
|
1090
|
-
VersoriKVAdapter,
|
|
1091
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
1092
|
-
import ordersExportMapping from '../../config/orders.export.xml.json' with { type: 'json' };
|
|
1093
|
-
|
|
1094
|
-
const ORDERS_QUERY = `
|
|
1095
|
-
query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, $first: Int!, $after: String) {
|
|
1096
|
-
orders(
|
|
1097
|
-
retailerId: $retailerId
|
|
1098
|
-
updatedOn: { after: $updatedAfter }
|
|
1099
|
-
first: $first
|
|
1100
|
-
after: $after
|
|
1101
|
-
) {
|
|
1102
|
-
edges {
|
|
1103
|
-
node {
|
|
1104
|
-
id
|
|
1105
|
-
ref
|
|
1106
|
-
status
|
|
1107
|
-
createdOn
|
|
1108
|
-
updatedOn
|
|
1109
|
-
customer {
|
|
1110
|
-
firstName
|
|
1111
|
-
lastName
|
|
1112
|
-
email
|
|
1113
|
-
}
|
|
1114
|
-
deliveryAddress {
|
|
1115
|
-
name
|
|
1116
|
-
street1
|
|
1117
|
-
street2
|
|
1118
|
-
city
|
|
1119
|
-
state
|
|
1120
|
-
postcode
|
|
1121
|
-
country
|
|
1122
|
-
}
|
|
1123
|
-
items {
|
|
1124
|
-
id
|
|
1125
|
-
quantity
|
|
1126
|
-
price
|
|
1127
|
-
product {
|
|
1128
|
-
ref
|
|
1129
|
-
name
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
cursor
|
|
1134
|
-
}
|
|
1135
|
-
pageInfo {
|
|
1136
|
-
hasNextPage
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
`;
|
|
1141
|
-
|
|
1142
|
-
// Initialize XMLBuilder for orders
|
|
1143
|
-
const xmlBuilder = new XMLBuilder({
|
|
1144
|
-
rootElement: 'Orders',
|
|
1145
|
-
prettyPrint: true,
|
|
1146
|
-
indent: ' ',
|
|
1147
|
-
xmlDeclaration: true,
|
|
1148
|
-
encoding: 'UTF-8',
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
function buildOrdersXML(orders: any[]): string {
|
|
1152
|
-
// Transform to XMLBuilder format with nested structures
|
|
1153
|
-
const ordersForXml = orders.map(order => ({
|
|
1154
|
-
OrderHeader: {
|
|
1155
|
-
order_id: order.order_id,
|
|
1156
|
-
order_date: order.order_date,
|
|
1157
|
-
order_status: order.order_status,
|
|
1158
|
-
customer_name: order.customer_name || '',
|
|
1159
|
-
customer_email: order.customer_email || '',
|
|
1160
|
-
},
|
|
1161
|
-
ShipTo: {
|
|
1162
|
-
ship_to_name: order.ship_to_name,
|
|
1163
|
-
ship_to_address1: order.ship_to_address1,
|
|
1164
|
-
ship_to_city: order.ship_to_city,
|
|
1165
|
-
ship_to_state: order.ship_to_state,
|
|
1166
|
-
ship_to_zip: order.ship_to_zip,
|
|
1167
|
-
ship_to_country: order.ship_to_country,
|
|
1168
|
-
},
|
|
1169
|
-
LineItems: {
|
|
1170
|
-
LineItem: order.line_items.map(item => ({
|
|
1171
|
-
line_item_sku: item.line_item_sku,
|
|
1172
|
-
line_item_qty: item.line_item_qty,
|
|
1173
|
-
line_item_price: item.line_item_price,
|
|
1174
|
-
})),
|
|
1175
|
-
},
|
|
1176
|
-
}));
|
|
1177
|
-
|
|
1178
|
-
return xmlBuilder.build({ Order: ordersForXml });
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
interface ExtractionOptions {
|
|
1182
|
-
jobId: string;
|
|
1183
|
-
triggeredBy: 'schedule' | 'webhook';
|
|
1184
|
-
fromDate?: string;
|
|
1185
|
-
toDate?: string;
|
|
1186
|
-
updateState: boolean;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
export async function executeOrderExtraction(ctx: any, options: ExtractionOptions) {
|
|
1190
|
-
const { jobId, triggeredBy, fromDate, toDate, updateState } = options;
|
|
1191
|
-
const log = ctx.log;
|
|
1192
|
-
const retailerId = ctx.activation?.getVariable('retailerId');
|
|
1193
|
-
const pageSize = parseInt(ctx.activation?.getVariable('pageSize') || '200', 10);
|
|
1194
|
-
const maxRecords = parseInt(ctx.activation?.getVariable('maxRecords') || '10000', 10);
|
|
1195
|
-
const fallbackStartDate =
|
|
1196
|
-
ctx.activation?.getVariable('fallbackStartDate') || '2024-01-01T00:00:00Z';
|
|
1197
|
-
|
|
1198
|
-
// Get SFTP credentials from Versori connection (Basic Auth)
|
|
1199
|
-
// RECOMMENDED: Use activation.connections (already decoded)
|
|
1200
|
-
const allConnections = ctx.activation.connections || [];
|
|
1201
|
-
const sftpConn = allConnections.find(c => c.name === 'versori_ftp_server');
|
|
1202
|
-
|
|
1203
|
-
if (!sftpConn) {
|
|
1204
|
-
throw new Error('SFTP connection "versori_ftp_server" not found');
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
const credential = sftpConn.credentials[0]?.credential;
|
|
1208
|
-
if (!credential?.data?.basicAuth) {
|
|
1209
|
-
throw new Error('SFTP connection not configured with Basic Authentication');
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
const { username, password } = credential.data.basicAuth;
|
|
1213
|
-
// ✅ Already decoded - no Buffer.from() needed!
|
|
1214
|
-
|
|
1215
|
-
const sftpSettings = {
|
|
1216
|
-
host: ctx.activation?.getVariable('sftpHost'),
|
|
1217
|
-
port: parseInt(ctx.activation?.getVariable('sftpPort') || '22', 10),
|
|
1218
|
-
username, // From connection (secure)
|
|
1219
|
-
password, // From connection (secure)
|
|
1220
|
-
privateKey: ctx.activation?.getVariable('sftpPrivateKey'),
|
|
1221
|
-
remotePath: ctx.activation?.getVariable('sftpRemotePath') || '/incoming/orders/',
|
|
1222
|
-
};
|
|
1223
|
-
|
|
1224
|
-
const missing: string[] = [];
|
|
1225
|
-
if (!retailerId) missing.push('retailerId');
|
|
1226
|
-
if (!sftpSettings.host) missing.push('sftpHost');
|
|
1227
|
-
if (!sftpSettings.username) missing.push('sftpUsername from connection');
|
|
1228
|
-
if (!sftpSettings.password && !sftpSettings.privateKey)
|
|
1229
|
-
missing.push('sftpPassword from connection or sftpPrivateKey');
|
|
1230
|
-
if (missing.length)
|
|
1231
|
-
return { success: false, error: `Missing required variables: ${missing.join(', ')}` };
|
|
1232
|
-
|
|
1233
|
-
// SFTP connection - MUST use try/finally with dispose()
|
|
1234
|
-
const sftp = new SftpDataSource(
|
|
1235
|
-
{
|
|
1236
|
-
type: 'SFTP_XML',
|
|
1237
|
-
connectionId: 'sftp-orders-xml-export',
|
|
1238
|
-
name: 'SFTP Orders XML Export',
|
|
1239
|
-
settings: {
|
|
1240
|
-
host: sftpSettings.host,
|
|
1241
|
-
port: sftpSettings.port,
|
|
1242
|
-
username: sftpSettings.username,
|
|
1243
|
-
password: sftpSettings.password,
|
|
1244
|
-
privateKey: sftpSettings.privateKey,
|
|
1245
|
-
remotePath: sftpSettings.remotePath,
|
|
1246
|
-
filePattern: '*.xml',
|
|
1247
|
-
},
|
|
1248
|
-
},
|
|
1249
|
-
log
|
|
1250
|
-
);
|
|
1251
|
-
|
|
1252
|
-
try {
|
|
1253
|
-
//
|
|
1254
|
-
// STEP 1/8: Initialize Job Tracking
|
|
1255
|
-
//
|
|
1256
|
-
const kv = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1257
|
-
const tracker = new JobTracker(kv, log);
|
|
1258
|
-
|
|
1259
|
-
await tracker.createJob(jobId, {
|
|
1260
|
-
triggeredBy,
|
|
1261
|
-
hasDateOverride: !!fromDate,
|
|
1262
|
-
fromDate,
|
|
1263
|
-
toDate,
|
|
1264
|
-
updateStateAfterRun: updateState,
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
log.info('Job created', { jobId, triggeredBy });
|
|
1268
|
-
|
|
1269
|
-
//
|
|
1270
|
-
// STEP 2/8: Load State & Calculate Time Window
|
|
1271
|
-
//
|
|
1272
|
-
await tracker.updateJob(jobId, {
|
|
1273
|
-
status: 'processing',
|
|
1274
|
-
stage: 'state_load',
|
|
1275
|
-
message: 'Loading last run state',
|
|
1276
|
-
});
|
|
1277
|
-
|
|
1278
|
-
const stateKey = ['extraction', 'orders-xml', 'lastRunTime'];
|
|
1279
|
-
const lastRunState = await kv.get(stateKey);
|
|
1280
|
-
const rawLastRunTime = fromDate || lastRunState?.value?.timestamp || fallbackStartDate;
|
|
1281
|
-
|
|
1282
|
-
// Overlap buffer configuration (default: 60 seconds)
|
|
1283
|
-
const overlapBufferSeconds = parseInt(
|
|
1284
|
-
ctx.activation?.getVariable('overlapBufferSeconds') || '60',
|
|
1285
|
-
10
|
|
1286
|
-
);
|
|
1287
|
-
const OVERLAP_BUFFER_MS = overlapBufferSeconds * 1000;
|
|
1288
|
-
|
|
1289
|
-
// Apply overlap buffer for query (safety window)
|
|
1290
|
-
const bufferedLastRunTime = new Date(
|
|
1291
|
-
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
|
|
1292
|
-
).toISOString();
|
|
1293
|
-
|
|
1294
|
-
const effectiveEndTime = toDate || new Date().toISOString();
|
|
1295
|
-
|
|
1296
|
-
log.info('Time window calculated', {
|
|
1297
|
-
rawLastRunTime,
|
|
1298
|
-
bufferedLastRunTime,
|
|
1299
|
-
effectiveEndTime,
|
|
1300
|
-
overlapBufferSeconds,
|
|
1301
|
-
retailerId,
|
|
1302
|
-
});
|
|
1303
|
-
|
|
1304
|
-
//
|
|
1305
|
-
// STEP 3/8: Initialize Fluent Client & ExtractionOrchestrator
|
|
1306
|
-
//
|
|
1307
|
-
await tracker.updateJob(jobId, {
|
|
1308
|
-
stage: 'client_init',
|
|
1309
|
-
message: 'Initializing Fluent client',
|
|
1310
|
-
});
|
|
1311
|
-
|
|
1312
|
-
const client = await createClient(ctx);
|
|
1313
|
-
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
1314
|
-
|
|
1315
|
-
//
|
|
1316
|
-
// STEP 4/8: Extract Data (ExtractionOrchestrator)
|
|
1317
|
-
//
|
|
1318
|
-
await tracker.updateJob(jobId, {
|
|
1319
|
-
stage: 'extraction',
|
|
1320
|
-
message: 'Extracting data with auto-pagination',
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
|
-
// ? Enhanced: Extract context for progress logging
|
|
1324
|
-
const dateRangeInfo = {
|
|
1325
|
-
start: bufferedLastRunTime,
|
|
1326
|
-
end: effectiveEndTime,
|
|
1327
|
-
retailerId
|
|
1328
|
-
};
|
|
1329
|
-
|
|
1330
|
-
// ? Enhanced: Start logging with context
|
|
1331
|
-
log.info(`🔍 [ExtractionOrchestrator] Starting extraction`, {
|
|
1332
|
-
query: 'orders',
|
|
1333
|
-
pageSize,
|
|
1334
|
-
maxRecords,
|
|
1335
|
-
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1336
|
-
retailerId: dateRangeInfo.retailerId,
|
|
1337
|
-
jobId
|
|
1338
|
-
});
|
|
1339
|
-
|
|
1340
|
-
const extractionResult = await orchestrator.extract({
|
|
1341
|
-
query: ORDERS_QUERY,
|
|
1342
|
-
resultPath: 'orders.edges.node',
|
|
1343
|
-
variables: {
|
|
1344
|
-
retailerId,
|
|
1345
|
-
updatedAfter: bufferedLastRunTime,
|
|
1346
|
-
first: pageSize,
|
|
1347
|
-
},
|
|
1348
|
-
pageSize,
|
|
1349
|
-
maxRecords,
|
|
1350
|
-
validateItem: item => !!(item.ref && item.customer),
|
|
1351
|
-
});
|
|
1352
|
-
|
|
1353
|
-
const rawRecords = extractionResult.data;
|
|
1354
|
-
|
|
1355
|
-
log.info('Extraction complete', {
|
|
1356
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1357
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1358
|
-
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1359
|
-
errors: extractionResult.errors ? extractionResult.errors.length : 0,
|
|
1360
|
-
});
|
|
1361
|
-
|
|
1362
|
-
// ? Enhanced: Completion logging with summary
|
|
1363
|
-
log.info(`✅ [ExtractionOrchestrator] Extraction completed`, {
|
|
1364
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1365
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1366
|
-
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1367
|
-
failedValidations: extractionResult.stats.failedValidations,
|
|
1368
|
-
truncated: extractionResult.stats.truncated,
|
|
1369
|
-
truncationReason: extractionResult.stats.truncationReason,
|
|
1370
|
-
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1371
|
-
jobId
|
|
1372
|
-
});
|
|
1373
|
-
|
|
1374
|
-
if (extractionResult.errors && extractionResult.errors.length > 0) {
|
|
1375
|
-
log.warn('Non-fatal extraction errors encountered', {
|
|
1376
|
-
errorCount: extractionResult.errors.length,
|
|
1377
|
-
sampleErrors: extractionResult.errors.slice(0, 3),
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
if (rawRecords.length === 0) {
|
|
1382
|
-
await tracker.markCompleted(jobId, {
|
|
1383
|
-
recordCount: 0,
|
|
1384
|
-
message: 'No new orders to extract',
|
|
1385
|
-
});
|
|
1386
|
-
|
|
1387
|
-
if (updateState) {
|
|
1388
|
-
await kv.set(stateKey, {
|
|
1389
|
-
timestamp: new Date().toISOString(),
|
|
1390
|
-
orderCount: 0,
|
|
1391
|
-
extractedAt: new Date().toISOString(),
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
return { success: true, message: 'No new orders to extract', lastRunTime: rawLastRunTime };
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
//
|
|
1399
|
-
// STEP 5/8: Validate Extraction Limits
|
|
1400
|
-
//
|
|
1401
|
-
await tracker.updateJob(jobId, {
|
|
1402
|
-
stage: 'validation',
|
|
1403
|
-
message: 'Validating extraction limits',
|
|
1404
|
-
});
|
|
1405
|
-
|
|
1406
|
-
const MAX_ORDERS_PER_RUN = 50000;
|
|
1407
|
-
const MAX_XML_ELEMENTS = 500000;
|
|
1408
|
-
|
|
1409
|
-
let totalLineItems = 0;
|
|
1410
|
-
for (const order of rawRecords) {
|
|
1411
|
-
totalLineItems += (order.items || []).length;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
const totalElements = rawRecords.length + totalLineItems + rawRecords.length * 2; // orders + items + headers/shipping
|
|
1415
|
-
const ESTIMATED_BYTES_PER_ORDER_XML = 2000;
|
|
1416
|
-
const estimatedSizeMB = (rawRecords.length * ESTIMATED_BYTES_PER_ORDER_XML) / (1024 * 1024);
|
|
1417
|
-
const MAX_XML_SIZE_MB = 200;
|
|
1418
|
-
|
|
1419
|
-
if (rawRecords.length > MAX_ORDERS_PER_RUN) {
|
|
1420
|
-
log.error('Extraction limit exceeded', {
|
|
1421
|
-
orderCount: rawRecords.length,
|
|
1422
|
-
maxAllowed: MAX_ORDERS_PER_RUN,
|
|
1423
|
-
});
|
|
1424
|
-
|
|
1425
|
-
await tracker.markFailed(jobId, {
|
|
1426
|
-
error: `Extraction limit exceeded: ${rawRecords.length} orders (max: ${MAX_ORDERS_PER_RUN})`,
|
|
1427
|
-
recommendation: 'Increase extraction frequency or add filters',
|
|
1428
|
-
});
|
|
1429
|
-
|
|
1430
|
-
return {
|
|
1431
|
-
success: false,
|
|
1432
|
-
error: `Extraction limit exceeded: ${rawRecords.length} orders (max: ${MAX_ORDERS_PER_RUN})`,
|
|
1433
|
-
recommendation: `Too many orders for single extraction. Consider:
|
|
1434
|
-
1. Increase extraction frequency (daily → hourly)
|
|
1435
|
-
2. Add order status filters (NEW, PAID only)
|
|
1436
|
-
3. Split by fulfillment location
|
|
1437
|
-
4. Contact support if consistently exceeding limits`,
|
|
1438
|
-
orderCount: rawRecords.length,
|
|
1439
|
-
maxAllowed: MAX_ORDERS_PER_RUN,
|
|
1440
|
-
};
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
if (totalElements > MAX_XML_ELEMENTS) {
|
|
1444
|
-
log.error('XML element limit exceeded', {
|
|
1445
|
-
orderCount: rawRecords.length,
|
|
1446
|
-
totalLineItems,
|
|
1447
|
-
totalElements,
|
|
1448
|
-
maxAllowed: MAX_XML_ELEMENTS,
|
|
1449
|
-
});
|
|
1450
|
-
|
|
1451
|
-
await tracker.markFailed(jobId, {
|
|
1452
|
-
error: `XML element limit exceeded: ${totalElements} elements`,
|
|
1453
|
-
recommendation: 'Increase extraction frequency',
|
|
1454
|
-
});
|
|
1455
|
-
|
|
1456
|
-
return {
|
|
1457
|
-
success: false,
|
|
1458
|
-
error: `XML element limit exceeded: ${totalElements} elements (max: ${MAX_XML_ELEMENTS})`,
|
|
1459
|
-
recommendation: `XML structure too large. Orders: ${rawRecords.length}, Line items: ${totalLineItems}. Increase extraction frequency.`,
|
|
1460
|
-
totalElements,
|
|
1461
|
-
maxAllowed: MAX_XML_ELEMENTS,
|
|
1462
|
-
};
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
if (estimatedSizeMB > MAX_XML_SIZE_MB) {
|
|
1466
|
-
log.warn('XML size approaching limit', {
|
|
1467
|
-
estimatedSizeMB: estimatedSizeMB.toFixed(2),
|
|
1468
|
-
maxAllowed: MAX_XML_SIZE_MB,
|
|
1469
|
-
recommendation: 'Consider file splitting or increase extraction frequency',
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
log.info('Extraction limits validated', {
|
|
1474
|
-
orderCount: rawRecords.length,
|
|
1475
|
-
totalLineItems,
|
|
1476
|
-
totalElements,
|
|
1477
|
-
estimatedSizeMB: estimatedSizeMB.toFixed(2),
|
|
1478
|
-
withinLimits: true,
|
|
1479
|
-
});
|
|
1480
|
-
|
|
1481
|
-
await tracker.updateJob(jobId, {
|
|
1482
|
-
stage: 'transformation',
|
|
1483
|
-
message: 'Transforming data with UniversalMapper',
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
const mapper = new UniversalMapper(ordersExportMapping);
|
|
1487
|
-
const transformedOrders: any[] = [];
|
|
1488
|
-
const mappingErrors: any[] = [];
|
|
1489
|
-
|
|
1490
|
-
for (let index = 0; index < rawRecords.length; index += 1) {
|
|
1491
|
-
const order = rawRecords[index];
|
|
1492
|
-
const headerResult = await mapper.map(order);
|
|
1493
|
-
|
|
1494
|
-
if (!headerResult.success || !headerResult.data) {
|
|
1495
|
-
mappingErrors.push({
|
|
1496
|
-
orderRef: order.ref,
|
|
1497
|
-
errors: headerResult.errors || ['Mapping failed'],
|
|
1498
|
-
});
|
|
1499
|
-
continue;
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
const header = headerResult.data as Record<string, any>;
|
|
1503
|
-
const lineItems = (order.items || []).map(item => ({
|
|
1504
|
-
line_item_sku: String(item.product?.ref ?? '').trim(),
|
|
1505
|
-
line_item_qty: Number.parseInt(String(item.quantity ?? '0'), 10) || 0,
|
|
1506
|
-
line_item_price: Number.parseFloat(String(item.price ?? '0')) || 0,
|
|
1507
|
-
}));
|
|
1508
|
-
|
|
1509
|
-
transformedOrders.push({
|
|
1510
|
-
order_id: header.order_id,
|
|
1511
|
-
order_date: header.order_date,
|
|
1512
|
-
order_status: header.order_status,
|
|
1513
|
-
customer_name: header.customer_name,
|
|
1514
|
-
customer_email: header.customer_email,
|
|
1515
|
-
ship_to_name: header.ship_to_name,
|
|
1516
|
-
ship_to_address1: header.ship_to_address1,
|
|
1517
|
-
ship_to_city: header.ship_to_city,
|
|
1518
|
-
ship_to_state: header.ship_to_state,
|
|
1519
|
-
ship_to_zip: header.ship_to_zip,
|
|
1520
|
-
ship_to_country: header.ship_to_country,
|
|
1521
|
-
updated_on: header.updated_on || order.updatedOn,
|
|
1522
|
-
line_items: lineItems,
|
|
1523
|
-
});
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
if (mappingErrors.length > 0) {
|
|
1527
|
-
log.warn('Some orders failed transformation', {
|
|
1528
|
-
jobId,
|
|
1529
|
-
errorCount: mappingErrors.length,
|
|
1530
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
if (transformedOrders.length === 0) {
|
|
1535
|
-
await tracker.markFailed(jobId, {
|
|
1536
|
-
error: 'All records failed mapping',
|
|
1537
|
-
failedCount: mappingErrors.length,
|
|
1538
|
-
});
|
|
1539
|
-
return {
|
|
1540
|
-
success: false,
|
|
1541
|
-
error: 'All records failed mapping',
|
|
1542
|
-
errors: mappingErrors,
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
log.info('Orders transformed', {
|
|
1547
|
-
jobId,
|
|
1548
|
-
transformedCount: transformedOrders.length,
|
|
1549
|
-
skippedRecords: rawRecords.length - transformedOrders.length,
|
|
1550
|
-
});
|
|
1551
|
-
|
|
1552
|
-
await tracker.updateJob(jobId, {
|
|
1553
|
-
stage: 'upload',
|
|
1554
|
-
message: 'Generating XML and uploading to SFTP',
|
|
1555
|
-
});
|
|
1556
|
-
|
|
1557
|
-
const xmlContent = buildOrdersXML(transformedOrders);
|
|
1558
|
-
|
|
1559
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1560
|
-
const fileName = `orders-${timestamp}.xml`;
|
|
1561
|
-
const remotePath = `${sftpSettings.remotePath}${fileName}`;
|
|
1562
|
-
|
|
1563
|
-
log.info('Generated XML file', {
|
|
1564
|
-
fileName,
|
|
1565
|
-
size: xmlContent.length,
|
|
1566
|
-
orderCount: transformedOrders.length,
|
|
1567
|
-
lineItemCount: totalLineItems,
|
|
1568
|
-
});
|
|
1569
|
-
|
|
1570
|
-
await sftp.uploadFile(remotePath, Buffer.from(xmlContent, 'utf8'));
|
|
1571
|
-
|
|
1572
|
-
log.info('XML file uploaded to SFTP', { remotePath });
|
|
1573
|
-
|
|
1574
|
-
await tracker.updateJob(jobId, {
|
|
1575
|
-
stage: 'state_update',
|
|
1576
|
-
message: 'Updating state and completing job',
|
|
1577
|
-
});
|
|
1578
|
-
|
|
1579
|
-
const maxUpdatedOn = transformedOrders.reduce((max, order) => {
|
|
1580
|
-
const orderTime = new Date(order.updated_on).getTime();
|
|
1581
|
-
return orderTime > max ? orderTime : max;
|
|
1582
|
-
}, new Date(rawLastRunTime).getTime());
|
|
1583
|
-
|
|
1584
|
-
const newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
1585
|
-
|
|
1586
|
-
if (updateState) {
|
|
1587
|
-
await kv.set(stateKey, {
|
|
1588
|
-
timestamp: newTimestamp,
|
|
1589
|
-
orderCount: transformedOrders.length,
|
|
1590
|
-
lineItemCount: totalLineItems,
|
|
1591
|
-
extractedAt: new Date().toISOString(),
|
|
1592
|
-
overlapBufferSeconds,
|
|
1593
|
-
fileName,
|
|
1594
|
-
remotePath,
|
|
1595
|
-
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1596
|
-
});
|
|
1597
|
-
|
|
1598
|
-
log.info('State updated with new timestamp (without buffer)', {
|
|
1599
|
-
newTimestamp,
|
|
1600
|
-
overlapBufferSeconds,
|
|
1601
|
-
});
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
await tracker.markCompleted(jobId, {
|
|
1605
|
-
recordCount: transformedOrders.length,
|
|
1606
|
-
fileName,
|
|
1607
|
-
sftpPath: remotePath,
|
|
1608
|
-
errorCount: mappingErrors.length,
|
|
1609
|
-
errors: mappingErrors,
|
|
1610
|
-
});
|
|
1611
|
-
|
|
1612
|
-
return {
|
|
1613
|
-
success: true,
|
|
1614
|
-
ordersExtracted: transformedOrders.length,
|
|
1615
|
-
lineItemsExtracted: totalLineItems,
|
|
1616
|
-
fileName,
|
|
1617
|
-
remotePath,
|
|
1618
|
-
lastRunTime: rawLastRunTime,
|
|
1619
|
-
newTimestamp,
|
|
1620
|
-
jobId,
|
|
1621
|
-
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1622
|
-
};
|
|
1623
|
-
} catch (error: any) {
|
|
1624
|
-
log.error('Extraction failed', error, {
|
|
1625
|
-
message: error?.message,
|
|
1626
|
-
});
|
|
1627
|
-
|
|
1628
|
-
const kv = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1629
|
-
const tracker = new JobTracker(kv, log);
|
|
1630
|
-
|
|
1631
|
-
await tracker.markFailed(jobId, {
|
|
1632
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1633
|
-
|
|
1634
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1635
|
-
|
|
1636
|
-
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
1637
|
-
});
|
|
1638
|
-
|
|
1639
|
-
return {
|
|
1640
|
-
success: false,
|
|
1641
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1642
|
-
|
|
1643
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1644
|
-
|
|
1645
|
-
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
1646
|
-
jobId,
|
|
1647
|
-
};
|
|
1648
|
-
} finally {
|
|
1649
|
-
// CRITICAL: Always clean up SFTP connections
|
|
1650
|
-
await sftp.dispose();
|
|
1651
|
-
log.info('SFTP connection disposed');
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
export async function getJobStatus(kv: any, jobId: string, log: any) {
|
|
1656
|
-
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
1657
|
-
return await tracker.getJob(jobId);
|
|
1658
|
-
}
|
|
1659
|
-
```
|
|
1660
|
-
|
|
1661
|
-
### 6. Job ID Generator (src/utils/job-id-generator.ts)
|
|
1662
|
-
|
|
1663
|
-
```typescript
|
|
1664
|
-
/**
|
|
1665
|
-
* Generate unique job ID
|
|
1666
|
-
* Format: {PREFIX}-{ENTITY}-{TIMESTAMP}
|
|
1667
|
-
*/
|
|
1668
|
-
export function generateJobId(prefix: 'SCHED' | 'ADHOC', entity: string): string {
|
|
1669
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1670
|
-
return `${prefix}-${entity}-${timestamp}`;
|
|
1671
|
-
}
|
|
1672
|
-
```
|
|
1673
|
-
|
|
1674
|
-
### 7. Package Configuration (package.json)
|
|
1675
|
-
|
|
1676
|
-
```json
|
|
1677
|
-
{
|
|
1678
|
-
"name": "orders-extraction-to-sftp-xml",
|
|
1679
|
-
"version": "1.0.0",
|
|
1680
|
-
"description": "Versori connector for orders extraction to SFTP XML",
|
|
1681
|
-
"main": "dist/index.js",
|
|
1682
|
-
"type": "module",
|
|
1683
|
-
"scripts": {
|
|
1684
|
-
"build": "tsc",
|
|
1685
|
-
"dev": "tsc --watch",
|
|
1686
|
-
"lint": "eslint src/**/*.ts",
|
|
1687
|
-
"test": "jest"
|
|
1688
|
-
},
|
|
1689
|
-
"dependencies": {
|
|
1690
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
1691
|
-
"@versori/run": "latest"
|
|
1692
|
-
},
|
|
1693
|
-
"devDependencies": {
|
|
1694
|
-
"@types/node": "^20.0.0",
|
|
1695
|
-
"typescript": "^5.0.0"
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
```
|
|
1699
|
-
|
|
1700
|
-
### 8. Deployment Instructions
|
|
1701
|
-
|
|
1702
|
-
```bash
|
|
1703
|
-
# 1. Install dependencies
|
|
1704
|
-
npm install
|
|
1705
|
-
|
|
1706
|
-
# 2. Build the connector
|
|
1707
|
-
npm run build
|
|
1708
|
-
|
|
1709
|
-
# 3. Test locally (optional)
|
|
1710
|
-
npm test
|
|
1711
|
-
|
|
1712
|
-
# 4. Deploy to Versori
|
|
1713
|
-
# - Upload to Versori workspace
|
|
1714
|
-
# - Configure activation variables
|
|
1715
|
-
# - Enable workflows
|
|
1716
|
-
|
|
1717
|
-
# 5. Test workflows
|
|
1718
|
-
# Scheduled: Wait for next cron trigger or manually trigger
|
|
1719
|
-
# Ad-hoc: POST to webhook URL with API key header
|
|
1720
|
-
# Status: Query job status by ID
|
|
1721
|
-
```
|
|
1722
|
-
|
|
1723
|
-
### 9. Testing
|
|
1724
|
-
|
|
1725
|
-
#### Test Scheduled Extraction
|
|
1726
|
-
|
|
1727
|
-
```bash
|
|
1728
|
-
# Trigger manually in Versori UI or wait for cron schedule
|
|
1729
|
-
# Expected: XML file uploaded to SFTP
|
|
1730
|
-
```
|
|
1731
|
-
|
|
1732
|
-
#### Test Ad-hoc Extraction
|
|
1733
|
-
|
|
1734
|
-
```bash
|
|
1735
|
-
curl -X POST https://your-workspace.versori.run/orders-adhoc \
|
|
1736
|
-
-H "Content-Type: application/json" \
|
|
1737
|
-
-d '{
|
|
1738
|
-
"fromDate": "2025-01-01T00:00:00Z",
|
|
1739
|
-
"toDate": "2025-01-22T23:59:59Z",
|
|
1740
|
-
"updateState": false
|
|
1741
|
-
}'
|
|
1742
|
-
```
|
|
1743
|
-
|
|
1744
|
-
#### Test Job Status Query
|
|
1745
|
-
|
|
1746
|
-
```bash
|
|
1747
|
-
curl -X POST https://your-workspace.versori.run/orders-job-status \
|
|
1748
|
-
-H "Content-Type: application/json" \
|
|
1749
|
-
-d '{
|
|
1750
|
-
"jobId": "SCHED-ORDERS-2025-01-22T02-00-00Z"
|
|
1751
|
-
}'
|
|
1752
|
-
```
|
|
1753
|
-
|
|
1754
|
-
## Key Patterns Explained
|
|
1755
|
-
|
|
1756
|
-
### Pattern 1: ExtractionOrchestrator for Auto-Pagination
|
|
1757
|
-
|
|
1758
|
-
```typescript
|
|
1759
|
-
// ✅ CORRECT - Use ExtractionOrchestrator (handles pagination automatically)
|
|
1760
|
-
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
1761
|
-
|
|
1762
|
-
const extractionResult = await orchestrator.extract({
|
|
1763
|
-
query: ORDERS_QUERY,
|
|
1764
|
-
resultPath: 'orders.edges.node',
|
|
1765
|
-
variables: { retailerId, updatedAfter: bufferedLastRunTime },
|
|
1766
|
-
pageSize,
|
|
1767
|
-
maxRecords,
|
|
1768
|
-
validateItem: item => !!(item.ref && item.customer),
|
|
1769
|
-
});
|
|
1770
|
-
|
|
1771
|
-
const rawRecords = extractionResult.data;
|
|
1772
|
-
|
|
1773
|
-
// WRONG - Manual pagination (avoid this pattern)
|
|
1774
|
-
// const result = await client.graphql({
|
|
1775
|
-
// query: ORDERS_QUERY,
|
|
1776
|
-
// variables: { first: pageSize },
|
|
1777
|
-
// pagination: { maxRecords }
|
|
1778
|
-
// });
|
|
1779
|
-
```
|
|
1780
|
-
|
|
1781
|
-
### Pattern 2: JobTracker for Lifecycle Management
|
|
1782
|
-
|
|
1783
|
-
```typescript
|
|
1784
|
-
// ✅ CORRECT - Use JobTracker throughout workflow
|
|
1785
|
-
const tracker = new JobTracker(kv, log);
|
|
1786
|
-
|
|
1787
|
-
// Create job
|
|
1788
|
-
await tracker.createJob(jobId, { triggeredBy, fromDate, toDate });
|
|
1789
|
-
|
|
1790
|
-
// Update progress
|
|
1791
|
-
await tracker.updateJob(jobId, { stage: 'extraction', message: 'Extracting data' });
|
|
1792
|
-
|
|
1793
|
-
// Mark completed
|
|
1794
|
-
await tracker.markCompleted(jobId, { recordCount, fileName });
|
|
1795
|
-
|
|
1796
|
-
// Query status
|
|
1797
|
-
const status = await tracker.getJob(jobId);
|
|
1798
|
-
```
|
|
1799
|
-
|
|
1800
|
-
### Pattern 3: 3-Workflow Pattern
|
|
1801
|
-
|
|
1802
|
-
```typescript
|
|
1803
|
-
// ✅ CORRECT - 3 workflows for different use cases
|
|
1804
|
-
// 1. Scheduled: Automated daily/hourly runs
|
|
1805
|
-
export const scheduledOrdersExtraction = schedule('orders-extract-xml-daily', '0 2 * * *')...
|
|
1806
|
-
|
|
1807
|
-
// 2. Ad-hoc: Manual webhook triggers with date overrides
|
|
1808
|
-
export const adhocOrdersExtraction = webhook('orders-adhoc', { response: { mode: 'sync' } })...
|
|
1809
|
-
|
|
1810
|
-
// 3. Status: Query job status by ID
|
|
1811
|
-
export const ordersJobStatus = webhook('orders-job-status', { response: { mode: 'sync' } })...
|
|
1812
|
-
```
|
|
1813
|
-
|
|
1814
|
-
### Pattern 4: XMLBuilder for Safe XML Generation (CRITICAL)
|
|
1815
|
-
|
|
1816
|
-
Use the SDK's `XMLBuilder` - it handles all XML escaping automatically:
|
|
1817
|
-
|
|
1818
|
-
```typescript
|
|
1819
|
-
import { Buffer } from 'node:buffer';
|
|
1820
|
-
import { XMLBuilder } from '@fluentcommerce/fc-connect-sdk';
|
|
1821
|
-
|
|
1822
|
-
// Initialize XMLBuilder (handles all escaping automatically)
|
|
1823
|
-
const xmlBuilder = new XMLBuilder({
|
|
1824
|
-
rootElement: 'Orders',
|
|
1825
|
-
prettyPrint: true,
|
|
1826
|
-
encoding: 'UTF-8',
|
|
1827
|
-
});
|
|
1828
|
-
|
|
1829
|
-
// ✅ CORRECT: XMLBuilder escapes automatically
|
|
1830
|
-
const orders = [
|
|
1831
|
-
{
|
|
1832
|
-
customer_name: 'Smith & Jones <Corp>', // Contains & and <>
|
|
1833
|
-
customer_email: 'contact@smith&jones.com',
|
|
1834
|
-
notes: 'Special chars: ¢, ©, ®, "quotes"',
|
|
1835
|
-
},
|
|
1836
|
-
];
|
|
1837
|
-
|
|
1838
|
-
const xml = xmlBuilder.build({ Order: orders });
|
|
1839
|
-
// Result: All special characters properly escaped
|
|
1840
|
-
// <customer_name>Smith & Jones <Corp></customer_name>
|
|
1841
|
-
// <customer_email>contact@smith&jones.com</customer_email>
|
|
1842
|
-
// <notes>Special chars: ¢, ©, ®, "quotes"</notes>
|
|
1843
|
-
|
|
1844
|
-
// WRONG: Manual string concatenation (dangerous)
|
|
1845
|
-
// const xml = `<customer_name>${order.customer_name}</customer_name>`;
|
|
1846
|
-
// This would produce INVALID XML: <customer_name>Smith & Jones <Corp></customer_name>
|
|
1847
|
-
```
|
|
1848
|
-
|
|
1849
|
-
**Why XMLBuilder?**
|
|
1850
|
-
|
|
1851
|
-
- ✅ Automatic escaping of &, <, >, ", '
|
|
1852
|
-
- ✅ Handles special characters (¢, ©, ®)
|
|
1853
|
-
- ✅ Prevents XML injection attacks
|
|
1854
|
-
- ✅ Validates structure
|
|
1855
|
-
- ✅ Consistent, maintainable code
|
|
1856
|
-
|
|
1857
|
-
### Pattern 5: SFTP Cleanup (CRITICAL)
|
|
1858
|
-
|
|
1859
|
-
```typescript
|
|
1860
|
-
const sftp = new SftpDataSource(config, log);
|
|
1861
|
-
|
|
1862
|
-
try {
|
|
1863
|
-
await sftp.uploadFile(remotePath, buffer);
|
|
1864
|
-
return { success: true };
|
|
1865
|
-
} finally {
|
|
1866
|
-
// ALWAYS dispose SFTP connection
|
|
1867
|
-
await sftp.dispose();
|
|
1868
|
-
}
|
|
1869
|
-
```
|
|
1870
|
-
|
|
1871
|
-
**Why?** SFTP maintains open connections. Not calling `dispose()` leads to connection exhaustion.
|
|
1872
|
-
|
|
1873
|
-
### Pattern 6: Consistent Field Names Across Formats
|
|
1874
|
-
|
|
1875
|
-
**Same data in CSV, JSON, and XML:**
|
|
1876
|
-
|
|
1877
|
-
- `order_id` (not orderId, not order-id, not OrderID)
|
|
1878
|
-
- `customer_email` (consistent with CSV version)
|
|
1879
|
-
- `ship_to_address1` (matches CSV exactly)
|
|
1880
|
-
|
|
1881
|
-
This allows users to switch formats without changing downstream systems.
|
|
1882
|
-
|
|
1883
|
-
## Common Issues
|
|
1884
|
-
|
|
1885
|
-
**Issue 1: Malformed XML from unescaped characters**
|
|
1886
|
-
|
|
1887
|
-
- Customer name contains `&` or `<`
|
|
1888
|
-
- Solution: Always use XMLBuilder (automatic escaping)
|
|
1889
|
-
|
|
1890
|
-
**Issue 2: 3PL system rejects XML**
|
|
1891
|
-
|
|
1892
|
-
- Missing required fields
|
|
1893
|
-
- Solution: Verify mapping matches 3PL schema requirements
|
|
1894
|
-
|
|
1895
|
-
**Issue 3: File too large for SFTP partner**
|
|
1896
|
-
|
|
1897
|
-
- Partner has 50MB limit, file is 100MB
|
|
1898
|
-
- Solution: Use file splitting (10k orders per file)
|
|
1899
|
-
|
|
1900
|
-
**Issue 4: SFTP connection timeouts**
|
|
1901
|
-
|
|
1902
|
-
- Not calling `dispose()` in finally block
|
|
1903
|
-
- Solution: Always use try/finally pattern
|
|
1904
|
-
|
|
1905
|
-
**Issue 5: Job status not updating**
|
|
1906
|
-
|
|
1907
|
-
- JobTracker not integrated
|
|
1908
|
-
- Solution: Use JobTracker throughout workflow
|
|
1909
|
-
|
|
1910
|
-
## Testing
|
|
1911
|
-
|
|
1912
|
-
### 1. Test XML Structure
|
|
1913
|
-
|
|
1914
|
-
```typescript
|
|
1915
|
-
export const testXmlGeneration = http('test-xml').then(
|
|
1916
|
-
fn('test-xml-gen', async () => {
|
|
1917
|
-
const testOrders = [
|
|
1918
|
-
{
|
|
1919
|
-
order_id: 'TEST-001',
|
|
1920
|
-
order_date: '2025-01-22',
|
|
1921
|
-
order_status: 'CREATED',
|
|
1922
|
-
customer_name: 'Test & Validate <Corp>',
|
|
1923
|
-
customer_email: 'test@example.com',
|
|
1924
|
-
ship_to_name: 'John Smith',
|
|
1925
|
-
ship_to_address1: '123 Main St',
|
|
1926
|
-
ship_to_city: 'New York',
|
|
1927
|
-
ship_to_state: 'NY',
|
|
1928
|
-
ship_to_zip: '10001',
|
|
1929
|
-
ship_to_country: 'US',
|
|
1930
|
-
line_items: [{ line_item_sku: 'SKU-001', line_item_qty: 2, line_item_price: 29.99 }],
|
|
1931
|
-
},
|
|
1932
|
-
];
|
|
1933
|
-
|
|
1934
|
-
const xml = buildOrdersXML(testOrders);
|
|
1935
|
-
|
|
1936
|
-
// Validate XML structure
|
|
1937
|
-
if (!xml.includes('<?xml version="1.0"')) {
|
|
1938
|
-
return { success: false, error: 'Missing XML declaration' };
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
if (!xml.includes('&') || !xml.includes('<')) {
|
|
1942
|
-
return { success: false, error: 'Special characters not escaped' };
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
return { success: true, xml };
|
|
1946
|
-
})
|
|
1947
|
-
);
|
|
1948
|
-
```
|
|
1949
|
-
|
|
1950
|
-
### 2. Test SFTP Upload
|
|
1951
|
-
|
|
1952
|
-
```bash
|
|
1953
|
-
curl https://your-workspace.versori.run/test-sftp-orders-xml
|
|
1954
|
-
```
|
|
1955
|
-
|
|
1956
|
-
### 3. Validate Against 3PL Schema
|
|
1957
|
-
|
|
1958
|
-
- Download partner's XSD schema
|
|
1959
|
-
- Validate generated XML against schema
|
|
1960
|
-
- Fix any missing/incorrect elements
|
|
1961
|
-
|
|
1962
|
-
## Production Checklist
|
|
1963
|
-
|
|
1964
|
-
- [ ] Test SFTP credentials and connection
|
|
1965
|
-
- [ ] Verify SFTP server has write permissions to remotePath
|
|
1966
|
-
- [ ] Set appropriate extraction frequency (hourly for 3PL feeds)
|
|
1967
|
-
- [ ] Configure correct order status filters
|
|
1968
|
-
- [ ] Test XML escaping with special characters (&, <, >, ", ')
|
|
1969
|
-
- [ ] Validate XML against partner's schema (if provided)
|
|
1970
|
-
- [ ] Test `dispose()` is always called (check logs)
|
|
1971
|
-
- [ ] Document XML schema for 3PL integration team
|
|
1972
|
-
- [ ] Set up monitoring for SFTP connection failures
|
|
1973
|
-
- [ ] Test with real order data (addresses with special chars)
|
|
1974
|
-
- [ ] Verify file size limits with SFTP partner
|
|
1975
|
-
- [ ] Configure SFTP server IP whitelisting for Versori
|
|
1976
|
-
- [ ] Test file splitting with large batches (>10k orders)
|
|
1977
|
-
- [ ] Test all 3 workflows (scheduled, ad-hoc, status)
|
|
1978
|
-
- [ ] Verify JobTracker integration and status updates
|
|
1979
|
-
- [ ] Test ExtractionOrchestrator pagination with large datasets
|
|
1980
|
-
|
|
1981
|
-
## Troubleshooting Guide
|
|
1982
|
-
|
|
1983
|
-
**Issue**: "Extraction timeout after 10 minutes"
|
|
1984
|
-
|
|
1985
|
-
- **Cause**: Too many records
|
|
1986
|
-
- **Fix**: Reduce maxRecords, increase frequency
|
|
1987
|
-
|
|
1988
|
-
**Issue**: "Mapping errors for 50% of records"
|
|
1989
|
-
|
|
1990
|
-
- **Cause**: Schema mismatch
|
|
1991
|
-
- **Fix**: Run schema validation, check field names
|
|
1992
|
-
|
|
1993
|
-
**Issue**: "State not updating"
|
|
1994
|
-
|
|
1995
|
-
- **Cause**: KV write failure or intentional retry
|
|
1996
|
-
- **Fix**: Check KV logs, verify state update code
|
|
1997
|
-
|
|
1998
|
-
**Issue**: "First run exceeds limits"
|
|
1999
|
-
|
|
2000
|
-
- **Cause**: No previous timestamp, fetches all
|
|
2001
|
-
- **Fix**: Set fallbackStartDate close to current, apply filters
|
|
2002
|
-
|
|
2003
|
-
**Issue**: "Excessive duplicates"
|
|
2004
|
-
|
|
2005
|
-
- **Cause**: Overlap buffer (expected) or timestamp not saved
|
|
2006
|
-
- **Fix**: Verify newTimestamp saved WITHOUT buffer
|
|
2007
|
-
|
|
2008
|
-
**Issue**: "Job status returns null"
|
|
2009
|
-
|
|
2010
|
-
- **Cause**: Invalid job ID or job expired
|
|
2011
|
-
- **Fix**: Verify job ID format, check KV TTL settings
|
|
2012
|
-
|
|
2013
|
-
## Security Best Practices
|
|
2014
|
-
|
|
2015
|
-
### Credential Management
|
|
2016
|
-
|
|
2017
|
-
**✅ DO**:
|
|
2018
|
-
|
|
2019
|
-
- Store credentials in Versori activation variables
|
|
2020
|
-
- Rotate credentials quarterly
|
|
2021
|
-
- Use least-privilege accounts
|
|
2022
|
-
|
|
2023
|
-
** DON'T**:
|
|
2024
|
-
|
|
2025
|
-
- Never log credentials
|
|
2026
|
-
- Never commit to git
|
|
2027
|
-
- Never share across environments
|
|
2028
|
-
|
|
2029
|
-
### Data Security
|
|
2030
|
-
|
|
2031
|
-
- Enable encryption in transit and at rest
|
|
2032
|
-
- Apply data retention policies
|
|
2033
|
-
- Monitor access logs
|
|
2034
|
-
- Use VPC/private networks for sensitive data
|
|
2035
|
-
|
|
2036
|
-
### Webhook Security
|
|
2037
|
-
|
|
2038
|
-
- Validate API keys for ad-hoc and status workflows
|
|
2039
|
-
- Use HTTPS for all webhook endpoints
|
|
2040
|
-
- Implement rate limiting
|
|
2041
|
-
- Monitor for suspicious activity
|
|
2042
|
-
|
|
2043
|
-
---
|
|
2044
|
-
|
|
2045
|
-
**Pattern**: Enterprise incremental extraction with ExtractionOrchestrator + JobTracker for orders via SFTP (XML format)
|
|
2046
|
-
**❌š ï¸ Versori Sample**: Reference implementation - adapt for your production use case
|
|
2047
|
-
**Key Learning**: Use ExtractionOrchestrator for auto-pagination, JobTracker for lifecycle management, always escape XML and dispose SFTP
|
|
2048
|
-
**Critical**: Apply 60-second overlap buffer to prevent missed records
|
|
2049
|
-
**Buffer Pattern**: Query WITH buffer (`updatedOn >= lastRunTime - 60s`), save WITHOUT buffer (`MAX(updatedOn)`)
|
|
2050
|
-
**Field Consistency**: Same field names as CSV version for easy format switching
|
|
2051
|
-
**SFTP**: Use proper connection cleanup in finally block to prevent connection leaks
|
|
2052
|
-
**XML**: Preserve hierarchical structure (no line item flattening needed like CSV)
|
|
2053
|
-
**3 Workflows**: Scheduled, ad-hoc webhook, job status query
|
|
2054
|
-
|
|
2055
|
-
---
|
|
2056
|
-
|
|
2057
|
-
### Pattern 8: Backward Pagination (Optional - Advanced)
|
|
2058
|
-
|
|
2059
|
-
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
2060
|
-
|
|
2061
|
-
**When to Use**:
|
|
2062
|
-
|
|
2063
|
-
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
2064
|
-
- ✅ Time-bounded reverse traversal for auditing
|
|
2065
|
-
- ✅ Display newest-first in UI/reports
|
|
2066
|
-
- **Don't use for standard incremental sync** - use forward pagination (default)
|
|
2067
|
-
|
|
2068
|
-
**GraphQL Query Requirements**:
|
|
2069
|
-
|
|
2070
|
-
Your query must support backward pagination by including `$last` and `$before`:
|
|
2071
|
-
|
|
2072
|
-
```graphql
|
|
2073
|
-
query GetData(
|
|
2074
|
-
$retailerId: ID!
|
|
2075
|
-
$first: Int # For forward pagination
|
|
2076
|
-
$after: String # For forward pagination
|
|
2077
|
-
$last: Int # For backward pagination
|
|
2078
|
-
$before: String # For backward pagination
|
|
2079
|
-
) {
|
|
2080
|
-
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
2081
|
-
edges {
|
|
2082
|
-
cursor # ✅ REQUIRED
|
|
2083
|
-
node {
|
|
2084
|
-
id
|
|
2085
|
-
createdAt
|
|
2086
|
-
# ... other fields
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
pageInfo {
|
|
2090
|
-
hasNextPage # For forward
|
|
2091
|
-
hasPreviousPage # ✅ REQUIRED for backward
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
```
|
|
2096
|
-
|
|
2097
|
-
**Implementation**:
|
|
2098
|
-
|
|
2099
|
-
```typescript
|
|
2100
|
-
// Backward pagination - newest records first
|
|
2101
|
-
const result = await orchestrator.extract({
|
|
2102
|
-
query: YOUR_QUERY,
|
|
2103
|
-
resultPath: 'data.edges.node',
|
|
2104
|
-
variables: {
|
|
2105
|
-
retailerId,
|
|
2106
|
-
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
2107
|
-
// Don't include last/before - orchestrator injects them
|
|
2108
|
-
},
|
|
2109
|
-
pageSize: 200,
|
|
2110
|
-
direction: 'backward', // ✅ Enable reverse pagination
|
|
2111
|
-
maxRecords: 10000,
|
|
2112
|
-
});
|
|
2113
|
-
|
|
2114
|
-
// Records are returned in reverse chronological order
|
|
2115
|
-
console.log(result.data[0].createdAt); // Newest
|
|
2116
|
-
console.log(result.data[result.data.length - 1].createdAt); // Oldest (within range)
|
|
2117
|
-
```
|
|
2118
|
-
|
|
2119
|
-
**Key Differences from Forward Pagination**:
|
|
2120
|
-
|
|
2121
|
-
| Aspect | Forward (Default) | Backward |
|
|
2122
|
-
| ---------------------- | -------------------------------- | ----------------------- |
|
|
2123
|
-
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
2124
|
-
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
2125
|
-
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
2126
|
-
| **Cursor Source** | Last edge of page | First edge of page |
|
|
2127
|
-
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
2128
|
-
|
|
2129
|
-
**Important Notes**:
|
|
2130
|
-
|
|
2131
|
-
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
2132
|
-
|
|
2133
|
-
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
2134
|
-
|
|
2135
|
-
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
2136
|
-
|
|
2137
|
-
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
2138
|
-
|
|
2139
|
-
**Example: Extract Latest 1000 Orders**
|
|
2140
|
-
|
|
2141
|
-
```typescript
|
|
2142
|
-
const latestOrders = await orchestrator.extract({
|
|
2143
|
-
query: ORDERS_QUERY,
|
|
2144
|
-
resultPath: 'orders.edges.node',
|
|
2145
|
-
variables: {
|
|
2146
|
-
retailerId,
|
|
2147
|
-
statuses: ['BOOKED', 'ALLOCATED'],
|
|
2148
|
-
},
|
|
2149
|
-
direction: 'backward', // Start from newest
|
|
2150
|
-
maxRecords: 1000, // Stop after 1000 records
|
|
2151
|
-
pageSize: 100, // 100 per page = 10 pages
|
|
2152
|
-
});
|
|
2153
|
-
|
|
2154
|
-
// latestOrders.data[0] is the newest order
|
|
2155
|
-
// latestOrders.data[999] is the 1000th newest order
|
|
2156
|
-
```
|
|
2157
|
-
|
|
2158
|
-
**When to Use Forward vs Backward**:
|
|
2159
|
-
|
|
2160
|
-
```typescript
|
|
2161
|
-
// ✅ Forward (default) - For incremental sync
|
|
2162
|
-
const incrementalData = await orchestrator.extract({
|
|
2163
|
-
query: YOUR_QUERY,
|
|
2164
|
-
resultPath: 'data.edges.node',
|
|
2165
|
-
variables: {
|
|
2166
|
-
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
2167
|
-
},
|
|
2168
|
-
// direction defaults to 'forward'
|
|
2169
|
-
// Processes oldest → newest for proper sequencing
|
|
2170
|
-
});
|
|
2171
|
-
|
|
2172
|
-
// ✅ Backward - For "latest N records" use cases
|
|
2173
|
-
const latestData = await orchestrator.extract({
|
|
2174
|
-
query: YOUR_QUERY,
|
|
2175
|
-
resultPath: 'data.edges.node',
|
|
2176
|
-
direction: 'backward',
|
|
2177
|
-
maxRecords: 100, // Just get latest 100
|
|
2178
|
-
// Gets newest → oldest
|
|
2179
|
-
});
|
|
2180
|
-
```
|
|
2181
|
-
|
|
2182
|
-
**Pagination Variables Reference**:
|
|
2183
|
-
|
|
2184
|
-
| Variable | Forward | Backward | Injected By | Notes |
|
|
2185
|
-
| -------- | ------------ | ------------ | ------------ | ------------------------ |
|
|
2186
|
-
| `first` | ✅ Used | Not used | Orchestrator | From `pageSize` |
|
|
2187
|
-
| `after` | ✅ Used | Not used | Orchestrator | From cursor (last edge) |
|
|
2188
|
-
| `last` | Not used | ✅ Used | Orchestrator | From `pageSize` |
|
|
2189
|
-
| `before` | Not used | ✅ Used | Orchestrator | From cursor (first edge) |
|
|
2190
|
-
|
|
2191
|
-
**Common Mistakes to Avoid**:
|
|
2192
|
-
|
|
2193
|
-
```typescript
|
|
2194
|
-
// WRONG - Don't pass pagination variables
|
|
2195
|
-
const result = await orchestrator.extract({
|
|
2196
|
-
variables: {
|
|
2197
|
-
last: 200, // Orchestrator will override this
|
|
2198
|
-
before: cursor, // Orchestrator manages cursor
|
|
2199
|
-
},
|
|
2200
|
-
direction: 'backward',
|
|
2201
|
-
});
|
|
2202
|
-
|
|
2203
|
-
// ✅ CORRECT - Let orchestrator inject pagination
|
|
2204
|
-
const result = await orchestrator.extract({
|
|
2205
|
-
variables: {
|
|
2206
|
-
retailerId, // ✅ Your business variables only
|
|
2207
|
-
},
|
|
2208
|
-
pageSize: 200, // ✅ Orchestrator uses this for last/before
|
|
2209
|
-
direction: 'backward',
|
|
2210
|
-
});
|
|
2211
|
-
```
|
|
2212
|
-
|
|
2213
|
-
#### Optional: Reverse Pagination
|
|
2214
|
-
|
|
2215
|
-
- Forward remains default. For reverse, require $last/$before and pageInfo.hasPreviousPage.
|
|
2216
|
-
- Do not pass last/before in variables; set direction='backward'.
|
|
2217
|
-
|
|
2218
|
-
GraphQL:
|
|
2219
|
-
|
|
2220
|
-
```graphql
|
|
2221
|
-
query GetOrdersBackward($retailerId: ID!, $last: Int!, $before: String) {
|
|
2222
|
-
orders(retailerId: $retailerId, last: $last, before: $before) {
|
|
2223
|
-
edges {
|
|
2224
|
-
cursor
|
|
2225
|
-
node {
|
|
2226
|
-
id
|
|
2227
|
-
ref
|
|
2228
|
-
updatedOn
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
pageInfo {
|
|
2232
|
-
hasPreviousPage
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
```
|
|
2237
|
-
|
|
2238
|
-
SDK:
|
|
2239
|
-
|
|
2240
|
-
```typescript
|
|
2241
|
-
await orchestrator.extract({
|
|
2242
|
-
query: ORDERS_BACKWARD_QUERY,
|
|
2243
|
-
resultPath: 'orders.edges.node',
|
|
2244
|
-
variables: { retailerId },
|
|
2245
|
-
pageSize,
|
|
2246
|
-
direction: 'backward',
|
|
2247
|
-
});
|
|
2248
|
-
```
|
|
2249
|
-
|
|
2250
|
-
---
|
|
2251
|
-
|
|
2252
|
-
## Testing Checklist
|
|
2253
|
-
|
|
2254
|
-
**Before production deployment:**
|
|
2255
|
-
|
|
2256
|
-
### 1. Schema Validation
|
|
2257
|
-
|
|
2258
|
-
- [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
|
|
2259
|
-
- [ ] Run `npx fc-connect validate-schema --mapping ./config/orders.export.xml.json --schema ./fluent-schema.json`
|
|
2260
|
-
- [ ] Run `npx fc-connect analyze-coverage --mapping ./config/orders.export.xml.json --schema ./fluent-schema.json`
|
|
2261
|
-
- [ ] Verify all `source` paths in mapping exist in GraphQL schema
|
|
2262
|
-
- [ ] Verify query structure matches schema (fields, types, filters)
|
|
2263
|
-
|
|
2264
|
-
### 2. Extraction Testing
|
|
2265
|
-
|
|
2266
|
-
- [ ] Test with small dataset first (maxRecords=10)
|
|
2267
|
-
- [ ] Verify ExtractionOrchestrator pagination works correctly
|
|
2268
|
-
- [ ] Test with multiple pages of data (verify cursor handling)
|
|
2269
|
-
- [ ] Verify date range filtering (updatedOn filter)
|
|
2270
|
-
- [ ] Test empty result handling (no records in date range)
|
|
2271
|
-
- [ ] Verify extraction stops at maxRecords limit
|
|
2272
|
-
|
|
2273
|
-
### 3. Mapping Testing
|
|
2274
|
-
|
|
2275
|
-
- [ ] Verify required fields are populated
|
|
2276
|
-
- [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
|
|
2277
|
-
- [ ] Test custom resolvers with edge cases (if any)
|
|
2278
|
-
- [ ] Verify nested field extraction
|
|
2279
|
-
- [ ] Test with null/missing fields
|
|
2280
|
-
- [ ] Verify mapping error collection works
|
|
2281
|
-
|
|
2282
|
-
### 4. XML Generation Testing
|
|
2283
|
-
|
|
2284
|
-
- [ ] Verify XML structure matches expected format
|
|
2285
|
-
- [ ] Test XML validation against XSD schema (if applicable)
|
|
2286
|
-
- [ ] Verify special character escaping in XML
|
|
2287
|
-
- [ ] Test with large datasets (>1000 records)
|
|
2288
|
-
- [ ] Verify UTF-8 encoding
|
|
2289
|
-
- [ ] Test XML namespace handling (if applicable)
|
|
2290
|
-
|
|
2291
|
-
### 5. SFTP Upload Testing
|
|
2292
|
-
|
|
2293
|
-
- [ ] Test SFTP connection and authentication
|
|
2294
|
-
- [ ] Verify file upload to correct path
|
|
2295
|
-
- [ ] Test file naming convention (timestamp format)
|
|
2296
|
-
- [ ] Verify file permissions on SFTP server
|
|
2297
|
-
- [ ] Test upload retry logic (simulate network failure)
|
|
2298
|
-
- [ ] Verify SFTP connection disposal (no connection leaks)
|
|
2299
|
-
|
|
2300
|
-
### 6. State Management Testing
|
|
2301
|
-
|
|
2302
|
-
- [ ] Verify overlap buffer prevents missed records (60-second default)
|
|
2303
|
-
- [ ] Test state recovery after extraction failure
|
|
2304
|
-
- [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
|
|
2305
|
-
- [ ] Test first run with no previous state (uses fallbackStartDate)
|
|
2306
|
-
- [ ] Verify state update only happens on successful upload
|
|
2307
|
-
- [ ] Test manual date override (doesn't update state)
|
|
2308
|
-
|
|
2309
|
-
### 7. Job Tracking Testing
|
|
2310
|
-
|
|
2311
|
-
- [ ] Test job creation with JobTracker
|
|
2312
|
-
- [ ] Verify job status updates at each stage
|
|
2313
|
-
- [ ] Test job completion with metadata
|
|
2314
|
-
- [ ] Test job failure handling
|
|
2315
|
-
- [ ] Query job status via webhook endpoint
|
|
2316
|
-
- [ ] Verify job status persists in KV store
|
|
2317
|
-
|
|
2318
|
-
### 8. Error Handling Testing
|
|
2319
|
-
|
|
2320
|
-
- [ ] Test with invalid GraphQL query
|
|
2321
|
-
- [ ] Test with mapping errors (invalid field paths)
|
|
2322
|
-
- [ ] Test with SFTP connection failures
|
|
2323
|
-
- [ ] Test with authentication failures
|
|
2324
|
-
- [ ] Test with network timeouts
|
|
2325
|
-
- [ ] Verify error logging includes context (jobId, stage, error details)
|
|
2326
|
-
- [ ] Test error threshold logic (if applicable)
|
|
2327
|
-
|
|
2328
|
-
### 9. Staging Environment Testing
|
|
2329
|
-
|
|
2330
|
-
- [ ] Run full extraction in staging environment
|
|
2331
|
-
- [ ] Verify XML file format with downstream system
|
|
2332
|
-
- [ ] Monitor extraction duration and resource usage
|
|
2333
|
-
- [ ] Test with production-like data volumes
|
|
2334
|
-
- [ ] Verify no performance degradation over time
|
|
2335
|
-
|
|
2336
|
-
### 10. Integration Testing
|
|
2337
|
-
|
|
2338
|
-
- [ ] Test scheduled workflow (cron trigger)
|
|
2339
|
-
- [ ] Test ad hoc webhook trigger
|
|
2340
|
-
- [ ] Test job status query webhook
|
|
2341
|
-
- [ ] Verify activation variables are read correctly
|
|
2342
|
-
- [ ] Test with different extraction modes (incremental, date range)
|
|
2343
|
-
- [ ] End-to-end test: trigger → extract → transform → upload → verify file
|
|
2344
|
-
|
|
2345
|
-
---
|
|
2346
|
-
## Monitoring & Alerting
|
|
2347
|
-
|
|
2348
|
-
### Success Response Example
|
|
2349
|
-
|
|
2350
|
-
```json
|
|
2351
|
-
{
|
|
2352
|
-
"success": true,
|
|
2353
|
-
"jobId": "SCHEDULED_ORD_20251102_140000_abc123",
|
|
2354
|
-
"recordsExtracted": 1523,
|
|
2355
|
-
"fileName": "orders-2025-11-02T14-00-00-000Z.xml",
|
|
2356
|
-
"sftpPath": "/outbound/orders/orders-2025-11-02T14-00-00-000Z.xml",
|
|
2357
|
-
"metrics": {
|
|
2358
|
-
"extractionDurationMs": 12543,
|
|
2359
|
-
"totalPages": 8,
|
|
2360
|
-
"pageSize": 200,
|
|
2361
|
-
"mappingErrors": 0,
|
|
2362
|
-
"fileSizeBytes": 524288,
|
|
2363
|
-
"uploadDurationMs": 1234
|
|
2364
|
-
},
|
|
2365
|
-
"timestamps": {
|
|
2366
|
-
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
2367
|
-
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
2368
|
-
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
2369
|
-
},
|
|
2370
|
-
"state": {
|
|
2371
|
-
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
2372
|
-
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
2373
|
-
"stateUpdated": true,
|
|
2374
|
-
"overlapBufferSeconds": 60
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
```
|
|
2378
|
-
|
|
2379
|
-
### Error Response Example
|
|
2380
|
-
|
|
2381
|
-
```json
|
|
2382
|
-
{
|
|
2383
|
-
"success": false,
|
|
2384
|
-
"jobId": "ADHOC_ORD_20251102_140500_xyz789",
|
|
2385
|
-
"error": "SFTP upload failed: Connection timeout",
|
|
2386
|
-
"errorCategory": "NETWORK",
|
|
2387
|
-
"recordsExtracted": 0,
|
|
2388
|
-
"stage": "sftp_upload",
|
|
2389
|
-
"details": {
|
|
2390
|
-
"message": "Failed to upload file after 3 retry attempts",
|
|
2391
|
-
"retryAttempts": 3,
|
|
2392
|
-
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
2393
|
-
},
|
|
2394
|
-
"state": {
|
|
2395
|
-
"stateUpdated": false,
|
|
2396
|
-
"willRetryNextRun": true,
|
|
2397
|
-
"note": "State not advanced - next extraction will retry same time window"
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
```
|
|
2401
|
-
|
|
2402
|
-
### Key Metrics to Track
|
|
2403
|
-
|
|
2404
|
-
```typescript
|
|
2405
|
-
const METRICS = {
|
|
2406
|
-
// Extraction Performance
|
|
2407
|
-
extractionDurationMs: Date.now() - extractionStart,
|
|
2408
|
-
recordCount: records.length,
|
|
2409
|
-
pageCount: extractionResult.stats.totalPages,
|
|
2410
|
-
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
2411
|
-
|
|
2412
|
-
// Transformation Performance
|
|
2413
|
-
transformedCount: transformedRecords.length,
|
|
2414
|
-
failedCount: mappingErrors.length,
|
|
2415
|
-
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
2416
|
-
|
|
2417
|
-
// File Generation
|
|
2418
|
-
fileSizeMB: (xmlContent.length / (1024 * 1024)).toFixed(2),
|
|
2419
|
-
|
|
2420
|
-
// Upload Performance
|
|
2421
|
-
uploadDurationMs: uploadEnd - uploadStart,
|
|
2422
|
-
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
2423
|
-
|
|
2424
|
-
// State Management
|
|
2425
|
-
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
2426
|
-
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
2427
|
-
};
|
|
2428
|
-
|
|
2429
|
-
log.info('Extraction metrics', metrics);
|
|
2430
|
-
```
|
|
2431
|
-
|
|
2432
|
-
### Alert Thresholds
|
|
2433
|
-
|
|
2434
|
-
```typescript
|
|
2435
|
-
const ALERT_THRESHOLDS = {
|
|
2436
|
-
// Duration Alerts
|
|
2437
|
-
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
2438
|
-
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
2439
|
-
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
2440
|
-
|
|
2441
|
-
// Error Rate Alerts
|
|
2442
|
-
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
2443
|
-
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
2444
|
-
|
|
2445
|
-
// Volume Alerts
|
|
2446
|
-
MAX_RECORDS_PER_RUN: 100000,
|
|
2447
|
-
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
2448
|
-
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
2449
|
-
|
|
2450
|
-
// State Alerts
|
|
2451
|
-
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
2452
|
-
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
2453
|
-
};
|
|
2454
|
-
|
|
2455
|
-
// Check thresholds
|
|
2456
|
-
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
2457
|
-
log.warn('Extraction duration exceeded threshold', {
|
|
2458
|
-
duration: metrics.extractionDurationMs,
|
|
2459
|
-
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
2460
|
-
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
2461
|
-
});
|
|
2462
|
-
}
|
|
2463
|
-
```
|
|
2464
|
-
|
|
2465
|
-
### Monitoring Dashboard Queries
|
|
2466
|
-
|
|
2467
|
-
**Versori Platform Logs Query:**
|
|
2468
|
-
|
|
2469
|
-
```
|
|
2470
|
-
# Successful extractions
|
|
2471
|
-
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
2472
|
-
|
|
2473
|
-
# Failed extractions
|
|
2474
|
-
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
2475
|
-
|
|
2476
|
-
# Performance issues
|
|
2477
|
-
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
2478
|
-
|
|
2479
|
-
# High error rates
|
|
2480
|
-
errorRate:>5
|
|
2481
|
-
|
|
2482
|
-
# State management issues
|
|
2483
|
-
stateUpdated:false AND success:true
|
|
2484
|
-
```
|
|
2485
|
-
|
|
2486
|
-
### Common Issues and Solutions
|
|
2487
|
-
|
|
2488
|
-
**Issue**: "Extraction timeout after 10 minutes"
|
|
2489
|
-
|
|
2490
|
-
- **Cause**: Too many records in single extraction
|
|
2491
|
-
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
2492
|
-
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
2493
|
-
|
|
2494
|
-
**Issue**: "Mapping errors for 50% of records"
|
|
2495
|
-
|
|
2496
|
-
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
2497
|
-
- **Fix**: Run schema validation, update mapping config paths
|
|
2498
|
-
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
2499
|
-
|
|
2500
|
-
**Issue**: "SFTP connection timeout"
|
|
2501
|
-
|
|
2502
|
-
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
2503
|
-
- **Fix**: Check SFTP credentials, verify network connectivity
|
|
2504
|
-
- **Prevention**: Implement connection health checks, monitor connection status
|
|
2505
|
-
|
|
2506
|
-
**Issue**: "State not updating after successful extraction"
|
|
2507
|
-
|
|
2508
|
-
- **Cause**: KV write failure or intentional retry logic
|
|
2509
|
-
- **Fix**: Check KV logs, verify state update code executed
|
|
2510
|
-
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
2511
|
-
|
|
2512
|
-
**Issue**: "First run exceeds record limits"
|
|
2513
|
-
|
|
2514
|
-
- **Cause**: No previous timestamp, fetches all historical records
|
|
2515
|
-
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
2516
|
-
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
2517
|
-
|
|
2518
|
-
**Issue**: "Excessive duplicate records in output"
|
|
2519
|
-
|
|
2520
|
-
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
2521
|
-
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
2522
|
-
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
2523
|
-
|
|
2524
|
-
---
|
|
2525
|
-
|
|
2526
|
-
## Troubleshooting Quick Reference
|
|
2527
|
-
|
|
2528
|
-
| Error Message | Likely Cause | Solution |
|
|
2529
|
-
|--------------|--------------|----------|
|
|
2530
|
-
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
2531
|
-
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
2532
|
-
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
2533
|
-
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
2534
|
-
| "SFTP authentication failed" | Invalid credentials | Verify SFTP credentials in activation variables |
|
|
2535
|
-
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
2536
|
-
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
2537
|
-
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
2538
|
-
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
2539
|
-
| "XML generation failed" | Format-specific error | Check XML generation logic, validate output |
|
|
2540
|
-
|
|
2541
|
-
---
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-extract-orders-to-sftp-xml
|
|
3
|
+
canonical_filename: template-extraction-orders-to-sftp-xml.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-xml
|
|
10
|
+
entity: orders
|
|
11
|
+
format: xml
|
|
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 - Orders to SFTP XML
|
|
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
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @fluentcommerce/fc-connect-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Always check [npm registry](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk) for the latest version.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 📚 STEP 1: Load These Docs (Human Checklist)
|
|
45
|
+
|
|
46
|
+
1. REQUIRED (load all)
|
|
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
|
+
Copy-paste list (open these):
|
|
55
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
56
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
57
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
58
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
59
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
60
|
+
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 📋 STEP 2: Tell Your AI (Prompt)
|
|
65
|
+
|
|
66
|
+
Copy/paste this prompt after loading the documentation above:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Create a Versori scheduled extractor for orders that uses ExtractionOrchestrator + JobTracker, incremental updatedOn with a 60s overlap buffer, transforms via UniversalMapper, generates XML with SDK's XMLBuilder, uploads to SFTP using SftpDataSource with dispose(). Include 3 workflows: scheduled, ad-hoc webhook, and job-status query with native Versori logging.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 📦 SDK Imports (Verified - Versori Optimized)
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { Buffer } from 'node:buffer';
|
|
78
|
+
import {
|
|
79
|
+
createClient,
|
|
80
|
+
ExtractionOrchestrator,
|
|
81
|
+
JobTracker,
|
|
82
|
+
UniversalMapper,
|
|
83
|
+
XMLBuilder,
|
|
84
|
+
SftpDataSource,
|
|
85
|
+
VersoriKVAdapter,
|
|
86
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
87
|
+
|
|
88
|
+
import { schedule, webhook, http, fn } from '@versori/run';
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
# Versori Scheduled: Orders Extraction to SFTP XML (Incremental)
|
|
94
|
+
|
|
95
|
+
**FC Connect SDK Use Case Guide**
|
|
96
|
+
|
|
97
|
+
> SDK: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
98
|
+
> Version: Check [npm](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk) for latest
|
|
99
|
+
|
|
100
|
+
Context: Scheduled Versori workflow that extracts new/updated orders from Fluent Commerce via GraphQL query with **ExtractionOrchestrator**, **JobTracker**, and **incremental timestamp tracking**, transforms with `UniversalMapper`, and writes **XML files** to partner SFTP server for 3PL/WMS/fulfillment center integration.
|
|
101
|
+
|
|
102
|
+
**Pattern**: EXTRACTION (Fluent → SFTP XML)
|
|
103
|
+
**Complexity**: High | Runtime: Versori Platform (Scheduled)
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## ⚠️ IMPORTANT: Production-Ready Base Template
|
|
108
|
+
|
|
109
|
+
> **📋 BASE TEMPLATE - Ready for Production (Customize for Your Needs)**
|
|
110
|
+
>
|
|
111
|
+
> This is a **production-ready base template** demonstrating FC Connect SDK best practices for order extraction workflows with XML output.
|
|
112
|
+
>
|
|
113
|
+
> **✅ INCLUDED FEATURES:**
|
|
114
|
+
>
|
|
115
|
+
> - ✅ Comprehensive error handling with retry logic
|
|
116
|
+
> - ✅ SFTP upload with exponential backoff (3 attempts)
|
|
117
|
+
> - ✅ State management with overlap buffer (prevents missed records)
|
|
118
|
+
> - ✅ Job tracking with lifecycle management
|
|
119
|
+
> - ✅ Security (credential masking in logs)
|
|
120
|
+
> - ✅ UTC time enforcement (prevents timezone bugs)
|
|
121
|
+
> - ✅ Incremental extraction (safe, efficient, production-ready)
|
|
122
|
+
> - ✅ Natural rate limiting via timestamps
|
|
123
|
+
>
|
|
124
|
+
> **📝 BEFORE DEPLOYING:**
|
|
125
|
+
>
|
|
126
|
+
> 1. Review and customize activation variables for your environment
|
|
127
|
+
> 2. Test with sample data in your Versori workspace
|
|
128
|
+
> 3. Adjust safety limits (pageSize, maxRecords) if needed
|
|
129
|
+
> 4. Configure monitoring alerts for extraction failures
|
|
130
|
+
> 5. Verify SFTP credentials and paths
|
|
131
|
+
>
|
|
132
|
+
> **This base template follows SDK best practices - tweak specific to your needs.**
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## What You'll Build
|
|
137
|
+
|
|
138
|
+
- **Incremental extraction** using `updatedOn >= (lastRunTime - buffer)` with **overlap buffer**
|
|
139
|
+
- **ExtractionOrchestrator** for auto-pagination and path-based extraction
|
|
140
|
+
- **JobTracker** for lifecycle management and status tracking
|
|
141
|
+
- **State management** with VersoriKV to track last successful run
|
|
142
|
+
- **Safety buffer** (60 seconds) to handle clock skew and race conditions
|
|
143
|
+
- GraphQL query with nested order lines
|
|
144
|
+
- UniversalMapper transformation with line item data
|
|
145
|
+
- **XML file generation** with proper structure for 3PL/WMS systems
|
|
146
|
+
- **SFTP upload** to partner server (with `dispose()` cleanup)
|
|
147
|
+
- **3 workflow patterns**: scheduled, ad-hoc webhook, job status query
|
|
148
|
+
- **Failure recovery** with timestamp tracking
|
|
149
|
+
|
|
150
|
+
## Business Use Case
|
|
151
|
+
|
|
152
|
+
**Hourly order feed to 3PL/fulfillment center:**
|
|
153
|
+
|
|
154
|
+
- Extract new and updated orders since last run
|
|
155
|
+
- Generate XML file with order header + line items
|
|
156
|
+
- Upload to 3PL SFTP server for warehouse management system
|
|
157
|
+
- Run every hour to enable real-time fulfillment
|
|
158
|
+
- Support order updates (address changes, item modifications)
|
|
159
|
+
- Standard XML format for EDI/ERP integration
|
|
160
|
+
|
|
161
|
+
## SDK Methods Used
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { Buffer } from 'node:buffer';
|
|
165
|
+
import {
|
|
166
|
+
createClient,
|
|
167
|
+
ExtractionOrchestrator,
|
|
168
|
+
JobTracker,
|
|
169
|
+
UniversalMapper,
|
|
170
|
+
XMLBuilder,
|
|
171
|
+
SftpDataSource,
|
|
172
|
+
VersoriKVAdapter,
|
|
173
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
174
|
+
|
|
175
|
+
await createClient(ctx); // Versori-aware client
|
|
176
|
+
const orchestrator = new ExtractionOrchestrator(client, log); // Auto-pagination
|
|
177
|
+
const tracker = new JobTracker(kv, log); // Job lifecycle tracking
|
|
178
|
+
await orchestrator.extract({ query, resultPath, variables, pageSize, maxRecords }); // Extract
|
|
179
|
+
new VersoriKVAdapter(ctx.openKv(':project:')); // State management
|
|
180
|
+
new UniversalMapper(exportMapping); // Field transformation
|
|
181
|
+
new XMLBuilder(options); // XML generation with auto-escaping
|
|
182
|
+
await sftp.uploadFile(remotePath, buffer); // SFTP upload (no options param)
|
|
183
|
+
await sftp.dispose(); // CRITICAL: Connection cleanup
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## SFTP Connection Setup (Recommended)
|
|
187
|
+
|
|
188
|
+
**✅ BEST PRACTICE:** Store SFTP credentials in a Versori connection object with Basic Auth:
|
|
189
|
+
|
|
190
|
+
**Connection Configuration:**
|
|
191
|
+
|
|
192
|
+
1. In Versori platform, create a connection named `versori_ftp_server`
|
|
193
|
+
2. Set **Authentication Type**: `Basic Auth`
|
|
194
|
+
3. Enter **Username**: Your SFTP username
|
|
195
|
+
4. Enter **Password**: Your SFTP password
|
|
196
|
+
5. The SDK will automatically decode the credentials using:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Get SFTP credentials from Versori connection (Basic Auth)
|
|
200
|
+
// RECOMMENDED: Use activation.connections (already decoded)
|
|
201
|
+
const allConnections = ctx.activation.connections || [];
|
|
202
|
+
const sftpConn = allConnections.find(c => c.name === 'versori_ftp_server');
|
|
203
|
+
|
|
204
|
+
if (!sftpConn) {
|
|
205
|
+
throw new Error('SFTP connection "versori_ftp_server" not found');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const credential = sftpConn.credentials[0]?.credential;
|
|
209
|
+
if (!credential?.data?.basicAuth) {
|
|
210
|
+
throw new Error('SFTP connection not configured with Basic Authentication');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const { username, password } = credential.data.basicAuth;
|
|
214
|
+
// ✅ Already decoded - no Buffer.from() needed!
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Why use connections instead of activation variables?**
|
|
218
|
+
|
|
219
|
+
- ✅ Credentials stored securely in Versori vault
|
|
220
|
+
- ✅ Connection can be reused across workflows
|
|
221
|
+
- ✅ No need to manage sensitive data in activation variables
|
|
222
|
+
- ✅ Easier credential rotation
|
|
223
|
+
|
|
224
|
+
### SFTP Credential Access Methods
|
|
225
|
+
|
|
226
|
+
**You have TWO methods to access SFTP credentials from Versori connections:**
|
|
227
|
+
|
|
228
|
+
#### Method 1: activation.connections (✅ RECOMMENDED)
|
|
229
|
+
|
|
230
|
+
**Best for:** All scenarios - cleanest, no decoding needed
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { Buffer } from 'node:buffer'; // Required for SFTP upload
|
|
234
|
+
|
|
235
|
+
const { activation, log } = ctx;
|
|
236
|
+
|
|
237
|
+
// Get the connection (credentials ALREADY DECODED!)
|
|
238
|
+
const allConnections = activation.connections || [];
|
|
239
|
+
const sftpConnection = allConnections.find(c => c.name === 'versori_ftp_server');
|
|
240
|
+
|
|
241
|
+
if (!sftpConnection) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`SFTP connection not found. Available: ${allConnections.map(c => c.name).join(', ')}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const sftpCred = sftpConnection.credentials[0]?.credential;
|
|
248
|
+
|
|
249
|
+
if (!sftpCred?.data?.basicAuth) {
|
|
250
|
+
throw new Error('SFTP connection not configured with Basic Auth');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ✅ Already decoded - no Buffer.from() needed!
|
|
254
|
+
const sftpUsername = sftpCred.data.basicAuth.username;
|
|
255
|
+
const sftpPassword = sftpCred.data.basicAuth.password;
|
|
256
|
+
const sftpHost = sftpConnection.baseUrl || ctx.activation.getVariable('sftpHost');
|
|
257
|
+
|
|
258
|
+
log.info('SFTP credentials retrieved', {
|
|
259
|
+
username: sftpUsername,
|
|
260
|
+
host: sftpHost,
|
|
261
|
+
hasPassword: !!sftpPassword,
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Why this is best:**
|
|
266
|
+
|
|
267
|
+
- ✅ No base64 decoding required
|
|
268
|
+
- ✅ Type-safe access to credential structure
|
|
269
|
+
- ✅ Works in all task types (fn, http, webhook)
|
|
270
|
+
- ✅ Cleaner error handling
|
|
271
|
+
|
|
272
|
+
#### Method 2: credentials().get() (Alternative)
|
|
273
|
+
|
|
274
|
+
**Use when:** Working in fn() tasks where activation.connections not available
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { Buffer } from 'node:buffer'; // Required for both decoding AND SFTP upload
|
|
278
|
+
|
|
279
|
+
// Retrieve credentials (returns base64-encoded accessToken)
|
|
280
|
+
const sftpCred = await ctx.credentials().getAccessToken('versori_ftp_server');
|
|
281
|
+
|
|
282
|
+
if (!sftpCred?.accessToken) {
|
|
283
|
+
throw new Error('No SFTP credentials found');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ⚠️ Manual base64 decoding required
|
|
287
|
+
const rawBasicAuth = Buffer.from(sftpCred.accessToken, 'base64').toString('utf-8');
|
|
288
|
+
const [sftpUsername, sftpPassword] = rawBasicAuth.split(':');
|
|
289
|
+
|
|
290
|
+
if (!sftpUsername || !sftpPassword) {
|
|
291
|
+
throw new Error('Invalid credential format');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
log.info('SFTP credentials decoded', {
|
|
295
|
+
hasUsername: !!sftpUsername,
|
|
296
|
+
hasPassword: !!sftpPassword,
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Limitations:**
|
|
301
|
+
|
|
302
|
+
- ⚠️ Requires manual base64 decoding
|
|
303
|
+
- ⚠️ More error-prone (string splitting)
|
|
304
|
+
- ⚠️ Only works in fn() tasks
|
|
305
|
+
|
|
306
|
+
### Buffer Import Reminder (CRITICAL!)
|
|
307
|
+
|
|
308
|
+
**For both methods, you MUST import Buffer in Versori/Deno runtime:**
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { Buffer } from 'node:buffer'; // ⚠️ ALWAYS REQUIRED
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Why:**
|
|
315
|
+
|
|
316
|
+
- SFTP uploads require Buffer: `Buffer.from(xmlContent, 'utf8')`
|
|
317
|
+
- Method 2 decoding requires Buffer: `Buffer.from(accessToken, 'base64')`
|
|
318
|
+
- Deno runtime doesn't have global Buffer (unlike Node.js)
|
|
319
|
+
|
|
320
|
+
**See:** [SFTP Credential Access & Security](../../../../../02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md) for complete documentation with troubleshooting and security best practices
|
|
321
|
+
|
|
322
|
+
## Activation Variables
|
|
323
|
+
|
|
324
|
+
**Configuration is driven by activation variables - modify these instead of code:**
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"retailerId": "your-retailer-id",
|
|
329
|
+
"sftpHost": "sftp.3pl-partner.com",
|
|
330
|
+
"sftpPort": 22,
|
|
331
|
+
"sftpPrivateKey": "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----",
|
|
332
|
+
"sftpRemotePath": "/incoming/orders/",
|
|
333
|
+
"pageSize": 200,
|
|
334
|
+
"maxRecords": 10000,
|
|
335
|
+
"fallbackStartDate": "2024-01-01T00:00:00Z",
|
|
336
|
+
"overlapBufferSeconds": "60"
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
> **Note:** `sftpUsername` and `sftpPassword` are fetched from the `versori_ftp_server` Basic Auth connection (see SFTP Connection Setup above).
|
|
341
|
+
|
|
342
|
+
## Export Mapping Configuration
|
|
343
|
+
|
|
344
|
+
**IMPORTANT**: Fields match CSV version exactly for consistency.
|
|
345
|
+
|
|
346
|
+
Create file: `./config/orders.export.xml.json`
|
|
347
|
+
|
|
348
|
+
```json
|
|
349
|
+
{
|
|
350
|
+
"name": "orders.export.xml",
|
|
351
|
+
"version": "1.0.0",
|
|
352
|
+
"description": "Fluent Orders → 3PL XML Export",
|
|
353
|
+
"fields": {
|
|
354
|
+
"order_id": { "source": "ref", "required": true, "resolver": "sdk.trim" },
|
|
355
|
+
"order_date": { "source": "createdOn", "required": true, "resolver": "sdk.formatDateShort" },
|
|
356
|
+
"order_status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
|
|
357
|
+
"customer_name": { "source": "customer.firstName", "required": false, "resolver": "sdk.trim" },
|
|
358
|
+
"customer_email": { "source": "customer.email", "required": false, "resolver": "sdk.trim" },
|
|
359
|
+
"ship_to_name": { "source": "deliveryAddress.name", "required": true, "resolver": "sdk.trim" },
|
|
360
|
+
"ship_to_address1": {
|
|
361
|
+
"source": "deliveryAddress.street1",
|
|
362
|
+
"required": true,
|
|
363
|
+
"resolver": "sdk.trim"
|
|
364
|
+
},
|
|
365
|
+
"ship_to_city": { "source": "deliveryAddress.city", "required": true, "resolver": "sdk.trim" },
|
|
366
|
+
"ship_to_state": {
|
|
367
|
+
"source": "deliveryAddress.state",
|
|
368
|
+
"required": true,
|
|
369
|
+
"resolver": "sdk.uppercase"
|
|
370
|
+
},
|
|
371
|
+
"ship_to_zip": {
|
|
372
|
+
"source": "deliveryAddress.postcode",
|
|
373
|
+
"required": true,
|
|
374
|
+
"resolver": "sdk.trim"
|
|
375
|
+
},
|
|
376
|
+
"ship_to_country": {
|
|
377
|
+
"source": "deliveryAddress.country",
|
|
378
|
+
"required": true,
|
|
379
|
+
"resolver": "sdk.uppercase"
|
|
380
|
+
},
|
|
381
|
+
"line_item_sku": { "source": "product.ref", "required": true, "resolver": "sdk.trim" },
|
|
382
|
+
"line_item_qty": { "source": "quantity", "required": true, "resolver": "sdk.parseInt" },
|
|
383
|
+
"line_item_price": { "source": "price", "required": true, "resolver": "sdk.parseFloat" }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Mapping & Resolvers Explained
|
|
389
|
+
|
|
390
|
+
### SDK Resolvers Used
|
|
391
|
+
|
|
392
|
+
The export mapping uses **SDK resolvers** to transform order data into 3PL-ready XML format:
|
|
393
|
+
|
|
394
|
+
| Field | Resolver | Why? | Example Transformation |
|
|
395
|
+
| ----------------- | --------------------- | ---------------------------- | ------------------------------------------------ |
|
|
396
|
+
| `order_id` | `sdk.trim` | Clean order references | `" ORD-123 "` → `"ORD-123"` |
|
|
397
|
+
| `order_date` | `sdk.formatDateShort` | 3PL-friendly date format | `"2025-01-22T14:30:00Z"` → `"2025-01-22"` |
|
|
398
|
+
| `order_status` | `sdk.uppercase` | Normalize status codes | `"created"` → `"CREATED"` |
|
|
399
|
+
| `customer_name` | `sdk.trim` | Clean customer data | `"John "` → `"John"` |
|
|
400
|
+
| `customer_email` | `sdk.trim` | Clean email addresses | `" user@example.com "` → `"user@example.com"` |
|
|
401
|
+
| `ship_to_name` | `sdk.trim` | Clean shipping contact | `"Jane Doe "` → `"Jane Doe"` |
|
|
402
|
+
| `ship_to_state` | `sdk.uppercase` | Normalize state codes | `"ny"` → `"NY"` |
|
|
403
|
+
| `ship_to_country` | `sdk.uppercase` | Normalize country codes | `"us"` → `"US"` |
|
|
404
|
+
| `line_item_sku` | `sdk.trim` | Clean SKU references | `" SKU-001 "` → `"SKU-001"` |
|
|
405
|
+
| `line_item_qty` | `sdk.parseInt` | Parse quantities as integers | `"5"` → `5` |
|
|
406
|
+
| `line_item_price` | `sdk.parseFloat` | Parse prices as decimals | `"19.99"` → `19.99` |
|
|
407
|
+
|
|
408
|
+
### Transformation Flow
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// 1. GraphQL Response (from Fluent API)
|
|
412
|
+
{
|
|
413
|
+
ref: " ORD-12345 ",
|
|
414
|
+
createdOn: "2025-01-22T14:30:00.000Z",
|
|
415
|
+
status: "created",
|
|
416
|
+
customer: {
|
|
417
|
+
firstName: "John ",
|
|
418
|
+
email: " john@example.com "
|
|
419
|
+
},
|
|
420
|
+
deliveryAddress: {
|
|
421
|
+
name: "Jane Doe ",
|
|
422
|
+
street1: "123 Main St",
|
|
423
|
+
city: "New York",
|
|
424
|
+
state: "ny",
|
|
425
|
+
postcode: "10001",
|
|
426
|
+
country: "us"
|
|
427
|
+
},
|
|
428
|
+
items: [
|
|
429
|
+
{
|
|
430
|
+
quantity: "2",
|
|
431
|
+
price: "29.99",
|
|
432
|
+
product: { ref: " SKU-001 ", name: "Widget" }
|
|
433
|
+
}
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 2. UniversalMapper applies resolvers
|
|
438
|
+
const mapper = new UniversalMapper(ordersExportMapping);
|
|
439
|
+
const result = await mapper.map(order);
|
|
440
|
+
|
|
441
|
+
// 3. Transformed Output (clean, normalized)
|
|
442
|
+
result.data = {
|
|
443
|
+
order_id: "ORD-12345", // ✅ Trimmed
|
|
444
|
+
order_date: "2025-01-22", // ✅ Short date format
|
|
445
|
+
order_status: "CREATED", // ✅ Uppercased
|
|
446
|
+
customer_name: "John", // ✅ Trimmed
|
|
447
|
+
customer_email: "john@example.com", // ✅ Trimmed
|
|
448
|
+
ship_to_name: "Jane Doe", // ✅ Trimmed
|
|
449
|
+
ship_to_address1: "123 Main St",
|
|
450
|
+
ship_to_city: "New York",
|
|
451
|
+
ship_to_state: "NY", // ✅ Uppercased
|
|
452
|
+
ship_to_zip: "10001",
|
|
453
|
+
ship_to_country: "US", // ✅ Uppercased
|
|
454
|
+
items: [
|
|
455
|
+
{
|
|
456
|
+
line_item_sku: "SKU-001", // ✅ Trimmed
|
|
457
|
+
line_item_qty: 2, // ✅ Integer
|
|
458
|
+
line_item_price: 29.99 // ✅ Float
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 4. Generate XML with proper escaping
|
|
464
|
+
const xml = buildOrdersXML([result.data]);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Custom Resolvers for Order-Specific Logic
|
|
468
|
+
|
|
469
|
+
You can add **custom resolvers** for 3PL-specific transformations:
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
const ordersExportMapping = {
|
|
473
|
+
name: 'orders.export.xml',
|
|
474
|
+
version: '1.0.0',
|
|
475
|
+
fields: {
|
|
476
|
+
order_id: { source: 'ref', required: true, resolver: 'sdk.trim' },
|
|
477
|
+
order_date: { source: 'createdOn', required: true, resolver: 'sdk.formatDateShort' },
|
|
478
|
+
|
|
479
|
+
// Custom resolver: Generate 3PL reference number
|
|
480
|
+
partner_order_ref: {
|
|
481
|
+
source: 'ref',
|
|
482
|
+
resolver: 'custom.generate3PLReference',
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
// Custom resolver: Calculate order total
|
|
486
|
+
order_total: {
|
|
487
|
+
source: 'items',
|
|
488
|
+
resolver: 'custom.calculateOrderTotal',
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
// Custom resolver: Map shipping service level
|
|
492
|
+
shipping_service_code: {
|
|
493
|
+
source: 'shippingMethod',
|
|
494
|
+
resolver: 'custom.mapShippingService',
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
// Custom resolver: Format phone number for 3PL
|
|
498
|
+
ship_to_phone: {
|
|
499
|
+
source: 'deliveryAddress.phone',
|
|
500
|
+
resolver: 'custom.formatPhone',
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Custom resolver implementations
|
|
506
|
+
const customResolvers = {
|
|
507
|
+
'custom.generate3PLReference': (orderRef: string) => {
|
|
508
|
+
// Format: 3PL-YYYYMMDD-ORDERREF
|
|
509
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
510
|
+
return `3PL-${today}-${orderRef}`;
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
'custom.calculateOrderTotal': (items: any[]) => {
|
|
514
|
+
const total = items.reduce((sum, item) => {
|
|
515
|
+
const qty = parseFloat(item.quantity) || 0;
|
|
516
|
+
const price = parseFloat(item.price) || 0;
|
|
517
|
+
return sum + qty * price;
|
|
518
|
+
}, 0);
|
|
519
|
+
return total.toFixed(2);
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
'custom.mapShippingService': (method: string) => {
|
|
523
|
+
const serviceMap: Record<string, string> = {
|
|
524
|
+
STANDARD: 'GROUND',
|
|
525
|
+
EXPRESS: '2DAY',
|
|
526
|
+
OVERNIGHT: 'NEXTDAY',
|
|
527
|
+
INTERNATIONAL: 'INTL',
|
|
528
|
+
};
|
|
529
|
+
return serviceMap[method] || 'GROUND';
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
'custom.formatPhone': (phone: string) => {
|
|
533
|
+
if (!phone) return '';
|
|
534
|
+
// Remove all non-digits
|
|
535
|
+
const digits = phone.replace(/\D/g, '');
|
|
536
|
+
// Format as (XXX) XXX-XXXX
|
|
537
|
+
if (digits.length === 10) {
|
|
538
|
+
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
539
|
+
}
|
|
540
|
+
return phone;
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// Use with UniversalMapper
|
|
545
|
+
const mapper = new UniversalMapper(ordersExportMapping, { customResolvers });
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Available SDK Resolvers
|
|
549
|
+
|
|
550
|
+
**String Transformations:**
|
|
551
|
+
|
|
552
|
+
- `sdk.trim` - Remove whitespace
|
|
553
|
+
- `sdk.uppercase` - Convert to uppercase
|
|
554
|
+
- `sdk.lowercase` - Convert to lowercase
|
|
555
|
+
- `sdk.toString` - Convert to string
|
|
556
|
+
|
|
557
|
+
**Number Transformations:**
|
|
558
|
+
|
|
559
|
+
- `sdk.parseInt` - Parse integer
|
|
560
|
+
- `sdk.parseFloat` - Parse decimal
|
|
561
|
+
- `sdk.number` - Generic number conversion
|
|
562
|
+
|
|
563
|
+
**Date Transformations:**
|
|
564
|
+
|
|
565
|
+
- `sdk.formatDate` - ISO 8601 format (`2025-01-22T14:30:00Z`)
|
|
566
|
+
- `sdk.formatDateShort` - Short date format (`2025-01-22`)
|
|
567
|
+
- `sdk.parseDate` - Parse date string
|
|
568
|
+
|
|
569
|
+
**Type Conversions:**
|
|
570
|
+
|
|
571
|
+
- `sdk.boolean` - Convert to boolean
|
|
572
|
+
- `sdk.parseJson` - Parse JSON string
|
|
573
|
+
- `sdk.toJson` - Convert to JSON string
|
|
574
|
+
|
|
575
|
+
**Utility:**
|
|
576
|
+
|
|
577
|
+
- `sdk.identity` - Pass through unchanged
|
|
578
|
+
- `sdk.coalesce` - Return first non-null value
|
|
579
|
+
|
|
580
|
+
See [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) for complete resolver documentation.
|
|
581
|
+
|
|
582
|
+
## GraphQL Query
|
|
583
|
+
|
|
584
|
+
```graphql
|
|
585
|
+
query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, $first: Int!, $after: String) {
|
|
586
|
+
orders(
|
|
587
|
+
retailerId: $retailerId
|
|
588
|
+
updatedOn: { after: $updatedAfter }
|
|
589
|
+
first: $first
|
|
590
|
+
after: $after
|
|
591
|
+
) {
|
|
592
|
+
edges {
|
|
593
|
+
node {
|
|
594
|
+
id
|
|
595
|
+
ref
|
|
596
|
+
status
|
|
597
|
+
createdOn
|
|
598
|
+
updatedOn
|
|
599
|
+
customer {
|
|
600
|
+
firstName
|
|
601
|
+
lastName
|
|
602
|
+
email
|
|
603
|
+
}
|
|
604
|
+
deliveryAddress {
|
|
605
|
+
name
|
|
606
|
+
street1
|
|
607
|
+
street2
|
|
608
|
+
city
|
|
609
|
+
state
|
|
610
|
+
postcode
|
|
611
|
+
country
|
|
612
|
+
}
|
|
613
|
+
items {
|
|
614
|
+
id
|
|
615
|
+
quantity
|
|
616
|
+
price
|
|
617
|
+
product {
|
|
618
|
+
ref
|
|
619
|
+
name
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
cursor
|
|
624
|
+
}
|
|
625
|
+
pageInfo {
|
|
626
|
+
hasNextPage
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## Expected XML Output
|
|
633
|
+
|
|
634
|
+
**IMPORTANT**: XML structure with same fields as CSV version for consistency.
|
|
635
|
+
|
|
636
|
+
```xml
|
|
637
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
638
|
+
<Orders>
|
|
639
|
+
<Order>
|
|
640
|
+
<OrderHeader>
|
|
641
|
+
<order_id>ORD-001</order_id>
|
|
642
|
+
<order_date>2025-01-22</order_date>
|
|
643
|
+
<order_status>CREATED</order_status>
|
|
644
|
+
<customer_name>John</customer_name>
|
|
645
|
+
<customer_email>john@example.com</customer_email>
|
|
646
|
+
</OrderHeader>
|
|
647
|
+
<ShipTo>
|
|
648
|
+
<ship_to_name>John Smith</ship_to_name>
|
|
649
|
+
<ship_to_address1>123 Main St</ship_to_address1>
|
|
650
|
+
<ship_to_city>New York</ship_to_city>
|
|
651
|
+
<ship_to_state>NY</ship_to_state>
|
|
652
|
+
<ship_to_zip>10001</ship_to_zip>
|
|
653
|
+
<ship_to_country>US</ship_to_country>
|
|
654
|
+
</ShipTo>
|
|
655
|
+
<LineItems>
|
|
656
|
+
<LineItem>
|
|
657
|
+
<line_item_sku>SKU-001</line_item_sku>
|
|
658
|
+
<line_item_qty>2</line_item_qty>
|
|
659
|
+
<line_item_price>29.99</line_item_price>
|
|
660
|
+
</LineItem>
|
|
661
|
+
<LineItem>
|
|
662
|
+
<line_item_sku>SKU-002</line_item_sku>
|
|
663
|
+
<line_item_qty>1</line_item_qty>
|
|
664
|
+
<line_item_price>49.99</line_item_price>
|
|
665
|
+
</LineItem>
|
|
666
|
+
</LineItems>
|
|
667
|
+
</Order>
|
|
668
|
+
<Order>
|
|
669
|
+
<OrderHeader>
|
|
670
|
+
<order_id>ORD-002</order_id>
|
|
671
|
+
<order_date>2025-01-22</order_date>
|
|
672
|
+
<order_status>PAID</order_status>
|
|
673
|
+
<customer_name>Jane</customer_name>
|
|
674
|
+
<customer_email>jane@example.com</customer_email>
|
|
675
|
+
</OrderHeader>
|
|
676
|
+
<ShipTo>
|
|
677
|
+
<ship_to_name>Jane Doe</ship_to_name>
|
|
678
|
+
<ship_to_address1>456 Oak Ave</ship_to_address1>
|
|
679
|
+
<ship_to_city>Los Angeles</ship_to_city>
|
|
680
|
+
<ship_to_state>CA</ship_to_state>
|
|
681
|
+
<ship_to_zip>90001</ship_to_zip>
|
|
682
|
+
<ship_to_country>US</ship_to_country>
|
|
683
|
+
</ShipTo>
|
|
684
|
+
<LineItems>
|
|
685
|
+
<LineItem>
|
|
686
|
+
<line_item_sku>SKU-003</line_item_sku>
|
|
687
|
+
<line_item_qty>1</line_item_qty>
|
|
688
|
+
<line_item_price>19.99</line_item_price>
|
|
689
|
+
</LineItem>
|
|
690
|
+
</LineItems>
|
|
691
|
+
</Order>
|
|
692
|
+
</Orders>
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
**Note**: XML preserves hierarchical structure (Order → LineItems) unlike CSV which flattens to rows.
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## Versori Workflows Structure
|
|
700
|
+
|
|
701
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
702
|
+
|
|
703
|
+
**Trigger Types:**
|
|
704
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
705
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
706
|
+
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
707
|
+
|
|
708
|
+
**Execution Steps (chained to triggers):**
|
|
709
|
+
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
710
|
+
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
711
|
+
|
|
712
|
+
### Recommended Project Structure
|
|
713
|
+
|
|
714
|
+
```
|
|
715
|
+
orders-extraction/
|
|
716
|
+
├── index.ts # Entry point - exports all workflows
|
|
717
|
+
└── src/
|
|
718
|
+
├── workflows/
|
|
719
|
+
│ ├── scheduled/
|
|
720
|
+
│ │ └── daily-orders-extraction.ts # Scheduled: Daily orders extraction
|
|
721
|
+
│ │
|
|
722
|
+
│ └── webhook/
|
|
723
|
+
│ ├── adhoc-orders-extraction.ts # Webhook: Manual trigger
|
|
724
|
+
│ └── job-status-check.ts # Webhook: Status query
|
|
725
|
+
│
|
|
726
|
+
├── services/
|
|
727
|
+
│ └── orders-extraction.service.ts # Shared orchestration logic (reusable)
|
|
728
|
+
│
|
|
729
|
+
└── config/
|
|
730
|
+
└── orders.export.xml.json # Mapping configuration
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
**Why This Structure:**
|
|
734
|
+
- ✅ Easier to maintain (each workflow in its own file)
|
|
735
|
+
- ✅ Clear separation of concerns (scheduled vs webhook)
|
|
736
|
+
- ✅ Better for team collaboration (fewer merge conflicts)
|
|
737
|
+
- ✅ Follows Versori best practices
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
### Overview
|
|
742
|
+
|
|
743
|
+
Even with **incremental-only** extraction, order data needs safeguards to prevent runtime failures:
|
|
744
|
+
|
|
745
|
+
- **Nested data**: Orders contain line items, addresses, customers (1 order → 5-20 line items)
|
|
746
|
+
- **XML generation**: More memory-intensive than CSV generation
|
|
747
|
+
- **Time-critical**: 3PL/fulfillment systems need reliable, timely feeds
|
|
748
|
+
- **High priority**: Order processing delays directly impact customers
|
|
749
|
+
|
|
750
|
+
### Hard Limits
|
|
751
|
+
|
|
752
|
+
```typescript
|
|
753
|
+
const SAFETY_LIMITS = {
|
|
754
|
+
MAX_ORDERS_PER_RUN: 50000, // 50k orders per run
|
|
755
|
+
MAX_XML_ELEMENTS: 500000, // 500k XML elements total
|
|
756
|
+
MAX_RECORDS_PER_FILE: 10000, // 10k orders per XML file (SFTP-friendly)
|
|
757
|
+
MAX_FILE_SIZE_MB: 100, // 100MB per file
|
|
758
|
+
MAX_XML_SIZE_MB: 200, // Total extraction size
|
|
759
|
+
CHUNK_SIZE: 5000, // Process in chunks
|
|
760
|
+
AVG_LINE_ITEMS_PER_ORDER: 3, // Conservative estimate
|
|
761
|
+
ESTIMATED_BYTES_PER_ORDER_XML: 2000, // XML with full structure
|
|
762
|
+
};
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
**Why XML needs special consideration?**
|
|
766
|
+
|
|
767
|
+
- **XML overhead**: Tags and structure add 2-3x size vs CSV
|
|
768
|
+
- **Memory during generation**: Building XML tree in memory
|
|
769
|
+
- **SFTP partners**: Often have stricter file size limits than S3
|
|
770
|
+
- **Validation**: 3PL systems validate XML against schemas
|
|
771
|
+
|
|
772
|
+
### Runtime Validation Function
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
/**
|
|
776
|
+
* Validate extraction safety limits before processing
|
|
777
|
+
* CRITICAL: Account for XML size overhead vs CSV
|
|
778
|
+
*/
|
|
779
|
+
function validateExtractionLimits(orderCount: number, totalLineItems: number) {
|
|
780
|
+
const MAX_ORDERS_PER_RUN = 50000;
|
|
781
|
+
const MAX_XML_ELEMENTS = 500000; // orders + line items + structure
|
|
782
|
+
const ESTIMATED_BYTES_PER_ORDER_XML = 2000; // Full XML order element
|
|
783
|
+
const estimatedSizeMB = (orderCount * ESTIMATED_BYTES_PER_ORDER_XML) / (1024 * 1024);
|
|
784
|
+
const MAX_XML_SIZE_MB = 200;
|
|
785
|
+
|
|
786
|
+
if (orderCount > MAX_ORDERS_PER_RUN) {
|
|
787
|
+
return {
|
|
788
|
+
valid: false,
|
|
789
|
+
error: `Extraction limit exceeded: ${orderCount} orders (max: ${MAX_ORDERS_PER_RUN})`,
|
|
790
|
+
recommendation: `Too many orders for single extraction. Consider:
|
|
791
|
+
1. Increase extraction frequency (daily → hourly)
|
|
792
|
+
2. Add order status filters (NEW, PAID only)
|
|
793
|
+
3. Split by fulfillment location
|
|
794
|
+
4. Contact support if consistently exceeding limits`,
|
|
795
|
+
orderCount,
|
|
796
|
+
maxAllowed: MAX_ORDERS_PER_RUN,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const totalElements = orderCount + totalLineItems + orderCount * 2; // headers + shipping
|
|
801
|
+
if (totalElements > MAX_XML_ELEMENTS) {
|
|
802
|
+
return {
|
|
803
|
+
valid: false,
|
|
804
|
+
error: `XML element limit exceeded: ${totalElements} elements (max: ${MAX_XML_ELEMENTS})`,
|
|
805
|
+
recommendation: `XML structure too large. Orders: ${orderCount}, Line items: ${totalLineItems}. Increase extraction frequency.`,
|
|
806
|
+
totalElements,
|
|
807
|
+
maxAllowed: MAX_XML_ELEMENTS,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (estimatedSizeMB > MAX_XML_SIZE_MB) {
|
|
812
|
+
return {
|
|
813
|
+
valid: false,
|
|
814
|
+
error: `XML size limit exceeded: ${estimatedSizeMB}MB (max: ${MAX_XML_SIZE_MB}MB)`,
|
|
815
|
+
recommendation:
|
|
816
|
+
'File splitting required. Increase extraction frequency to reduce batch size.',
|
|
817
|
+
estimatedSizeMB,
|
|
818
|
+
maxAllowed: MAX_XML_SIZE_MB,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return { valid: true };
|
|
823
|
+
}
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### Memory-Safe XML Generation with XMLBuilder
|
|
827
|
+
|
|
828
|
+
The SDK's `XMLBuilder` handles XML generation efficiently with automatic escaping:
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
import { Buffer } from 'node:buffer';
|
|
832
|
+
import { XMLBuilder } from '@fluentcommerce/fc-connect-sdk';
|
|
833
|
+
|
|
834
|
+
// Initialize XMLBuilder (reusable)
|
|
835
|
+
const xmlBuilder = new XMLBuilder({
|
|
836
|
+
rootElement: 'Orders',
|
|
837
|
+
prettyPrint: true,
|
|
838
|
+
indent: ' ',
|
|
839
|
+
xmlDeclaration: true,
|
|
840
|
+
encoding: 'UTF-8',
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Generate XML from orders using XMLBuilder
|
|
845
|
+
* Handles nested structures (OrderHeader, ShipTo, LineItems) automatically
|
|
846
|
+
*/
|
|
847
|
+
function buildOrdersXML(orders: any[]): string {
|
|
848
|
+
// Transform to XMLBuilder format with nested structures
|
|
849
|
+
const ordersForXml = orders.map(order => ({
|
|
850
|
+
OrderHeader: {
|
|
851
|
+
order_id: order.order_id,
|
|
852
|
+
order_date: order.order_date,
|
|
853
|
+
order_status: order.order_status,
|
|
854
|
+
customer_name: order.customer_name || '',
|
|
855
|
+
customer_email: order.customer_email || '',
|
|
856
|
+
},
|
|
857
|
+
ShipTo: {
|
|
858
|
+
ship_to_name: order.ship_to_name,
|
|
859
|
+
ship_to_address1: order.ship_to_address1,
|
|
860
|
+
ship_to_city: order.ship_to_city,
|
|
861
|
+
ship_to_state: order.ship_to_state,
|
|
862
|
+
ship_to_zip: order.ship_to_zip,
|
|
863
|
+
ship_to_country: order.ship_to_country,
|
|
864
|
+
},
|
|
865
|
+
LineItems: {
|
|
866
|
+
LineItem: order.line_items.map(item => ({
|
|
867
|
+
line_item_sku: item.line_item_sku,
|
|
868
|
+
line_item_qty: item.line_item_qty,
|
|
869
|
+
line_item_price: item.line_item_price,
|
|
870
|
+
})),
|
|
871
|
+
},
|
|
872
|
+
}));
|
|
873
|
+
|
|
874
|
+
// XMLBuilder handles all escaping and structure automatically
|
|
875
|
+
return xmlBuilder.build({ Order: ordersForXml });
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Example output:
|
|
879
|
+
// <Orders>
|
|
880
|
+
// <Order>
|
|
881
|
+
// <OrderHeader>
|
|
882
|
+
// <order_id>ORD-001</order_id>
|
|
883
|
+
// <customer_name>Smith & Jones</customer_name>
|
|
884
|
+
// </OrderHeader>
|
|
885
|
+
// <ShipTo>
|
|
886
|
+
// <ship_to_name>John Smith</ship_to_name>
|
|
887
|
+
// <ship_to_address1>123 Main St</ship_to_address1>
|
|
888
|
+
// </ShipTo>
|
|
889
|
+
// <LineItems>
|
|
890
|
+
// <LineItem>
|
|
891
|
+
// <line_item_sku>SKU-001</line_item_sku>
|
|
892
|
+
// <line_item_qty>2</line_item_qty>
|
|
893
|
+
// </LineItem>
|
|
894
|
+
// </LineItems>
|
|
895
|
+
// </Order>
|
|
896
|
+
// </Orders>
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
**Benefits:**
|
|
900
|
+
|
|
901
|
+
- ✅ Automatic XML escaping (handles &, <, >, ", ', etc.)
|
|
902
|
+
- ✅ Nested structure support (OrderHeader, ShipTo, LineItems)
|
|
903
|
+
- ✅ Array handling (multiple LineItem elements)
|
|
904
|
+
- ✅ Memory-efficient streaming
|
|
905
|
+
- ✅ Pretty printing with proper indentation
|
|
906
|
+
|
|
907
|
+
## Complete Workflow Code
|
|
908
|
+
|
|
909
|
+
The following code examples demonstrate the implementation across separate files following the recommended modular structure.
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
### 1. Entry Point (src/index.ts)
|
|
913
|
+
|
|
914
|
+
```typescript
|
|
915
|
+
/**
|
|
916
|
+
* Entry point - Export all workflows for Versori platform
|
|
917
|
+
*
|
|
918
|
+
* This file exports all workflows to be registered with Versori.
|
|
919
|
+
* Each workflow is defined in its own file for better organization.
|
|
920
|
+
*/
|
|
921
|
+
|
|
922
|
+
// Scheduled workflows
|
|
923
|
+
export { scheduledOrdersExtraction } from './workflows/scheduled/daily-orders-extraction';
|
|
924
|
+
|
|
925
|
+
// Webhook workflows
|
|
926
|
+
export { adhocOrdersExtraction } from './workflows/webhook/adhoc-orders-extraction';
|
|
927
|
+
export { ordersJobStatus } from './workflows/webhook/job-status-check';
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
### 2. Scheduled Workflow (src/workflows/scheduled/daily-orders-extraction.ts)
|
|
931
|
+
|
|
932
|
+
```typescript
|
|
933
|
+
import { schedule, http } from '@versori/run';
|
|
934
|
+
import {
|
|
935
|
+
executeOrderExtraction,
|
|
936
|
+
generateJobId,
|
|
937
|
+
} from '../../services/extraction-orchestration';
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Scheduled workflow: Daily orders extraction to SFTP XML
|
|
941
|
+
* Runs at 2:00 AM daily
|
|
942
|
+
*
|
|
943
|
+
* DELEGATION PATTERN:
|
|
944
|
+
* - Workflow receives ctx from Versori
|
|
945
|
+
* - Passes entire ctx to service function
|
|
946
|
+
* - Service handles all business logic
|
|
947
|
+
*/
|
|
948
|
+
export const scheduledOrdersExtraction = schedule('orders-extract-xml-daily', '0 2 * * *').then(
|
|
949
|
+
http('execute-scheduled-extraction', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
950
|
+
const { log } = ctx;
|
|
951
|
+
const executionStartTime = Date.now();
|
|
952
|
+
const jobId = generateJobId('SCHED', 'ORDERS');
|
|
953
|
+
|
|
954
|
+
log.info('🚀 [WORKFLOW] Starting scheduled extraction', { jobId });
|
|
955
|
+
|
|
956
|
+
const result = await executeOrderExtraction(ctx, {
|
|
957
|
+
jobId,
|
|
958
|
+
triggeredBy: 'schedule',
|
|
959
|
+
updateState: true, // Always update state for scheduled runs
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
const duration = Date.now() - executionStartTime;
|
|
963
|
+
|
|
964
|
+
if (result.success) {
|
|
965
|
+
log.info('✅ [WORKFLOW] Extraction completed successfully', {
|
|
966
|
+
jobId,
|
|
967
|
+
ordersExtracted: result.ordersExtracted,
|
|
968
|
+
duration: `${duration}ms`,
|
|
969
|
+
});
|
|
970
|
+
} else {
|
|
971
|
+
log.error('❌ [WORKFLOW] Extraction failed', {
|
|
972
|
+
jobId,
|
|
973
|
+
error: result.error,
|
|
974
|
+
duration: `${duration}ms`,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return { ...result, duration };
|
|
979
|
+
})
|
|
980
|
+
);
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### 3. Ad-hoc Webhook (src/workflows/webhook/adhoc-orders-extraction.ts)
|
|
984
|
+
|
|
985
|
+
```typescript
|
|
986
|
+
import { webhook, http } from '@versori/run';
|
|
987
|
+
import {
|
|
988
|
+
executeOrderExtraction,
|
|
989
|
+
generateJobId,
|
|
990
|
+
} from '../../services/extraction-orchestration';
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Webhook workflow: Ad-hoc orders extraction
|
|
994
|
+
* Allows manual/on-demand extraction with date range overrides
|
|
995
|
+
*/
|
|
996
|
+
export const adhocOrdersExtraction = webhook('orders-adhoc', {
|
|
997
|
+
connection: 'orders-adhoc',
|
|
998
|
+
response: { mode: 'sync' },
|
|
999
|
+
}).then(
|
|
1000
|
+
http('execute-adhoc-extraction', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
1001
|
+
const { log } = ctx;
|
|
1002
|
+
const executionStartTime = Date.now();
|
|
1003
|
+
const jobId = generateJobId('ADHOC', 'ORDERS');
|
|
1004
|
+
|
|
1005
|
+
log.info('🔧 [WEBHOOK] Starting ad-hoc extraction', {
|
|
1006
|
+
jobId,
|
|
1007
|
+
fromDate: ctx.data.fromDate,
|
|
1008
|
+
toDate: ctx.data.toDate
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
const result = await executeOrderExtraction(ctx, {
|
|
1012
|
+
jobId,
|
|
1013
|
+
triggeredBy: 'webhook',
|
|
1014
|
+
fromDate: ctx.data.fromDate,
|
|
1015
|
+
toDate: ctx.data.toDate,
|
|
1016
|
+
updateState: ctx.data.updateState !== false,
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
const duration = Date.now() - executionStartTime;
|
|
1020
|
+
|
|
1021
|
+
if (result.success) {
|
|
1022
|
+
log.info('✅ [WEBHOOK] Ad-hoc extraction completed', {
|
|
1023
|
+
jobId,
|
|
1024
|
+
ordersExtracted: result.ordersExtracted,
|
|
1025
|
+
duration: `${duration}ms`,
|
|
1026
|
+
});
|
|
1027
|
+
} else {
|
|
1028
|
+
log.error('❌ [WEBHOOK] Ad-hoc extraction failed', {
|
|
1029
|
+
jobId,
|
|
1030
|
+
error: result.error,
|
|
1031
|
+
duration: `${duration}ms`,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return { ...result, duration };
|
|
1036
|
+
})
|
|
1037
|
+
);
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
### 4. Job Status Query (src/workflows/webhook/job-status-check.ts)
|
|
1041
|
+
|
|
1042
|
+
```typescript
|
|
1043
|
+
import { webhook, fn } from '@versori/run';
|
|
1044
|
+
import { getJobStatus } from '../../services/extraction-orchestration';
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Webhook workflow: Job status query
|
|
1048
|
+
* Allows checking the status of extraction jobs by ID
|
|
1049
|
+
*/
|
|
1050
|
+
export const ordersJobStatus = webhook('orders-job-status', {
|
|
1051
|
+
connection: 'orders-job-status',
|
|
1052
|
+
response: { mode: 'sync' },
|
|
1053
|
+
}).then(
|
|
1054
|
+
fn('query-job-status', async (ctx: any) => {
|
|
1055
|
+
const { data, log, openKv } = ctx;
|
|
1056
|
+
// Security is enforced by the 'orders-job-status' connection
|
|
1057
|
+
|
|
1058
|
+
log.info('🔍 [STATUS] Querying job status', { jobId: data.jobId });
|
|
1059
|
+
|
|
1060
|
+
const jobId = data.jobId;
|
|
1061
|
+
if (!jobId) {
|
|
1062
|
+
log.error('❌ [STATUS] Missing job ID');
|
|
1063
|
+
return { success: false, error: 'Job ID required' };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const status = await getJobStatus(openKv(':project:'), jobId, log);
|
|
1067
|
+
|
|
1068
|
+
if (status) {
|
|
1069
|
+
log.info('✅ [STATUS] Job found', { jobId, status: status.status });
|
|
1070
|
+
return { success: true, jobId, ...status };
|
|
1071
|
+
} else {
|
|
1072
|
+
log.warn('⚠️ [STATUS] Job not found', { jobId });
|
|
1073
|
+
return { success: false, error: 'Job not found', jobId };
|
|
1074
|
+
}
|
|
1075
|
+
})
|
|
1076
|
+
);
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### 5. Main Orchestration Service (src/services/extraction-orchestration.ts)
|
|
1080
|
+
|
|
1081
|
+
```typescript
|
|
1082
|
+
import { Buffer } from 'node:buffer';
|
|
1083
|
+
import {
|
|
1084
|
+
createClient,
|
|
1085
|
+
ExtractionOrchestrator,
|
|
1086
|
+
JobTracker,
|
|
1087
|
+
UniversalMapper,
|
|
1088
|
+
XMLBuilder,
|
|
1089
|
+
SftpDataSource,
|
|
1090
|
+
VersoriKVAdapter,
|
|
1091
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
1092
|
+
import ordersExportMapping from '../../config/orders.export.xml.json' with { type: 'json' };
|
|
1093
|
+
|
|
1094
|
+
const ORDERS_QUERY = `
|
|
1095
|
+
query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, $first: Int!, $after: String) {
|
|
1096
|
+
orders(
|
|
1097
|
+
retailerId: $retailerId
|
|
1098
|
+
updatedOn: { after: $updatedAfter }
|
|
1099
|
+
first: $first
|
|
1100
|
+
after: $after
|
|
1101
|
+
) {
|
|
1102
|
+
edges {
|
|
1103
|
+
node {
|
|
1104
|
+
id
|
|
1105
|
+
ref
|
|
1106
|
+
status
|
|
1107
|
+
createdOn
|
|
1108
|
+
updatedOn
|
|
1109
|
+
customer {
|
|
1110
|
+
firstName
|
|
1111
|
+
lastName
|
|
1112
|
+
email
|
|
1113
|
+
}
|
|
1114
|
+
deliveryAddress {
|
|
1115
|
+
name
|
|
1116
|
+
street1
|
|
1117
|
+
street2
|
|
1118
|
+
city
|
|
1119
|
+
state
|
|
1120
|
+
postcode
|
|
1121
|
+
country
|
|
1122
|
+
}
|
|
1123
|
+
items {
|
|
1124
|
+
id
|
|
1125
|
+
quantity
|
|
1126
|
+
price
|
|
1127
|
+
product {
|
|
1128
|
+
ref
|
|
1129
|
+
name
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
cursor
|
|
1134
|
+
}
|
|
1135
|
+
pageInfo {
|
|
1136
|
+
hasNextPage
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
`;
|
|
1141
|
+
|
|
1142
|
+
// Initialize XMLBuilder for orders
|
|
1143
|
+
const xmlBuilder = new XMLBuilder({
|
|
1144
|
+
rootElement: 'Orders',
|
|
1145
|
+
prettyPrint: true,
|
|
1146
|
+
indent: ' ',
|
|
1147
|
+
xmlDeclaration: true,
|
|
1148
|
+
encoding: 'UTF-8',
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
function buildOrdersXML(orders: any[]): string {
|
|
1152
|
+
// Transform to XMLBuilder format with nested structures
|
|
1153
|
+
const ordersForXml = orders.map(order => ({
|
|
1154
|
+
OrderHeader: {
|
|
1155
|
+
order_id: order.order_id,
|
|
1156
|
+
order_date: order.order_date,
|
|
1157
|
+
order_status: order.order_status,
|
|
1158
|
+
customer_name: order.customer_name || '',
|
|
1159
|
+
customer_email: order.customer_email || '',
|
|
1160
|
+
},
|
|
1161
|
+
ShipTo: {
|
|
1162
|
+
ship_to_name: order.ship_to_name,
|
|
1163
|
+
ship_to_address1: order.ship_to_address1,
|
|
1164
|
+
ship_to_city: order.ship_to_city,
|
|
1165
|
+
ship_to_state: order.ship_to_state,
|
|
1166
|
+
ship_to_zip: order.ship_to_zip,
|
|
1167
|
+
ship_to_country: order.ship_to_country,
|
|
1168
|
+
},
|
|
1169
|
+
LineItems: {
|
|
1170
|
+
LineItem: order.line_items.map(item => ({
|
|
1171
|
+
line_item_sku: item.line_item_sku,
|
|
1172
|
+
line_item_qty: item.line_item_qty,
|
|
1173
|
+
line_item_price: item.line_item_price,
|
|
1174
|
+
})),
|
|
1175
|
+
},
|
|
1176
|
+
}));
|
|
1177
|
+
|
|
1178
|
+
return xmlBuilder.build({ Order: ordersForXml });
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
interface ExtractionOptions {
|
|
1182
|
+
jobId: string;
|
|
1183
|
+
triggeredBy: 'schedule' | 'webhook';
|
|
1184
|
+
fromDate?: string;
|
|
1185
|
+
toDate?: string;
|
|
1186
|
+
updateState: boolean;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
export async function executeOrderExtraction(ctx: any, options: ExtractionOptions) {
|
|
1190
|
+
const { jobId, triggeredBy, fromDate, toDate, updateState } = options;
|
|
1191
|
+
const log = ctx.log;
|
|
1192
|
+
const retailerId = ctx.activation?.getVariable('retailerId');
|
|
1193
|
+
const pageSize = parseInt(ctx.activation?.getVariable('pageSize') || '200', 10);
|
|
1194
|
+
const maxRecords = parseInt(ctx.activation?.getVariable('maxRecords') || '10000', 10);
|
|
1195
|
+
const fallbackStartDate =
|
|
1196
|
+
ctx.activation?.getVariable('fallbackStartDate') || '2024-01-01T00:00:00Z';
|
|
1197
|
+
|
|
1198
|
+
// Get SFTP credentials from Versori connection (Basic Auth)
|
|
1199
|
+
// RECOMMENDED: Use activation.connections (already decoded)
|
|
1200
|
+
const allConnections = ctx.activation.connections || [];
|
|
1201
|
+
const sftpConn = allConnections.find(c => c.name === 'versori_ftp_server');
|
|
1202
|
+
|
|
1203
|
+
if (!sftpConn) {
|
|
1204
|
+
throw new Error('SFTP connection "versori_ftp_server" not found');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const credential = sftpConn.credentials[0]?.credential;
|
|
1208
|
+
if (!credential?.data?.basicAuth) {
|
|
1209
|
+
throw new Error('SFTP connection not configured with Basic Authentication');
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const { username, password } = credential.data.basicAuth;
|
|
1213
|
+
// ✅ Already decoded - no Buffer.from() needed!
|
|
1214
|
+
|
|
1215
|
+
const sftpSettings = {
|
|
1216
|
+
host: ctx.activation?.getVariable('sftpHost'),
|
|
1217
|
+
port: parseInt(ctx.activation?.getVariable('sftpPort') || '22', 10),
|
|
1218
|
+
username, // From connection (secure)
|
|
1219
|
+
password, // From connection (secure)
|
|
1220
|
+
privateKey: ctx.activation?.getVariable('sftpPrivateKey'),
|
|
1221
|
+
remotePath: ctx.activation?.getVariable('sftpRemotePath') || '/incoming/orders/',
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
const missing: string[] = [];
|
|
1225
|
+
if (!retailerId) missing.push('retailerId');
|
|
1226
|
+
if (!sftpSettings.host) missing.push('sftpHost');
|
|
1227
|
+
if (!sftpSettings.username) missing.push('sftpUsername from connection');
|
|
1228
|
+
if (!sftpSettings.password && !sftpSettings.privateKey)
|
|
1229
|
+
missing.push('sftpPassword from connection or sftpPrivateKey');
|
|
1230
|
+
if (missing.length)
|
|
1231
|
+
return { success: false, error: `Missing required variables: ${missing.join(', ')}` };
|
|
1232
|
+
|
|
1233
|
+
// SFTP connection - MUST use try/finally with dispose()
|
|
1234
|
+
const sftp = new SftpDataSource(
|
|
1235
|
+
{
|
|
1236
|
+
type: 'SFTP_XML',
|
|
1237
|
+
connectionId: 'sftp-orders-xml-export',
|
|
1238
|
+
name: 'SFTP Orders XML Export',
|
|
1239
|
+
settings: {
|
|
1240
|
+
host: sftpSettings.host,
|
|
1241
|
+
port: sftpSettings.port,
|
|
1242
|
+
username: sftpSettings.username,
|
|
1243
|
+
password: sftpSettings.password,
|
|
1244
|
+
privateKey: sftpSettings.privateKey,
|
|
1245
|
+
remotePath: sftpSettings.remotePath,
|
|
1246
|
+
filePattern: '*.xml',
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1249
|
+
log
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
try {
|
|
1253
|
+
//
|
|
1254
|
+
// STEP 1/8: Initialize Job Tracking
|
|
1255
|
+
//
|
|
1256
|
+
const kv = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1257
|
+
const tracker = new JobTracker(kv, log);
|
|
1258
|
+
|
|
1259
|
+
await tracker.createJob(jobId, {
|
|
1260
|
+
triggeredBy,
|
|
1261
|
+
hasDateOverride: !!fromDate,
|
|
1262
|
+
fromDate,
|
|
1263
|
+
toDate,
|
|
1264
|
+
updateStateAfterRun: updateState,
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
log.info('Job created', { jobId, triggeredBy });
|
|
1268
|
+
|
|
1269
|
+
//
|
|
1270
|
+
// STEP 2/8: Load State & Calculate Time Window
|
|
1271
|
+
//
|
|
1272
|
+
await tracker.updateJob(jobId, {
|
|
1273
|
+
status: 'processing',
|
|
1274
|
+
stage: 'state_load',
|
|
1275
|
+
message: 'Loading last run state',
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
const stateKey = ['extraction', 'orders-xml', 'lastRunTime'];
|
|
1279
|
+
const lastRunState = await kv.get(stateKey);
|
|
1280
|
+
const rawLastRunTime = fromDate || lastRunState?.value?.timestamp || fallbackStartDate;
|
|
1281
|
+
|
|
1282
|
+
// Overlap buffer configuration (default: 60 seconds)
|
|
1283
|
+
const overlapBufferSeconds = parseInt(
|
|
1284
|
+
ctx.activation?.getVariable('overlapBufferSeconds') || '60',
|
|
1285
|
+
10
|
|
1286
|
+
);
|
|
1287
|
+
const OVERLAP_BUFFER_MS = overlapBufferSeconds * 1000;
|
|
1288
|
+
|
|
1289
|
+
// Apply overlap buffer for query (safety window)
|
|
1290
|
+
const bufferedLastRunTime = new Date(
|
|
1291
|
+
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
|
|
1292
|
+
).toISOString();
|
|
1293
|
+
|
|
1294
|
+
const effectiveEndTime = toDate || new Date().toISOString();
|
|
1295
|
+
|
|
1296
|
+
log.info('Time window calculated', {
|
|
1297
|
+
rawLastRunTime,
|
|
1298
|
+
bufferedLastRunTime,
|
|
1299
|
+
effectiveEndTime,
|
|
1300
|
+
overlapBufferSeconds,
|
|
1301
|
+
retailerId,
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
//
|
|
1305
|
+
// STEP 3/8: Initialize Fluent Client & ExtractionOrchestrator
|
|
1306
|
+
//
|
|
1307
|
+
await tracker.updateJob(jobId, {
|
|
1308
|
+
stage: 'client_init',
|
|
1309
|
+
message: 'Initializing Fluent client',
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
const client = await createClient(ctx);
|
|
1313
|
+
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
1314
|
+
|
|
1315
|
+
//
|
|
1316
|
+
// STEP 4/8: Extract Data (ExtractionOrchestrator)
|
|
1317
|
+
//
|
|
1318
|
+
await tracker.updateJob(jobId, {
|
|
1319
|
+
stage: 'extraction',
|
|
1320
|
+
message: 'Extracting data with auto-pagination',
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
// ? Enhanced: Extract context for progress logging
|
|
1324
|
+
const dateRangeInfo = {
|
|
1325
|
+
start: bufferedLastRunTime,
|
|
1326
|
+
end: effectiveEndTime,
|
|
1327
|
+
retailerId
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
// ? Enhanced: Start logging with context
|
|
1331
|
+
log.info(`🔍 [ExtractionOrchestrator] Starting extraction`, {
|
|
1332
|
+
query: 'orders',
|
|
1333
|
+
pageSize,
|
|
1334
|
+
maxRecords,
|
|
1335
|
+
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1336
|
+
retailerId: dateRangeInfo.retailerId,
|
|
1337
|
+
jobId
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
const extractionResult = await orchestrator.extract({
|
|
1341
|
+
query: ORDERS_QUERY,
|
|
1342
|
+
resultPath: 'orders.edges.node',
|
|
1343
|
+
variables: {
|
|
1344
|
+
retailerId,
|
|
1345
|
+
updatedAfter: bufferedLastRunTime,
|
|
1346
|
+
first: pageSize,
|
|
1347
|
+
},
|
|
1348
|
+
pageSize,
|
|
1349
|
+
maxRecords,
|
|
1350
|
+
validateItem: item => !!(item.ref && item.customer),
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
const rawRecords = extractionResult.data;
|
|
1354
|
+
|
|
1355
|
+
log.info('Extraction complete', {
|
|
1356
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1357
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1358
|
+
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1359
|
+
errors: extractionResult.errors ? extractionResult.errors.length : 0,
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
// ? Enhanced: Completion logging with summary
|
|
1363
|
+
log.info(`✅ [ExtractionOrchestrator] Extraction completed`, {
|
|
1364
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1365
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1366
|
+
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1367
|
+
failedValidations: extractionResult.stats.failedValidations,
|
|
1368
|
+
truncated: extractionResult.stats.truncated,
|
|
1369
|
+
truncationReason: extractionResult.stats.truncationReason,
|
|
1370
|
+
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1371
|
+
jobId
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
if (extractionResult.errors && extractionResult.errors.length > 0) {
|
|
1375
|
+
log.warn('Non-fatal extraction errors encountered', {
|
|
1376
|
+
errorCount: extractionResult.errors.length,
|
|
1377
|
+
sampleErrors: extractionResult.errors.slice(0, 3),
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (rawRecords.length === 0) {
|
|
1382
|
+
await tracker.markCompleted(jobId, {
|
|
1383
|
+
recordCount: 0,
|
|
1384
|
+
message: 'No new orders to extract',
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
if (updateState) {
|
|
1388
|
+
await kv.set(stateKey, {
|
|
1389
|
+
timestamp: new Date().toISOString(),
|
|
1390
|
+
orderCount: 0,
|
|
1391
|
+
extractedAt: new Date().toISOString(),
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
return { success: true, message: 'No new orders to extract', lastRunTime: rawLastRunTime };
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
//
|
|
1399
|
+
// STEP 5/8: Validate Extraction Limits
|
|
1400
|
+
//
|
|
1401
|
+
await tracker.updateJob(jobId, {
|
|
1402
|
+
stage: 'validation',
|
|
1403
|
+
message: 'Validating extraction limits',
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
const MAX_ORDERS_PER_RUN = 50000;
|
|
1407
|
+
const MAX_XML_ELEMENTS = 500000;
|
|
1408
|
+
|
|
1409
|
+
let totalLineItems = 0;
|
|
1410
|
+
for (const order of rawRecords) {
|
|
1411
|
+
totalLineItems += (order.items || []).length;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const totalElements = rawRecords.length + totalLineItems + rawRecords.length * 2; // orders + items + headers/shipping
|
|
1415
|
+
const ESTIMATED_BYTES_PER_ORDER_XML = 2000;
|
|
1416
|
+
const estimatedSizeMB = (rawRecords.length * ESTIMATED_BYTES_PER_ORDER_XML) / (1024 * 1024);
|
|
1417
|
+
const MAX_XML_SIZE_MB = 200;
|
|
1418
|
+
|
|
1419
|
+
if (rawRecords.length > MAX_ORDERS_PER_RUN) {
|
|
1420
|
+
log.error('Extraction limit exceeded', {
|
|
1421
|
+
orderCount: rawRecords.length,
|
|
1422
|
+
maxAllowed: MAX_ORDERS_PER_RUN,
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
await tracker.markFailed(jobId, {
|
|
1426
|
+
error: `Extraction limit exceeded: ${rawRecords.length} orders (max: ${MAX_ORDERS_PER_RUN})`,
|
|
1427
|
+
recommendation: 'Increase extraction frequency or add filters',
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
return {
|
|
1431
|
+
success: false,
|
|
1432
|
+
error: `Extraction limit exceeded: ${rawRecords.length} orders (max: ${MAX_ORDERS_PER_RUN})`,
|
|
1433
|
+
recommendation: `Too many orders for single extraction. Consider:
|
|
1434
|
+
1. Increase extraction frequency (daily → hourly)
|
|
1435
|
+
2. Add order status filters (NEW, PAID only)
|
|
1436
|
+
3. Split by fulfillment location
|
|
1437
|
+
4. Contact support if consistently exceeding limits`,
|
|
1438
|
+
orderCount: rawRecords.length,
|
|
1439
|
+
maxAllowed: MAX_ORDERS_PER_RUN,
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
if (totalElements > MAX_XML_ELEMENTS) {
|
|
1444
|
+
log.error('XML element limit exceeded', {
|
|
1445
|
+
orderCount: rawRecords.length,
|
|
1446
|
+
totalLineItems,
|
|
1447
|
+
totalElements,
|
|
1448
|
+
maxAllowed: MAX_XML_ELEMENTS,
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
await tracker.markFailed(jobId, {
|
|
1452
|
+
error: `XML element limit exceeded: ${totalElements} elements`,
|
|
1453
|
+
recommendation: 'Increase extraction frequency',
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
return {
|
|
1457
|
+
success: false,
|
|
1458
|
+
error: `XML element limit exceeded: ${totalElements} elements (max: ${MAX_XML_ELEMENTS})`,
|
|
1459
|
+
recommendation: `XML structure too large. Orders: ${rawRecords.length}, Line items: ${totalLineItems}. Increase extraction frequency.`,
|
|
1460
|
+
totalElements,
|
|
1461
|
+
maxAllowed: MAX_XML_ELEMENTS,
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (estimatedSizeMB > MAX_XML_SIZE_MB) {
|
|
1466
|
+
log.warn('XML size approaching limit', {
|
|
1467
|
+
estimatedSizeMB: estimatedSizeMB.toFixed(2),
|
|
1468
|
+
maxAllowed: MAX_XML_SIZE_MB,
|
|
1469
|
+
recommendation: 'Consider file splitting or increase extraction frequency',
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
log.info('Extraction limits validated', {
|
|
1474
|
+
orderCount: rawRecords.length,
|
|
1475
|
+
totalLineItems,
|
|
1476
|
+
totalElements,
|
|
1477
|
+
estimatedSizeMB: estimatedSizeMB.toFixed(2),
|
|
1478
|
+
withinLimits: true,
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
await tracker.updateJob(jobId, {
|
|
1482
|
+
stage: 'transformation',
|
|
1483
|
+
message: 'Transforming data with UniversalMapper',
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
const mapper = new UniversalMapper(ordersExportMapping);
|
|
1487
|
+
const transformedOrders: any[] = [];
|
|
1488
|
+
const mappingErrors: any[] = [];
|
|
1489
|
+
|
|
1490
|
+
for (let index = 0; index < rawRecords.length; index += 1) {
|
|
1491
|
+
const order = rawRecords[index];
|
|
1492
|
+
const headerResult = await mapper.map(order);
|
|
1493
|
+
|
|
1494
|
+
if (!headerResult.success || !headerResult.data) {
|
|
1495
|
+
mappingErrors.push({
|
|
1496
|
+
orderRef: order.ref,
|
|
1497
|
+
errors: headerResult.errors || ['Mapping failed'],
|
|
1498
|
+
});
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const header = headerResult.data as Record<string, any>;
|
|
1503
|
+
const lineItems = (order.items || []).map(item => ({
|
|
1504
|
+
line_item_sku: String(item.product?.ref ?? '').trim(),
|
|
1505
|
+
line_item_qty: Number.parseInt(String(item.quantity ?? '0'), 10) || 0,
|
|
1506
|
+
line_item_price: Number.parseFloat(String(item.price ?? '0')) || 0,
|
|
1507
|
+
}));
|
|
1508
|
+
|
|
1509
|
+
transformedOrders.push({
|
|
1510
|
+
order_id: header.order_id,
|
|
1511
|
+
order_date: header.order_date,
|
|
1512
|
+
order_status: header.order_status,
|
|
1513
|
+
customer_name: header.customer_name,
|
|
1514
|
+
customer_email: header.customer_email,
|
|
1515
|
+
ship_to_name: header.ship_to_name,
|
|
1516
|
+
ship_to_address1: header.ship_to_address1,
|
|
1517
|
+
ship_to_city: header.ship_to_city,
|
|
1518
|
+
ship_to_state: header.ship_to_state,
|
|
1519
|
+
ship_to_zip: header.ship_to_zip,
|
|
1520
|
+
ship_to_country: header.ship_to_country,
|
|
1521
|
+
updated_on: header.updated_on || order.updatedOn,
|
|
1522
|
+
line_items: lineItems,
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if (mappingErrors.length > 0) {
|
|
1527
|
+
log.warn('Some orders failed transformation', {
|
|
1528
|
+
jobId,
|
|
1529
|
+
errorCount: mappingErrors.length,
|
|
1530
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
if (transformedOrders.length === 0) {
|
|
1535
|
+
await tracker.markFailed(jobId, {
|
|
1536
|
+
error: 'All records failed mapping',
|
|
1537
|
+
failedCount: mappingErrors.length,
|
|
1538
|
+
});
|
|
1539
|
+
return {
|
|
1540
|
+
success: false,
|
|
1541
|
+
error: 'All records failed mapping',
|
|
1542
|
+
errors: mappingErrors,
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
log.info('Orders transformed', {
|
|
1547
|
+
jobId,
|
|
1548
|
+
transformedCount: transformedOrders.length,
|
|
1549
|
+
skippedRecords: rawRecords.length - transformedOrders.length,
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
await tracker.updateJob(jobId, {
|
|
1553
|
+
stage: 'upload',
|
|
1554
|
+
message: 'Generating XML and uploading to SFTP',
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
const xmlContent = buildOrdersXML(transformedOrders);
|
|
1558
|
+
|
|
1559
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1560
|
+
const fileName = `orders-${timestamp}.xml`;
|
|
1561
|
+
const remotePath = `${sftpSettings.remotePath}${fileName}`;
|
|
1562
|
+
|
|
1563
|
+
log.info('Generated XML file', {
|
|
1564
|
+
fileName,
|
|
1565
|
+
size: xmlContent.length,
|
|
1566
|
+
orderCount: transformedOrders.length,
|
|
1567
|
+
lineItemCount: totalLineItems,
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
await sftp.uploadFile(remotePath, Buffer.from(xmlContent, 'utf8'));
|
|
1571
|
+
|
|
1572
|
+
log.info('XML file uploaded to SFTP', { remotePath });
|
|
1573
|
+
|
|
1574
|
+
await tracker.updateJob(jobId, {
|
|
1575
|
+
stage: 'state_update',
|
|
1576
|
+
message: 'Updating state and completing job',
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
const maxUpdatedOn = transformedOrders.reduce((max, order) => {
|
|
1580
|
+
const orderTime = new Date(order.updated_on).getTime();
|
|
1581
|
+
return orderTime > max ? orderTime : max;
|
|
1582
|
+
}, new Date(rawLastRunTime).getTime());
|
|
1583
|
+
|
|
1584
|
+
const newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
1585
|
+
|
|
1586
|
+
if (updateState) {
|
|
1587
|
+
await kv.set(stateKey, {
|
|
1588
|
+
timestamp: newTimestamp,
|
|
1589
|
+
orderCount: transformedOrders.length,
|
|
1590
|
+
lineItemCount: totalLineItems,
|
|
1591
|
+
extractedAt: new Date().toISOString(),
|
|
1592
|
+
overlapBufferSeconds,
|
|
1593
|
+
fileName,
|
|
1594
|
+
remotePath,
|
|
1595
|
+
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
log.info('State updated with new timestamp (without buffer)', {
|
|
1599
|
+
newTimestamp,
|
|
1600
|
+
overlapBufferSeconds,
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
await tracker.markCompleted(jobId, {
|
|
1605
|
+
recordCount: transformedOrders.length,
|
|
1606
|
+
fileName,
|
|
1607
|
+
sftpPath: remotePath,
|
|
1608
|
+
errorCount: mappingErrors.length,
|
|
1609
|
+
errors: mappingErrors,
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
return {
|
|
1613
|
+
success: true,
|
|
1614
|
+
ordersExtracted: transformedOrders.length,
|
|
1615
|
+
lineItemsExtracted: totalLineItems,
|
|
1616
|
+
fileName,
|
|
1617
|
+
remotePath,
|
|
1618
|
+
lastRunTime: rawLastRunTime,
|
|
1619
|
+
newTimestamp,
|
|
1620
|
+
jobId,
|
|
1621
|
+
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1622
|
+
};
|
|
1623
|
+
} catch (error: any) {
|
|
1624
|
+
log.error('Extraction failed', error, {
|
|
1625
|
+
message: error?.message,
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
const kv = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
1629
|
+
const tracker = new JobTracker(kv, log);
|
|
1630
|
+
|
|
1631
|
+
await tracker.markFailed(jobId, {
|
|
1632
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1633
|
+
|
|
1634
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1635
|
+
|
|
1636
|
+
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
return {
|
|
1640
|
+
success: false,
|
|
1641
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1642
|
+
|
|
1643
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1644
|
+
|
|
1645
|
+
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
1646
|
+
jobId,
|
|
1647
|
+
};
|
|
1648
|
+
} finally {
|
|
1649
|
+
// CRITICAL: Always clean up SFTP connections
|
|
1650
|
+
await sftp.dispose();
|
|
1651
|
+
log.info('SFTP connection disposed');
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
export async function getJobStatus(kv: any, jobId: string, log: any) {
|
|
1656
|
+
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
1657
|
+
return await tracker.getJob(jobId);
|
|
1658
|
+
}
|
|
1659
|
+
```
|
|
1660
|
+
|
|
1661
|
+
### 6. Job ID Generator (src/utils/job-id-generator.ts)
|
|
1662
|
+
|
|
1663
|
+
```typescript
|
|
1664
|
+
/**
|
|
1665
|
+
* Generate unique job ID
|
|
1666
|
+
* Format: {PREFIX}-{ENTITY}-{TIMESTAMP}
|
|
1667
|
+
*/
|
|
1668
|
+
export function generateJobId(prefix: 'SCHED' | 'ADHOC', entity: string): string {
|
|
1669
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1670
|
+
return `${prefix}-${entity}-${timestamp}`;
|
|
1671
|
+
}
|
|
1672
|
+
```
|
|
1673
|
+
|
|
1674
|
+
### 7. Package Configuration (package.json)
|
|
1675
|
+
|
|
1676
|
+
```json
|
|
1677
|
+
{
|
|
1678
|
+
"name": "orders-extraction-to-sftp-xml",
|
|
1679
|
+
"version": "1.0.0",
|
|
1680
|
+
"description": "Versori connector for orders extraction to SFTP XML",
|
|
1681
|
+
"main": "dist/index.js",
|
|
1682
|
+
"type": "module",
|
|
1683
|
+
"scripts": {
|
|
1684
|
+
"build": "tsc",
|
|
1685
|
+
"dev": "tsc --watch",
|
|
1686
|
+
"lint": "eslint src/**/*.ts",
|
|
1687
|
+
"test": "jest"
|
|
1688
|
+
},
|
|
1689
|
+
"dependencies": {
|
|
1690
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
1691
|
+
"@versori/run": "latest"
|
|
1692
|
+
},
|
|
1693
|
+
"devDependencies": {
|
|
1694
|
+
"@types/node": "^20.0.0",
|
|
1695
|
+
"typescript": "^5.0.0"
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
```
|
|
1699
|
+
|
|
1700
|
+
### 8. Deployment Instructions
|
|
1701
|
+
|
|
1702
|
+
```bash
|
|
1703
|
+
# 1. Install dependencies
|
|
1704
|
+
npm install
|
|
1705
|
+
|
|
1706
|
+
# 2. Build the connector
|
|
1707
|
+
npm run build
|
|
1708
|
+
|
|
1709
|
+
# 3. Test locally (optional)
|
|
1710
|
+
npm test
|
|
1711
|
+
|
|
1712
|
+
# 4. Deploy to Versori
|
|
1713
|
+
# - Upload to Versori workspace
|
|
1714
|
+
# - Configure activation variables
|
|
1715
|
+
# - Enable workflows
|
|
1716
|
+
|
|
1717
|
+
# 5. Test workflows
|
|
1718
|
+
# Scheduled: Wait for next cron trigger or manually trigger
|
|
1719
|
+
# Ad-hoc: POST to webhook URL with API key header
|
|
1720
|
+
# Status: Query job status by ID
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
### 9. Testing
|
|
1724
|
+
|
|
1725
|
+
#### Test Scheduled Extraction
|
|
1726
|
+
|
|
1727
|
+
```bash
|
|
1728
|
+
# Trigger manually in Versori UI or wait for cron schedule
|
|
1729
|
+
# Expected: XML file uploaded to SFTP
|
|
1730
|
+
```
|
|
1731
|
+
|
|
1732
|
+
#### Test Ad-hoc Extraction
|
|
1733
|
+
|
|
1734
|
+
```bash
|
|
1735
|
+
curl -X POST https://your-workspace.versori.run/orders-adhoc \
|
|
1736
|
+
-H "Content-Type: application/json" \
|
|
1737
|
+
-d '{
|
|
1738
|
+
"fromDate": "2025-01-01T00:00:00Z",
|
|
1739
|
+
"toDate": "2025-01-22T23:59:59Z",
|
|
1740
|
+
"updateState": false
|
|
1741
|
+
}'
|
|
1742
|
+
```
|
|
1743
|
+
|
|
1744
|
+
#### Test Job Status Query
|
|
1745
|
+
|
|
1746
|
+
```bash
|
|
1747
|
+
curl -X POST https://your-workspace.versori.run/orders-job-status \
|
|
1748
|
+
-H "Content-Type: application/json" \
|
|
1749
|
+
-d '{
|
|
1750
|
+
"jobId": "SCHED-ORDERS-2025-01-22T02-00-00Z"
|
|
1751
|
+
}'
|
|
1752
|
+
```
|
|
1753
|
+
|
|
1754
|
+
## Key Patterns Explained
|
|
1755
|
+
|
|
1756
|
+
### Pattern 1: ExtractionOrchestrator for Auto-Pagination
|
|
1757
|
+
|
|
1758
|
+
```typescript
|
|
1759
|
+
// ✅ CORRECT - Use ExtractionOrchestrator (handles pagination automatically)
|
|
1760
|
+
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
1761
|
+
|
|
1762
|
+
const extractionResult = await orchestrator.extract({
|
|
1763
|
+
query: ORDERS_QUERY,
|
|
1764
|
+
resultPath: 'orders.edges.node',
|
|
1765
|
+
variables: { retailerId, updatedAfter: bufferedLastRunTime },
|
|
1766
|
+
pageSize,
|
|
1767
|
+
maxRecords,
|
|
1768
|
+
validateItem: item => !!(item.ref && item.customer),
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
const rawRecords = extractionResult.data;
|
|
1772
|
+
|
|
1773
|
+
// WRONG - Manual pagination (avoid this pattern)
|
|
1774
|
+
// const result = await client.graphql({
|
|
1775
|
+
// query: ORDERS_QUERY,
|
|
1776
|
+
// variables: { first: pageSize },
|
|
1777
|
+
// pagination: { maxRecords }
|
|
1778
|
+
// });
|
|
1779
|
+
```
|
|
1780
|
+
|
|
1781
|
+
### Pattern 2: JobTracker for Lifecycle Management
|
|
1782
|
+
|
|
1783
|
+
```typescript
|
|
1784
|
+
// ✅ CORRECT - Use JobTracker throughout workflow
|
|
1785
|
+
const tracker = new JobTracker(kv, log);
|
|
1786
|
+
|
|
1787
|
+
// Create job
|
|
1788
|
+
await tracker.createJob(jobId, { triggeredBy, fromDate, toDate });
|
|
1789
|
+
|
|
1790
|
+
// Update progress
|
|
1791
|
+
await tracker.updateJob(jobId, { stage: 'extraction', message: 'Extracting data' });
|
|
1792
|
+
|
|
1793
|
+
// Mark completed
|
|
1794
|
+
await tracker.markCompleted(jobId, { recordCount, fileName });
|
|
1795
|
+
|
|
1796
|
+
// Query status
|
|
1797
|
+
const status = await tracker.getJob(jobId);
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
### Pattern 3: 3-Workflow Pattern
|
|
1801
|
+
|
|
1802
|
+
```typescript
|
|
1803
|
+
// ✅ CORRECT - 3 workflows for different use cases
|
|
1804
|
+
// 1. Scheduled: Automated daily/hourly runs
|
|
1805
|
+
export const scheduledOrdersExtraction = schedule('orders-extract-xml-daily', '0 2 * * *')...
|
|
1806
|
+
|
|
1807
|
+
// 2. Ad-hoc: Manual webhook triggers with date overrides
|
|
1808
|
+
export const adhocOrdersExtraction = webhook('orders-adhoc', { response: { mode: 'sync' } })...
|
|
1809
|
+
|
|
1810
|
+
// 3. Status: Query job status by ID
|
|
1811
|
+
export const ordersJobStatus = webhook('orders-job-status', { response: { mode: 'sync' } })...
|
|
1812
|
+
```
|
|
1813
|
+
|
|
1814
|
+
### Pattern 4: XMLBuilder for Safe XML Generation (CRITICAL)
|
|
1815
|
+
|
|
1816
|
+
Use the SDK's `XMLBuilder` - it handles all XML escaping automatically:
|
|
1817
|
+
|
|
1818
|
+
```typescript
|
|
1819
|
+
import { Buffer } from 'node:buffer';
|
|
1820
|
+
import { XMLBuilder } from '@fluentcommerce/fc-connect-sdk';
|
|
1821
|
+
|
|
1822
|
+
// Initialize XMLBuilder (handles all escaping automatically)
|
|
1823
|
+
const xmlBuilder = new XMLBuilder({
|
|
1824
|
+
rootElement: 'Orders',
|
|
1825
|
+
prettyPrint: true,
|
|
1826
|
+
encoding: 'UTF-8',
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
// ✅ CORRECT: XMLBuilder escapes automatically
|
|
1830
|
+
const orders = [
|
|
1831
|
+
{
|
|
1832
|
+
customer_name: 'Smith & Jones <Corp>', // Contains & and <>
|
|
1833
|
+
customer_email: 'contact@smith&jones.com',
|
|
1834
|
+
notes: 'Special chars: ¢, ©, ®, "quotes"',
|
|
1835
|
+
},
|
|
1836
|
+
];
|
|
1837
|
+
|
|
1838
|
+
const xml = xmlBuilder.build({ Order: orders });
|
|
1839
|
+
// Result: All special characters properly escaped
|
|
1840
|
+
// <customer_name>Smith & Jones <Corp></customer_name>
|
|
1841
|
+
// <customer_email>contact@smith&jones.com</customer_email>
|
|
1842
|
+
// <notes>Special chars: ¢, ©, ®, "quotes"</notes>
|
|
1843
|
+
|
|
1844
|
+
// WRONG: Manual string concatenation (dangerous)
|
|
1845
|
+
// const xml = `<customer_name>${order.customer_name}</customer_name>`;
|
|
1846
|
+
// This would produce INVALID XML: <customer_name>Smith & Jones <Corp></customer_name>
|
|
1847
|
+
```
|
|
1848
|
+
|
|
1849
|
+
**Why XMLBuilder?**
|
|
1850
|
+
|
|
1851
|
+
- ✅ Automatic escaping of &, <, >, ", '
|
|
1852
|
+
- ✅ Handles special characters (¢, ©, ®)
|
|
1853
|
+
- ✅ Prevents XML injection attacks
|
|
1854
|
+
- ✅ Validates structure
|
|
1855
|
+
- ✅ Consistent, maintainable code
|
|
1856
|
+
|
|
1857
|
+
### Pattern 5: SFTP Cleanup (CRITICAL)
|
|
1858
|
+
|
|
1859
|
+
```typescript
|
|
1860
|
+
const sftp = new SftpDataSource(config, log);
|
|
1861
|
+
|
|
1862
|
+
try {
|
|
1863
|
+
await sftp.uploadFile(remotePath, buffer);
|
|
1864
|
+
return { success: true };
|
|
1865
|
+
} finally {
|
|
1866
|
+
// ALWAYS dispose SFTP connection
|
|
1867
|
+
await sftp.dispose();
|
|
1868
|
+
}
|
|
1869
|
+
```
|
|
1870
|
+
|
|
1871
|
+
**Why?** SFTP maintains open connections. Not calling `dispose()` leads to connection exhaustion.
|
|
1872
|
+
|
|
1873
|
+
### Pattern 6: Consistent Field Names Across Formats
|
|
1874
|
+
|
|
1875
|
+
**Same data in CSV, JSON, and XML:**
|
|
1876
|
+
|
|
1877
|
+
- `order_id` (not orderId, not order-id, not OrderID)
|
|
1878
|
+
- `customer_email` (consistent with CSV version)
|
|
1879
|
+
- `ship_to_address1` (matches CSV exactly)
|
|
1880
|
+
|
|
1881
|
+
This allows users to switch formats without changing downstream systems.
|
|
1882
|
+
|
|
1883
|
+
## Common Issues
|
|
1884
|
+
|
|
1885
|
+
**Issue 1: Malformed XML from unescaped characters**
|
|
1886
|
+
|
|
1887
|
+
- Customer name contains `&` or `<`
|
|
1888
|
+
- Solution: Always use XMLBuilder (automatic escaping)
|
|
1889
|
+
|
|
1890
|
+
**Issue 2: 3PL system rejects XML**
|
|
1891
|
+
|
|
1892
|
+
- Missing required fields
|
|
1893
|
+
- Solution: Verify mapping matches 3PL schema requirements
|
|
1894
|
+
|
|
1895
|
+
**Issue 3: File too large for SFTP partner**
|
|
1896
|
+
|
|
1897
|
+
- Partner has 50MB limit, file is 100MB
|
|
1898
|
+
- Solution: Use file splitting (10k orders per file)
|
|
1899
|
+
|
|
1900
|
+
**Issue 4: SFTP connection timeouts**
|
|
1901
|
+
|
|
1902
|
+
- Not calling `dispose()` in finally block
|
|
1903
|
+
- Solution: Always use try/finally pattern
|
|
1904
|
+
|
|
1905
|
+
**Issue 5: Job status not updating**
|
|
1906
|
+
|
|
1907
|
+
- JobTracker not integrated
|
|
1908
|
+
- Solution: Use JobTracker throughout workflow
|
|
1909
|
+
|
|
1910
|
+
## Testing
|
|
1911
|
+
|
|
1912
|
+
### 1. Test XML Structure
|
|
1913
|
+
|
|
1914
|
+
```typescript
|
|
1915
|
+
export const testXmlGeneration = http('test-xml').then(
|
|
1916
|
+
fn('test-xml-gen', async () => {
|
|
1917
|
+
const testOrders = [
|
|
1918
|
+
{
|
|
1919
|
+
order_id: 'TEST-001',
|
|
1920
|
+
order_date: '2025-01-22',
|
|
1921
|
+
order_status: 'CREATED',
|
|
1922
|
+
customer_name: 'Test & Validate <Corp>',
|
|
1923
|
+
customer_email: 'test@example.com',
|
|
1924
|
+
ship_to_name: 'John Smith',
|
|
1925
|
+
ship_to_address1: '123 Main St',
|
|
1926
|
+
ship_to_city: 'New York',
|
|
1927
|
+
ship_to_state: 'NY',
|
|
1928
|
+
ship_to_zip: '10001',
|
|
1929
|
+
ship_to_country: 'US',
|
|
1930
|
+
line_items: [{ line_item_sku: 'SKU-001', line_item_qty: 2, line_item_price: 29.99 }],
|
|
1931
|
+
},
|
|
1932
|
+
];
|
|
1933
|
+
|
|
1934
|
+
const xml = buildOrdersXML(testOrders);
|
|
1935
|
+
|
|
1936
|
+
// Validate XML structure
|
|
1937
|
+
if (!xml.includes('<?xml version="1.0"')) {
|
|
1938
|
+
return { success: false, error: 'Missing XML declaration' };
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
if (!xml.includes('&') || !xml.includes('<')) {
|
|
1942
|
+
return { success: false, error: 'Special characters not escaped' };
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
return { success: true, xml };
|
|
1946
|
+
})
|
|
1947
|
+
);
|
|
1948
|
+
```
|
|
1949
|
+
|
|
1950
|
+
### 2. Test SFTP Upload
|
|
1951
|
+
|
|
1952
|
+
```bash
|
|
1953
|
+
curl https://your-workspace.versori.run/test-sftp-orders-xml
|
|
1954
|
+
```
|
|
1955
|
+
|
|
1956
|
+
### 3. Validate Against 3PL Schema
|
|
1957
|
+
|
|
1958
|
+
- Download partner's XSD schema
|
|
1959
|
+
- Validate generated XML against schema
|
|
1960
|
+
- Fix any missing/incorrect elements
|
|
1961
|
+
|
|
1962
|
+
## Production Checklist
|
|
1963
|
+
|
|
1964
|
+
- [ ] Test SFTP credentials and connection
|
|
1965
|
+
- [ ] Verify SFTP server has write permissions to remotePath
|
|
1966
|
+
- [ ] Set appropriate extraction frequency (hourly for 3PL feeds)
|
|
1967
|
+
- [ ] Configure correct order status filters
|
|
1968
|
+
- [ ] Test XML escaping with special characters (&, <, >, ", ')
|
|
1969
|
+
- [ ] Validate XML against partner's schema (if provided)
|
|
1970
|
+
- [ ] Test `dispose()` is always called (check logs)
|
|
1971
|
+
- [ ] Document XML schema for 3PL integration team
|
|
1972
|
+
- [ ] Set up monitoring for SFTP connection failures
|
|
1973
|
+
- [ ] Test with real order data (addresses with special chars)
|
|
1974
|
+
- [ ] Verify file size limits with SFTP partner
|
|
1975
|
+
- [ ] Configure SFTP server IP whitelisting for Versori
|
|
1976
|
+
- [ ] Test file splitting with large batches (>10k orders)
|
|
1977
|
+
- [ ] Test all 3 workflows (scheduled, ad-hoc, status)
|
|
1978
|
+
- [ ] Verify JobTracker integration and status updates
|
|
1979
|
+
- [ ] Test ExtractionOrchestrator pagination with large datasets
|
|
1980
|
+
|
|
1981
|
+
## Troubleshooting Guide
|
|
1982
|
+
|
|
1983
|
+
**Issue**: "Extraction timeout after 10 minutes"
|
|
1984
|
+
|
|
1985
|
+
- **Cause**: Too many records
|
|
1986
|
+
- **Fix**: Reduce maxRecords, increase frequency
|
|
1987
|
+
|
|
1988
|
+
**Issue**: "Mapping errors for 50% of records"
|
|
1989
|
+
|
|
1990
|
+
- **Cause**: Schema mismatch
|
|
1991
|
+
- **Fix**: Run schema validation, check field names
|
|
1992
|
+
|
|
1993
|
+
**Issue**: "State not updating"
|
|
1994
|
+
|
|
1995
|
+
- **Cause**: KV write failure or intentional retry
|
|
1996
|
+
- **Fix**: Check KV logs, verify state update code
|
|
1997
|
+
|
|
1998
|
+
**Issue**: "First run exceeds limits"
|
|
1999
|
+
|
|
2000
|
+
- **Cause**: No previous timestamp, fetches all
|
|
2001
|
+
- **Fix**: Set fallbackStartDate close to current, apply filters
|
|
2002
|
+
|
|
2003
|
+
**Issue**: "Excessive duplicates"
|
|
2004
|
+
|
|
2005
|
+
- **Cause**: Overlap buffer (expected) or timestamp not saved
|
|
2006
|
+
- **Fix**: Verify newTimestamp saved WITHOUT buffer
|
|
2007
|
+
|
|
2008
|
+
**Issue**: "Job status returns null"
|
|
2009
|
+
|
|
2010
|
+
- **Cause**: Invalid job ID or job expired
|
|
2011
|
+
- **Fix**: Verify job ID format, check KV TTL settings
|
|
2012
|
+
|
|
2013
|
+
## Security Best Practices
|
|
2014
|
+
|
|
2015
|
+
### Credential Management
|
|
2016
|
+
|
|
2017
|
+
**✅ DO**:
|
|
2018
|
+
|
|
2019
|
+
- Store credentials in Versori activation variables
|
|
2020
|
+
- Rotate credentials quarterly
|
|
2021
|
+
- Use least-privilege accounts
|
|
2022
|
+
|
|
2023
|
+
** DON'T**:
|
|
2024
|
+
|
|
2025
|
+
- Never log credentials
|
|
2026
|
+
- Never commit to git
|
|
2027
|
+
- Never share across environments
|
|
2028
|
+
|
|
2029
|
+
### Data Security
|
|
2030
|
+
|
|
2031
|
+
- Enable encryption in transit and at rest
|
|
2032
|
+
- Apply data retention policies
|
|
2033
|
+
- Monitor access logs
|
|
2034
|
+
- Use VPC/private networks for sensitive data
|
|
2035
|
+
|
|
2036
|
+
### Webhook Security
|
|
2037
|
+
|
|
2038
|
+
- Validate API keys for ad-hoc and status workflows
|
|
2039
|
+
- Use HTTPS for all webhook endpoints
|
|
2040
|
+
- Implement rate limiting
|
|
2041
|
+
- Monitor for suspicious activity
|
|
2042
|
+
|
|
2043
|
+
---
|
|
2044
|
+
|
|
2045
|
+
**Pattern**: Enterprise incremental extraction with ExtractionOrchestrator + JobTracker for orders via SFTP (XML format)
|
|
2046
|
+
**❌š ï¸ Versori Sample**: Reference implementation - adapt for your production use case
|
|
2047
|
+
**Key Learning**: Use ExtractionOrchestrator for auto-pagination, JobTracker for lifecycle management, always escape XML and dispose SFTP
|
|
2048
|
+
**Critical**: Apply 60-second overlap buffer to prevent missed records
|
|
2049
|
+
**Buffer Pattern**: Query WITH buffer (`updatedOn >= lastRunTime - 60s`), save WITHOUT buffer (`MAX(updatedOn)`)
|
|
2050
|
+
**Field Consistency**: Same field names as CSV version for easy format switching
|
|
2051
|
+
**SFTP**: Use proper connection cleanup in finally block to prevent connection leaks
|
|
2052
|
+
**XML**: Preserve hierarchical structure (no line item flattening needed like CSV)
|
|
2053
|
+
**3 Workflows**: Scheduled, ad-hoc webhook, job status query
|
|
2054
|
+
|
|
2055
|
+
---
|
|
2056
|
+
|
|
2057
|
+
### Pattern 8: Backward Pagination (Optional - Advanced)
|
|
2058
|
+
|
|
2059
|
+
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
2060
|
+
|
|
2061
|
+
**When to Use**:
|
|
2062
|
+
|
|
2063
|
+
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
2064
|
+
- ✅ Time-bounded reverse traversal for auditing
|
|
2065
|
+
- ✅ Display newest-first in UI/reports
|
|
2066
|
+
- **Don't use for standard incremental sync** - use forward pagination (default)
|
|
2067
|
+
|
|
2068
|
+
**GraphQL Query Requirements**:
|
|
2069
|
+
|
|
2070
|
+
Your query must support backward pagination by including `$last` and `$before`:
|
|
2071
|
+
|
|
2072
|
+
```graphql
|
|
2073
|
+
query GetData(
|
|
2074
|
+
$retailerId: ID!
|
|
2075
|
+
$first: Int # For forward pagination
|
|
2076
|
+
$after: String # For forward pagination
|
|
2077
|
+
$last: Int # For backward pagination
|
|
2078
|
+
$before: String # For backward pagination
|
|
2079
|
+
) {
|
|
2080
|
+
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
2081
|
+
edges {
|
|
2082
|
+
cursor # ✅ REQUIRED
|
|
2083
|
+
node {
|
|
2084
|
+
id
|
|
2085
|
+
createdAt
|
|
2086
|
+
# ... other fields
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
pageInfo {
|
|
2090
|
+
hasNextPage # For forward
|
|
2091
|
+
hasPreviousPage # ✅ REQUIRED for backward
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
```
|
|
2096
|
+
|
|
2097
|
+
**Implementation**:
|
|
2098
|
+
|
|
2099
|
+
```typescript
|
|
2100
|
+
// Backward pagination - newest records first
|
|
2101
|
+
const result = await orchestrator.extract({
|
|
2102
|
+
query: YOUR_QUERY,
|
|
2103
|
+
resultPath: 'data.edges.node',
|
|
2104
|
+
variables: {
|
|
2105
|
+
retailerId,
|
|
2106
|
+
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
2107
|
+
// Don't include last/before - orchestrator injects them
|
|
2108
|
+
},
|
|
2109
|
+
pageSize: 200,
|
|
2110
|
+
direction: 'backward', // ✅ Enable reverse pagination
|
|
2111
|
+
maxRecords: 10000,
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
// Records are returned in reverse chronological order
|
|
2115
|
+
console.log(result.data[0].createdAt); // Newest
|
|
2116
|
+
console.log(result.data[result.data.length - 1].createdAt); // Oldest (within range)
|
|
2117
|
+
```
|
|
2118
|
+
|
|
2119
|
+
**Key Differences from Forward Pagination**:
|
|
2120
|
+
|
|
2121
|
+
| Aspect | Forward (Default) | Backward |
|
|
2122
|
+
| ---------------------- | -------------------------------- | ----------------------- |
|
|
2123
|
+
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
2124
|
+
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
2125
|
+
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
2126
|
+
| **Cursor Source** | Last edge of page | First edge of page |
|
|
2127
|
+
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
2128
|
+
|
|
2129
|
+
**Important Notes**:
|
|
2130
|
+
|
|
2131
|
+
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
2132
|
+
|
|
2133
|
+
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
2134
|
+
|
|
2135
|
+
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
2136
|
+
|
|
2137
|
+
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
2138
|
+
|
|
2139
|
+
**Example: Extract Latest 1000 Orders**
|
|
2140
|
+
|
|
2141
|
+
```typescript
|
|
2142
|
+
const latestOrders = await orchestrator.extract({
|
|
2143
|
+
query: ORDERS_QUERY,
|
|
2144
|
+
resultPath: 'orders.edges.node',
|
|
2145
|
+
variables: {
|
|
2146
|
+
retailerId,
|
|
2147
|
+
statuses: ['BOOKED', 'ALLOCATED'],
|
|
2148
|
+
},
|
|
2149
|
+
direction: 'backward', // Start from newest
|
|
2150
|
+
maxRecords: 1000, // Stop after 1000 records
|
|
2151
|
+
pageSize: 100, // 100 per page = 10 pages
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
// latestOrders.data[0] is the newest order
|
|
2155
|
+
// latestOrders.data[999] is the 1000th newest order
|
|
2156
|
+
```
|
|
2157
|
+
|
|
2158
|
+
**When to Use Forward vs Backward**:
|
|
2159
|
+
|
|
2160
|
+
```typescript
|
|
2161
|
+
// ✅ Forward (default) - For incremental sync
|
|
2162
|
+
const incrementalData = await orchestrator.extract({
|
|
2163
|
+
query: YOUR_QUERY,
|
|
2164
|
+
resultPath: 'data.edges.node',
|
|
2165
|
+
variables: {
|
|
2166
|
+
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
2167
|
+
},
|
|
2168
|
+
// direction defaults to 'forward'
|
|
2169
|
+
// Processes oldest → newest for proper sequencing
|
|
2170
|
+
});
|
|
2171
|
+
|
|
2172
|
+
// ✅ Backward - For "latest N records" use cases
|
|
2173
|
+
const latestData = await orchestrator.extract({
|
|
2174
|
+
query: YOUR_QUERY,
|
|
2175
|
+
resultPath: 'data.edges.node',
|
|
2176
|
+
direction: 'backward',
|
|
2177
|
+
maxRecords: 100, // Just get latest 100
|
|
2178
|
+
// Gets newest → oldest
|
|
2179
|
+
});
|
|
2180
|
+
```
|
|
2181
|
+
|
|
2182
|
+
**Pagination Variables Reference**:
|
|
2183
|
+
|
|
2184
|
+
| Variable | Forward | Backward | Injected By | Notes |
|
|
2185
|
+
| -------- | ------------ | ------------ | ------------ | ------------------------ |
|
|
2186
|
+
| `first` | ✅ Used | Not used | Orchestrator | From `pageSize` |
|
|
2187
|
+
| `after` | ✅ Used | Not used | Orchestrator | From cursor (last edge) |
|
|
2188
|
+
| `last` | Not used | ✅ Used | Orchestrator | From `pageSize` |
|
|
2189
|
+
| `before` | Not used | ✅ Used | Orchestrator | From cursor (first edge) |
|
|
2190
|
+
|
|
2191
|
+
**Common Mistakes to Avoid**:
|
|
2192
|
+
|
|
2193
|
+
```typescript
|
|
2194
|
+
// WRONG - Don't pass pagination variables
|
|
2195
|
+
const result = await orchestrator.extract({
|
|
2196
|
+
variables: {
|
|
2197
|
+
last: 200, // Orchestrator will override this
|
|
2198
|
+
before: cursor, // Orchestrator manages cursor
|
|
2199
|
+
},
|
|
2200
|
+
direction: 'backward',
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
// ✅ CORRECT - Let orchestrator inject pagination
|
|
2204
|
+
const result = await orchestrator.extract({
|
|
2205
|
+
variables: {
|
|
2206
|
+
retailerId, // ✅ Your business variables only
|
|
2207
|
+
},
|
|
2208
|
+
pageSize: 200, // ✅ Orchestrator uses this for last/before
|
|
2209
|
+
direction: 'backward',
|
|
2210
|
+
});
|
|
2211
|
+
```
|
|
2212
|
+
|
|
2213
|
+
#### Optional: Reverse Pagination
|
|
2214
|
+
|
|
2215
|
+
- Forward remains default. For reverse, require $last/$before and pageInfo.hasPreviousPage.
|
|
2216
|
+
- Do not pass last/before in variables; set direction='backward'.
|
|
2217
|
+
|
|
2218
|
+
GraphQL:
|
|
2219
|
+
|
|
2220
|
+
```graphql
|
|
2221
|
+
query GetOrdersBackward($retailerId: ID!, $last: Int!, $before: String) {
|
|
2222
|
+
orders(retailerId: $retailerId, last: $last, before: $before) {
|
|
2223
|
+
edges {
|
|
2224
|
+
cursor
|
|
2225
|
+
node {
|
|
2226
|
+
id
|
|
2227
|
+
ref
|
|
2228
|
+
updatedOn
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
pageInfo {
|
|
2232
|
+
hasPreviousPage
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
```
|
|
2237
|
+
|
|
2238
|
+
SDK:
|
|
2239
|
+
|
|
2240
|
+
```typescript
|
|
2241
|
+
await orchestrator.extract({
|
|
2242
|
+
query: ORDERS_BACKWARD_QUERY,
|
|
2243
|
+
resultPath: 'orders.edges.node',
|
|
2244
|
+
variables: { retailerId },
|
|
2245
|
+
pageSize,
|
|
2246
|
+
direction: 'backward',
|
|
2247
|
+
});
|
|
2248
|
+
```
|
|
2249
|
+
|
|
2250
|
+
---
|
|
2251
|
+
|
|
2252
|
+
## Testing Checklist
|
|
2253
|
+
|
|
2254
|
+
**Before production deployment:**
|
|
2255
|
+
|
|
2256
|
+
### 1. Schema Validation
|
|
2257
|
+
|
|
2258
|
+
- [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
|
|
2259
|
+
- [ ] Run `npx fc-connect validate-schema --mapping ./config/orders.export.xml.json --schema ./fluent-schema.json`
|
|
2260
|
+
- [ ] Run `npx fc-connect analyze-coverage --mapping ./config/orders.export.xml.json --schema ./fluent-schema.json`
|
|
2261
|
+
- [ ] Verify all `source` paths in mapping exist in GraphQL schema
|
|
2262
|
+
- [ ] Verify query structure matches schema (fields, types, filters)
|
|
2263
|
+
|
|
2264
|
+
### 2. Extraction Testing
|
|
2265
|
+
|
|
2266
|
+
- [ ] Test with small dataset first (maxRecords=10)
|
|
2267
|
+
- [ ] Verify ExtractionOrchestrator pagination works correctly
|
|
2268
|
+
- [ ] Test with multiple pages of data (verify cursor handling)
|
|
2269
|
+
- [ ] Verify date range filtering (updatedOn filter)
|
|
2270
|
+
- [ ] Test empty result handling (no records in date range)
|
|
2271
|
+
- [ ] Verify extraction stops at maxRecords limit
|
|
2272
|
+
|
|
2273
|
+
### 3. Mapping Testing
|
|
2274
|
+
|
|
2275
|
+
- [ ] Verify required fields are populated
|
|
2276
|
+
- [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
|
|
2277
|
+
- [ ] Test custom resolvers with edge cases (if any)
|
|
2278
|
+
- [ ] Verify nested field extraction
|
|
2279
|
+
- [ ] Test with null/missing fields
|
|
2280
|
+
- [ ] Verify mapping error collection works
|
|
2281
|
+
|
|
2282
|
+
### 4. XML Generation Testing
|
|
2283
|
+
|
|
2284
|
+
- [ ] Verify XML structure matches expected format
|
|
2285
|
+
- [ ] Test XML validation against XSD schema (if applicable)
|
|
2286
|
+
- [ ] Verify special character escaping in XML
|
|
2287
|
+
- [ ] Test with large datasets (>1000 records)
|
|
2288
|
+
- [ ] Verify UTF-8 encoding
|
|
2289
|
+
- [ ] Test XML namespace handling (if applicable)
|
|
2290
|
+
|
|
2291
|
+
### 5. SFTP Upload Testing
|
|
2292
|
+
|
|
2293
|
+
- [ ] Test SFTP connection and authentication
|
|
2294
|
+
- [ ] Verify file upload to correct path
|
|
2295
|
+
- [ ] Test file naming convention (timestamp format)
|
|
2296
|
+
- [ ] Verify file permissions on SFTP server
|
|
2297
|
+
- [ ] Test upload retry logic (simulate network failure)
|
|
2298
|
+
- [ ] Verify SFTP connection disposal (no connection leaks)
|
|
2299
|
+
|
|
2300
|
+
### 6. State Management Testing
|
|
2301
|
+
|
|
2302
|
+
- [ ] Verify overlap buffer prevents missed records (60-second default)
|
|
2303
|
+
- [ ] Test state recovery after extraction failure
|
|
2304
|
+
- [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
|
|
2305
|
+
- [ ] Test first run with no previous state (uses fallbackStartDate)
|
|
2306
|
+
- [ ] Verify state update only happens on successful upload
|
|
2307
|
+
- [ ] Test manual date override (doesn't update state)
|
|
2308
|
+
|
|
2309
|
+
### 7. Job Tracking Testing
|
|
2310
|
+
|
|
2311
|
+
- [ ] Test job creation with JobTracker
|
|
2312
|
+
- [ ] Verify job status updates at each stage
|
|
2313
|
+
- [ ] Test job completion with metadata
|
|
2314
|
+
- [ ] Test job failure handling
|
|
2315
|
+
- [ ] Query job status via webhook endpoint
|
|
2316
|
+
- [ ] Verify job status persists in KV store
|
|
2317
|
+
|
|
2318
|
+
### 8. Error Handling Testing
|
|
2319
|
+
|
|
2320
|
+
- [ ] Test with invalid GraphQL query
|
|
2321
|
+
- [ ] Test with mapping errors (invalid field paths)
|
|
2322
|
+
- [ ] Test with SFTP connection failures
|
|
2323
|
+
- [ ] Test with authentication failures
|
|
2324
|
+
- [ ] Test with network timeouts
|
|
2325
|
+
- [ ] Verify error logging includes context (jobId, stage, error details)
|
|
2326
|
+
- [ ] Test error threshold logic (if applicable)
|
|
2327
|
+
|
|
2328
|
+
### 9. Staging Environment Testing
|
|
2329
|
+
|
|
2330
|
+
- [ ] Run full extraction in staging environment
|
|
2331
|
+
- [ ] Verify XML file format with downstream system
|
|
2332
|
+
- [ ] Monitor extraction duration and resource usage
|
|
2333
|
+
- [ ] Test with production-like data volumes
|
|
2334
|
+
- [ ] Verify no performance degradation over time
|
|
2335
|
+
|
|
2336
|
+
### 10. Integration Testing
|
|
2337
|
+
|
|
2338
|
+
- [ ] Test scheduled workflow (cron trigger)
|
|
2339
|
+
- [ ] Test ad hoc webhook trigger
|
|
2340
|
+
- [ ] Test job status query webhook
|
|
2341
|
+
- [ ] Verify activation variables are read correctly
|
|
2342
|
+
- [ ] Test with different extraction modes (incremental, date range)
|
|
2343
|
+
- [ ] End-to-end test: trigger → extract → transform → upload → verify file
|
|
2344
|
+
|
|
2345
|
+
---
|
|
2346
|
+
## Monitoring & Alerting
|
|
2347
|
+
|
|
2348
|
+
### Success Response Example
|
|
2349
|
+
|
|
2350
|
+
```json
|
|
2351
|
+
{
|
|
2352
|
+
"success": true,
|
|
2353
|
+
"jobId": "SCHEDULED_ORD_20251102_140000_abc123",
|
|
2354
|
+
"recordsExtracted": 1523,
|
|
2355
|
+
"fileName": "orders-2025-11-02T14-00-00-000Z.xml",
|
|
2356
|
+
"sftpPath": "/outbound/orders/orders-2025-11-02T14-00-00-000Z.xml",
|
|
2357
|
+
"metrics": {
|
|
2358
|
+
"extractionDurationMs": 12543,
|
|
2359
|
+
"totalPages": 8,
|
|
2360
|
+
"pageSize": 200,
|
|
2361
|
+
"mappingErrors": 0,
|
|
2362
|
+
"fileSizeBytes": 524288,
|
|
2363
|
+
"uploadDurationMs": 1234
|
|
2364
|
+
},
|
|
2365
|
+
"timestamps": {
|
|
2366
|
+
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
2367
|
+
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
2368
|
+
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
2369
|
+
},
|
|
2370
|
+
"state": {
|
|
2371
|
+
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
2372
|
+
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
2373
|
+
"stateUpdated": true,
|
|
2374
|
+
"overlapBufferSeconds": 60
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
```
|
|
2378
|
+
|
|
2379
|
+
### Error Response Example
|
|
2380
|
+
|
|
2381
|
+
```json
|
|
2382
|
+
{
|
|
2383
|
+
"success": false,
|
|
2384
|
+
"jobId": "ADHOC_ORD_20251102_140500_xyz789",
|
|
2385
|
+
"error": "SFTP upload failed: Connection timeout",
|
|
2386
|
+
"errorCategory": "NETWORK",
|
|
2387
|
+
"recordsExtracted": 0,
|
|
2388
|
+
"stage": "sftp_upload",
|
|
2389
|
+
"details": {
|
|
2390
|
+
"message": "Failed to upload file after 3 retry attempts",
|
|
2391
|
+
"retryAttempts": 3,
|
|
2392
|
+
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
2393
|
+
},
|
|
2394
|
+
"state": {
|
|
2395
|
+
"stateUpdated": false,
|
|
2396
|
+
"willRetryNextRun": true,
|
|
2397
|
+
"note": "State not advanced - next extraction will retry same time window"
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
```
|
|
2401
|
+
|
|
2402
|
+
### Key Metrics to Track
|
|
2403
|
+
|
|
2404
|
+
```typescript
|
|
2405
|
+
const METRICS = {
|
|
2406
|
+
// Extraction Performance
|
|
2407
|
+
extractionDurationMs: Date.now() - extractionStart,
|
|
2408
|
+
recordCount: records.length,
|
|
2409
|
+
pageCount: extractionResult.stats.totalPages,
|
|
2410
|
+
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
2411
|
+
|
|
2412
|
+
// Transformation Performance
|
|
2413
|
+
transformedCount: transformedRecords.length,
|
|
2414
|
+
failedCount: mappingErrors.length,
|
|
2415
|
+
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
2416
|
+
|
|
2417
|
+
// File Generation
|
|
2418
|
+
fileSizeMB: (xmlContent.length / (1024 * 1024)).toFixed(2),
|
|
2419
|
+
|
|
2420
|
+
// Upload Performance
|
|
2421
|
+
uploadDurationMs: uploadEnd - uploadStart,
|
|
2422
|
+
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
2423
|
+
|
|
2424
|
+
// State Management
|
|
2425
|
+
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
2426
|
+
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
2427
|
+
};
|
|
2428
|
+
|
|
2429
|
+
log.info('Extraction metrics', metrics);
|
|
2430
|
+
```
|
|
2431
|
+
|
|
2432
|
+
### Alert Thresholds
|
|
2433
|
+
|
|
2434
|
+
```typescript
|
|
2435
|
+
const ALERT_THRESHOLDS = {
|
|
2436
|
+
// Duration Alerts
|
|
2437
|
+
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
2438
|
+
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
2439
|
+
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
2440
|
+
|
|
2441
|
+
// Error Rate Alerts
|
|
2442
|
+
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
2443
|
+
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
2444
|
+
|
|
2445
|
+
// Volume Alerts
|
|
2446
|
+
MAX_RECORDS_PER_RUN: 100000,
|
|
2447
|
+
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
2448
|
+
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
2449
|
+
|
|
2450
|
+
// State Alerts
|
|
2451
|
+
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
2452
|
+
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
2453
|
+
};
|
|
2454
|
+
|
|
2455
|
+
// Check thresholds
|
|
2456
|
+
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
2457
|
+
log.warn('Extraction duration exceeded threshold', {
|
|
2458
|
+
duration: metrics.extractionDurationMs,
|
|
2459
|
+
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
2460
|
+
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
```
|
|
2464
|
+
|
|
2465
|
+
### Monitoring Dashboard Queries
|
|
2466
|
+
|
|
2467
|
+
**Versori Platform Logs Query:**
|
|
2468
|
+
|
|
2469
|
+
```
|
|
2470
|
+
# Successful extractions
|
|
2471
|
+
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
2472
|
+
|
|
2473
|
+
# Failed extractions
|
|
2474
|
+
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
2475
|
+
|
|
2476
|
+
# Performance issues
|
|
2477
|
+
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
2478
|
+
|
|
2479
|
+
# High error rates
|
|
2480
|
+
errorRate:>5
|
|
2481
|
+
|
|
2482
|
+
# State management issues
|
|
2483
|
+
stateUpdated:false AND success:true
|
|
2484
|
+
```
|
|
2485
|
+
|
|
2486
|
+
### Common Issues and Solutions
|
|
2487
|
+
|
|
2488
|
+
**Issue**: "Extraction timeout after 10 minutes"
|
|
2489
|
+
|
|
2490
|
+
- **Cause**: Too many records in single extraction
|
|
2491
|
+
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
2492
|
+
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
2493
|
+
|
|
2494
|
+
**Issue**: "Mapping errors for 50% of records"
|
|
2495
|
+
|
|
2496
|
+
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
2497
|
+
- **Fix**: Run schema validation, update mapping config paths
|
|
2498
|
+
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
2499
|
+
|
|
2500
|
+
**Issue**: "SFTP connection timeout"
|
|
2501
|
+
|
|
2502
|
+
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
2503
|
+
- **Fix**: Check SFTP credentials, verify network connectivity
|
|
2504
|
+
- **Prevention**: Implement connection health checks, monitor connection status
|
|
2505
|
+
|
|
2506
|
+
**Issue**: "State not updating after successful extraction"
|
|
2507
|
+
|
|
2508
|
+
- **Cause**: KV write failure or intentional retry logic
|
|
2509
|
+
- **Fix**: Check KV logs, verify state update code executed
|
|
2510
|
+
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
2511
|
+
|
|
2512
|
+
**Issue**: "First run exceeds record limits"
|
|
2513
|
+
|
|
2514
|
+
- **Cause**: No previous timestamp, fetches all historical records
|
|
2515
|
+
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
2516
|
+
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
2517
|
+
|
|
2518
|
+
**Issue**: "Excessive duplicate records in output"
|
|
2519
|
+
|
|
2520
|
+
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
2521
|
+
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
2522
|
+
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
2523
|
+
|
|
2524
|
+
---
|
|
2525
|
+
|
|
2526
|
+
## Troubleshooting Quick Reference
|
|
2527
|
+
|
|
2528
|
+
| Error Message | Likely Cause | Solution |
|
|
2529
|
+
|--------------|--------------|----------|
|
|
2530
|
+
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
2531
|
+
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
2532
|
+
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
2533
|
+
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
2534
|
+
| "SFTP authentication failed" | Invalid credentials | Verify SFTP credentials in activation variables |
|
|
2535
|
+
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
2536
|
+
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
2537
|
+
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
2538
|
+
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
2539
|
+
| "XML generation failed" | Format-specific error | Check XML generation logic, validate output |
|
|
2540
|
+
|
|
2541
|
+
---
|