@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,1379 +1,1379 @@
|
|
|
1
|
-
# Module 3: SFTP Operations
|
|
2
|
-
|
|
3
|
-
Complete guide to SftpDataSource - SSH authentication, connection pooling, file operations, and production patterns.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
- [SSH Architecture](#ssh-04-REFERENCE/architecture)
|
|
8
|
-
- [Authentication Methods](#authentication-methods)
|
|
9
|
-
- [Configuration](#configuration)
|
|
10
|
-
- [File Operations](#file-operations)
|
|
11
|
-
- [Connection Management](#connection-management)
|
|
12
|
-
- [Multi-Format Support](#multi-format-support)
|
|
13
|
-
- [Error Handling](#error-handling)
|
|
14
|
-
- [Production Patterns](#production-patterns)
|
|
15
|
-
- [API Reference](#api-reference)
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## SSH Architecture
|
|
20
|
-
|
|
21
|
-
### How SftpDataSource Works
|
|
22
|
-
|
|
23
|
-
`SftpDataSource` uses **direct SSH connections** via `ssh2-sftp-client` library:
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
27
|
-
‚ SftpDataSource (fc-connect-sdk) ‚
|
|
28
|
-
”€€€€€€€€€€€€€€€€€€¬€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€˜
|
|
29
|
-
‚
|
|
30
|
-
¼
|
|
31
|
-
Œ€€€€€€€€€€€€€€€€€€€€€
|
|
32
|
-
‚ ssh2-sftp-client ‚
|
|
33
|
-
‚ (SFTP protocol) ‚
|
|
34
|
-
”€€€€€€€€¬€€€€€€€€€€€€˜
|
|
35
|
-
‚
|
|
36
|
-
¼
|
|
37
|
-
Establish SSH Connection (port 22)
|
|
38
|
-
Œ€€€€€€€€€€€€€€€€€€€€€€€€
|
|
39
|
-
‚ SSH Handshake ‚
|
|
40
|
-
‚ - Protocol negotiation ‚
|
|
41
|
-
‚ - Key exchange ‚
|
|
42
|
-
‚ - Authentication ‚
|
|
43
|
-
”€€€€€€€€¬€€€€€€€€€€€€€€€˜
|
|
44
|
-
‚
|
|
45
|
-
¼
|
|
46
|
-
Œ€€€€€€€€€€€€€€€€€€€€€€€€
|
|
47
|
-
‚ Connection Pool ‚
|
|
48
|
-
‚ (Reuse connections) ‚
|
|
49
|
-
‚ Max: 5 (default) ‚
|
|
50
|
-
”€€€€€€€€¬€€€€€€€€€€€€€€€˜
|
|
51
|
-
‚
|
|
52
|
-
¼
|
|
53
|
-
Œ€€€€€€€€€€€€€€€€€€€€€€€€
|
|
54
|
-
‚ SFTP Server ‚
|
|
55
|
-
‚ (Remote filesystem) ‚
|
|
56
|
-
”€€€€€€€€€€€€€€€€€€€€€€€€˜
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**Key Benefits**:
|
|
60
|
-
|
|
61
|
-
1. **Connection Pooling** - Reuses SSH connections for efficiency
|
|
62
|
-
2. **Direct SSH** - No intermediary, full encryption
|
|
63
|
-
3. **Universal Protocol** - Works with any SFTP server (RFC 4253)
|
|
64
|
-
4. **Automatic Retry** - Handles connection failures with exponential backoff
|
|
65
|
-
5. **No Platform Dependencies** - Works in Node.js, Deno, Versori
|
|
66
|
-
|
|
67
|
-
### Connection Pool Behavior
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// Connection pool lifecycle:
|
|
71
|
-
// 1. First operation creates connection
|
|
72
|
-
// 2. Connection added to pool (max: 5 default)
|
|
73
|
-
// 3. Subsequent operations reuse existing connections
|
|
74
|
-
// 4. Idle connections auto-close after timeout
|
|
75
|
-
// 5. Failed connections automatically recreated
|
|
76
|
-
|
|
77
|
-
// Example: 10 file operations
|
|
78
|
-
const files = ['file1.csv', 'file2.csv', ...]; // 10 files
|
|
79
|
-
|
|
80
|
-
for (const file of files) {
|
|
81
|
-
// First 5 files: Create new SSH connections (pool: 0 → 5)
|
|
82
|
-
// Remaining files: Reuse pool connections (pool: stable at 5)
|
|
83
|
-
const content = await sftpSource.downloadFile(file);
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Connection Pool Configuration
|
|
88
|
-
|
|
89
|
-
Connection pooling supports configurable pool size and wait timeout.
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
const sftpSource = new SftpDataSource(
|
|
93
|
-
{
|
|
94
|
-
type: 'SFTP_CSV',
|
|
95
|
-
connectionId: 'high-throughput-sftp',
|
|
96
|
-
name: 'High Throughput SFTP',
|
|
97
|
-
settings: {
|
|
98
|
-
host: 'sftp.vendor.com',
|
|
99
|
-
port: 22,
|
|
100
|
-
username: 'integration',
|
|
101
|
-
privateKey: process.env.SFTP_PRIVATE_KEY,
|
|
102
|
-
remotePath: '/data/inventory',
|
|
103
|
-
|
|
104
|
-
// Connection pool settings (NEW)
|
|
105
|
-
maxConnections: 5, // Max concurrent connections (default: 5)
|
|
106
|
-
// Note: Connection wait timeout is fixed at 30000ms (30 seconds)
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
logger
|
|
110
|
-
);
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### Connection Pool Wait Queue
|
|
114
|
-
|
|
115
|
-
When the connection pool reaches maximum capacity, **new requests automatically wait in a queue** instead of failing.
|
|
116
|
-
|
|
117
|
-
**How the Wait Queue Works:**
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
// Scenario: High concurrency (10 simultaneous operations)
|
|
121
|
-
const operations = Array.from({ length: 10 }, (_, i) => `file${i}.csv`);
|
|
122
|
-
|
|
123
|
-
// All 10 operations start simultaneously
|
|
124
|
-
const promises = operations.map(async file => {
|
|
125
|
-
// First 5 requests: Get connections immediately (pool: 0 → 5)
|
|
126
|
-
// Requests 6-10: Wait in queue until a connection is released
|
|
127
|
-
return await sftpSource.downloadFile(file);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
await Promise.all(promises); // All complete successfully!
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
**Queue Behavior:**
|
|
134
|
-
|
|
135
|
-
1. **Pool Full Detection**
|
|
136
|
-
- Pool has 5 active connections (default max)
|
|
137
|
-
- All connections currently in use
|
|
138
|
-
- New request arrives → added to wait queue
|
|
139
|
-
|
|
140
|
-
2. **Automatic Waiting**
|
|
141
|
-
- Request waits up to 30 seconds (default timeout)
|
|
142
|
-
- Uses FIFO (First-In-First-Out) order
|
|
143
|
-
- No busy-waiting - efficient Promise-based waiting
|
|
144
|
-
|
|
145
|
-
3. **Connection Released**
|
|
146
|
-
- Operation completes, connection returned to pool
|
|
147
|
-
- First waiter in queue gets the connection
|
|
148
|
-
- Process repeats until queue empty
|
|
149
|
-
|
|
150
|
-
4. **Timeout Protection**
|
|
151
|
-
- If no connection available after 30 seconds:
|
|
152
|
-
- Throws `DataSourceError` with context:
|
|
153
|
-
- Pool size
|
|
154
|
-
- Queue length
|
|
155
|
-
- Timeout duration
|
|
156
|
-
|
|
157
|
-
**Race Condition Protection:**
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
// Internal tracking prevents race conditions:
|
|
161
|
-
// - pendingConnections: Connections being created but not ready
|
|
162
|
-
// - connectionCount: Active connections in pool
|
|
163
|
-
// - Total = connectionCount + pendingConnections ≤ maxConnections
|
|
164
|
-
|
|
165
|
-
// This prevents:
|
|
166
|
-
// Creating 7 connections when max is 5
|
|
167
|
-
// Two requests creating connections simultaneously
|
|
168
|
-
// ✅ Safe concurrent connection creation
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
**Configuration Options:**
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
// Connection pool configuration:
|
|
175
|
-
// - maxConnections: Configurable (default: 5)
|
|
176
|
-
// - connectionWaitTimeout: Fixed at 30000ms (30 seconds), not configurable
|
|
177
|
-
|
|
178
|
-
const sftpSource = new SftpDataSource(
|
|
179
|
-
{
|
|
180
|
-
type: 'SFTP_CSV',
|
|
181
|
-
connectionId: 'high-concurrency-sftp',
|
|
182
|
-
name: 'High Concurrency SFTP',
|
|
183
|
-
settings: {
|
|
184
|
-
host: 'sftp.vendor.com',
|
|
185
|
-
port: 22,
|
|
186
|
-
username: 'integration_user',
|
|
187
|
-
password: process.env.SFTP_PASSWORD,
|
|
188
|
-
remotePath: '/data/inventory',
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
logger
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
// Pool automatically handles up to 5 concurrent operations
|
|
195
|
-
// Additional operations wait in queue (up to 30s timeout)
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
**Wait Queue Logging:**
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
// Enable debug logging to see queue behavior
|
|
202
|
-
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
203
|
-
logLevel: 'debug',
|
|
204
|
-
});
|
|
205
|
-
const sftpSource = new SftpDataSource(config, logger);
|
|
206
|
-
|
|
207
|
-
// When pool is full, you'll see:
|
|
208
|
-
// "Connection pool full" {
|
|
209
|
-
// poolSize: 5,
|
|
210
|
-
// pendingConnections: 0,
|
|
211
|
-
// maxSize: 5,
|
|
212
|
-
// queueLength: 3 // 3 operations waiting
|
|
213
|
-
// }
|
|
214
|
-
|
|
215
|
-
// When connection is released:
|
|
216
|
-
// "Connection returned to pool" {
|
|
217
|
-
// connectionId: 'vendor-sftp',
|
|
218
|
-
// poolSize: 5,
|
|
219
|
-
// totalConnections: 5,
|
|
220
|
-
// queueLength: 2 // One waiter got connection
|
|
221
|
-
// }
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
**Error Handling:**
|
|
225
|
-
|
|
226
|
-
```typescript
|
|
227
|
-
import { Buffer } from 'node:buffer';
|
|
228
|
-
import { DataSourceError } from '@fluentcommerce/fc-connect-sdk';
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
// If wait exceeds 30 seconds
|
|
232
|
-
await sftpSource.downloadFile('file.csv');
|
|
233
|
-
} catch (error) {
|
|
234
|
-
if (error instanceof DataSourceError) {
|
|
235
|
-
console.error('SFTP Operation Failed:', {
|
|
236
|
-
message: error.message,
|
|
237
|
-
// "Connection pool exhausted - timeout waiting for available connection after 30000ms"
|
|
238
|
-
poolSize: error.context?.poolSize, // 5
|
|
239
|
-
maxSize: error.context?.maxSize, // 5
|
|
240
|
-
queueLength: error.context?.queueLength, // How many were waiting
|
|
241
|
-
timeoutMs: error.context?.timeoutMs, // 30000
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Possible solutions:
|
|
245
|
-
// 1. Reduce concurrency in your application
|
|
246
|
-
// 2. Increase operation timeout if operations are slow
|
|
247
|
-
// 3. Batch operations instead of parallel execution
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
**Production Patterns:**
|
|
253
|
-
|
|
254
|
-
**Pattern 1: Controlled Concurrency**
|
|
255
|
-
|
|
256
|
-
```typescript
|
|
257
|
-
// Use p-limit to control concurrency below pool limit
|
|
258
|
-
import pLimit from 'p-limit';
|
|
259
|
-
|
|
260
|
-
const limit = pLimit(3); // Lower than pool max (5)
|
|
261
|
-
|
|
262
|
-
const files = ['file1.csv', 'file2.csv', ...]; // 20 files
|
|
263
|
-
const promises = files.map(file =>
|
|
264
|
-
limit(() => sftpSource.downloadFile(file))
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
await Promise.all(promises);
|
|
268
|
-
// Ensures max 3 concurrent operations
|
|
269
|
-
// Never hits wait queue
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
**Pattern 2: Batch Processing**
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
// Process in batches to avoid queue exhaustion
|
|
276
|
-
async function processBatch(files: string[], batchSize = 4) {
|
|
277
|
-
for (let i = 0; i < files.length; i += batchSize) {
|
|
278
|
-
const batch = files.slice(i, i + batchSize);
|
|
279
|
-
await Promise.all(batch.map(file => sftpSource.downloadFile(file)));
|
|
280
|
-
console.log(`Batch ${i / batchSize + 1} complete`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
await processBatch(allFiles, 4); // 4 concurrent operations per batch
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
**Pattern 3: Graceful Degradation**
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
// Retry with exponential backoff if pool exhausted
|
|
291
|
-
async function downloadWithFallback(sftpSource: SftpDataSource, file: string, maxRetries = 3) {
|
|
292
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
293
|
-
try {
|
|
294
|
-
return await sftpSource.downloadFile(file);
|
|
295
|
-
} catch (error) {
|
|
296
|
-
if (error instanceof DataSourceError && error.message.includes('Connection pool exhausted')) {
|
|
297
|
-
if (attempt < maxRetries) {
|
|
298
|
-
const delay = Math.pow(2, attempt) * 1000;
|
|
299
|
-
console.log(`Pool exhausted, retry ${attempt} after ${delay}ms`);
|
|
300
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
throw error;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
**When Wait Queue Helps:**
|
|
311
|
-
|
|
312
|
-
✅ **Good Use Cases:**
|
|
313
|
-
|
|
314
|
-
- Burst traffic (10 operations arrive simultaneously)
|
|
315
|
-
- Scheduled jobs with variable timing
|
|
316
|
-
- Webhook-triggered SFTP operations
|
|
317
|
-
- Prevents "connection pool exhausted" errors
|
|
318
|
-
- Smooth handling of temporary overload
|
|
319
|
-
|
|
320
|
-
**Not Suitable For:**
|
|
321
|
-
|
|
322
|
-
- Sustained high concurrency (> 5 operations/second)
|
|
323
|
-
- Long-running operations (> 30 seconds each)
|
|
324
|
-
- Real-time critical operations with <1s SLA
|
|
325
|
-
|
|
326
|
-
**Performance Impact:**
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
// Scenario: 10 concurrent operations
|
|
330
|
-
// Without wait queue: 5 succeed, 5 fail immediately
|
|
331
|
-
// With wait queue: All 10 succeed (some wait)
|
|
332
|
-
|
|
333
|
-
// Example timing:
|
|
334
|
-
// Operations 1-5: Start immediately (0ms wait)
|
|
335
|
-
// Operations 6-10: Wait for connection release
|
|
336
|
-
// - Op 6: Waits ~2s (when Op 1 completes)
|
|
337
|
-
// - Op 7: Waits ~3s (when Op 2 completes)
|
|
338
|
-
// - Op 8: Waits ~4s (when Op 3 completes)
|
|
339
|
-
// - Op 9: Waits ~5s (when Op 4 completes)
|
|
340
|
-
// - Op 10: Waits ~6s (when Op 5 completes)
|
|
341
|
-
|
|
342
|
-
// Total throughput: Same (limited by pool size)
|
|
343
|
-
// Reliability: Much better (no failures)
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
---
|
|
347
|
-
|
|
348
|
-
## Authentication Methods
|
|
349
|
-
|
|
350
|
-
### Versori Platform - Secure Credential Management
|
|
351
|
-
|
|
352
|
-
**✅ BEST PRACTICE: Use Versori Connection for Credential Storage**
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
import { schedule, http } from '@versori/run';
|
|
356
|
-
import { SftpDataSource } from '@fluentcommerce/fc-connect-sdk';
|
|
357
|
-
|
|
358
|
-
export const sftpIngestion = schedule('sftp-sync', '0 * * * *')
|
|
359
|
-
.then(http('process', async (ctx) => {
|
|
360
|
-
const { log, activation } = ctx;
|
|
361
|
-
|
|
362
|
-
// Get SFTP credentials from Versori connection (Basic Auth)
|
|
363
|
-
const { connections } = ctx;
|
|
364
|
-
const conn = connections.versori_ftp_server;
|
|
365
|
-
const rawAccessToken = conn.accessToken;
|
|
366
|
-
const rawBasicAuth = Buffer.from(rawAccessToken, 'base64').toString('utf-8');
|
|
367
|
-
const [username, password] = rawBasicAuth.split(':');
|
|
368
|
-
|
|
369
|
-
// Host and port from activation variables
|
|
370
|
-
const sftpSource = new SftpDataSource({
|
|
371
|
-
type: 'SFTP_CSV',
|
|
372
|
-
connectionId: 'versori-sftp',
|
|
373
|
-
name: 'Versori SFTP',
|
|
374
|
-
settings: {
|
|
375
|
-
host: activation.getVariable('sftpHost'),
|
|
376
|
-
port: parseInt(activation.getVariable('sftpPort') || '22'),
|
|
377
|
-
username, // From Versori connection (secure)
|
|
378
|
-
password, // From Versori connection (secure)
|
|
379
|
-
remotePath: '/data/inventory'
|
|
380
|
-
}
|
|
381
|
-
}, log); // Use native log from context
|
|
382
|
-
|
|
383
|
-
const files = await sftpSource.listFiles();
|
|
384
|
-
// Process files...
|
|
385
|
-
}));
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
**Benefits:**
|
|
389
|
-
- ✅ Credentials stored securely in Versori vault
|
|
390
|
-
- ✅ No sensitive data in activation variables
|
|
391
|
-
- ✅ Connection can be reused across workflows
|
|
392
|
-
- ✅ Easier credential rotation
|
|
393
|
-
- ✅ Audit trail of credential usage
|
|
394
|
-
|
|
395
|
-
**Connection Setup:**
|
|
396
|
-
1. In Versori platform, create connection named `versori_ftp_server`
|
|
397
|
-
2. Set Authentication Type: Basic Auth
|
|
398
|
-
3. Enter SFTP username and password
|
|
399
|
-
4. Reference in code: `ctx.connections.versori_ftp_server`
|
|
400
|
-
|
|
401
|
-
**Credential Access Methods:**
|
|
402
|
-
|
|
403
|
-
The example above shows one method (METHOD 3). Here are all three supported methods:
|
|
404
|
-
|
|
405
|
-
```typescript
|
|
406
|
-
import { Buffer } from 'node:buffer'; // Required for Deno/Versori
|
|
407
|
-
|
|
408
|
-
// METHOD 1: activation.connections (direct access)
|
|
409
|
-
const sftpConn = activation.connections.find(c => c.name === 'versori_ftp_server');
|
|
410
|
-
const { username, password } = sftpConn.credentials[0].credential.data.basicAuth;
|
|
411
|
-
|
|
412
|
-
// METHOD 2: credentials().get() with base64 decoding
|
|
413
|
-
const sftpCred = await ctx.credentials().getAccessToken('SFTP');
|
|
414
|
-
const [username, password] = Buffer.from(sftpCred.accessToken, 'base64')
|
|
415
|
-
.toString('utf-8')
|
|
416
|
-
.split(':');
|
|
417
|
-
|
|
418
|
-
// METHOD 3: ctx.connections with base64 decoding (shown above)
|
|
419
|
-
const conn = ctx.connections.versori_ftp_server;
|
|
420
|
-
const [username, password] = Buffer.from(conn.accessToken, 'base64')
|
|
421
|
-
.toString('utf-8')
|
|
422
|
-
.split(':');
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
**See:** Complete credential access guide at [`sftp-credential-access-security.md`](../data-sources-sftp-credential-access-security.md) for detailed examples, troubleshooting, and security best practices.
|
|
426
|
-
|
|
427
|
-
### Standalone - Password Authentication
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
430
|
-
import {
|
|
431
|
-
SftpDataSource,
|
|
432
|
-
createConsoleLogger,
|
|
433
|
-
toStructuredLogger,
|
|
434
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
435
|
-
|
|
436
|
-
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
437
|
-
logLevel: 'info',
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
const sftpSource = new SftpDataSource(
|
|
441
|
-
{
|
|
442
|
-
type: 'SFTP_CSV',
|
|
443
|
-
connectionId: 'vendor-sftp',
|
|
444
|
-
name: 'Vendor SFTP Connection',
|
|
445
|
-
settings: {
|
|
446
|
-
host: 'sftp.vendor.com',
|
|
447
|
-
port: 22,
|
|
448
|
-
username: 'integration_user',
|
|
449
|
-
password: process.env.SFTP_PASSWORD, // Password auth
|
|
450
|
-
remotePath: '/data/inventory',
|
|
451
|
-
filePattern: '*.csv',
|
|
452
|
-
},
|
|
453
|
-
},
|
|
454
|
-
logger
|
|
455
|
-
);
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
**When to Use Password Auth**:
|
|
459
|
-
|
|
460
|
-
- ✅ Standalone scripts (Node.js/Deno)
|
|
461
|
-
- ✅ Simple setup (no key management)
|
|
462
|
-
- ✅ Vendor provides password credentials
|
|
463
|
-
- Less secure than SSH keys
|
|
464
|
-
- Passwords can be intercepted if environment is compromised
|
|
465
|
-
|
|
466
|
-
### SSH Private Key Authentication
|
|
467
|
-
|
|
468
|
-
```typescript
|
|
469
|
-
const sftpSource = new SftpDataSource(
|
|
470
|
-
{
|
|
471
|
-
type: 'SFTP_CSV',
|
|
472
|
-
connectionId: 'vendor-sftp-secure',
|
|
473
|
-
name: 'Secure Vendor SFTP',
|
|
474
|
-
settings: {
|
|
475
|
-
host: 'sftp.vendor.com',
|
|
476
|
-
port: 22,
|
|
477
|
-
username: 'secure_integration',
|
|
478
|
-
privateKey: process.env.SFTP_PRIVATE_KEY, // PEM format private key
|
|
479
|
-
remotePath: '/data/inventory',
|
|
480
|
-
filePattern: '*.csv',
|
|
481
|
-
},
|
|
482
|
-
},
|
|
483
|
-
logger
|
|
484
|
-
);
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
**Private Key Format (PEM)**:
|
|
488
|
-
|
|
489
|
-
```bash
|
|
490
|
-
# .env file - key content as string (NOT file path)
|
|
491
|
-
SFTP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
|
492
|
-
MIIEpAIBAAKCAQEA1a2b3c4d5e6f7g8h9i0j...
|
|
493
|
-
...
|
|
494
|
-
-----END RSA PRIVATE KEY-----"
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
**When to Use SSH Key Auth**:
|
|
498
|
-
|
|
499
|
-
- ✅ More secure (no password transmission)
|
|
500
|
-
- ✅ Industry best practice for B2B integrations
|
|
501
|
-
- ✅ Key rotation without password changes
|
|
502
|
-
- ✅ Supports non-interactive authentication
|
|
503
|
-
|
|
504
|
-
### Encrypted Private Key (with Passphrase)
|
|
505
|
-
|
|
506
|
-
```typescript
|
|
507
|
-
const sftpSource = new SftpDataSource(
|
|
508
|
-
{
|
|
509
|
-
type: 'SFTP_CSV',
|
|
510
|
-
connectionId: 'vendor-sftp-encrypted',
|
|
511
|
-
name: 'Encrypted Key SFTP',
|
|
512
|
-
settings: {
|
|
513
|
-
host: 'sftp.vendor.com',
|
|
514
|
-
port: 22,
|
|
515
|
-
username: 'secure_integration',
|
|
516
|
-
privateKey: process.env.SFTP_PRIVATE_KEY, // Encrypted PEM key
|
|
517
|
-
passphrase: process.env.SFTP_KEY_PASSPHRASE, // Key passphrase
|
|
518
|
-
remotePath: '/data/inventory',
|
|
519
|
-
filePattern: '*.csv',
|
|
520
|
-
},
|
|
521
|
-
},
|
|
522
|
-
logger
|
|
523
|
-
);
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
**Environment Variables**:
|
|
527
|
-
|
|
528
|
-
```bash
|
|
529
|
-
SFTP_PRIVATE_KEY="-----BEGIN ENCRYPTED PRIVATE KEY-----
|
|
530
|
-
...encrypted key content...
|
|
531
|
-
-----END ENCRYPTED PRIVATE KEY-----"
|
|
532
|
-
SFTP_KEY_PASSPHRASE=your-secure-passphrase
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
---
|
|
536
|
-
|
|
537
|
-
## Configuration
|
|
538
|
-
|
|
539
|
-
### Basic Configuration
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
import { SftpDataSource, SftpDataSourceConfig } from '@fluentcommerce/fc-connect-sdk';
|
|
543
|
-
|
|
544
|
-
const config: SftpDataSourceConfig = {
|
|
545
|
-
type: 'SFTP_CSV', // SFTP_JSON, SFTP_JSONL, SFTP_PARQUET
|
|
546
|
-
connectionId: 'vendor-sftp',
|
|
547
|
-
name: 'Vendor SFTP Connection',
|
|
548
|
-
settings: {
|
|
549
|
-
// Connection
|
|
550
|
-
host: 'sftp.vendor.com',
|
|
551
|
-
port: 22,
|
|
552
|
-
username: 'integration',
|
|
553
|
-
password: process.env.SFTP_PASSWORD, // or privateKey
|
|
554
|
-
|
|
555
|
-
// Paths
|
|
556
|
-
remotePath: '/data/inventory',
|
|
557
|
-
filePattern: '*.csv',
|
|
558
|
-
|
|
559
|
-
// Connection pool
|
|
560
|
-
connectionTimeout: 30000, // 30 seconds
|
|
561
|
-
keepaliveInterval: 5000, // 5 seconds keepalive
|
|
562
|
-
maxConnections: 5, // Pool size
|
|
563
|
-
|
|
564
|
-
// Write settings
|
|
565
|
-
writeCreateDirectories: true,
|
|
566
|
-
writeOverwrite: false,
|
|
567
|
-
writePermissions: '0644',
|
|
568
|
-
},
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
const sftpSource = new SftpDataSource(config, logger);
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
### Configuration Options
|
|
575
|
-
|
|
576
|
-
| Property | Required | Description | Default | Example |
|
|
577
|
-
| --------------------------------- | -------- | ------------------------------------- | --------- | ------------------- |
|
|
578
|
-
| `type` | ✅ | Data source type | - | `'SFTP_CSV'` |
|
|
579
|
-
| `connectionId` | ✅ | Unique connection ID | - | `'vendor-sftp'` |
|
|
580
|
-
| `name` | | Connection display name | - | `'Vendor SFTP'` |
|
|
581
|
-
| `settings.host` | ✅ | SFTP server hostname | - | `'sftp.vendor.com'` |
|
|
582
|
-
| `settings.port` | | SSH port | 22 | `2222` |
|
|
583
|
-
| `settings.username` | ✅ | SSH username | - | `'integration'` |
|
|
584
|
-
| `settings.password` | ✅\* | Password auth | - | `'secret'` |
|
|
585
|
-
| `settings.privateKey` | ✅\* | SSH private key (PEM) | - | `'-----BEGIN...'` |
|
|
586
|
-
| `settings.passphrase` | | Key passphrase (if encrypted) | - | `'key-pass'` |
|
|
587
|
-
| `settings.remotePath` | | Default remote directory | `/` | `'/data/inventory'` |
|
|
588
|
-
| `settings.filePattern` | | File glob pattern | `*` | `'*.csv'` |
|
|
589
|
-
| `settings.maxConnections` | | Connection pool size | 5 | `3` |
|
|
590
|
-
| `settings.connectionTimeout` | | Connection timeout (ms) | 30000 | `60000` |
|
|
591
|
-
| `settings.keepaliveInterval` | | Keepalive interval (ms) | 5000 | `10000` |
|
|
592
|
-
| `settings.retry` | | Retry configuration object | See below | See below |
|
|
593
|
-
| `settings.writeCreateDirectories` | | Auto-create dirs on write | `true` | `false` |
|
|
594
|
-
| `settings.writeOverwrite` | | Overwrite existing files | `false` | `true` |
|
|
595
|
-
| `settings.writePermissions` | | File permissions (octal) | `'0644'` | `'0600'` |
|
|
596
|
-
|
|
597
|
-
\* Either `password` or `privateKey` required
|
|
598
|
-
|
|
599
|
-
### Enhanced Retry Configuration
|
|
600
|
-
|
|
601
|
-
Fine-grained retry control with exponential backoff, jitter, and custom predicates.
|
|
602
|
-
|
|
603
|
-
```typescript
|
|
604
|
-
settings: {
|
|
605
|
-
host: 'sftp.vendor.com',
|
|
606
|
-
username: 'integration',
|
|
607
|
-
privateKey: process.env.SFTP_PRIVATE_KEY,
|
|
608
|
-
|
|
609
|
-
// Enhanced retry configuration (optional)
|
|
610
|
-
retry: {
|
|
611
|
-
maxAttempts: 3, // Max retry attempts (default: 3)
|
|
612
|
-
baseDelayMs: 1000, // Initial delay in ms (default: 1000)
|
|
613
|
-
backoffFactor: 2, // Exponential factor (default: 2)
|
|
614
|
-
maxDelayMs: 8000, // Max delay cap (default: 8000)
|
|
615
|
-
jitter: 'full', // Jitter strategy: 'none' | 'full' (default: 'full')
|
|
616
|
-
reconnectOnFailure: true, // Reconnect between retries (default: true)
|
|
617
|
-
|
|
618
|
-
// Optional: Custom retry predicate
|
|
619
|
-
isRetryable: (error: Error) => {
|
|
620
|
-
// Return true to retry, false to fail immediately
|
|
621
|
-
return error.message.includes('ECONNRESET');
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
**Retry Configuration Options:**
|
|
628
|
-
|
|
629
|
-
| Option | Type | Default | Description |
|
|
630
|
-
| -------------------- | ---------- | -------- | ------------------------------------------------------- |
|
|
631
|
-
| `maxAttempts` | `number` | `3` | Maximum retry attempts (including initial attempt) |
|
|
632
|
-
| `baseDelayMs` | `number` | `1000` | Base delay in milliseconds |
|
|
633
|
-
| `backoffFactor` | `number` | `2` | Exponential backoff multiplier |
|
|
634
|
-
| `maxDelayMs` | `number` | `8000` | Maximum delay cap in milliseconds |
|
|
635
|
-
| `jitter` | `string` | `'full'` | Jitter strategy: `'none'` or `'full'` (randomization) |
|
|
636
|
-
| `reconnectOnFailure` | `boolean` | `true` | Reconnect to SFTP between retry attempts |
|
|
637
|
-
| `isRetryable` | `function` | Built-in | Custom function to determine if error should be retried |
|
|
638
|
-
|
|
639
|
-
**Default Retry Behavior:**
|
|
640
|
-
|
|
641
|
-
The SDK automatically retries these error types:
|
|
642
|
-
|
|
643
|
-
- **Network errors**: `ECONNRESET`, `ETIMEDOUT`, `ENOTFOUND`, `EHOSTUNREACH`, `ECONNREFUSED`, `EPIPE`
|
|
644
|
-
- **Transient messages**: `connection lost`, `timeout`, `socket hang up`, `network error`
|
|
645
|
-
|
|
646
|
-
The SDK **never retries** these errors (require manual intervention):
|
|
647
|
-
|
|
648
|
-
- **Authentication failures**: `permission denied`, `authentication`
|
|
649
|
-
- **File not found**: `no such file`, `file not found`
|
|
650
|
-
- **Configuration errors**: `algorithm`, `cipher`, `key exchange`
|
|
651
|
-
|
|
652
|
-
### Environment Variables Pattern
|
|
653
|
-
|
|
654
|
-
```bash
|
|
655
|
-
# .env file
|
|
656
|
-
SFTP_HOST=sftp.vendor.com
|
|
657
|
-
SFTP_PORT=22
|
|
658
|
-
SFTP_USERNAME=integration_user
|
|
659
|
-
SFTP_PASSWORD=secret-password
|
|
660
|
-
|
|
661
|
-
# Or SSH key (recommended)
|
|
662
|
-
SFTP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
|
663
|
-
...key content...
|
|
664
|
-
-----END RSA PRIVATE KEY-----"
|
|
665
|
-
SFTP_PASSPHRASE=optional-passphrase
|
|
666
|
-
|
|
667
|
-
# Paths
|
|
668
|
-
SFTP_REMOTE_PATH=/data/inventory
|
|
669
|
-
SFTP_FILE_PATTERN=*.csv
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
```typescript
|
|
673
|
-
// Load from environment
|
|
674
|
-
const sftpSource = new SftpDataSource(
|
|
675
|
-
{
|
|
676
|
-
type: 'SFTP_CSV',
|
|
677
|
-
connectionId: 'vendor-sftp',
|
|
678
|
-
settings: {
|
|
679
|
-
host: process.env.SFTP_HOST!,
|
|
680
|
-
port: parseInt(process.env.SFTP_PORT || '22'),
|
|
681
|
-
username: process.env.SFTP_USERNAME!,
|
|
682
|
-
password: process.env.SFTP_PASSWORD, // or
|
|
683
|
-
privateKey: process.env.SFTP_PRIVATE_KEY, // privateKey
|
|
684
|
-
passphrase: process.env.SFTP_PASSPHRASE,
|
|
685
|
-
remotePath: process.env.SFTP_REMOTE_PATH || '/',
|
|
686
|
-
filePattern: process.env.SFTP_FILE_PATTERN || '*',
|
|
687
|
-
maxConnections: parseInt(process.env.SFTP_MAX_CONN || '5'),
|
|
688
|
-
},
|
|
689
|
-
},
|
|
690
|
-
logger
|
|
691
|
-
);
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
---
|
|
695
|
-
|
|
696
|
-
## File Operations
|
|
697
|
-
|
|
698
|
-
### Listing Files
|
|
699
|
-
|
|
700
|
-
```typescript
|
|
701
|
-
// List all files in default remotePath
|
|
702
|
-
const allFiles = await sftpSource.listFiles();
|
|
703
|
-
|
|
704
|
-
// List with filters
|
|
705
|
-
const recentFiles = await sftpSource.listFiles({
|
|
706
|
-
remotePath: '/data/current',
|
|
707
|
-
filePattern: '*.csv',
|
|
708
|
-
lastProcessedTimestamp: '2025-01-01T00:00:00Z', // Only files modified after
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
// List by file name substring
|
|
712
|
-
const inventoryFiles = await sftpSource.listFiles({
|
|
713
|
-
fileName: 'inventory', // Contains 'inventory'
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
// Process file metadata
|
|
717
|
-
for (const file of recentFiles) {
|
|
718
|
-
console.log({
|
|
719
|
-
name: file.name, // 'inventory-2025-01-15.csv'
|
|
720
|
-
path: file.path, // '/data/current/inventory-2025-01-15.csv'
|
|
721
|
-
size: file.size, // 1024000 (bytes)
|
|
722
|
-
lastModified: file.lastModified, // Date object
|
|
723
|
-
type: file.type, // 'file' or 'directory'
|
|
724
|
-
permissions: file.permissions, // '-rw-r--r--'
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
**List Options**:
|
|
730
|
-
|
|
731
|
-
| Option | Type | Description |
|
|
732
|
-
| ------------------------ | -------- | ------------------------------------------- |
|
|
733
|
-
| `remotePath` | `string` | Remote directory to list (overrides config) |
|
|
734
|
-
| `filePattern` | `string` | Glob pattern filter (e.g., `'*.csv'`) |
|
|
735
|
-
| `fileName` | `string` | Substring match filter |
|
|
736
|
-
| `lastProcessedTimestamp` | `string` | ISO timestamp - only files modified after |
|
|
737
|
-
|
|
738
|
-
### Reading Files
|
|
739
|
-
|
|
740
|
-
```typescript
|
|
741
|
-
// Read text file (CSV, JSON, etc.)
|
|
742
|
-
const csvContent = await sftpSource.downloadFile('/data/inventory.csv');
|
|
743
|
-
// Returns: string
|
|
744
|
-
|
|
745
|
-
// Read binary file (Parquet, images, etc.)
|
|
746
|
-
const parquetData = await sftpSource.downloadFile('data.parquet');
|
|
747
|
-
// Returns: Buffer
|
|
748
|
-
|
|
749
|
-
// Read with full path
|
|
750
|
-
const content = await sftpSource.downloadFile('/remote/path/to/file.csv');
|
|
751
|
-
|
|
752
|
-
// Read relative to remotePath
|
|
753
|
-
const content2 = await sftpSource.downloadFile('subdir/file.csv');
|
|
754
|
-
// Resolves to: /data/inventory/subdir/file.csv (if remotePath=/data/inventory)
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
### Writing Files
|
|
758
|
-
|
|
759
|
-
```typescript
|
|
760
|
-
// Upload file content (string)
|
|
761
|
-
await sftpSource.uploadFile('/exports/inventory.csv', csvData, {
|
|
762
|
-
createDirectories: true,
|
|
763
|
-
overwrite: false,
|
|
764
|
-
permissions: '0644',
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
// Upload binary content (Buffer)
|
|
768
|
-
await sftpSource.uploadFile('/exports/data.parquet', parquetBuffer, {
|
|
769
|
-
createDirectories: true,
|
|
770
|
-
permissions: '0600', // Read/write owner only
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
// Upload with auto-create directories
|
|
774
|
-
await sftpSource.uploadFile(
|
|
775
|
-
'/exports/2025/01/15/inventory.csv', // Creates /exports/2025/01/15/ if missing
|
|
776
|
-
csvData,
|
|
777
|
-
{ createDirectories: true }
|
|
778
|
-
);
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
**Write Options**:
|
|
782
|
-
|
|
783
|
-
| Option | Type | Description | Default |
|
|
784
|
-
| ------------------- | -------------------- | -------------------------- | -------- |
|
|
785
|
-
| `createDirectories` | `boolean` | Create missing parent dirs | `true` |
|
|
786
|
-
| `overwrite` | `boolean` | Overwrite existing file | `false` |
|
|
787
|
-
| `permissions` | `string` | File permissions (octal) | `'0644'` |
|
|
788
|
-
| `encoding` | `'utf8' \| 'binary'` | Content encoding | `'utf8'` |
|
|
789
|
-
|
|
790
|
-
### Deleting Files
|
|
791
|
-
|
|
792
|
-
```typescript
|
|
793
|
-
// Delete single file
|
|
794
|
-
await sftpSource.deleteFile('/temp/old-file.csv');
|
|
795
|
-
|
|
796
|
-
// Delete with error handling
|
|
797
|
-
try {
|
|
798
|
-
await sftpSource.deleteFile('/important-file.csv');
|
|
799
|
-
console.log('File deleted successfully');
|
|
800
|
-
} catch (error: any) {
|
|
801
|
-
if (error.message.includes('No such file')) {
|
|
802
|
-
console.log('File already deleted or does not exist');
|
|
803
|
-
} else {
|
|
804
|
-
console.error('Delete failed:', error.message);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
```
|
|
808
|
-
|
|
809
|
-
### Moving Files (Atomic Rename)
|
|
810
|
-
|
|
811
|
-
```typescript
|
|
812
|
-
// Move file (atomic SFTP rename operation)
|
|
813
|
-
await sftpSource.moveFile(
|
|
814
|
-
'/incoming/data.csv',
|
|
815
|
-
'/processed/data.csv',
|
|
816
|
-
false // Don't overwrite if target exists
|
|
817
|
-
);
|
|
818
|
-
|
|
819
|
-
// Move to archive with date-based organization
|
|
820
|
-
const today = new Date().toISOString().split('T')[0];
|
|
821
|
-
await sftpSource.moveFile('/incoming/inventory.csv', `/archive/${today}/inventory.csv`);
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
**IMPORTANT**: Unlike S3's copy+delete, SFTP `moveFile()` uses the **native SFTP rename command**, which is **atomic**. The file is renamed in a single operation with no risk of existing in both locations.
|
|
825
|
-
|
|
826
|
-
### Copying Files
|
|
827
|
-
|
|
828
|
-
```typescript
|
|
829
|
-
// Copy file (download + upload)
|
|
830
|
-
await sftpSource.copyFile(
|
|
831
|
-
'/source/inventory.csv',
|
|
832
|
-
'/backup/inventory_backup.csv',
|
|
833
|
-
true // Overwrite if exists
|
|
834
|
-
);
|
|
835
|
-
|
|
836
|
-
// Copy with different permissions
|
|
837
|
-
await sftpSource.copyFile(
|
|
838
|
-
'/source/data.csv',
|
|
839
|
-
'/archive/data.csv',
|
|
840
|
-
false,
|
|
841
|
-
'0600' // Restrictive permissions for archive
|
|
842
|
-
);
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
**Note**: `copyFile()` is implemented as download + upload (not atomic). For atomic operations, use `moveFile()`.
|
|
846
|
-
|
|
847
|
-
### Directory Operations
|
|
848
|
-
|
|
849
|
-
```typescript
|
|
850
|
-
// Create directory
|
|
851
|
-
await sftpSource.createDirectory('/exports/daily', true, '0755');
|
|
852
|
-
|
|
853
|
-
// Create nested directories
|
|
854
|
-
await sftpSource.createDirectory('/exports/2025/01/15', true);
|
|
855
|
-
// Creates all missing parents: /exports → /exports/2025 → /exports/2025/01 → /exports/2025/01/15
|
|
856
|
-
|
|
857
|
-
// Check if directory exists
|
|
858
|
-
const dirExists = await sftpSource.directoryExists('/data/exports');
|
|
859
|
-
console.log(`Directory exists: ${dirExists}`);
|
|
860
|
-
|
|
861
|
-
// Check if file exists
|
|
862
|
-
const fileExists = await sftpSource.fileExists('/data/inventory.csv');
|
|
863
|
-
console.log(`File exists: ${fileExists}`);
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
---
|
|
867
|
-
|
|
868
|
-
## Connection Management
|
|
869
|
-
|
|
870
|
-
### Connection Pool Information
|
|
871
|
-
|
|
872
|
-
```typescript
|
|
873
|
-
// Get connection details
|
|
874
|
-
const connectionInfo = sftpSource.getConnectionInfo();
|
|
875
|
-
console.log('Connection Info:', {
|
|
876
|
-
type: connectionInfo.type, // 'SFTP_CSV'
|
|
877
|
-
host: connectionInfo.host, // 'sftp.vendor.com'
|
|
878
|
-
port: connectionInfo.port, // 22
|
|
879
|
-
username: connectionInfo.username, // 'integration'
|
|
880
|
-
remotePath: connectionInfo.remotePath, // '/data/inventory'
|
|
881
|
-
filePattern: connectionInfo.filePattern, // '*.csv'
|
|
882
|
-
currentConnections: connectionInfo.connectionPoolSize, // 3
|
|
883
|
-
maxConnections: connectionInfo.maxConnections, // 5
|
|
884
|
-
});
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
### Connection Health
|
|
888
|
-
|
|
889
|
-
The SFTP data source automatically manages connection health:
|
|
890
|
-
|
|
891
|
-
```
|
|
892
|
-
Connection Lifecycle:
|
|
893
|
-
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
894
|
-
‚ 1. Connection Created ‚
|
|
895
|
-
‚ - SSH handshake ‚
|
|
896
|
-
‚ - Authentication ‚
|
|
897
|
-
‚ - Add to pool ‚
|
|
898
|
-
”€€€€€€€€€€¬€€€€€€€€€€€€€€€€€€€€€€˜
|
|
899
|
-
‚
|
|
900
|
-
¼
|
|
901
|
-
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
902
|
-
‚ 2. Connection Active ‚
|
|
903
|
-
‚ - Reused for operations ‚
|
|
904
|
-
‚ - Keepalive pings (5s) ‚
|
|
905
|
-
‚ - Health monitoring ‚
|
|
906
|
-
”€€€€€€€€€€¬€€€€€€€€€€€€€€€€€€€€€€˜
|
|
907
|
-
‚
|
|
908
|
-
¼
|
|
909
|
-
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
910
|
-
‚ 3. Connection Idle ‚
|
|
911
|
-
‚ - No operations for timeout ‚
|
|
912
|
-
‚ - Auto-close after TTL ‚
|
|
913
|
-
‚ - Removed from pool ‚
|
|
914
|
-
”€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€˜
|
|
915
|
-
|
|
916
|
-
Failure Handling:
|
|
917
|
-
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
918
|
-
‚ Operation Fails ‚
|
|
919
|
-
‚ - Network error ‚
|
|
920
|
-
‚ - Connection timeout ‚
|
|
921
|
-
‚ - Authentication failure ‚
|
|
922
|
-
”€€€€€€€€€€¬€€€€€€€€€€€€€€€€€€€€€€˜
|
|
923
|
-
‚
|
|
924
|
-
¼
|
|
925
|
-
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
926
|
-
‚ Automatic Retry ‚
|
|
927
|
-
‚ - Exponential backoff ‚
|
|
928
|
-
‚ - New connection created ‚
|
|
929
|
-
‚ - Operation retried ‚
|
|
930
|
-
”€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€˜
|
|
931
|
-
```
|
|
932
|
-
|
|
933
|
-
### Validating Connection
|
|
934
|
-
|
|
935
|
-
```typescript
|
|
936
|
-
// Test SFTP connectivity
|
|
937
|
-
const isValid = await sftpSource.validateConnection();
|
|
938
|
-
|
|
939
|
-
if (isValid) {
|
|
940
|
-
console.log('SFTP connection successful');
|
|
941
|
-
} else {
|
|
942
|
-
console.error('Failed to connect to SFTP server');
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Use in startup health check
|
|
946
|
-
async function startupHealthCheck() {
|
|
947
|
-
try {
|
|
948
|
-
const valid = await sftpSource.validateConnection();
|
|
949
|
-
if (!valid) {
|
|
950
|
-
throw new Error('SFTP connection validation failed');
|
|
951
|
-
}
|
|
952
|
-
console.log('❌œ“ SFTP connection validated');
|
|
953
|
-
} catch (error) {
|
|
954
|
-
console.error('❌œ— SFTP connection failed:', error);
|
|
955
|
-
process.exit(1);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
```
|
|
959
|
-
|
|
960
|
-
---
|
|
961
|
-
|
|
962
|
-
## Multi-Format Support
|
|
963
|
-
|
|
964
|
-
### CSV Format
|
|
965
|
-
|
|
966
|
-
```typescript
|
|
967
|
-
// CSV-specific configuration
|
|
968
|
-
const csvConfig: SftpDataSourceConfig = {
|
|
969
|
-
type: 'SFTP_CSV',
|
|
970
|
-
connectionId: 'vendor-csv',
|
|
971
|
-
settings: {
|
|
972
|
-
host: 'sftp.vendor.com',
|
|
973
|
-
username: 'integration',
|
|
974
|
-
privateKey: process.env.SFTP_KEY,
|
|
975
|
-
|
|
976
|
-
// CSV parsing options
|
|
977
|
-
csvDelimiter: ',',
|
|
978
|
-
csvHeaders: ['sku', 'quantity', 'location', 'status'],
|
|
979
|
-
requiredCsvHeaders: ['sku', 'quantity'], // Validation
|
|
980
|
-
csvSkipEmptyLines: true,
|
|
981
|
-
csvTrimValues: true,
|
|
982
|
-
csvQuote: '"',
|
|
983
|
-
csvEscape: '\\',
|
|
984
|
-
},
|
|
985
|
-
};
|
|
986
|
-
|
|
987
|
-
// Parse CSV content
|
|
988
|
-
const csvContent = await sftpSource.downloadFile('inventory.csv');
|
|
989
|
-
const records = sftpSource.parseCsvContent(csvContent, fileMetadata);
|
|
990
|
-
// Uses csvDelimiter, csvSkipEmptyLines, etc. from config
|
|
991
|
-
|
|
992
|
-
// Generate CSV content
|
|
993
|
-
const csvOutput = sftpSource.writeCsvContent(records, {
|
|
994
|
-
delimiter: ',',
|
|
995
|
-
headers: ['sku', 'qty', 'location'],
|
|
996
|
-
includeHeader: true,
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
await sftpSource.writeFile('/exports/inventory.csv', csvOutput);
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
### JSON Format
|
|
1003
|
-
|
|
1004
|
-
```typescript
|
|
1005
|
-
// JSON-specific configuration
|
|
1006
|
-
const jsonConfig: SftpDataSourceConfig = {
|
|
1007
|
-
type: 'SFTP_JSON',
|
|
1008
|
-
connectionId: 'vendor-json',
|
|
1009
|
-
settings: {
|
|
1010
|
-
host: 'sftp.vendor.com',
|
|
1011
|
-
username: 'integration',
|
|
1012
|
-
privateKey: process.env.SFTP_KEY,
|
|
1013
|
-
|
|
1014
|
-
// JSON options
|
|
1015
|
-
jsonFormat: 'json', // or 'jsonl'
|
|
1016
|
-
jsonValidate: true,
|
|
1017
|
-
jsonMaxDepth: 10,
|
|
1018
|
-
jsonIndent: 2,
|
|
1019
|
-
jsonSortKeys: true,
|
|
1020
|
-
},
|
|
1021
|
-
};
|
|
1022
|
-
|
|
1023
|
-
// Parse JSON content
|
|
1024
|
-
const jsonContent = await sftpSource.downloadFile('data.json');
|
|
1025
|
-
const records = sftpSource.parseJsonContent(jsonContent, fileMetadata, {
|
|
1026
|
-
format: 'json',
|
|
1027
|
-
validate: true,
|
|
1028
|
-
maxDepth: 10,
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
// Generate JSON content
|
|
1032
|
-
const jsonOutput = sftpSource.writeJsonContent(records, {
|
|
1033
|
-
format: 'json',
|
|
1034
|
-
indent: 2,
|
|
1035
|
-
sortKeys: true,
|
|
1036
|
-
});
|
|
1037
|
-
|
|
1038
|
-
await sftpSource.writeFile('/exports/data.json', jsonOutput);
|
|
1039
|
-
```
|
|
1040
|
-
|
|
1041
|
-
### JSON Lines (JSONL) Format
|
|
1042
|
-
|
|
1043
|
-
```typescript
|
|
1044
|
-
// Parse JSONL (one JSON object per line)
|
|
1045
|
-
const jsonlContent = await sftpSource.downloadFile('stream-data.jsonl');
|
|
1046
|
-
const records = sftpSource.parseJsonContent(jsonlContent, fileMetadata, {
|
|
1047
|
-
format: 'jsonl',
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
// Generate JSONL content
|
|
1051
|
-
const jsonlOutput = sftpSource.writeJsonContent(records, {
|
|
1052
|
-
format: 'jsonl',
|
|
1053
|
-
});
|
|
1054
|
-
// Output:
|
|
1055
|
-
// {"id":1,"name":"Product A"}
|
|
1056
|
-
// {"id":2,"name":"Product B"}
|
|
1057
|
-
|
|
1058
|
-
await sftpSource.writeFile('/exports/stream.jsonl', jsonlOutput);
|
|
1059
|
-
```
|
|
1060
|
-
|
|
1061
|
-
### Parquet Format
|
|
1062
|
-
|
|
1063
|
-
```typescript
|
|
1064
|
-
// Parquet-specific configuration
|
|
1065
|
-
const parquetConfig: SftpDataSourceConfig = {
|
|
1066
|
-
type: 'SFTP_PARQUET',
|
|
1067
|
-
connectionId: 'vendor-parquet',
|
|
1068
|
-
settings: {
|
|
1069
|
-
host: 'sftp.vendor.com',
|
|
1070
|
-
username: 'integration',
|
|
1071
|
-
privateKey: process.env.SFTP_KEY,
|
|
1072
|
-
|
|
1073
|
-
// Parquet options
|
|
1074
|
-
parquetCompression: 'snappy', // 'none', 'gzip', 'snappy', 'lz4'
|
|
1075
|
-
parquetSchema: {
|
|
1076
|
-
productRef: 'string',
|
|
1077
|
-
quantity: 'int32',
|
|
1078
|
-
price: 'double',
|
|
1079
|
-
},
|
|
1080
|
-
parquetRowGroupSize: 1000,
|
|
1081
|
-
},
|
|
1082
|
-
};
|
|
1083
|
-
|
|
1084
|
-
// Parse Parquet content (async)
|
|
1085
|
-
const parquetBuffer = await sftpSource.downloadFile('data.parquet');
|
|
1086
|
-
// Note: parseParquetContent currently throws (not implemented). Use ParquetParserService.
|
|
1087
|
-
// const records = await sftpSource.parseParquetContent(parquetBuffer as Buffer, fileMetadata);
|
|
1088
|
-
|
|
1089
|
-
// Parquet generation would use ParquetParserService separately
|
|
1090
|
-
```
|
|
1091
|
-
|
|
1092
|
-
### Processing Mixed File Types
|
|
1093
|
-
|
|
1094
|
-
```typescript
|
|
1095
|
-
async function processMultipleFormats() {
|
|
1096
|
-
const files = await sftpSource.listFiles({
|
|
1097
|
-
filePattern: '*.{csv,json,parquet}',
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
for (const file of files) {
|
|
1101
|
-
const extension = file.name.split('.').pop()?.toLowerCase();
|
|
1102
|
-
let records: any[] = [];
|
|
1103
|
-
|
|
1104
|
-
switch (extension) {
|
|
1105
|
-
case 'csv':
|
|
1106
|
-
const csvContent = await sftpSource.downloadFile(file.path);
|
|
1107
|
-
records = sftpSource.parseCsvContent(csvContent, file);
|
|
1108
|
-
break;
|
|
1109
|
-
|
|
1110
|
-
case 'json':
|
|
1111
|
-
const jsonContent = await sftpSource.downloadFile(file.path);
|
|
1112
|
-
records = sftpSource.parseJsonContent(jsonContent, file, {
|
|
1113
|
-
format: 'json',
|
|
1114
|
-
});
|
|
1115
|
-
break;
|
|
1116
|
-
|
|
1117
|
-
case 'parquet':
|
|
1118
|
-
const parquetBuffer = await sftpSource.downloadFile(file.path);
|
|
1119
|
-
// Use ParquetParserService here instead of parseParquetContent (not implemented)
|
|
1120
|
-
// records = await sftpParser.parseParquetContent(parquetBuffer as Buffer, file);
|
|
1121
|
-
break;
|
|
1122
|
-
|
|
1123
|
-
default:
|
|
1124
|
-
logger.warn(`Unsupported format: ${extension}`);
|
|
1125
|
-
continue;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
await processRecords(records, file.name);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
```
|
|
1132
|
-
|
|
1133
|
-
---
|
|
1134
|
-
|
|
1135
|
-
## Error Handling
|
|
1136
|
-
|
|
1137
|
-
### Common Error Patterns
|
|
1138
|
-
|
|
1139
|
-
```typescript
|
|
1140
|
-
import { DataSourceError } from '@fluentcommerce/fc-connect-sdk';
|
|
1141
|
-
|
|
1142
|
-
try {
|
|
1143
|
-
await sftpSource.writeFile(remotePath, content);
|
|
1144
|
-
} catch (error) {
|
|
1145
|
-
if (error instanceof DataSourceError) {
|
|
1146
|
-
switch (error.operation) {
|
|
1147
|
-
case 'uploadFile':
|
|
1148
|
-
if (error.message.includes('Permission denied')) {
|
|
1149
|
-
logger.error('SFTP permission error', {
|
|
1150
|
-
remotePath,
|
|
1151
|
-
suggestion: 'Check file permissions and directory access',
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
break;
|
|
1155
|
-
|
|
1156
|
-
case 'listFiles':
|
|
1157
|
-
if (error.message.includes('No such file')) {
|
|
1158
|
-
logger.warn('Remote directory not found', {
|
|
1159
|
-
remotePath: error.context?.remotePath,
|
|
1160
|
-
suggestion: 'Verify remote path exists',
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
break;
|
|
1164
|
-
|
|
1165
|
-
case 'downloadFile':
|
|
1166
|
-
if (error.message.includes('Connection timeout')) {
|
|
1167
|
-
logger.error('SFTP connection timeout', {
|
|
1168
|
-
suggestion: 'Check network connectivity and server status',
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
break;
|
|
1172
|
-
}
|
|
1173
|
-
} else {
|
|
1174
|
-
logger.error('Unexpected error', error);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
```
|
|
1178
|
-
|
|
1179
|
-
### Retry with Exponential Backoff
|
|
1180
|
-
|
|
1181
|
-
```typescript
|
|
1182
|
-
async function uploadWithRetry(
|
|
1183
|
-
content: string,
|
|
1184
|
-
remotePath: string,
|
|
1185
|
-
maxRetries: number = 3
|
|
1186
|
-
): Promise<void> {
|
|
1187
|
-
let lastError: Error;
|
|
1188
|
-
|
|
1189
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1190
|
-
try {
|
|
1191
|
-
await sftpSource.writeFile(remotePath, content);
|
|
1192
|
-
logger.info(`Upload successful on attempt ${attempt}`);
|
|
1193
|
-
return;
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
lastError = error as Error;
|
|
1196
|
-
logger.warn(`Upload attempt ${attempt} failed`, {
|
|
1197
|
-
error: (error as Error).message,
|
|
1198
|
-
retriesLeft: maxRetries - attempt,
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
if (attempt < maxRetries) {
|
|
1202
|
-
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
|
|
1203
|
-
logger.info(`Retrying in ${delay}ms...`);
|
|
1204
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
throw new Error(`Upload failed after ${maxRetries} attempts: ${lastError!.message}`);
|
|
1210
|
-
}
|
|
1211
|
-
```
|
|
1212
|
-
|
|
1213
|
-
---
|
|
1214
|
-
|
|
1215
|
-
## Production Patterns
|
|
1216
|
-
|
|
1217
|
-
### Pattern 1: Vendor Inventory Ingestion
|
|
1218
|
-
|
|
1219
|
-
```typescript
|
|
1220
|
-
import {
|
|
1221
|
-
SftpDataSource,
|
|
1222
|
-
CSVParserService,
|
|
1223
|
-
UniversalMapper,
|
|
1224
|
-
StateService,
|
|
1225
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
1226
|
-
|
|
1227
|
-
async function ingestVendorInventory() {
|
|
1228
|
-
const sftpSource = new SftpDataSource(
|
|
1229
|
-
{
|
|
1230
|
-
/* config */
|
|
1231
|
-
},
|
|
1232
|
-
logger
|
|
1233
|
-
);
|
|
1234
|
-
const csvParser = new CSVParserService();
|
|
1235
|
-
const mapper = new UniversalMapper(mappingConfig);
|
|
1236
|
-
const stateService = new StateService(kvAdapter);
|
|
1237
|
-
|
|
1238
|
-
// List new files
|
|
1239
|
-
const files = await sftpSource.listFiles({
|
|
1240
|
-
remotePath: '/outbound/inventory',
|
|
1241
|
-
filePattern: 'inventory_*.csv',
|
|
1242
|
-
});
|
|
1243
|
-
|
|
1244
|
-
for (const file of files) {
|
|
1245
|
-
// Check if already processed
|
|
1246
|
-
const stateKey = `processed:${file.name}:${file.lastModified?.toString()}`;
|
|
1247
|
-
const processed = await stateService.isFileProcessed(kvAdapter as any, stateKey);
|
|
1248
|
-
|
|
1249
|
-
if (processed) {
|
|
1250
|
-
logger.info('File already processed', { file: file.name });
|
|
1251
|
-
continue;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
try {
|
|
1255
|
-
// Download and parse CSV
|
|
1256
|
-
const content = await sftpSource.downloadFile(file.path);
|
|
1257
|
-
const records = await csvParser.parse(content as string);
|
|
1258
|
-
|
|
1259
|
-
// Map fields
|
|
1260
|
-
const result = await mapper.map(records);
|
|
1261
|
-
if (!result.success) {
|
|
1262
|
-
logger.error('Mapping failed', undefined, { errors: result.errors });
|
|
1263
|
-
continue;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// Send to Fluent
|
|
1267
|
-
await sendToFluent(result.data);
|
|
1268
|
-
|
|
1269
|
-
// Mark as processed
|
|
1270
|
-
await stateService.updateSyncState(
|
|
1271
|
-
kvAdapter as any,
|
|
1272
|
-
[
|
|
1273
|
-
{
|
|
1274
|
-
fileName: file.name,
|
|
1275
|
-
recordCount: records.length,
|
|
1276
|
-
processedAt: new Date().toISOString(),
|
|
1277
|
-
},
|
|
1278
|
-
],
|
|
1279
|
-
'sftp-ingestion'
|
|
1280
|
-
);
|
|
1281
|
-
|
|
1282
|
-
// Move to processed folder
|
|
1283
|
-
await sftpSource.moveFile(file.path, file.path.replace('/outbound/', '/processed/'));
|
|
1284
|
-
|
|
1285
|
-
logger.info('File processed successfully', { file: file.name });
|
|
1286
|
-
} catch (error) {
|
|
1287
|
-
logger.error('Failed to process file', error as Error, { file: file.path });
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
```
|
|
1292
|
-
|
|
1293
|
-
### Pattern 2: State Management with SFTP
|
|
1294
|
-
|
|
1295
|
-
```typescript
|
|
1296
|
-
async function processFilesWithState() {
|
|
1297
|
-
const files = await sftpSource.listFiles();
|
|
1298
|
-
const stateService = new StateService(logger);
|
|
1299
|
-
|
|
1300
|
-
for (const file of files) {
|
|
1301
|
-
// Use file name + modification time as unique key
|
|
1302
|
-
const stateKey = `processed:${file.name}:${file.lastModified.toISOString()}`;
|
|
1303
|
-
|
|
1304
|
-
if (await stateService.isFileProcessed(kvAdapter as any, stateKey)) {
|
|
1305
|
-
logger.info('File already processed', { fileName: file.name });
|
|
1306
|
-
continue;
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
try {
|
|
1310
|
-
const content = await sftpSource.downloadFile(file.path);
|
|
1311
|
-
await processFile(content, file.name);
|
|
1312
|
-
|
|
1313
|
-
// Mark as processed
|
|
1314
|
-
await stateService.updateSyncState(
|
|
1315
|
-
kvAdapter as any,
|
|
1316
|
-
[
|
|
1317
|
-
{
|
|
1318
|
-
fileName: file.name,
|
|
1319
|
-
size: file.size,
|
|
1320
|
-
processedAt: new Date().toISOString(),
|
|
1321
|
-
},
|
|
1322
|
-
],
|
|
1323
|
-
'sftp-processing'
|
|
1324
|
-
);
|
|
1325
|
-
} catch (error) {
|
|
1326
|
-
logger.error('Failed to process file', error as Error, { file: file.path });
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
```
|
|
1331
|
-
|
|
1332
|
-
---
|
|
1333
|
-
|
|
1334
|
-
## API Reference
|
|
1335
|
-
|
|
1336
|
-
### SftpDataSource Constructor
|
|
1337
|
-
|
|
1338
|
-
```typescript
|
|
1339
|
-
new SftpDataSource(config: SftpDataSourceConfig, logger?: StructuredLogger)
|
|
1340
|
-
```
|
|
1341
|
-
|
|
1342
|
-
**Parameters**:
|
|
1343
|
-
|
|
1344
|
-
- `config`: SFTP data source configuration
|
|
1345
|
-
- `logger`: Optional logging service
|
|
1346
|
-
|
|
1347
|
-
### Methods
|
|
1348
|
-
|
|
1349
|
-
| Method | Parameters | Returns | Description |
|
|
1350
|
-
| ----------------------- | -------------------------------------------------- | ------------------------- | ----------------------------- | --------------------- |
|
|
1351
|
-
| `listFiles()` | `options?` | `Promise<FileMetadata[]>` | List files in remote path |
|
|
1352
|
-
| `downloadFile()` | `path` | `Promise<string | Buffer>` | Download file content |
|
|
1353
|
-
| `downloadFile()` | `path` | `Promise<Buffer>` | Download file as buffer |
|
|
1354
|
-
| `writeFile()` | `path, content, options?` | `Promise<void>` | Write file to SFTP server |
|
|
1355
|
-
| `deleteFile()` | `path` | `Promise<void>` | Delete file from server |
|
|
1356
|
-
| `copyFile()` | `sourcePath, targetPath, overwrite?, permissions?` | `Promise<void>` | Copy file (download + upload) |
|
|
1357
|
-
| `moveFile()` | `sourcePath, targetPath, overwrite?` | `Promise<void>` | Move file (atomic rename) |
|
|
1358
|
-
| `fileExists()` | `path` | `Promise<boolean>` | Check if file exists |
|
|
1359
|
-
| `directoryExists()` | `path` | `Promise<boolean>` | Check if directory exists |
|
|
1360
|
-
| `createDirectory()` | `path, recursive?, permissions?` | `Promise<void>` | Create directory |
|
|
1361
|
-
| `validateConnection()` | - | `Promise<boolean>` | Test SFTP connectivity |
|
|
1362
|
-
| `getConnectionInfo()` | - | `ConnectionInfo` | Get connection details |
|
|
1363
|
-
| `parseCsvContent()` | `content, metadata` | `any[]` | Parse CSV content |
|
|
1364
|
-
| `parseJsonContent()` | `content, metadata, options?` | `any[]` | Parse JSON/JSONL content |
|
|
1365
|
-
| `parseParquetContent()` | `buffer, metadata` | `Promise<any[]>` | Not implemented (throws) |
|
|
1366
|
-
| `writeCsvContent()` | `records, options?` | `string` | Generate CSV content |
|
|
1367
|
-
| `writeJsonContent()` | `records, options?` | `string` | Generate JSON/JSONL content |
|
|
1368
|
-
|
|
1369
|
-
---
|
|
1370
|
-
|
|
1371
|
-
## Next Steps
|
|
1372
|
-
|
|
1373
|
-
**Learn file processing patterns** → [Module 4: File Processing Patterns](./data-sources-04-file-patterns.md)
|
|
1374
|
-
|
|
1375
|
-
**Explore advanced topics** → [Module 5: Advanced Topics](./data-sources-05-advanced-topics.md)
|
|
1376
|
-
|
|
1377
|
-
---
|
|
1378
|
-
|
|
1379
|
-
[→ Back to S3 Operations](./data-sources-02-s3-operations.md) | [Next: File Processing Patterns →](./data-sources-04-file-patterns.md) | [→‘ Back to Guide](../data-sources-readme.md)
|
|
1
|
+
# Module 3: SFTP Operations
|
|
2
|
+
|
|
3
|
+
Complete guide to SftpDataSource - SSH authentication, connection pooling, file operations, and production patterns.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [SSH Architecture](#ssh-04-REFERENCE/architecture)
|
|
8
|
+
- [Authentication Methods](#authentication-methods)
|
|
9
|
+
- [Configuration](#configuration)
|
|
10
|
+
- [File Operations](#file-operations)
|
|
11
|
+
- [Connection Management](#connection-management)
|
|
12
|
+
- [Multi-Format Support](#multi-format-support)
|
|
13
|
+
- [Error Handling](#error-handling)
|
|
14
|
+
- [Production Patterns](#production-patterns)
|
|
15
|
+
- [API Reference](#api-reference)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## SSH Architecture
|
|
20
|
+
|
|
21
|
+
### How SftpDataSource Works
|
|
22
|
+
|
|
23
|
+
`SftpDataSource` uses **direct SSH connections** via `ssh2-sftp-client` library:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
27
|
+
‚ SftpDataSource (fc-connect-sdk) ‚
|
|
28
|
+
”€€€€€€€€€€€€€€€€€€¬€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€˜
|
|
29
|
+
‚
|
|
30
|
+
¼
|
|
31
|
+
Œ€€€€€€€€€€€€€€€€€€€€€
|
|
32
|
+
‚ ssh2-sftp-client ‚
|
|
33
|
+
‚ (SFTP protocol) ‚
|
|
34
|
+
”€€€€€€€€¬€€€€€€€€€€€€˜
|
|
35
|
+
‚
|
|
36
|
+
¼
|
|
37
|
+
Establish SSH Connection (port 22)
|
|
38
|
+
Œ€€€€€€€€€€€€€€€€€€€€€€€€
|
|
39
|
+
‚ SSH Handshake ‚
|
|
40
|
+
‚ - Protocol negotiation ‚
|
|
41
|
+
‚ - Key exchange ‚
|
|
42
|
+
‚ - Authentication ‚
|
|
43
|
+
”€€€€€€€€¬€€€€€€€€€€€€€€€˜
|
|
44
|
+
‚
|
|
45
|
+
¼
|
|
46
|
+
Œ€€€€€€€€€€€€€€€€€€€€€€€€
|
|
47
|
+
‚ Connection Pool ‚
|
|
48
|
+
‚ (Reuse connections) ‚
|
|
49
|
+
‚ Max: 5 (default) ‚
|
|
50
|
+
”€€€€€€€€¬€€€€€€€€€€€€€€€˜
|
|
51
|
+
‚
|
|
52
|
+
¼
|
|
53
|
+
Œ€€€€€€€€€€€€€€€€€€€€€€€€
|
|
54
|
+
‚ SFTP Server ‚
|
|
55
|
+
‚ (Remote filesystem) ‚
|
|
56
|
+
”€€€€€€€€€€€€€€€€€€€€€€€€˜
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Key Benefits**:
|
|
60
|
+
|
|
61
|
+
1. **Connection Pooling** - Reuses SSH connections for efficiency
|
|
62
|
+
2. **Direct SSH** - No intermediary, full encryption
|
|
63
|
+
3. **Universal Protocol** - Works with any SFTP server (RFC 4253)
|
|
64
|
+
4. **Automatic Retry** - Handles connection failures with exponential backoff
|
|
65
|
+
5. **No Platform Dependencies** - Works in Node.js, Deno, Versori
|
|
66
|
+
|
|
67
|
+
### Connection Pool Behavior
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Connection pool lifecycle:
|
|
71
|
+
// 1. First operation creates connection
|
|
72
|
+
// 2. Connection added to pool (max: 5 default)
|
|
73
|
+
// 3. Subsequent operations reuse existing connections
|
|
74
|
+
// 4. Idle connections auto-close after timeout
|
|
75
|
+
// 5. Failed connections automatically recreated
|
|
76
|
+
|
|
77
|
+
// Example: 10 file operations
|
|
78
|
+
const files = ['file1.csv', 'file2.csv', ...]; // 10 files
|
|
79
|
+
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
// First 5 files: Create new SSH connections (pool: 0 → 5)
|
|
82
|
+
// Remaining files: Reuse pool connections (pool: stable at 5)
|
|
83
|
+
const content = await sftpSource.downloadFile(file);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Connection Pool Configuration
|
|
88
|
+
|
|
89
|
+
Connection pooling supports configurable pool size and wait timeout.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const sftpSource = new SftpDataSource(
|
|
93
|
+
{
|
|
94
|
+
type: 'SFTP_CSV',
|
|
95
|
+
connectionId: 'high-throughput-sftp',
|
|
96
|
+
name: 'High Throughput SFTP',
|
|
97
|
+
settings: {
|
|
98
|
+
host: 'sftp.vendor.com',
|
|
99
|
+
port: 22,
|
|
100
|
+
username: 'integration',
|
|
101
|
+
privateKey: process.env.SFTP_PRIVATE_KEY,
|
|
102
|
+
remotePath: '/data/inventory',
|
|
103
|
+
|
|
104
|
+
// Connection pool settings (NEW)
|
|
105
|
+
maxConnections: 5, // Max concurrent connections (default: 5)
|
|
106
|
+
// Note: Connection wait timeout is fixed at 30000ms (30 seconds)
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
logger
|
|
110
|
+
);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Connection Pool Wait Queue
|
|
114
|
+
|
|
115
|
+
When the connection pool reaches maximum capacity, **new requests automatically wait in a queue** instead of failing.
|
|
116
|
+
|
|
117
|
+
**How the Wait Queue Works:**
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Scenario: High concurrency (10 simultaneous operations)
|
|
121
|
+
const operations = Array.from({ length: 10 }, (_, i) => `file${i}.csv`);
|
|
122
|
+
|
|
123
|
+
// All 10 operations start simultaneously
|
|
124
|
+
const promises = operations.map(async file => {
|
|
125
|
+
// First 5 requests: Get connections immediately (pool: 0 → 5)
|
|
126
|
+
// Requests 6-10: Wait in queue until a connection is released
|
|
127
|
+
return await sftpSource.downloadFile(file);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await Promise.all(promises); // All complete successfully!
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Queue Behavior:**
|
|
134
|
+
|
|
135
|
+
1. **Pool Full Detection**
|
|
136
|
+
- Pool has 5 active connections (default max)
|
|
137
|
+
- All connections currently in use
|
|
138
|
+
- New request arrives → added to wait queue
|
|
139
|
+
|
|
140
|
+
2. **Automatic Waiting**
|
|
141
|
+
- Request waits up to 30 seconds (default timeout)
|
|
142
|
+
- Uses FIFO (First-In-First-Out) order
|
|
143
|
+
- No busy-waiting - efficient Promise-based waiting
|
|
144
|
+
|
|
145
|
+
3. **Connection Released**
|
|
146
|
+
- Operation completes, connection returned to pool
|
|
147
|
+
- First waiter in queue gets the connection
|
|
148
|
+
- Process repeats until queue empty
|
|
149
|
+
|
|
150
|
+
4. **Timeout Protection**
|
|
151
|
+
- If no connection available after 30 seconds:
|
|
152
|
+
- Throws `DataSourceError` with context:
|
|
153
|
+
- Pool size
|
|
154
|
+
- Queue length
|
|
155
|
+
- Timeout duration
|
|
156
|
+
|
|
157
|
+
**Race Condition Protection:**
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// Internal tracking prevents race conditions:
|
|
161
|
+
// - pendingConnections: Connections being created but not ready
|
|
162
|
+
// - connectionCount: Active connections in pool
|
|
163
|
+
// - Total = connectionCount + pendingConnections ≤ maxConnections
|
|
164
|
+
|
|
165
|
+
// This prevents:
|
|
166
|
+
// Creating 7 connections when max is 5
|
|
167
|
+
// Two requests creating connections simultaneously
|
|
168
|
+
// ✅ Safe concurrent connection creation
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Configuration Options:**
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Connection pool configuration:
|
|
175
|
+
// - maxConnections: Configurable (default: 5)
|
|
176
|
+
// - connectionWaitTimeout: Fixed at 30000ms (30 seconds), not configurable
|
|
177
|
+
|
|
178
|
+
const sftpSource = new SftpDataSource(
|
|
179
|
+
{
|
|
180
|
+
type: 'SFTP_CSV',
|
|
181
|
+
connectionId: 'high-concurrency-sftp',
|
|
182
|
+
name: 'High Concurrency SFTP',
|
|
183
|
+
settings: {
|
|
184
|
+
host: 'sftp.vendor.com',
|
|
185
|
+
port: 22,
|
|
186
|
+
username: 'integration_user',
|
|
187
|
+
password: process.env.SFTP_PASSWORD,
|
|
188
|
+
remotePath: '/data/inventory',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
logger
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Pool automatically handles up to 5 concurrent operations
|
|
195
|
+
// Additional operations wait in queue (up to 30s timeout)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Wait Queue Logging:**
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// Enable debug logging to see queue behavior
|
|
202
|
+
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
203
|
+
logLevel: 'debug',
|
|
204
|
+
});
|
|
205
|
+
const sftpSource = new SftpDataSource(config, logger);
|
|
206
|
+
|
|
207
|
+
// When pool is full, you'll see:
|
|
208
|
+
// "Connection pool full" {
|
|
209
|
+
// poolSize: 5,
|
|
210
|
+
// pendingConnections: 0,
|
|
211
|
+
// maxSize: 5,
|
|
212
|
+
// queueLength: 3 // 3 operations waiting
|
|
213
|
+
// }
|
|
214
|
+
|
|
215
|
+
// When connection is released:
|
|
216
|
+
// "Connection returned to pool" {
|
|
217
|
+
// connectionId: 'vendor-sftp',
|
|
218
|
+
// poolSize: 5,
|
|
219
|
+
// totalConnections: 5,
|
|
220
|
+
// queueLength: 2 // One waiter got connection
|
|
221
|
+
// }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Error Handling:**
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { Buffer } from 'node:buffer';
|
|
228
|
+
import { DataSourceError } from '@fluentcommerce/fc-connect-sdk';
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
// If wait exceeds 30 seconds
|
|
232
|
+
await sftpSource.downloadFile('file.csv');
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error instanceof DataSourceError) {
|
|
235
|
+
console.error('SFTP Operation Failed:', {
|
|
236
|
+
message: error.message,
|
|
237
|
+
// "Connection pool exhausted - timeout waiting for available connection after 30000ms"
|
|
238
|
+
poolSize: error.context?.poolSize, // 5
|
|
239
|
+
maxSize: error.context?.maxSize, // 5
|
|
240
|
+
queueLength: error.context?.queueLength, // How many were waiting
|
|
241
|
+
timeoutMs: error.context?.timeoutMs, // 30000
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Possible solutions:
|
|
245
|
+
// 1. Reduce concurrency in your application
|
|
246
|
+
// 2. Increase operation timeout if operations are slow
|
|
247
|
+
// 3. Batch operations instead of parallel execution
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Production Patterns:**
|
|
253
|
+
|
|
254
|
+
**Pattern 1: Controlled Concurrency**
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Use p-limit to control concurrency below pool limit
|
|
258
|
+
import pLimit from 'p-limit';
|
|
259
|
+
|
|
260
|
+
const limit = pLimit(3); // Lower than pool max (5)
|
|
261
|
+
|
|
262
|
+
const files = ['file1.csv', 'file2.csv', ...]; // 20 files
|
|
263
|
+
const promises = files.map(file =>
|
|
264
|
+
limit(() => sftpSource.downloadFile(file))
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await Promise.all(promises);
|
|
268
|
+
// Ensures max 3 concurrent operations
|
|
269
|
+
// Never hits wait queue
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Pattern 2: Batch Processing**
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// Process in batches to avoid queue exhaustion
|
|
276
|
+
async function processBatch(files: string[], batchSize = 4) {
|
|
277
|
+
for (let i = 0; i < files.length; i += batchSize) {
|
|
278
|
+
const batch = files.slice(i, i + batchSize);
|
|
279
|
+
await Promise.all(batch.map(file => sftpSource.downloadFile(file)));
|
|
280
|
+
console.log(`Batch ${i / batchSize + 1} complete`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await processBatch(allFiles, 4); // 4 concurrent operations per batch
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Pattern 3: Graceful Degradation**
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// Retry with exponential backoff if pool exhausted
|
|
291
|
+
async function downloadWithFallback(sftpSource: SftpDataSource, file: string, maxRetries = 3) {
|
|
292
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
293
|
+
try {
|
|
294
|
+
return await sftpSource.downloadFile(file);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error instanceof DataSourceError && error.message.includes('Connection pool exhausted')) {
|
|
297
|
+
if (attempt < maxRetries) {
|
|
298
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
299
|
+
console.log(`Pool exhausted, retry ${attempt} after ${delay}ms`);
|
|
300
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**When Wait Queue Helps:**
|
|
311
|
+
|
|
312
|
+
✅ **Good Use Cases:**
|
|
313
|
+
|
|
314
|
+
- Burst traffic (10 operations arrive simultaneously)
|
|
315
|
+
- Scheduled jobs with variable timing
|
|
316
|
+
- Webhook-triggered SFTP operations
|
|
317
|
+
- Prevents "connection pool exhausted" errors
|
|
318
|
+
- Smooth handling of temporary overload
|
|
319
|
+
|
|
320
|
+
**Not Suitable For:**
|
|
321
|
+
|
|
322
|
+
- Sustained high concurrency (> 5 operations/second)
|
|
323
|
+
- Long-running operations (> 30 seconds each)
|
|
324
|
+
- Real-time critical operations with <1s SLA
|
|
325
|
+
|
|
326
|
+
**Performance Impact:**
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// Scenario: 10 concurrent operations
|
|
330
|
+
// Without wait queue: 5 succeed, 5 fail immediately
|
|
331
|
+
// With wait queue: All 10 succeed (some wait)
|
|
332
|
+
|
|
333
|
+
// Example timing:
|
|
334
|
+
// Operations 1-5: Start immediately (0ms wait)
|
|
335
|
+
// Operations 6-10: Wait for connection release
|
|
336
|
+
// - Op 6: Waits ~2s (when Op 1 completes)
|
|
337
|
+
// - Op 7: Waits ~3s (when Op 2 completes)
|
|
338
|
+
// - Op 8: Waits ~4s (when Op 3 completes)
|
|
339
|
+
// - Op 9: Waits ~5s (when Op 4 completes)
|
|
340
|
+
// - Op 10: Waits ~6s (when Op 5 completes)
|
|
341
|
+
|
|
342
|
+
// Total throughput: Same (limited by pool size)
|
|
343
|
+
// Reliability: Much better (no failures)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Authentication Methods
|
|
349
|
+
|
|
350
|
+
### Versori Platform - Secure Credential Management
|
|
351
|
+
|
|
352
|
+
**✅ BEST PRACTICE: Use Versori Connection for Credential Storage**
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import { schedule, http } from '@versori/run';
|
|
356
|
+
import { SftpDataSource } from '@fluentcommerce/fc-connect-sdk';
|
|
357
|
+
|
|
358
|
+
export const sftpIngestion = schedule('sftp-sync', '0 * * * *')
|
|
359
|
+
.then(http('process', async (ctx) => {
|
|
360
|
+
const { log, activation } = ctx;
|
|
361
|
+
|
|
362
|
+
// Get SFTP credentials from Versori connection (Basic Auth)
|
|
363
|
+
const { connections } = ctx;
|
|
364
|
+
const conn = connections.versori_ftp_server;
|
|
365
|
+
const rawAccessToken = conn.accessToken;
|
|
366
|
+
const rawBasicAuth = Buffer.from(rawAccessToken, 'base64').toString('utf-8');
|
|
367
|
+
const [username, password] = rawBasicAuth.split(':');
|
|
368
|
+
|
|
369
|
+
// Host and port from activation variables
|
|
370
|
+
const sftpSource = new SftpDataSource({
|
|
371
|
+
type: 'SFTP_CSV',
|
|
372
|
+
connectionId: 'versori-sftp',
|
|
373
|
+
name: 'Versori SFTP',
|
|
374
|
+
settings: {
|
|
375
|
+
host: activation.getVariable('sftpHost'),
|
|
376
|
+
port: parseInt(activation.getVariable('sftpPort') || '22'),
|
|
377
|
+
username, // From Versori connection (secure)
|
|
378
|
+
password, // From Versori connection (secure)
|
|
379
|
+
remotePath: '/data/inventory'
|
|
380
|
+
}
|
|
381
|
+
}, log); // Use native log from context
|
|
382
|
+
|
|
383
|
+
const files = await sftpSource.listFiles();
|
|
384
|
+
// Process files...
|
|
385
|
+
}));
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Benefits:**
|
|
389
|
+
- ✅ Credentials stored securely in Versori vault
|
|
390
|
+
- ✅ No sensitive data in activation variables
|
|
391
|
+
- ✅ Connection can be reused across workflows
|
|
392
|
+
- ✅ Easier credential rotation
|
|
393
|
+
- ✅ Audit trail of credential usage
|
|
394
|
+
|
|
395
|
+
**Connection Setup:**
|
|
396
|
+
1. In Versori platform, create connection named `versori_ftp_server`
|
|
397
|
+
2. Set Authentication Type: Basic Auth
|
|
398
|
+
3. Enter SFTP username and password
|
|
399
|
+
4. Reference in code: `ctx.connections.versori_ftp_server`
|
|
400
|
+
|
|
401
|
+
**Credential Access Methods:**
|
|
402
|
+
|
|
403
|
+
The example above shows one method (METHOD 3). Here are all three supported methods:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
import { Buffer } from 'node:buffer'; // Required for Deno/Versori
|
|
407
|
+
|
|
408
|
+
// METHOD 1: activation.connections (direct access)
|
|
409
|
+
const sftpConn = activation.connections.find(c => c.name === 'versori_ftp_server');
|
|
410
|
+
const { username, password } = sftpConn.credentials[0].credential.data.basicAuth;
|
|
411
|
+
|
|
412
|
+
// METHOD 2: credentials().get() with base64 decoding
|
|
413
|
+
const sftpCred = await ctx.credentials().getAccessToken('SFTP');
|
|
414
|
+
const [username, password] = Buffer.from(sftpCred.accessToken, 'base64')
|
|
415
|
+
.toString('utf-8')
|
|
416
|
+
.split(':');
|
|
417
|
+
|
|
418
|
+
// METHOD 3: ctx.connections with base64 decoding (shown above)
|
|
419
|
+
const conn = ctx.connections.versori_ftp_server;
|
|
420
|
+
const [username, password] = Buffer.from(conn.accessToken, 'base64')
|
|
421
|
+
.toString('utf-8')
|
|
422
|
+
.split(':');
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**See:** Complete credential access guide at [`sftp-credential-access-security.md`](../data-sources-sftp-credential-access-security.md) for detailed examples, troubleshooting, and security best practices.
|
|
426
|
+
|
|
427
|
+
### Standalone - Password Authentication
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
import {
|
|
431
|
+
SftpDataSource,
|
|
432
|
+
createConsoleLogger,
|
|
433
|
+
toStructuredLogger,
|
|
434
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
435
|
+
|
|
436
|
+
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
437
|
+
logLevel: 'info',
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const sftpSource = new SftpDataSource(
|
|
441
|
+
{
|
|
442
|
+
type: 'SFTP_CSV',
|
|
443
|
+
connectionId: 'vendor-sftp',
|
|
444
|
+
name: 'Vendor SFTP Connection',
|
|
445
|
+
settings: {
|
|
446
|
+
host: 'sftp.vendor.com',
|
|
447
|
+
port: 22,
|
|
448
|
+
username: 'integration_user',
|
|
449
|
+
password: process.env.SFTP_PASSWORD, // Password auth
|
|
450
|
+
remotePath: '/data/inventory',
|
|
451
|
+
filePattern: '*.csv',
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
logger
|
|
455
|
+
);
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**When to Use Password Auth**:
|
|
459
|
+
|
|
460
|
+
- ✅ Standalone scripts (Node.js/Deno)
|
|
461
|
+
- ✅ Simple setup (no key management)
|
|
462
|
+
- ✅ Vendor provides password credentials
|
|
463
|
+
- Less secure than SSH keys
|
|
464
|
+
- Passwords can be intercepted if environment is compromised
|
|
465
|
+
|
|
466
|
+
### SSH Private Key Authentication
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
const sftpSource = new SftpDataSource(
|
|
470
|
+
{
|
|
471
|
+
type: 'SFTP_CSV',
|
|
472
|
+
connectionId: 'vendor-sftp-secure',
|
|
473
|
+
name: 'Secure Vendor SFTP',
|
|
474
|
+
settings: {
|
|
475
|
+
host: 'sftp.vendor.com',
|
|
476
|
+
port: 22,
|
|
477
|
+
username: 'secure_integration',
|
|
478
|
+
privateKey: process.env.SFTP_PRIVATE_KEY, // PEM format private key
|
|
479
|
+
remotePath: '/data/inventory',
|
|
480
|
+
filePattern: '*.csv',
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
logger
|
|
484
|
+
);
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Private Key Format (PEM)**:
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
# .env file - key content as string (NOT file path)
|
|
491
|
+
SFTP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
|
492
|
+
MIIEpAIBAAKCAQEA1a2b3c4d5e6f7g8h9i0j...
|
|
493
|
+
...
|
|
494
|
+
-----END RSA PRIVATE KEY-----"
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**When to Use SSH Key Auth**:
|
|
498
|
+
|
|
499
|
+
- ✅ More secure (no password transmission)
|
|
500
|
+
- ✅ Industry best practice for B2B integrations
|
|
501
|
+
- ✅ Key rotation without password changes
|
|
502
|
+
- ✅ Supports non-interactive authentication
|
|
503
|
+
|
|
504
|
+
### Encrypted Private Key (with Passphrase)
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
const sftpSource = new SftpDataSource(
|
|
508
|
+
{
|
|
509
|
+
type: 'SFTP_CSV',
|
|
510
|
+
connectionId: 'vendor-sftp-encrypted',
|
|
511
|
+
name: 'Encrypted Key SFTP',
|
|
512
|
+
settings: {
|
|
513
|
+
host: 'sftp.vendor.com',
|
|
514
|
+
port: 22,
|
|
515
|
+
username: 'secure_integration',
|
|
516
|
+
privateKey: process.env.SFTP_PRIVATE_KEY, // Encrypted PEM key
|
|
517
|
+
passphrase: process.env.SFTP_KEY_PASSPHRASE, // Key passphrase
|
|
518
|
+
remotePath: '/data/inventory',
|
|
519
|
+
filePattern: '*.csv',
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
logger
|
|
523
|
+
);
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**Environment Variables**:
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
SFTP_PRIVATE_KEY="-----BEGIN ENCRYPTED PRIVATE KEY-----
|
|
530
|
+
...encrypted key content...
|
|
531
|
+
-----END ENCRYPTED PRIVATE KEY-----"
|
|
532
|
+
SFTP_KEY_PASSPHRASE=your-secure-passphrase
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Configuration
|
|
538
|
+
|
|
539
|
+
### Basic Configuration
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
import { SftpDataSource, SftpDataSourceConfig } from '@fluentcommerce/fc-connect-sdk';
|
|
543
|
+
|
|
544
|
+
const config: SftpDataSourceConfig = {
|
|
545
|
+
type: 'SFTP_CSV', // SFTP_JSON, SFTP_JSONL, SFTP_PARQUET
|
|
546
|
+
connectionId: 'vendor-sftp',
|
|
547
|
+
name: 'Vendor SFTP Connection',
|
|
548
|
+
settings: {
|
|
549
|
+
// Connection
|
|
550
|
+
host: 'sftp.vendor.com',
|
|
551
|
+
port: 22,
|
|
552
|
+
username: 'integration',
|
|
553
|
+
password: process.env.SFTP_PASSWORD, // or privateKey
|
|
554
|
+
|
|
555
|
+
// Paths
|
|
556
|
+
remotePath: '/data/inventory',
|
|
557
|
+
filePattern: '*.csv',
|
|
558
|
+
|
|
559
|
+
// Connection pool
|
|
560
|
+
connectionTimeout: 30000, // 30 seconds
|
|
561
|
+
keepaliveInterval: 5000, // 5 seconds keepalive
|
|
562
|
+
maxConnections: 5, // Pool size
|
|
563
|
+
|
|
564
|
+
// Write settings
|
|
565
|
+
writeCreateDirectories: true,
|
|
566
|
+
writeOverwrite: false,
|
|
567
|
+
writePermissions: '0644',
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const sftpSource = new SftpDataSource(config, logger);
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Configuration Options
|
|
575
|
+
|
|
576
|
+
| Property | Required | Description | Default | Example |
|
|
577
|
+
| --------------------------------- | -------- | ------------------------------------- | --------- | ------------------- |
|
|
578
|
+
| `type` | ✅ | Data source type | - | `'SFTP_CSV'` |
|
|
579
|
+
| `connectionId` | ✅ | Unique connection ID | - | `'vendor-sftp'` |
|
|
580
|
+
| `name` | | Connection display name | - | `'Vendor SFTP'` |
|
|
581
|
+
| `settings.host` | ✅ | SFTP server hostname | - | `'sftp.vendor.com'` |
|
|
582
|
+
| `settings.port` | | SSH port | 22 | `2222` |
|
|
583
|
+
| `settings.username` | ✅ | SSH username | - | `'integration'` |
|
|
584
|
+
| `settings.password` | ✅\* | Password auth | - | `'secret'` |
|
|
585
|
+
| `settings.privateKey` | ✅\* | SSH private key (PEM) | - | `'-----BEGIN...'` |
|
|
586
|
+
| `settings.passphrase` | | Key passphrase (if encrypted) | - | `'key-pass'` |
|
|
587
|
+
| `settings.remotePath` | | Default remote directory | `/` | `'/data/inventory'` |
|
|
588
|
+
| `settings.filePattern` | | File glob pattern | `*` | `'*.csv'` |
|
|
589
|
+
| `settings.maxConnections` | | Connection pool size | 5 | `3` |
|
|
590
|
+
| `settings.connectionTimeout` | | Connection timeout (ms) | 30000 | `60000` |
|
|
591
|
+
| `settings.keepaliveInterval` | | Keepalive interval (ms) | 5000 | `10000` |
|
|
592
|
+
| `settings.retry` | | Retry configuration object | See below | See below |
|
|
593
|
+
| `settings.writeCreateDirectories` | | Auto-create dirs on write | `true` | `false` |
|
|
594
|
+
| `settings.writeOverwrite` | | Overwrite existing files | `false` | `true` |
|
|
595
|
+
| `settings.writePermissions` | | File permissions (octal) | `'0644'` | `'0600'` |
|
|
596
|
+
|
|
597
|
+
\* Either `password` or `privateKey` required
|
|
598
|
+
|
|
599
|
+
### Enhanced Retry Configuration
|
|
600
|
+
|
|
601
|
+
Fine-grained retry control with exponential backoff, jitter, and custom predicates.
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
settings: {
|
|
605
|
+
host: 'sftp.vendor.com',
|
|
606
|
+
username: 'integration',
|
|
607
|
+
privateKey: process.env.SFTP_PRIVATE_KEY,
|
|
608
|
+
|
|
609
|
+
// Enhanced retry configuration (optional)
|
|
610
|
+
retry: {
|
|
611
|
+
maxAttempts: 3, // Max retry attempts (default: 3)
|
|
612
|
+
baseDelayMs: 1000, // Initial delay in ms (default: 1000)
|
|
613
|
+
backoffFactor: 2, // Exponential factor (default: 2)
|
|
614
|
+
maxDelayMs: 8000, // Max delay cap (default: 8000)
|
|
615
|
+
jitter: 'full', // Jitter strategy: 'none' | 'full' (default: 'full')
|
|
616
|
+
reconnectOnFailure: true, // Reconnect between retries (default: true)
|
|
617
|
+
|
|
618
|
+
// Optional: Custom retry predicate
|
|
619
|
+
isRetryable: (error: Error) => {
|
|
620
|
+
// Return true to retry, false to fail immediately
|
|
621
|
+
return error.message.includes('ECONNRESET');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Retry Configuration Options:**
|
|
628
|
+
|
|
629
|
+
| Option | Type | Default | Description |
|
|
630
|
+
| -------------------- | ---------- | -------- | ------------------------------------------------------- |
|
|
631
|
+
| `maxAttempts` | `number` | `3` | Maximum retry attempts (including initial attempt) |
|
|
632
|
+
| `baseDelayMs` | `number` | `1000` | Base delay in milliseconds |
|
|
633
|
+
| `backoffFactor` | `number` | `2` | Exponential backoff multiplier |
|
|
634
|
+
| `maxDelayMs` | `number` | `8000` | Maximum delay cap in milliseconds |
|
|
635
|
+
| `jitter` | `string` | `'full'` | Jitter strategy: `'none'` or `'full'` (randomization) |
|
|
636
|
+
| `reconnectOnFailure` | `boolean` | `true` | Reconnect to SFTP between retry attempts |
|
|
637
|
+
| `isRetryable` | `function` | Built-in | Custom function to determine if error should be retried |
|
|
638
|
+
|
|
639
|
+
**Default Retry Behavior:**
|
|
640
|
+
|
|
641
|
+
The SDK automatically retries these error types:
|
|
642
|
+
|
|
643
|
+
- **Network errors**: `ECONNRESET`, `ETIMEDOUT`, `ENOTFOUND`, `EHOSTUNREACH`, `ECONNREFUSED`, `EPIPE`
|
|
644
|
+
- **Transient messages**: `connection lost`, `timeout`, `socket hang up`, `network error`
|
|
645
|
+
|
|
646
|
+
The SDK **never retries** these errors (require manual intervention):
|
|
647
|
+
|
|
648
|
+
- **Authentication failures**: `permission denied`, `authentication`
|
|
649
|
+
- **File not found**: `no such file`, `file not found`
|
|
650
|
+
- **Configuration errors**: `algorithm`, `cipher`, `key exchange`
|
|
651
|
+
|
|
652
|
+
### Environment Variables Pattern
|
|
653
|
+
|
|
654
|
+
```bash
|
|
655
|
+
# .env file
|
|
656
|
+
SFTP_HOST=sftp.vendor.com
|
|
657
|
+
SFTP_PORT=22
|
|
658
|
+
SFTP_USERNAME=integration_user
|
|
659
|
+
SFTP_PASSWORD=secret-password
|
|
660
|
+
|
|
661
|
+
# Or SSH key (recommended)
|
|
662
|
+
SFTP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
|
663
|
+
...key content...
|
|
664
|
+
-----END RSA PRIVATE KEY-----"
|
|
665
|
+
SFTP_PASSPHRASE=optional-passphrase
|
|
666
|
+
|
|
667
|
+
# Paths
|
|
668
|
+
SFTP_REMOTE_PATH=/data/inventory
|
|
669
|
+
SFTP_FILE_PATTERN=*.csv
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
// Load from environment
|
|
674
|
+
const sftpSource = new SftpDataSource(
|
|
675
|
+
{
|
|
676
|
+
type: 'SFTP_CSV',
|
|
677
|
+
connectionId: 'vendor-sftp',
|
|
678
|
+
settings: {
|
|
679
|
+
host: process.env.SFTP_HOST!,
|
|
680
|
+
port: parseInt(process.env.SFTP_PORT || '22'),
|
|
681
|
+
username: process.env.SFTP_USERNAME!,
|
|
682
|
+
password: process.env.SFTP_PASSWORD, // or
|
|
683
|
+
privateKey: process.env.SFTP_PRIVATE_KEY, // privateKey
|
|
684
|
+
passphrase: process.env.SFTP_PASSPHRASE,
|
|
685
|
+
remotePath: process.env.SFTP_REMOTE_PATH || '/',
|
|
686
|
+
filePattern: process.env.SFTP_FILE_PATTERN || '*',
|
|
687
|
+
maxConnections: parseInt(process.env.SFTP_MAX_CONN || '5'),
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
logger
|
|
691
|
+
);
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## File Operations
|
|
697
|
+
|
|
698
|
+
### Listing Files
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
// List all files in default remotePath
|
|
702
|
+
const allFiles = await sftpSource.listFiles();
|
|
703
|
+
|
|
704
|
+
// List with filters
|
|
705
|
+
const recentFiles = await sftpSource.listFiles({
|
|
706
|
+
remotePath: '/data/current',
|
|
707
|
+
filePattern: '*.csv',
|
|
708
|
+
lastProcessedTimestamp: '2025-01-01T00:00:00Z', // Only files modified after
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// List by file name substring
|
|
712
|
+
const inventoryFiles = await sftpSource.listFiles({
|
|
713
|
+
fileName: 'inventory', // Contains 'inventory'
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// Process file metadata
|
|
717
|
+
for (const file of recentFiles) {
|
|
718
|
+
console.log({
|
|
719
|
+
name: file.name, // 'inventory-2025-01-15.csv'
|
|
720
|
+
path: file.path, // '/data/current/inventory-2025-01-15.csv'
|
|
721
|
+
size: file.size, // 1024000 (bytes)
|
|
722
|
+
lastModified: file.lastModified, // Date object
|
|
723
|
+
type: file.type, // 'file' or 'directory'
|
|
724
|
+
permissions: file.permissions, // '-rw-r--r--'
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**List Options**:
|
|
730
|
+
|
|
731
|
+
| Option | Type | Description |
|
|
732
|
+
| ------------------------ | -------- | ------------------------------------------- |
|
|
733
|
+
| `remotePath` | `string` | Remote directory to list (overrides config) |
|
|
734
|
+
| `filePattern` | `string` | Glob pattern filter (e.g., `'*.csv'`) |
|
|
735
|
+
| `fileName` | `string` | Substring match filter |
|
|
736
|
+
| `lastProcessedTimestamp` | `string` | ISO timestamp - only files modified after |
|
|
737
|
+
|
|
738
|
+
### Reading Files
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
// Read text file (CSV, JSON, etc.)
|
|
742
|
+
const csvContent = await sftpSource.downloadFile('/data/inventory.csv');
|
|
743
|
+
// Returns: string
|
|
744
|
+
|
|
745
|
+
// Read binary file (Parquet, images, etc.)
|
|
746
|
+
const parquetData = await sftpSource.downloadFile('data.parquet');
|
|
747
|
+
// Returns: Buffer
|
|
748
|
+
|
|
749
|
+
// Read with full path
|
|
750
|
+
const content = await sftpSource.downloadFile('/remote/path/to/file.csv');
|
|
751
|
+
|
|
752
|
+
// Read relative to remotePath
|
|
753
|
+
const content2 = await sftpSource.downloadFile('subdir/file.csv');
|
|
754
|
+
// Resolves to: /data/inventory/subdir/file.csv (if remotePath=/data/inventory)
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### Writing Files
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
// Upload file content (string)
|
|
761
|
+
await sftpSource.uploadFile('/exports/inventory.csv', csvData, {
|
|
762
|
+
createDirectories: true,
|
|
763
|
+
overwrite: false,
|
|
764
|
+
permissions: '0644',
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Upload binary content (Buffer)
|
|
768
|
+
await sftpSource.uploadFile('/exports/data.parquet', parquetBuffer, {
|
|
769
|
+
createDirectories: true,
|
|
770
|
+
permissions: '0600', // Read/write owner only
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// Upload with auto-create directories
|
|
774
|
+
await sftpSource.uploadFile(
|
|
775
|
+
'/exports/2025/01/15/inventory.csv', // Creates /exports/2025/01/15/ if missing
|
|
776
|
+
csvData,
|
|
777
|
+
{ createDirectories: true }
|
|
778
|
+
);
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
**Write Options**:
|
|
782
|
+
|
|
783
|
+
| Option | Type | Description | Default |
|
|
784
|
+
| ------------------- | -------------------- | -------------------------- | -------- |
|
|
785
|
+
| `createDirectories` | `boolean` | Create missing parent dirs | `true` |
|
|
786
|
+
| `overwrite` | `boolean` | Overwrite existing file | `false` |
|
|
787
|
+
| `permissions` | `string` | File permissions (octal) | `'0644'` |
|
|
788
|
+
| `encoding` | `'utf8' \| 'binary'` | Content encoding | `'utf8'` |
|
|
789
|
+
|
|
790
|
+
### Deleting Files
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
// Delete single file
|
|
794
|
+
await sftpSource.deleteFile('/temp/old-file.csv');
|
|
795
|
+
|
|
796
|
+
// Delete with error handling
|
|
797
|
+
try {
|
|
798
|
+
await sftpSource.deleteFile('/important-file.csv');
|
|
799
|
+
console.log('File deleted successfully');
|
|
800
|
+
} catch (error: any) {
|
|
801
|
+
if (error.message.includes('No such file')) {
|
|
802
|
+
console.log('File already deleted or does not exist');
|
|
803
|
+
} else {
|
|
804
|
+
console.error('Delete failed:', error.message);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
### Moving Files (Atomic Rename)
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
// Move file (atomic SFTP rename operation)
|
|
813
|
+
await sftpSource.moveFile(
|
|
814
|
+
'/incoming/data.csv',
|
|
815
|
+
'/processed/data.csv',
|
|
816
|
+
false // Don't overwrite if target exists
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
// Move to archive with date-based organization
|
|
820
|
+
const today = new Date().toISOString().split('T')[0];
|
|
821
|
+
await sftpSource.moveFile('/incoming/inventory.csv', `/archive/${today}/inventory.csv`);
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**IMPORTANT**: Unlike S3's copy+delete, SFTP `moveFile()` uses the **native SFTP rename command**, which is **atomic**. The file is renamed in a single operation with no risk of existing in both locations.
|
|
825
|
+
|
|
826
|
+
### Copying Files
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
// Copy file (download + upload)
|
|
830
|
+
await sftpSource.copyFile(
|
|
831
|
+
'/source/inventory.csv',
|
|
832
|
+
'/backup/inventory_backup.csv',
|
|
833
|
+
true // Overwrite if exists
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
// Copy with different permissions
|
|
837
|
+
await sftpSource.copyFile(
|
|
838
|
+
'/source/data.csv',
|
|
839
|
+
'/archive/data.csv',
|
|
840
|
+
false,
|
|
841
|
+
'0600' // Restrictive permissions for archive
|
|
842
|
+
);
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**Note**: `copyFile()` is implemented as download + upload (not atomic). For atomic operations, use `moveFile()`.
|
|
846
|
+
|
|
847
|
+
### Directory Operations
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
// Create directory
|
|
851
|
+
await sftpSource.createDirectory('/exports/daily', true, '0755');
|
|
852
|
+
|
|
853
|
+
// Create nested directories
|
|
854
|
+
await sftpSource.createDirectory('/exports/2025/01/15', true);
|
|
855
|
+
// Creates all missing parents: /exports → /exports/2025 → /exports/2025/01 → /exports/2025/01/15
|
|
856
|
+
|
|
857
|
+
// Check if directory exists
|
|
858
|
+
const dirExists = await sftpSource.directoryExists('/data/exports');
|
|
859
|
+
console.log(`Directory exists: ${dirExists}`);
|
|
860
|
+
|
|
861
|
+
// Check if file exists
|
|
862
|
+
const fileExists = await sftpSource.fileExists('/data/inventory.csv');
|
|
863
|
+
console.log(`File exists: ${fileExists}`);
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
## Connection Management
|
|
869
|
+
|
|
870
|
+
### Connection Pool Information
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
// Get connection details
|
|
874
|
+
const connectionInfo = sftpSource.getConnectionInfo();
|
|
875
|
+
console.log('Connection Info:', {
|
|
876
|
+
type: connectionInfo.type, // 'SFTP_CSV'
|
|
877
|
+
host: connectionInfo.host, // 'sftp.vendor.com'
|
|
878
|
+
port: connectionInfo.port, // 22
|
|
879
|
+
username: connectionInfo.username, // 'integration'
|
|
880
|
+
remotePath: connectionInfo.remotePath, // '/data/inventory'
|
|
881
|
+
filePattern: connectionInfo.filePattern, // '*.csv'
|
|
882
|
+
currentConnections: connectionInfo.connectionPoolSize, // 3
|
|
883
|
+
maxConnections: connectionInfo.maxConnections, // 5
|
|
884
|
+
});
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### Connection Health
|
|
888
|
+
|
|
889
|
+
The SFTP data source automatically manages connection health:
|
|
890
|
+
|
|
891
|
+
```
|
|
892
|
+
Connection Lifecycle:
|
|
893
|
+
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
894
|
+
‚ 1. Connection Created ‚
|
|
895
|
+
‚ - SSH handshake ‚
|
|
896
|
+
‚ - Authentication ‚
|
|
897
|
+
‚ - Add to pool ‚
|
|
898
|
+
”€€€€€€€€€€¬€€€€€€€€€€€€€€€€€€€€€€˜
|
|
899
|
+
‚
|
|
900
|
+
¼
|
|
901
|
+
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
902
|
+
‚ 2. Connection Active ‚
|
|
903
|
+
‚ - Reused for operations ‚
|
|
904
|
+
‚ - Keepalive pings (5s) ‚
|
|
905
|
+
‚ - Health monitoring ‚
|
|
906
|
+
”€€€€€€€€€€¬€€€€€€€€€€€€€€€€€€€€€€˜
|
|
907
|
+
‚
|
|
908
|
+
¼
|
|
909
|
+
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
910
|
+
‚ 3. Connection Idle ‚
|
|
911
|
+
‚ - No operations for timeout ‚
|
|
912
|
+
‚ - Auto-close after TTL ‚
|
|
913
|
+
‚ - Removed from pool ‚
|
|
914
|
+
”€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€˜
|
|
915
|
+
|
|
916
|
+
Failure Handling:
|
|
917
|
+
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
918
|
+
‚ Operation Fails ‚
|
|
919
|
+
‚ - Network error ‚
|
|
920
|
+
‚ - Connection timeout ‚
|
|
921
|
+
‚ - Authentication failure ‚
|
|
922
|
+
”€€€€€€€€€€¬€€€€€€€€€€€€€€€€€€€€€€˜
|
|
923
|
+
‚
|
|
924
|
+
¼
|
|
925
|
+
Œ€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
|
|
926
|
+
‚ Automatic Retry ‚
|
|
927
|
+
‚ - Exponential backoff ‚
|
|
928
|
+
‚ - New connection created ‚
|
|
929
|
+
‚ - Operation retried ‚
|
|
930
|
+
”€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€˜
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
### Validating Connection
|
|
934
|
+
|
|
935
|
+
```typescript
|
|
936
|
+
// Test SFTP connectivity
|
|
937
|
+
const isValid = await sftpSource.validateConnection();
|
|
938
|
+
|
|
939
|
+
if (isValid) {
|
|
940
|
+
console.log('SFTP connection successful');
|
|
941
|
+
} else {
|
|
942
|
+
console.error('Failed to connect to SFTP server');
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Use in startup health check
|
|
946
|
+
async function startupHealthCheck() {
|
|
947
|
+
try {
|
|
948
|
+
const valid = await sftpSource.validateConnection();
|
|
949
|
+
if (!valid) {
|
|
950
|
+
throw new Error('SFTP connection validation failed');
|
|
951
|
+
}
|
|
952
|
+
console.log('❌œ“ SFTP connection validated');
|
|
953
|
+
} catch (error) {
|
|
954
|
+
console.error('❌œ— SFTP connection failed:', error);
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
## Multi-Format Support
|
|
963
|
+
|
|
964
|
+
### CSV Format
|
|
965
|
+
|
|
966
|
+
```typescript
|
|
967
|
+
// CSV-specific configuration
|
|
968
|
+
const csvConfig: SftpDataSourceConfig = {
|
|
969
|
+
type: 'SFTP_CSV',
|
|
970
|
+
connectionId: 'vendor-csv',
|
|
971
|
+
settings: {
|
|
972
|
+
host: 'sftp.vendor.com',
|
|
973
|
+
username: 'integration',
|
|
974
|
+
privateKey: process.env.SFTP_KEY,
|
|
975
|
+
|
|
976
|
+
// CSV parsing options
|
|
977
|
+
csvDelimiter: ',',
|
|
978
|
+
csvHeaders: ['sku', 'quantity', 'location', 'status'],
|
|
979
|
+
requiredCsvHeaders: ['sku', 'quantity'], // Validation
|
|
980
|
+
csvSkipEmptyLines: true,
|
|
981
|
+
csvTrimValues: true,
|
|
982
|
+
csvQuote: '"',
|
|
983
|
+
csvEscape: '\\',
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// Parse CSV content
|
|
988
|
+
const csvContent = await sftpSource.downloadFile('inventory.csv');
|
|
989
|
+
const records = sftpSource.parseCsvContent(csvContent, fileMetadata);
|
|
990
|
+
// Uses csvDelimiter, csvSkipEmptyLines, etc. from config
|
|
991
|
+
|
|
992
|
+
// Generate CSV content
|
|
993
|
+
const csvOutput = sftpSource.writeCsvContent(records, {
|
|
994
|
+
delimiter: ',',
|
|
995
|
+
headers: ['sku', 'qty', 'location'],
|
|
996
|
+
includeHeader: true,
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
await sftpSource.writeFile('/exports/inventory.csv', csvOutput);
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### JSON Format
|
|
1003
|
+
|
|
1004
|
+
```typescript
|
|
1005
|
+
// JSON-specific configuration
|
|
1006
|
+
const jsonConfig: SftpDataSourceConfig = {
|
|
1007
|
+
type: 'SFTP_JSON',
|
|
1008
|
+
connectionId: 'vendor-json',
|
|
1009
|
+
settings: {
|
|
1010
|
+
host: 'sftp.vendor.com',
|
|
1011
|
+
username: 'integration',
|
|
1012
|
+
privateKey: process.env.SFTP_KEY,
|
|
1013
|
+
|
|
1014
|
+
// JSON options
|
|
1015
|
+
jsonFormat: 'json', // or 'jsonl'
|
|
1016
|
+
jsonValidate: true,
|
|
1017
|
+
jsonMaxDepth: 10,
|
|
1018
|
+
jsonIndent: 2,
|
|
1019
|
+
jsonSortKeys: true,
|
|
1020
|
+
},
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// Parse JSON content
|
|
1024
|
+
const jsonContent = await sftpSource.downloadFile('data.json');
|
|
1025
|
+
const records = sftpSource.parseJsonContent(jsonContent, fileMetadata, {
|
|
1026
|
+
format: 'json',
|
|
1027
|
+
validate: true,
|
|
1028
|
+
maxDepth: 10,
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// Generate JSON content
|
|
1032
|
+
const jsonOutput = sftpSource.writeJsonContent(records, {
|
|
1033
|
+
format: 'json',
|
|
1034
|
+
indent: 2,
|
|
1035
|
+
sortKeys: true,
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
await sftpSource.writeFile('/exports/data.json', jsonOutput);
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
### JSON Lines (JSONL) Format
|
|
1042
|
+
|
|
1043
|
+
```typescript
|
|
1044
|
+
// Parse JSONL (one JSON object per line)
|
|
1045
|
+
const jsonlContent = await sftpSource.downloadFile('stream-data.jsonl');
|
|
1046
|
+
const records = sftpSource.parseJsonContent(jsonlContent, fileMetadata, {
|
|
1047
|
+
format: 'jsonl',
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// Generate JSONL content
|
|
1051
|
+
const jsonlOutput = sftpSource.writeJsonContent(records, {
|
|
1052
|
+
format: 'jsonl',
|
|
1053
|
+
});
|
|
1054
|
+
// Output:
|
|
1055
|
+
// {"id":1,"name":"Product A"}
|
|
1056
|
+
// {"id":2,"name":"Product B"}
|
|
1057
|
+
|
|
1058
|
+
await sftpSource.writeFile('/exports/stream.jsonl', jsonlOutput);
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
### Parquet Format
|
|
1062
|
+
|
|
1063
|
+
```typescript
|
|
1064
|
+
// Parquet-specific configuration
|
|
1065
|
+
const parquetConfig: SftpDataSourceConfig = {
|
|
1066
|
+
type: 'SFTP_PARQUET',
|
|
1067
|
+
connectionId: 'vendor-parquet',
|
|
1068
|
+
settings: {
|
|
1069
|
+
host: 'sftp.vendor.com',
|
|
1070
|
+
username: 'integration',
|
|
1071
|
+
privateKey: process.env.SFTP_KEY,
|
|
1072
|
+
|
|
1073
|
+
// Parquet options
|
|
1074
|
+
parquetCompression: 'snappy', // 'none', 'gzip', 'snappy', 'lz4'
|
|
1075
|
+
parquetSchema: {
|
|
1076
|
+
productRef: 'string',
|
|
1077
|
+
quantity: 'int32',
|
|
1078
|
+
price: 'double',
|
|
1079
|
+
},
|
|
1080
|
+
parquetRowGroupSize: 1000,
|
|
1081
|
+
},
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
// Parse Parquet content (async)
|
|
1085
|
+
const parquetBuffer = await sftpSource.downloadFile('data.parquet');
|
|
1086
|
+
// Note: parseParquetContent currently throws (not implemented). Use ParquetParserService.
|
|
1087
|
+
// const records = await sftpSource.parseParquetContent(parquetBuffer as Buffer, fileMetadata);
|
|
1088
|
+
|
|
1089
|
+
// Parquet generation would use ParquetParserService separately
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
### Processing Mixed File Types
|
|
1093
|
+
|
|
1094
|
+
```typescript
|
|
1095
|
+
async function processMultipleFormats() {
|
|
1096
|
+
const files = await sftpSource.listFiles({
|
|
1097
|
+
filePattern: '*.{csv,json,parquet}',
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
for (const file of files) {
|
|
1101
|
+
const extension = file.name.split('.').pop()?.toLowerCase();
|
|
1102
|
+
let records: any[] = [];
|
|
1103
|
+
|
|
1104
|
+
switch (extension) {
|
|
1105
|
+
case 'csv':
|
|
1106
|
+
const csvContent = await sftpSource.downloadFile(file.path);
|
|
1107
|
+
records = sftpSource.parseCsvContent(csvContent, file);
|
|
1108
|
+
break;
|
|
1109
|
+
|
|
1110
|
+
case 'json':
|
|
1111
|
+
const jsonContent = await sftpSource.downloadFile(file.path);
|
|
1112
|
+
records = sftpSource.parseJsonContent(jsonContent, file, {
|
|
1113
|
+
format: 'json',
|
|
1114
|
+
});
|
|
1115
|
+
break;
|
|
1116
|
+
|
|
1117
|
+
case 'parquet':
|
|
1118
|
+
const parquetBuffer = await sftpSource.downloadFile(file.path);
|
|
1119
|
+
// Use ParquetParserService here instead of parseParquetContent (not implemented)
|
|
1120
|
+
// records = await sftpParser.parseParquetContent(parquetBuffer as Buffer, file);
|
|
1121
|
+
break;
|
|
1122
|
+
|
|
1123
|
+
default:
|
|
1124
|
+
logger.warn(`Unsupported format: ${extension}`);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
await processRecords(records, file.name);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
---
|
|
1134
|
+
|
|
1135
|
+
## Error Handling
|
|
1136
|
+
|
|
1137
|
+
### Common Error Patterns
|
|
1138
|
+
|
|
1139
|
+
```typescript
|
|
1140
|
+
import { DataSourceError } from '@fluentcommerce/fc-connect-sdk';
|
|
1141
|
+
|
|
1142
|
+
try {
|
|
1143
|
+
await sftpSource.writeFile(remotePath, content);
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if (error instanceof DataSourceError) {
|
|
1146
|
+
switch (error.operation) {
|
|
1147
|
+
case 'uploadFile':
|
|
1148
|
+
if (error.message.includes('Permission denied')) {
|
|
1149
|
+
logger.error('SFTP permission error', {
|
|
1150
|
+
remotePath,
|
|
1151
|
+
suggestion: 'Check file permissions and directory access',
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
break;
|
|
1155
|
+
|
|
1156
|
+
case 'listFiles':
|
|
1157
|
+
if (error.message.includes('No such file')) {
|
|
1158
|
+
logger.warn('Remote directory not found', {
|
|
1159
|
+
remotePath: error.context?.remotePath,
|
|
1160
|
+
suggestion: 'Verify remote path exists',
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
break;
|
|
1164
|
+
|
|
1165
|
+
case 'downloadFile':
|
|
1166
|
+
if (error.message.includes('Connection timeout')) {
|
|
1167
|
+
logger.error('SFTP connection timeout', {
|
|
1168
|
+
suggestion: 'Check network connectivity and server status',
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
} else {
|
|
1174
|
+
logger.error('Unexpected error', error);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
### Retry with Exponential Backoff
|
|
1180
|
+
|
|
1181
|
+
```typescript
|
|
1182
|
+
async function uploadWithRetry(
|
|
1183
|
+
content: string,
|
|
1184
|
+
remotePath: string,
|
|
1185
|
+
maxRetries: number = 3
|
|
1186
|
+
): Promise<void> {
|
|
1187
|
+
let lastError: Error;
|
|
1188
|
+
|
|
1189
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1190
|
+
try {
|
|
1191
|
+
await sftpSource.writeFile(remotePath, content);
|
|
1192
|
+
logger.info(`Upload successful on attempt ${attempt}`);
|
|
1193
|
+
return;
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
lastError = error as Error;
|
|
1196
|
+
logger.warn(`Upload attempt ${attempt} failed`, {
|
|
1197
|
+
error: (error as Error).message,
|
|
1198
|
+
retriesLeft: maxRetries - attempt,
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
if (attempt < maxRetries) {
|
|
1202
|
+
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
|
|
1203
|
+
logger.info(`Retrying in ${delay}ms...`);
|
|
1204
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
throw new Error(`Upload failed after ${maxRetries} attempts: ${lastError!.message}`);
|
|
1210
|
+
}
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
---
|
|
1214
|
+
|
|
1215
|
+
## Production Patterns
|
|
1216
|
+
|
|
1217
|
+
### Pattern 1: Vendor Inventory Ingestion
|
|
1218
|
+
|
|
1219
|
+
```typescript
|
|
1220
|
+
import {
|
|
1221
|
+
SftpDataSource,
|
|
1222
|
+
CSVParserService,
|
|
1223
|
+
UniversalMapper,
|
|
1224
|
+
StateService,
|
|
1225
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
1226
|
+
|
|
1227
|
+
async function ingestVendorInventory() {
|
|
1228
|
+
const sftpSource = new SftpDataSource(
|
|
1229
|
+
{
|
|
1230
|
+
/* config */
|
|
1231
|
+
},
|
|
1232
|
+
logger
|
|
1233
|
+
);
|
|
1234
|
+
const csvParser = new CSVParserService();
|
|
1235
|
+
const mapper = new UniversalMapper(mappingConfig);
|
|
1236
|
+
const stateService = new StateService(kvAdapter);
|
|
1237
|
+
|
|
1238
|
+
// List new files
|
|
1239
|
+
const files = await sftpSource.listFiles({
|
|
1240
|
+
remotePath: '/outbound/inventory',
|
|
1241
|
+
filePattern: 'inventory_*.csv',
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
for (const file of files) {
|
|
1245
|
+
// Check if already processed
|
|
1246
|
+
const stateKey = `processed:${file.name}:${file.lastModified?.toString()}`;
|
|
1247
|
+
const processed = await stateService.isFileProcessed(kvAdapter as any, stateKey);
|
|
1248
|
+
|
|
1249
|
+
if (processed) {
|
|
1250
|
+
logger.info('File already processed', { file: file.name });
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
try {
|
|
1255
|
+
// Download and parse CSV
|
|
1256
|
+
const content = await sftpSource.downloadFile(file.path);
|
|
1257
|
+
const records = await csvParser.parse(content as string);
|
|
1258
|
+
|
|
1259
|
+
// Map fields
|
|
1260
|
+
const result = await mapper.map(records);
|
|
1261
|
+
if (!result.success) {
|
|
1262
|
+
logger.error('Mapping failed', undefined, { errors: result.errors });
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Send to Fluent
|
|
1267
|
+
await sendToFluent(result.data);
|
|
1268
|
+
|
|
1269
|
+
// Mark as processed
|
|
1270
|
+
await stateService.updateSyncState(
|
|
1271
|
+
kvAdapter as any,
|
|
1272
|
+
[
|
|
1273
|
+
{
|
|
1274
|
+
fileName: file.name,
|
|
1275
|
+
recordCount: records.length,
|
|
1276
|
+
processedAt: new Date().toISOString(),
|
|
1277
|
+
},
|
|
1278
|
+
],
|
|
1279
|
+
'sftp-ingestion'
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
// Move to processed folder
|
|
1283
|
+
await sftpSource.moveFile(file.path, file.path.replace('/outbound/', '/processed/'));
|
|
1284
|
+
|
|
1285
|
+
logger.info('File processed successfully', { file: file.name });
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
logger.error('Failed to process file', error as Error, { file: file.path });
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
### Pattern 2: State Management with SFTP
|
|
1294
|
+
|
|
1295
|
+
```typescript
|
|
1296
|
+
async function processFilesWithState() {
|
|
1297
|
+
const files = await sftpSource.listFiles();
|
|
1298
|
+
const stateService = new StateService(logger);
|
|
1299
|
+
|
|
1300
|
+
for (const file of files) {
|
|
1301
|
+
// Use file name + modification time as unique key
|
|
1302
|
+
const stateKey = `processed:${file.name}:${file.lastModified.toISOString()}`;
|
|
1303
|
+
|
|
1304
|
+
if (await stateService.isFileProcessed(kvAdapter as any, stateKey)) {
|
|
1305
|
+
logger.info('File already processed', { fileName: file.name });
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
try {
|
|
1310
|
+
const content = await sftpSource.downloadFile(file.path);
|
|
1311
|
+
await processFile(content, file.name);
|
|
1312
|
+
|
|
1313
|
+
// Mark as processed
|
|
1314
|
+
await stateService.updateSyncState(
|
|
1315
|
+
kvAdapter as any,
|
|
1316
|
+
[
|
|
1317
|
+
{
|
|
1318
|
+
fileName: file.name,
|
|
1319
|
+
size: file.size,
|
|
1320
|
+
processedAt: new Date().toISOString(),
|
|
1321
|
+
},
|
|
1322
|
+
],
|
|
1323
|
+
'sftp-processing'
|
|
1324
|
+
);
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
logger.error('Failed to process file', error as Error, { file: file.path });
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
---
|
|
1333
|
+
|
|
1334
|
+
## API Reference
|
|
1335
|
+
|
|
1336
|
+
### SftpDataSource Constructor
|
|
1337
|
+
|
|
1338
|
+
```typescript
|
|
1339
|
+
new SftpDataSource(config: SftpDataSourceConfig, logger?: StructuredLogger)
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
**Parameters**:
|
|
1343
|
+
|
|
1344
|
+
- `config`: SFTP data source configuration
|
|
1345
|
+
- `logger`: Optional logging service
|
|
1346
|
+
|
|
1347
|
+
### Methods
|
|
1348
|
+
|
|
1349
|
+
| Method | Parameters | Returns | Description |
|
|
1350
|
+
| ----------------------- | -------------------------------------------------- | ------------------------- | ----------------------------- | --------------------- |
|
|
1351
|
+
| `listFiles()` | `options?` | `Promise<FileMetadata[]>` | List files in remote path |
|
|
1352
|
+
| `downloadFile()` | `path` | `Promise<string | Buffer>` | Download file content |
|
|
1353
|
+
| `downloadFile()` | `path` | `Promise<Buffer>` | Download file as buffer |
|
|
1354
|
+
| `writeFile()` | `path, content, options?` | `Promise<void>` | Write file to SFTP server |
|
|
1355
|
+
| `deleteFile()` | `path` | `Promise<void>` | Delete file from server |
|
|
1356
|
+
| `copyFile()` | `sourcePath, targetPath, overwrite?, permissions?` | `Promise<void>` | Copy file (download + upload) |
|
|
1357
|
+
| `moveFile()` | `sourcePath, targetPath, overwrite?` | `Promise<void>` | Move file (atomic rename) |
|
|
1358
|
+
| `fileExists()` | `path` | `Promise<boolean>` | Check if file exists |
|
|
1359
|
+
| `directoryExists()` | `path` | `Promise<boolean>` | Check if directory exists |
|
|
1360
|
+
| `createDirectory()` | `path, recursive?, permissions?` | `Promise<void>` | Create directory |
|
|
1361
|
+
| `validateConnection()` | - | `Promise<boolean>` | Test SFTP connectivity |
|
|
1362
|
+
| `getConnectionInfo()` | - | `ConnectionInfo` | Get connection details |
|
|
1363
|
+
| `parseCsvContent()` | `content, metadata` | `any[]` | Parse CSV content |
|
|
1364
|
+
| `parseJsonContent()` | `content, metadata, options?` | `any[]` | Parse JSON/JSONL content |
|
|
1365
|
+
| `parseParquetContent()` | `buffer, metadata` | `Promise<any[]>` | Not implemented (throws) |
|
|
1366
|
+
| `writeCsvContent()` | `records, options?` | `string` | Generate CSV content |
|
|
1367
|
+
| `writeJsonContent()` | `records, options?` | `string` | Generate JSON/JSONL content |
|
|
1368
|
+
|
|
1369
|
+
---
|
|
1370
|
+
|
|
1371
|
+
## Next Steps
|
|
1372
|
+
|
|
1373
|
+
**Learn file processing patterns** → [Module 4: File Processing Patterns](./data-sources-04-file-patterns.md)
|
|
1374
|
+
|
|
1375
|
+
**Explore advanced topics** → [Module 5: Advanced Topics](./data-sources-05-advanced-topics.md)
|
|
1376
|
+
|
|
1377
|
+
---
|
|
1378
|
+
|
|
1379
|
+
[→ Back to S3 Operations](./data-sources-02-s3-operations.md) | [Next: File Processing Patterns →](./data-sources-04-file-patterns.md) | [→‘ Back to Guide](../data-sources-readme.md)
|