@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
|
@@ -1,1893 +1,1893 @@
|
|
|
1
|
-
# Module 9: Best Practices
|
|
2
|
-
|
|
3
|
-
[← Back to Ingestion Guide](../ingestion-readme.md)
|
|
4
|
-
|
|
5
|
-
**Module 9 of 9** | **Level**: All Levels | **Time**: 30 minutes
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Overview
|
|
10
|
-
|
|
11
|
-
This module covers production-ready patterns for data ingestion workflows. Learn comprehensive error handling strategies, monitoring approaches, security best practices, scheduling patterns, data quality validation, and complete production deployment patterns.
|
|
12
|
-
|
|
13
|
-
New in this version:
|
|
14
|
-
|
|
15
|
-
- Preflight validation before runs (PreflightValidator)
|
|
16
|
-
- Job lifecycle tracking (JobTracker)
|
|
17
|
-
- Partial batch failure recovery (PartialBatchRecovery)
|
|
18
|
-
- Versori file-level deduplication (VersoriFileTracker)
|
|
19
|
-
|
|
20
|
-
## Learning Objectives
|
|
21
|
-
|
|
22
|
-
By the end of this module, you will:
|
|
23
|
-
|
|
24
|
-
- ✅ Implement robust error handling with retry logic and circuit breakers
|
|
25
|
-
- ✅ Set up comprehensive monitoring, logging, and alerting
|
|
26
|
-
- ✅ Apply security best practices for credentials and data
|
|
27
|
-
- ✅ Design effective scheduling and trigger strategies
|
|
28
|
-
- ✅ Validate data quality before ingestion
|
|
29
|
-
- ✅ Build production-ready ingestion pipelines
|
|
30
|
-
- ✅ Troubleshoot common issues effectively
|
|
31
|
-
- ✅ Follow deployment checklists for success
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## Error Handling
|
|
36
|
-
|
|
37
|
-
### Retry Strategy with Exponential Backoff
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
async function retryWithBackoff<T>(
|
|
41
|
-
operation: () => Promise<T>,
|
|
42
|
-
maxRetries: number = 3
|
|
43
|
-
): Promise<T> {
|
|
44
|
-
let lastError: Error;
|
|
45
|
-
let delay = 1000; // Start with 1 second
|
|
46
|
-
|
|
47
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
48
|
-
try {
|
|
49
|
-
return await operation();
|
|
50
|
-
} catch (error) {
|
|
51
|
-
lastError = error;
|
|
52
|
-
|
|
53
|
-
if (!isRetryable(error) || attempt === maxRetries) {
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
console.log(\`Retry \${attempt}/\${maxRetries} after \${delay}ms\`);
|
|
58
|
-
await sleep(delay);
|
|
59
|
-
delay *= 2; // Exponential backoff
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
throw lastError;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function isRetryable(error: any): boolean {
|
|
67
|
-
const retryableCodes = [
|
|
68
|
-
'RATE_LIMIT_ERROR',
|
|
69
|
-
'NETWORK_ERROR',
|
|
70
|
-
'TIMEOUT_ERROR',
|
|
71
|
-
'SERVICE_UNAVAILABLE'
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
retryableCodes.includes(error.code) ||
|
|
76
|
-
error.message.includes('timeout') ||
|
|
77
|
-
error.message.includes('ECONNRESET')
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### SDK-Specific Error Handling
|
|
83
|
-
|
|
84
|
-
The SDK throws specific error types that require different handling strategies:
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
import {
|
|
88
|
-
FileParsingError,
|
|
89
|
-
MappingError,
|
|
90
|
-
BatchAPIError,
|
|
91
|
-
StateError,
|
|
92
|
-
createConsoleLogger,
|
|
93
|
-
toStructuredLogger,
|
|
94
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
95
|
-
|
|
96
|
-
async function handleSDKErrors(fileKey: string) {
|
|
97
|
-
try {
|
|
98
|
-
await processFile(fileKey);
|
|
99
|
-
} catch (error) {
|
|
100
|
-
if (error instanceof FileParsingError) {
|
|
101
|
-
// File format issues - log and skip
|
|
102
|
-
console.error(`Invalid file format: ${fileKey}`, {
|
|
103
|
-
line: error.line,
|
|
104
|
-
column: error.column,
|
|
105
|
-
message: error.message,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Move to error bucket for manual review
|
|
109
|
-
await moveToErrorBucket(fileKey, error);
|
|
110
|
-
return { status: 'skipped', reason: 'parsing_error' };
|
|
111
|
-
} else if (error instanceof MappingError) {
|
|
112
|
-
// Field mapping failures - log details
|
|
113
|
-
console.error(`Field mapping failed: ${fileKey}`, {
|
|
114
|
-
invalidFields: error.invalidFields,
|
|
115
|
-
recordIndex: error.recordIndex,
|
|
116
|
-
errors: error.errors,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Save error report
|
|
120
|
-
await saveErrorReport(fileKey, error);
|
|
121
|
-
return { status: 'failed', reason: 'mapping_error' };
|
|
122
|
-
} else if (error instanceof BatchAPIError) {
|
|
123
|
-
// Batch API rejections - check if retryable
|
|
124
|
-
if (error.isRetryable) {
|
|
125
|
-
console.warn(`Retryable batch error: ${error.code}`);
|
|
126
|
-
throw error; // Will be caught by retry logic
|
|
127
|
-
} else {
|
|
128
|
-
console.error(`Permanent batch error: ${error.code}`, {
|
|
129
|
-
jobId: error.jobId,
|
|
130
|
-
batchId: error.batchId,
|
|
131
|
-
details: error.details,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Save to dead letter queue
|
|
135
|
-
await saveToDLQ(fileKey, error);
|
|
136
|
-
return { status: 'failed', reason: 'batch_api_error' };
|
|
137
|
-
}
|
|
138
|
-
} else if (error instanceof StateError) {
|
|
139
|
-
// State management issues - critical
|
|
140
|
-
console.error(`State management error: ${error.message}`);
|
|
141
|
-
|
|
142
|
-
// Alert operations team
|
|
143
|
-
await sendAlert('CRITICAL: State management failure', error);
|
|
144
|
-
throw error; // Don't continue if state is broken
|
|
145
|
-
} else {
|
|
146
|
-
// Unknown error - log and alert
|
|
147
|
-
console.error(`Unexpected error processing ${fileKey}:`, error);
|
|
148
|
-
await sendAlert('Unknown ingestion error', error);
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Circuit Breaker Pattern
|
|
156
|
-
|
|
157
|
-
Prevent cascading failures when external services are down:
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
interface CircuitBreakerConfig {
|
|
161
|
-
failureThreshold: number;
|
|
162
|
-
resetTimeoutMs: number;
|
|
163
|
-
monitoringWindowMs: number;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
enum CircuitState {
|
|
167
|
-
CLOSED = 'CLOSED', // Normal operation
|
|
168
|
-
OPEN = 'OPEN', // Blocking requests
|
|
169
|
-
HALF_OPEN = 'HALF_OPEN', // Testing recovery
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
class CircuitBreaker {
|
|
173
|
-
private state: CircuitState = CircuitState.CLOSED;
|
|
174
|
-
private failureCount: number = 0;
|
|
175
|
-
private lastFailureTime: number = 0;
|
|
176
|
-
private failures: number[] = [];
|
|
177
|
-
|
|
178
|
-
constructor(private config: CircuitBreakerConfig) {}
|
|
179
|
-
|
|
180
|
-
async execute<T>(operation: () => Promise<T>): Promise<T> {
|
|
181
|
-
// Check if circuit should reset
|
|
182
|
-
if (this.shouldAttemptReset()) {
|
|
183
|
-
this.state = CircuitState.HALF_OPEN;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Block if circuit is open
|
|
187
|
-
if (this.state === CircuitState.OPEN) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
`Circuit breaker OPEN. Last failure: ${new Date(this.lastFailureTime).toISOString()}`
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
const result = await operation();
|
|
195
|
-
|
|
196
|
-
// Success - reset if in half-open state
|
|
197
|
-
if (this.state === CircuitState.HALF_OPEN) {
|
|
198
|
-
console.log('Circuit breaker: Service recovered, closing circuit');
|
|
199
|
-
this.reset();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return result;
|
|
203
|
-
} catch (error) {
|
|
204
|
-
this.recordFailure();
|
|
205
|
-
|
|
206
|
-
// Open circuit if threshold exceeded
|
|
207
|
-
if (this.failureCount >= this.config.failureThreshold) {
|
|
208
|
-
this.state = CircuitState.OPEN;
|
|
209
|
-
this.lastFailureTime = Date.now();
|
|
210
|
-
console.error(`Circuit breaker OPENED after ${this.failureCount} failures`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private recordFailure(): void {
|
|
218
|
-
const now = Date.now();
|
|
219
|
-
this.failures.push(now);
|
|
220
|
-
|
|
221
|
-
// Remove old failures outside monitoring window
|
|
222
|
-
this.failures = this.failures.filter(time => now - time < this.config.monitoringWindowMs);
|
|
223
|
-
|
|
224
|
-
this.failureCount = this.failures.length;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private shouldAttemptReset(): boolean {
|
|
228
|
-
if (this.state !== CircuitState.OPEN) {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const timeSinceLastFailure = Date.now() - this.lastFailureTime;
|
|
233
|
-
return timeSinceLastFailure >= this.config.resetTimeoutMs;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private reset(): void {
|
|
237
|
-
this.state = CircuitState.CLOSED;
|
|
238
|
-
this.failureCount = 0;
|
|
239
|
-
this.failures = [];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
getState(): CircuitState {
|
|
243
|
-
return this.state;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Usage
|
|
248
|
-
const fluentAPICircuit = new CircuitBreaker({
|
|
249
|
-
failureThreshold: 5, // Open after 5 failures
|
|
250
|
-
resetTimeoutMs: 60000, // Try again after 1 minute
|
|
251
|
-
monitoringWindowMs: 120000, // Track failures over 2 minutes
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
async function processWithCircuitBreaker(fileKey: string) {
|
|
255
|
-
try {
|
|
256
|
-
await fluentAPICircuit.execute(async () => {
|
|
257
|
-
return await processFile(fileKey);
|
|
258
|
-
});
|
|
259
|
-
} catch (error) {
|
|
260
|
-
if (error.message.includes('Circuit breaker OPEN')) {
|
|
261
|
-
console.log('Service temporarily unavailable, requeueing file');
|
|
262
|
-
await requeueFile(fileKey);
|
|
263
|
-
} else {
|
|
264
|
-
throw error;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
### Dead Letter Queue Pattern
|
|
271
|
-
|
|
272
|
-
Store failed records for later processing:
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
|
|
276
|
-
|
|
277
|
-
interface DLQRecord {
|
|
278
|
-
originalFile: string;
|
|
279
|
-
failedAt: string;
|
|
280
|
-
errorType: string;
|
|
281
|
-
errorMessage: string;
|
|
282
|
-
recordData: any;
|
|
283
|
-
retryCount: number;
|
|
284
|
-
metadata: Record<string, any>;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
class DeadLetterQueue {
|
|
288
|
-
constructor(
|
|
289
|
-
private s3: S3DataSource,
|
|
290
|
-
private dlqBucket: string,
|
|
291
|
-
private dlqPrefix: string = 'dlq/'
|
|
292
|
-
) {}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Save failed record to DLQ
|
|
296
|
-
*/
|
|
297
|
-
async save(record: DLQRecord): Promise<void> {
|
|
298
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
299
|
-
const key = `${this.dlqPrefix}${record.errorType}/${timestamp}-${record.originalFile}`;
|
|
300
|
-
|
|
301
|
-
await this.s3.uploadFile(key, JSON.stringify(record, null, 2));
|
|
302
|
-
|
|
303
|
-
console.log(`Saved to DLQ: ${key}`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Retry processing records from DLQ
|
|
308
|
-
*/
|
|
309
|
-
async retryAll(
|
|
310
|
-
processor: (record: any) => Promise<void>
|
|
311
|
-
): Promise<{ successful: number; failed: number }> {
|
|
312
|
-
const files = await this.s3.listFiles({ prefix: this.dlqPrefix });
|
|
313
|
-
let successful = 0;
|
|
314
|
-
let failed = 0;
|
|
315
|
-
|
|
316
|
-
for (const file of files) {
|
|
317
|
-
try {
|
|
318
|
-
const content = await this.s3.downloadFile(file.path);
|
|
319
|
-
const dlqRecord: DLQRecord = JSON.parse(content);
|
|
320
|
-
|
|
321
|
-
// Attempt reprocessing
|
|
322
|
-
await processor(dlqRecord.recordData);
|
|
323
|
-
|
|
324
|
-
// Success - delete from DLQ (Note: deleteObject method may not exist - use uploadFile with empty content or check SDK)
|
|
325
|
-
// await this.s3.deleteObject(this.dlqBucket, file.path);
|
|
326
|
-
successful++;
|
|
327
|
-
} catch (error) {
|
|
328
|
-
console.error(`DLQ retry failed for ${file.path}:`, error);
|
|
329
|
-
failed++;
|
|
330
|
-
|
|
331
|
-
// Update retry count
|
|
332
|
-
await this.incrementRetryCount(file.path);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return { successful, failed };
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Increment retry count in DLQ record
|
|
341
|
-
*/
|
|
342
|
-
private async incrementRetryCount(key: string): Promise<void> {
|
|
343
|
-
try {
|
|
344
|
-
const content = await this.s3.downloadFile(key);
|
|
345
|
-
const record: DLQRecord = JSON.parse(content);
|
|
346
|
-
|
|
347
|
-
record.retryCount = (record.retryCount || 0) + 1;
|
|
348
|
-
record.metadata.lastRetryAttempt = new Date().toISOString();
|
|
349
|
-
|
|
350
|
-
await this.s3.uploadFile(key, JSON.stringify(record, null, 2));
|
|
351
|
-
} catch (error) {
|
|
352
|
-
console.error(`Failed to update retry count for ${key}:`, error);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Usage
|
|
358
|
-
const dlq = new DeadLetterQueue(s3DataSource, 'my-dlq-bucket');
|
|
359
|
-
|
|
360
|
-
async function processFileWithDLQ(fileKey: string) {
|
|
361
|
-
try {
|
|
362
|
-
const records = await parseFile(fileKey);
|
|
363
|
-
|
|
364
|
-
for (const [index, record] of records.entries()) {
|
|
365
|
-
try {
|
|
366
|
-
await processRecord(record);
|
|
367
|
-
} catch (error) {
|
|
368
|
-
// Save failed record to DLQ
|
|
369
|
-
await dlq.save({
|
|
370
|
-
originalFile: fileKey,
|
|
371
|
-
failedAt: new Date().toISOString(),
|
|
372
|
-
errorType: error.constructor.name,
|
|
373
|
-
errorMessage: error.message,
|
|
374
|
-
recordData: record,
|
|
375
|
-
retryCount: 0,
|
|
376
|
-
metadata: {
|
|
377
|
-
recordIndex: index,
|
|
378
|
-
totalRecords: records.length,
|
|
379
|
-
},
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
} catch (error) {
|
|
384
|
-
console.error(`Complete file failure: ${fileKey}`, error);
|
|
385
|
-
throw error;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### Graceful Degradation
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
async function processFileWithFallback(fileKey: string) {
|
|
394
|
-
try {
|
|
395
|
-
// Primary processing path
|
|
396
|
-
await processFileNormally(fileKey);
|
|
397
|
-
} catch (error) {
|
|
398
|
-
console.error(\`Primary processing failed: \${error.message}\`);
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
// Fallback: Process with reduced batch size
|
|
402
|
-
await processFileWithSmallerBatches(fileKey);
|
|
403
|
-
} catch (fallbackError) {
|
|
404
|
-
console.error(\`Fallback processing failed: \${fallbackError.message}\`);
|
|
405
|
-
|
|
406
|
-
// Last resort: Save to dead letter queue
|
|
407
|
-
await saveToDeadLetterQueue(fileKey, error);
|
|
408
|
-
throw new Error(\`File processing failed completely: \${fileKey}\`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
## Validation Best Practices
|
|
415
|
-
|
|
416
|
-
### Pre-Flight Validation
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
async function validateBeforeIngestion(records: any[]): Promise<ValidationResult> {
|
|
420
|
-
const errors = [];
|
|
421
|
-
const warnings = [];
|
|
422
|
-
|
|
423
|
-
// Schema validation
|
|
424
|
-
for (const [index, record] of records.entries()) {
|
|
425
|
-
// Required fields
|
|
426
|
-
if (!record.ref) {
|
|
427
|
-
errors.push(\`Row \${index}: Missing required field 'ref'\`);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (!record.productRef) {
|
|
431
|
-
errors.push(\`Row \${index}: Missing required field 'productRef'\`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (!record.locationRef) {
|
|
435
|
-
errors.push(\`Row \${index}: Missing required field 'locationRef'\`);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (record.qty === undefined || record.qty === null) {
|
|
439
|
-
errors.push(\`Row \${index}: Missing required field 'qty'\`);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Type validation
|
|
443
|
-
if (typeof record.qty !== 'number') {
|
|
444
|
-
errors.push(\`Row \${index}: 'qty' must be a number\`);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Business rules
|
|
448
|
-
if (record.qty < 0) {
|
|
449
|
-
errors.push(\`Row \${index}: Quantity cannot be negative\`);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (record.qty > 1000000) {
|
|
453
|
-
warnings.push(\`Row \${index}: Unusually large quantity\`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return {
|
|
458
|
-
isValid: errors.length === 0,
|
|
459
|
-
errors,
|
|
460
|
-
warnings
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Usage
|
|
465
|
-
const validation = await validateBeforeIngestion(records);
|
|
466
|
-
|
|
467
|
-
if (!validation.isValid) {
|
|
468
|
-
console.error('Validation errors:', validation.errors);
|
|
469
|
-
throw new Error('Data validation failed');
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (validation.warnings.length > 0) {
|
|
473
|
-
console.warn('Validation warnings:', validation.warnings);
|
|
474
|
-
}
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
## Scheduling and Triggers
|
|
478
|
-
|
|
479
|
-
###Cron Scheduling Patterns
|
|
480
|
-
|
|
481
|
-
Different scheduling strategies for different ingestion scenarios:
|
|
482
|
-
|
|
483
|
-
```typescript
|
|
484
|
-
// Daily midnight sync (00:00 UTC)
|
|
485
|
-
const dailyMidnightCron = '0 0 * * *';
|
|
486
|
-
|
|
487
|
-
// Every 6 hours
|
|
488
|
-
const every6HoursCron = '0 */6 * * *';
|
|
489
|
-
|
|
490
|
-
// Every hour during business hours (9 AM - 5 PM, Mon-Fri)
|
|
491
|
-
const businessHoursCron = '0 9-17 * * 1-5';
|
|
492
|
-
|
|
493
|
-
// Every 15 minutes
|
|
494
|
-
const every15MinutesCron = '*/15 * * * *';
|
|
495
|
-
|
|
496
|
-
// Weekly Sunday at 2 AM
|
|
497
|
-
const weeklySundayCron = '0 2 * * 0';
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
**Recommended schedules by use case:**
|
|
501
|
-
|
|
502
|
-
| Use Case | Schedule | Cron Expression | Rationale |
|
|
503
|
-
| ----------------------- | ------------- | ---------------- | ---------------------------- |
|
|
504
|
-
| **WMS Daily Sync** | Once daily | `0 2 * * *` | Process overnight updates |
|
|
505
|
-
| **3PL Cycle Counts** | Every 6 hours | `0 */6 * * *` | Keep inventory fresh |
|
|
506
|
-
| **Real-time Updates** | Every 15 min | `*/15 * * * *` | Near real-time sync |
|
|
507
|
-
| **Weekly Full Sync** | Sunday 2 AM | `0 2 * * 0` | Comprehensive reconciliation |
|
|
508
|
-
| **Business Hours Only** | 9 AM - 5 PM | `0 9-17 * * 1-5` | During operational hours |
|
|
509
|
-
|
|
510
|
-
### Trigger Mechanisms
|
|
511
|
-
|
|
512
|
-
Different ways to trigger ingestion workflows:
|
|
513
|
-
|
|
514
|
-
#### 1. S3 Event Triggers
|
|
515
|
-
|
|
516
|
-
```typescript
|
|
517
|
-
// Versori workflow triggered by S3 event
|
|
518
|
-
export const s3TriggeredIngestion = webhook('s3-inventory-upload', {
|
|
519
|
-
response: { mode: 'sync' },
|
|
520
|
-
})
|
|
521
|
-
.then(
|
|
522
|
-
fn('parse-s3-event', async ({ data }) => {
|
|
523
|
-
const event = JSON.parse(data);
|
|
524
|
-
const bucket = event.Records[0].s3.bucket.name;
|
|
525
|
-
const key = decodeURIComponent(event.Records[0].s3.object.key);
|
|
526
|
-
|
|
527
|
-
return { bucket, key };
|
|
528
|
-
})
|
|
529
|
-
)
|
|
530
|
-
.then(
|
|
531
|
-
fn('process-file', async ({ data, connections, log, openKv }) => {
|
|
532
|
-
const client = await createClient({ connections, log, openKv });
|
|
533
|
-
|
|
534
|
-
// Initialize logger and state management
|
|
535
|
-
const logger = toStructuredLogger(log, {
|
|
536
|
-
service: 'ingestion',
|
|
537
|
-
correlationId: generateCorrelationId()
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
const stateService = new StateService(logger);
|
|
541
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
542
|
-
|
|
543
|
-
const s3 = new S3DataSource(
|
|
544
|
-
{
|
|
545
|
-
type: 'S3_CSV',
|
|
546
|
-
connectionId: 's3-triggered',
|
|
547
|
-
name: 'S3 Triggered Ingestion',
|
|
548
|
-
s3Config: { region: 'us-east-1', bucket: data.bucket },
|
|
549
|
-
},
|
|
550
|
-
logger
|
|
551
|
-
);
|
|
552
|
-
|
|
553
|
-
// Check if already processed
|
|
554
|
-
if (await stateService.isFileProcessed(kvAdapter, data.key)) {
|
|
555
|
-
log.info(`File already processed: ${data.key}`);
|
|
556
|
-
return { status: 'skipped', reason: 'already_processed' };
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Process file
|
|
560
|
-
await processInventoryFile(client, s3, data.bucket, data.key);
|
|
561
|
-
|
|
562
|
-
// Mark as processed
|
|
563
|
-
await stateService.updateSyncState(kvAdapter, [{
|
|
564
|
-
fileName: data.key,
|
|
565
|
-
lastModified: new Date().toISOString(),
|
|
566
|
-
}]);
|
|
567
|
-
|
|
568
|
-
return { status: 'success', file: data.key };
|
|
569
|
-
})
|
|
570
|
-
)
|
|
571
|
-
.catch(({ data, log }) => {
|
|
572
|
-
log.error('S3 triggered ingestion failed:', data);
|
|
573
|
-
return { status: 'error', error: data };
|
|
574
|
-
});
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
#### 2. SFTP Polling
|
|
578
|
-
|
|
579
|
-
```typescript
|
|
580
|
-
import { SftpDataSource, StateService, VersoriKVAdapter, createConsoleLogger, toStructuredLogger } from '@fluentcommerce/fc-connect-sdk';
|
|
581
|
-
|
|
582
|
-
async function pollSftpDirectory(kv: any) {
|
|
583
|
-
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
584
|
-
service: 'ingestion',
|
|
585
|
-
correlationId: generateCorrelationId()
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
const stateService = new StateService(logger);
|
|
589
|
-
const kvAdapter = new VersoriKVAdapter(kv);
|
|
590
|
-
|
|
591
|
-
const sftp = new SftpDataSource({
|
|
592
|
-
type: 'SFTP_CSV',
|
|
593
|
-
connectionId: 'sftp-polling',
|
|
594
|
-
name: 'SFTP Polling',
|
|
595
|
-
settings: {
|
|
596
|
-
host: process.env.SFTP_HOST!,
|
|
597
|
-
port: 22,
|
|
598
|
-
username: process.env.SFTP_USER!,
|
|
599
|
-
privateKey: fs.readFileSync('/path/to/private/key'),
|
|
600
|
-
}
|
|
601
|
-
}, logger);
|
|
602
|
-
|
|
603
|
-
const remoteDir = '/inbound/inventory/';
|
|
604
|
-
const files = await sftp.listFiles(remoteDir);
|
|
605
|
-
|
|
606
|
-
for (const file of files) {
|
|
607
|
-
if (await stateService.isFileProcessed(kvAdapter, file.name)) {
|
|
608
|
-
continue; // Skip already processed
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Download and process
|
|
612
|
-
const localPath = `/tmp/${file.name}`;
|
|
613
|
-
await sftp.downloadFile(file.path, localPath); // Use file.path (full path) for download
|
|
614
|
-
|
|
615
|
-
await processLocalFile(localPath);
|
|
616
|
-
|
|
617
|
-
// Mark as processed
|
|
618
|
-
await stateService.updateSyncState(kvAdapter, [{
|
|
619
|
-
fileName: file.name,
|
|
620
|
-
lastModified: new Date().toISOString(),
|
|
621
|
-
}]);
|
|
622
|
-
|
|
623
|
-
// Optionally move to processed folder
|
|
624
|
-
await sftp.moveFile(`${remoteDir}${file.name}`, `/processed/${file.name}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
#### 3. Webhook Triggers
|
|
630
|
-
|
|
631
|
-
```typescript
|
|
632
|
-
// Manual or external system triggered ingestion
|
|
633
|
-
export const manualIngestion = webhook('manual-trigger', {
|
|
634
|
-
response: { mode: 'sync' },
|
|
635
|
-
})
|
|
636
|
-
.then(
|
|
637
|
-
fn('validate-request', ({ data }) => {
|
|
638
|
-
const { bucket, prefix, batchSize = 2000 } = JSON.parse(data);
|
|
639
|
-
|
|
640
|
-
if (!bucket || !prefix) {
|
|
641
|
-
throw new Error('Missing required parameters: bucket, prefix');
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
return { bucket, prefix, batchSize };
|
|
645
|
-
})
|
|
646
|
-
)
|
|
647
|
-
.then(
|
|
648
|
-
fn('run-ingestion', async ({ data, connections, log }) => {
|
|
649
|
-
const client = await createClient(ctx); // Auto-detects Versori context
|
|
650
|
-
const s3 = new S3DataSource(
|
|
651
|
-
{
|
|
652
|
-
type: 'S3_CSV',
|
|
653
|
-
s3Config: { region: 'us-east-1' },
|
|
654
|
-
},
|
|
655
|
-
log
|
|
656
|
-
);
|
|
657
|
-
|
|
658
|
-
const files = await s3.listFiles({ prefix: data.prefix });
|
|
659
|
-
|
|
660
|
-
log.info(`Found ${files.length} files to process`);
|
|
661
|
-
|
|
662
|
-
const results = await processFilesInParallel(client, s3, data.bucket, files, {
|
|
663
|
-
batchSize: data.batchSize,
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
return {
|
|
667
|
-
filesProcessed: results.successful,
|
|
668
|
-
filesFailed: results.failed,
|
|
669
|
-
totalRecords: results.totalRecords,
|
|
670
|
-
};
|
|
671
|
-
})
|
|
672
|
-
);
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
### Backfill Strategies
|
|
676
|
-
|
|
677
|
-
Handle missed files or catch-up processing:
|
|
678
|
-
|
|
679
|
-
```typescript
|
|
680
|
-
async function backfillMissedFiles(startDate: Date, endDate: Date, kv: any): Promise<void> {
|
|
681
|
-
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
682
|
-
service: 'backfill',
|
|
683
|
-
correlationId: generateCorrelationId()
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
const stateService = new StateService(logger);
|
|
687
|
-
const kvAdapter = new VersoriKVAdapter(kv);
|
|
688
|
-
|
|
689
|
-
const s3 = new S3DataSource(
|
|
690
|
-
{
|
|
691
|
-
type: 'S3_CSV',
|
|
692
|
-
connectionId: 's3-backfill',
|
|
693
|
-
name: 'S3 Backfill',
|
|
694
|
-
s3Config: s3Config,
|
|
695
|
-
},
|
|
696
|
-
logger
|
|
697
|
-
);
|
|
698
|
-
|
|
699
|
-
console.log(`Backfilling files from ${startDate} to ${endDate}`);
|
|
700
|
-
|
|
701
|
-
// List all files in date range
|
|
702
|
-
const allFiles = await s3.listFiles({ prefix: 'data/' });
|
|
703
|
-
|
|
704
|
-
const filesInRange = allFiles.filter(file => {
|
|
705
|
-
const fileDate = extractDateFromKey(file.path);
|
|
706
|
-
return fileDate >= startDate && fileDate <= endDate;
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
console.log(`Found ${filesInRange.length} files in date range`);
|
|
710
|
-
|
|
711
|
-
// Check processing status
|
|
712
|
-
const unprocessedFiles = [];
|
|
713
|
-
for (const file of filesInRange) {
|
|
714
|
-
if (!(await stateService.isFileProcessed(kvAdapter, file.path))) {
|
|
715
|
-
unprocessedFiles.push(file);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
console.log(`${unprocessedFiles.length} files need processing`);
|
|
720
|
-
|
|
721
|
-
// Process with lower concurrency to avoid overwhelming system
|
|
722
|
-
await processFilesInParallel(unprocessedFiles, 2); // Only 2 concurrent
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function extractDateFromKey(key: string): Date {
|
|
726
|
-
// Extract date from key like "data/inventory-2025-01-15.csv"
|
|
727
|
-
const match = key.match(/(\d{4}-\d{2}-\d{2})/);
|
|
728
|
-
return match ? new Date(match[1]) : new Date(0);
|
|
729
|
-
}
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
## Data Quality and Validation
|
|
733
|
-
|
|
734
|
-
### Pre-Ingestion Data Quality Checks
|
|
735
|
-
|
|
736
|
-
```typescript
|
|
737
|
-
interface DataQualityRule {
|
|
738
|
-
name: string;
|
|
739
|
-
validate: (record: any) => { isValid: boolean; error?: string };
|
|
740
|
-
severity: 'error' | 'warning';
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
class DataQualityValidator {
|
|
744
|
-
private rules: DataQualityRule[] = [];
|
|
745
|
-
|
|
746
|
-
addRule(rule: DataQualityRule): void {
|
|
747
|
-
this.rules.push(rule);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
validate(records: any[]): {
|
|
751
|
-
passed: any[];
|
|
752
|
-
failed: Array<{ record: any; errors: string[] }>;
|
|
753
|
-
warnings: Array<{ record: any; warnings: string[] }>;
|
|
754
|
-
} {
|
|
755
|
-
const passed: any[] = [];
|
|
756
|
-
const failed: Array<{ record: any; errors: string[] }> = [];
|
|
757
|
-
const warnings: Array<{ record: any; warnings: string[] }> = [];
|
|
758
|
-
|
|
759
|
-
for (const record of records) {
|
|
760
|
-
const errors: string[] = [];
|
|
761
|
-
const warns: string[] = [];
|
|
762
|
-
|
|
763
|
-
for (const rule of this.rules) {
|
|
764
|
-
const result = rule.validate(record);
|
|
765
|
-
|
|
766
|
-
if (!result.isValid) {
|
|
767
|
-
if (rule.severity === 'error') {
|
|
768
|
-
errors.push(`${rule.name}: ${result.error}`);
|
|
769
|
-
} else {
|
|
770
|
-
warns.push(`${rule.name}: ${result.error}`);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
if (errors.length > 0) {
|
|
776
|
-
failed.push({ record, errors });
|
|
777
|
-
} else {
|
|
778
|
-
passed.push(record);
|
|
779
|
-
|
|
780
|
-
if (warns.length > 0) {
|
|
781
|
-
warnings.push({ record, warnings: warns });
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
return { passed, failed, warnings };
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Define quality rules
|
|
791
|
-
const qualityValidator = new DataQualityValidator();
|
|
792
|
-
|
|
793
|
-
qualityValidator.addRule({
|
|
794
|
-
name: 'Required Fields',
|
|
795
|
-
severity: 'error',
|
|
796
|
-
validate: record => {
|
|
797
|
-
const required = ['sku', 'warehouse', 'quantity'];
|
|
798
|
-
const missing = required.filter(field => !record[field]);
|
|
799
|
-
|
|
800
|
-
if (missing.length > 0) {
|
|
801
|
-
return {
|
|
802
|
-
isValid: false,
|
|
803
|
-
error: `Missing required fields: ${missing.join(', ')}`,
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
return { isValid: true };
|
|
808
|
-
},
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
qualityValidator.addRule({
|
|
812
|
-
name: 'Quantity Range',
|
|
813
|
-
severity: 'error',
|
|
814
|
-
validate: record => {
|
|
815
|
-
const qty = parseInt(record.quantity, 10);
|
|
816
|
-
|
|
817
|
-
if (isNaN(qty) || qty < 0) {
|
|
818
|
-
return {
|
|
819
|
-
isValid: false,
|
|
820
|
-
error: `Invalid quantity: ${record.quantity}`,
|
|
821
|
-
};
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
return { isValid: true };
|
|
825
|
-
},
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
qualityValidator.addRule({
|
|
829
|
-
name: 'SKU Format',
|
|
830
|
-
severity: 'error',
|
|
831
|
-
validate: record => {
|
|
832
|
-
const skuPattern = /^SKU-[A-Z0-9]{2,10}$/;
|
|
833
|
-
|
|
834
|
-
if (!skuPattern.test(record.sku)) {
|
|
835
|
-
return {
|
|
836
|
-
isValid: false,
|
|
837
|
-
error: `Invalid SKU format: ${record.sku}`,
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
return { isValid: true };
|
|
842
|
-
},
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
qualityValidator.addRule({
|
|
846
|
-
name: 'Suspiciously Large Quantity',
|
|
847
|
-
severity: 'warning',
|
|
848
|
-
validate: record => {
|
|
849
|
-
const qty = parseInt(record.quantity, 10);
|
|
850
|
-
|
|
851
|
-
if (qty > 100000) {
|
|
852
|
-
return {
|
|
853
|
-
isValid: false,
|
|
854
|
-
error: `Unusually large quantity: ${qty}`,
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
return { isValid: true };
|
|
859
|
-
},
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
// Usage
|
|
863
|
-
const records = await parseCSV(fileContent);
|
|
864
|
-
const validation = qualityValidator.validate(records);
|
|
865
|
-
|
|
866
|
-
console.log(`Passed: ${validation.passed.length}`);
|
|
867
|
-
console.log(`Failed: ${validation.failed.length}`);
|
|
868
|
-
console.log(`Warnings: ${validation.warnings.length}`);
|
|
869
|
-
|
|
870
|
-
// Log failures
|
|
871
|
-
if (validation.failed.length > 0) {
|
|
872
|
-
console.error('Data quality failures:');
|
|
873
|
-
validation.failed.forEach(({ record, errors }) => {
|
|
874
|
-
console.error(` SKU ${record.sku}:`, errors);
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
// Save failed records for review
|
|
878
|
-
await saveDataQualityReport(validation.failed);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Proceed with passed records only
|
|
882
|
-
await processRecords(validation.passed);
|
|
883
|
-
```
|
|
884
|
-
|
|
885
|
-
### Reconciliation Reports
|
|
886
|
-
|
|
887
|
-
```typescript
|
|
888
|
-
interface ReconciliationReport {
|
|
889
|
-
fileKey: string;
|
|
890
|
-
processedAt: string;
|
|
891
|
-
totalRecords: number;
|
|
892
|
-
successfulRecords: number;
|
|
893
|
-
failedRecords: number;
|
|
894
|
-
duplicateRecords: number;
|
|
895
|
-
dataQualityIssues: number;
|
|
896
|
-
batchesCreated: number;
|
|
897
|
-
processingTimeMs: number;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
async function generateReconciliationReport(
|
|
901
|
-
fileKey: string,
|
|
902
|
-
results: ProcessingResult
|
|
903
|
-
): Promise<ReconciliationReport> {
|
|
904
|
-
return {
|
|
905
|
-
fileKey,
|
|
906
|
-
processedAt: new Date().toISOString(),
|
|
907
|
-
totalRecords: results.totalRecords,
|
|
908
|
-
successfulRecords: results.successful,
|
|
909
|
-
failedRecords: results.failed,
|
|
910
|
-
duplicateRecords: results.duplicates || 0,
|
|
911
|
-
dataQualityIssues: results.qualityIssues || 0,
|
|
912
|
-
batchesCreated: results.batches,
|
|
913
|
-
processingTimeMs: results.processingTime,
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// Save report to S3
|
|
918
|
-
async function saveReconciliationReport(report: ReconciliationReport): Promise<void> {
|
|
919
|
-
const s3 = new S3DataSource(
|
|
920
|
-
{
|
|
921
|
-
type: 'S3_JSON',
|
|
922
|
-
connectionId: 's3-reports',
|
|
923
|
-
name: 'S3 Reports',
|
|
924
|
-
s3Config: s3Config,
|
|
925
|
-
},
|
|
926
|
-
logger
|
|
927
|
-
);
|
|
928
|
-
const reportKey = `reports/reconciliation/${report.fileKey}-${Date.now()}.json`;
|
|
929
|
-
|
|
930
|
-
await s3.uploadFile(reportKey, JSON.stringify(report, null, 2));
|
|
931
|
-
|
|
932
|
-
console.log(`Reconciliation report saved: ${reportKey}`);
|
|
933
|
-
}
|
|
934
|
-
```
|
|
935
|
-
|
|
936
|
-
## Security Best Practices
|
|
937
|
-
|
|
938
|
-
### Credential Management
|
|
939
|
-
|
|
940
|
-
```typescript
|
|
941
|
-
// ✅ CORRECT - Use environment variables
|
|
942
|
-
const config = {
|
|
943
|
-
fluent: {
|
|
944
|
-
baseUrl: process.env.FLUENT_BASE_URL!,
|
|
945
|
-
clientId: process.env.FLUENT_CLIENT_ID!,
|
|
946
|
-
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
|
|
947
|
-
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
948
|
-
},
|
|
949
|
-
s3: {
|
|
950
|
-
region: process.env.AWS_REGION!,
|
|
951
|
-
credentials: {
|
|
952
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
953
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
954
|
-
},
|
|
955
|
-
},
|
|
956
|
-
};
|
|
957
|
-
|
|
958
|
-
// ❌ WRONG - Never hardcode credentials
|
|
959
|
-
const badConfig = {
|
|
960
|
-
fluent: {
|
|
961
|
-
clientId: 'hardcoded-client-id', // ❌ Don't do this
|
|
962
|
-
clientSecret: 'hardcoded-secret', // ❌ Security risk
|
|
963
|
-
},
|
|
964
|
-
};
|
|
965
|
-
```
|
|
966
|
-
|
|
967
|
-
### Secrets Manager Integration
|
|
968
|
-
|
|
969
|
-
```typescript
|
|
970
|
-
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
|
|
971
|
-
|
|
972
|
-
async function loadSecretsFromAWS(): Promise<any> {
|
|
973
|
-
const client = new SecretsManagerClient({ region: 'us-east-1' });
|
|
974
|
-
|
|
975
|
-
const command = new GetSecretValueCommand({
|
|
976
|
-
SecretId: 'fluent-commerce/production',
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
const response = await client.send(command);
|
|
980
|
-
return JSON.parse(response.SecretString!);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// Usage
|
|
984
|
-
const secrets = await loadSecretsFromAWS();
|
|
985
|
-
|
|
986
|
-
const client = await createClient({
|
|
987
|
-
config: {
|
|
988
|
-
baseUrl: secrets.FLUENT_BASE_URL,
|
|
989
|
-
clientId: secrets.FLUENT_CLIENT_ID,
|
|
990
|
-
clientSecret: secrets.FLUENT_CLIENT_SECRET,
|
|
991
|
-
retailerId: secrets.FLUENT_RETAILER_ID,
|
|
992
|
-
}
|
|
993
|
-
});
|
|
994
|
-
```
|
|
995
|
-
|
|
996
|
-
### Data Encryption
|
|
997
|
-
|
|
998
|
-
```typescript
|
|
999
|
-
// Encrypt sensitive data before storing
|
|
1000
|
-
import * as crypto from 'crypto';
|
|
1001
|
-
|
|
1002
|
-
function encryptSensitiveData(data: string, key: string): string {
|
|
1003
|
-
const iv = crypto.randomBytes(16);
|
|
1004
|
-
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
|
|
1005
|
-
|
|
1006
|
-
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
1007
|
-
encrypted += cipher.final('hex');
|
|
1008
|
-
|
|
1009
|
-
return iv.toString('hex') + ':' + encrypted;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
function decryptSensitiveData(encrypted: string, key: string): string {
|
|
1013
|
-
const parts = encrypted.split(':');
|
|
1014
|
-
const iv = Buffer.from(parts[0], 'hex');
|
|
1015
|
-
const encryptedData = parts[1];
|
|
1016
|
-
|
|
1017
|
-
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
|
|
1018
|
-
|
|
1019
|
-
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
|
1020
|
-
decrypted += decipher.final('utf8');
|
|
1021
|
-
|
|
1022
|
-
return decrypted;
|
|
1023
|
-
}
|
|
1024
|
-
```
|
|
1025
|
-
|
|
1026
|
-
### Audit Logging
|
|
1027
|
-
|
|
1028
|
-
```typescript
|
|
1029
|
-
interface AuditLog {
|
|
1030
|
-
timestamp: string;
|
|
1031
|
-
operation: string;
|
|
1032
|
-
user: string;
|
|
1033
|
-
resource: string;
|
|
1034
|
-
status: 'success' | 'failure';
|
|
1035
|
-
details: Record<string, any>;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
async function logAuditEvent(event: Omit<AuditLog, 'timestamp'>): Promise<void> {
|
|
1039
|
-
const auditLog: AuditLog = {
|
|
1040
|
-
timestamp: new Date().toISOString(),
|
|
1041
|
-
...event,
|
|
1042
|
-
};
|
|
1043
|
-
|
|
1044
|
-
// Log to CloudWatch, Datadog, or custom logging service
|
|
1045
|
-
console.log('[AUDIT]', JSON.stringify(auditLog));
|
|
1046
|
-
|
|
1047
|
-
// Optionally save to S3 for long-term retention
|
|
1048
|
-
await saveAuditLog(auditLog);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Usage
|
|
1052
|
-
await logAuditEvent({
|
|
1053
|
-
operation: 'INVENTORY_INGESTION',
|
|
1054
|
-
user: process.env.USER || 'system',
|
|
1055
|
-
resource: `s3://bucket/file.csv`,
|
|
1056
|
-
status: 'success',
|
|
1057
|
-
details: {
|
|
1058
|
-
recordsProcessed: 1000,
|
|
1059
|
-
jobId: 'job-123',
|
|
1060
|
-
duration: 5000,
|
|
1061
|
-
},
|
|
1062
|
-
});
|
|
1063
|
-
```
|
|
1064
|
-
|
|
1065
|
-
## Monitoring and Alerting
|
|
1066
|
-
|
|
1067
|
-
### Metrics Tracking
|
|
1068
|
-
|
|
1069
|
-
```typescript
|
|
1070
|
-
interface IngestionMetrics {
|
|
1071
|
-
timestamp: string;
|
|
1072
|
-
filesProcessed: number;
|
|
1073
|
-
recordsIngested: number;
|
|
1074
|
-
failedRecords: number;
|
|
1075
|
-
processingTimeMs: number;
|
|
1076
|
-
avgRecordsPerSecond: number;
|
|
1077
|
-
errorRate: number;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
class MetricsCollector {
|
|
1081
|
-
private metrics: IngestionMetrics[] = [];
|
|
1082
|
-
|
|
1083
|
-
recordIngestion(result: IngestionResult): void {
|
|
1084
|
-
const metrics: IngestionMetrics = {
|
|
1085
|
-
timestamp: new Date().toISOString(),
|
|
1086
|
-
filesProcessed: result.filesProcessed,
|
|
1087
|
-
recordsIngested: result.recordsIngested,
|
|
1088
|
-
failedRecords: result.failedRecords,
|
|
1089
|
-
processingTimeMs: result.processingTimeMs,
|
|
1090
|
-
avgRecordsPerSecond: result.recordsIngested / (result.processingTimeMs / 1000),
|
|
1091
|
-
errorRate: result.failedRecords / result.recordsIngested
|
|
1092
|
-
};
|
|
1093
|
-
|
|
1094
|
-
this.metrics.push(metrics);
|
|
1095
|
-
|
|
1096
|
-
// Alert on high error rate
|
|
1097
|
-
if (metrics.errorRate > 0.05) { // > 5% error rate
|
|
1098
|
-
this.sendAlert('High error rate detected', metrics);
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Alert on slow processing
|
|
1102
|
-
if (metrics.avgRecordsPerSecond < 10) {
|
|
1103
|
-
this.sendAlert('Slow processing speed', metrics);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
private sendAlert(message: string, metrics: IngestionMetrics): void {
|
|
1108
|
-
console.error(\`[ALERT] \${message}\`, metrics);
|
|
1109
|
-
// Send to monitoring service (e.g., Datadog, New Relic)
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
```
|
|
1113
|
-
|
|
1114
|
-
### Health Checks
|
|
1115
|
-
|
|
1116
|
-
```typescript
|
|
1117
|
-
async function performHealthCheck(): Promise<HealthCheckResult> {
|
|
1118
|
-
const checks = [];
|
|
1119
|
-
|
|
1120
|
-
// Check Fluent API connectivity
|
|
1121
|
-
try {
|
|
1122
|
-
await client.graphql({
|
|
1123
|
-
query: '{ __typename }'
|
|
1124
|
-
});
|
|
1125
|
-
checks.push({ name: 'Fluent API', status: 'healthy' });
|
|
1126
|
-
} catch (error) {
|
|
1127
|
-
checks.push({ name: 'Fluent API', status: 'unhealthy', error: error.message });
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// Check S3 connectivity
|
|
1131
|
-
try {
|
|
1132
|
-
await s3.listFiles({ prefix: 'health-check/', maxKeys: 1 });
|
|
1133
|
-
checks.push({ name: 'S3', status: 'healthy' });
|
|
1134
|
-
} catch (error) {
|
|
1135
|
-
checks.push({ name: 'S3', status: 'unhealthy', error: error.message });
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
// Check state storage
|
|
1139
|
-
try {
|
|
1140
|
-
await state.set('health-check', Date.now());
|
|
1141
|
-
await state.get('health-check');
|
|
1142
|
-
checks.push({ name: 'State Storage', status: 'healthy' });
|
|
1143
|
-
} catch (error) {
|
|
1144
|
-
checks.push({ name: 'State Storage', status: 'unhealthy', error: error.message });
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
const allHealthy = checks.every(c => c.status === 'healthy');
|
|
1148
|
-
|
|
1149
|
-
return {
|
|
1150
|
-
status: allHealthy ? 'healthy' : 'degraded',
|
|
1151
|
-
checks,
|
|
1152
|
-
timestamp: new Date().toISOString(),
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
```
|
|
1156
|
-
|
|
1157
|
-
## Complete Production Pipeline Example
|
|
1158
|
-
|
|
1159
|
-
Putting all best practices together into a production-ready ingestion pipeline:
|
|
1160
|
-
|
|
1161
|
-
```typescript
|
|
1162
|
-
import {
|
|
1163
|
-
createClient,
|
|
1164
|
-
S3DataSource,
|
|
1165
|
-
CSVParserService,
|
|
1166
|
-
UniversalMapper,
|
|
1167
|
-
StateService,
|
|
1168
|
-
VersoriKVAdapter,
|
|
1169
|
-
FluentClient,
|
|
1170
|
-
FileParsingError,
|
|
1171
|
-
MappingError,
|
|
1172
|
-
BatchAPIError,
|
|
1173
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
1174
|
-
import * as winston from 'winston';
|
|
1175
|
-
|
|
1176
|
-
/**
|
|
1177
|
-
* Production-grade ingestion pipeline with all best practices
|
|
1178
|
-
* - Error handling with retry and circuit breaker
|
|
1179
|
-
* - Comprehensive monitoring and logging
|
|
1180
|
-
* - Security best practices
|
|
1181
|
-
* - Data quality validation
|
|
1182
|
-
* - State management
|
|
1183
|
-
*/
|
|
1184
|
-
class ProductionIngestionPipeline {
|
|
1185
|
-
private client: FluentClient;
|
|
1186
|
-
private s3: S3DataSource;
|
|
1187
|
-
private parser: CSVParserService;
|
|
1188
|
-
private mapper: UniversalMapper;
|
|
1189
|
-
private state: StateService;
|
|
1190
|
-
private kv: KVStore;
|
|
1191
|
-
private logger: winston.Logger;
|
|
1192
|
-
private circuitBreaker: CircuitBreaker;
|
|
1193
|
-
private dlq: DeadLetterQueue;
|
|
1194
|
-
private metrics: MetricsCollector;
|
|
1195
|
-
private qualityValidator: DataQualityValidator;
|
|
1196
|
-
|
|
1197
|
-
constructor(private config: ProductionConfig) {}
|
|
1198
|
-
|
|
1199
|
-
async initialize(): Promise<void> {
|
|
1200
|
-
// Setup structured logging
|
|
1201
|
-
this.logger = winston.createLogger({
|
|
1202
|
-
level: 'info',
|
|
1203
|
-
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
|
1204
|
-
transports: [
|
|
1205
|
-
new winston.transports.Console(),
|
|
1206
|
-
new winston.transports.File({ filename: 'ingestion-error.log', level: 'error' }),
|
|
1207
|
-
new winston.transports.File({ filename: 'ingestion-combined.log' }),
|
|
1208
|
-
],
|
|
1209
|
-
});
|
|
1210
|
-
|
|
1211
|
-
this.logger.info('Initializing production ingestion pipeline');
|
|
1212
|
-
|
|
1213
|
-
// Initialize Fluent client
|
|
1214
|
-
this.client = await createClient({ config: this.config.fluent });
|
|
1215
|
-
|
|
1216
|
-
// Initialize data sources
|
|
1217
|
-
this.s3 = new S3DataSource(
|
|
1218
|
-
{
|
|
1219
|
-
type: 'S3_CSV',
|
|
1220
|
-
connectionId: 's3-production',
|
|
1221
|
-
name: 'S3 Production',
|
|
1222
|
-
s3Config: this.config.s3,
|
|
1223
|
-
},
|
|
1224
|
-
this.logger
|
|
1225
|
-
);
|
|
1226
|
-
this.parser = new CSVParserService();
|
|
1227
|
-
|
|
1228
|
-
// Initialize field mapper with custom resolvers
|
|
1229
|
-
this.mapper = new UniversalMapper(this.config.mapping, {
|
|
1230
|
-
customResolvers: this.config.customResolvers || {},
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1233
|
-
// Initialize state management
|
|
1234
|
-
const logger = toStructuredLogger(this.logger, {
|
|
1235
|
-
service: 'production-pipeline',
|
|
1236
|
-
correlationId: generateCorrelationId()
|
|
1237
|
-
});
|
|
1238
|
-
|
|
1239
|
-
this.kv = new VersoriKVAdapter(this.config.kv);
|
|
1240
|
-
this.stateService = new StateService(logger);
|
|
1241
|
-
|
|
1242
|
-
// Initialize circuit breaker
|
|
1243
|
-
this.circuitBreaker = new CircuitBreaker({
|
|
1244
|
-
failureThreshold: 5,
|
|
1245
|
-
resetTimeoutMs: 60000,
|
|
1246
|
-
monitoringWindowMs: 120000,
|
|
1247
|
-
});
|
|
1248
|
-
|
|
1249
|
-
// Initialize dead letter queue
|
|
1250
|
-
this.dlq = new DeadLetterQueue(this.s3, this.config.dlqBucket, 'dlq/');
|
|
1251
|
-
|
|
1252
|
-
// Initialize metrics collector
|
|
1253
|
-
this.metrics = new MetricsCollector();
|
|
1254
|
-
|
|
1255
|
-
// Initialize data quality validator
|
|
1256
|
-
this.qualityValidator = this.createQualityValidator();
|
|
1257
|
-
|
|
1258
|
-
this.logger.info('Pipeline initialization complete');
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
/**
|
|
1262
|
-
* Run the complete ingestion pipeline
|
|
1263
|
-
*/
|
|
1264
|
-
async run(bucket: string, prefix: string): Promise<ExecutionReport> {
|
|
1265
|
-
const startTime = Date.now();
|
|
1266
|
-
|
|
1267
|
-
try {
|
|
1268
|
-
this.logger.info('Starting ingestion pipeline', { bucket, prefix });
|
|
1269
|
-
|
|
1270
|
-
// Health check before starting
|
|
1271
|
-
const healthCheck = await this.performHealthCheck();
|
|
1272
|
-
if (healthCheck.status !== 'healthy') {
|
|
1273
|
-
throw new Error(`System unhealthy: ${JSON.stringify(healthCheck.checks)}`);
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// List files
|
|
1277
|
-
const allFiles = await this.s3.listFiles({ prefix });
|
|
1278
|
-
this.logger.info(`Found ${allFiles.length} total files`);
|
|
1279
|
-
|
|
1280
|
-
// Filter unprocessed files
|
|
1281
|
-
const unprocessedFiles = await this.filterUnprocessedFiles(allFiles);
|
|
1282
|
-
this.logger.info(`${unprocessedFiles.length} files to process`);
|
|
1283
|
-
|
|
1284
|
-
if (unprocessedFiles.length === 0) {
|
|
1285
|
-
return this.createReport(startTime, { filesProcessed: 0 });
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
// Process files with circuit breaker
|
|
1289
|
-
const results = await this.circuitBreaker.execute(async () => {
|
|
1290
|
-
return await this.processFilesWithRetry(bucket, unprocessedFiles);
|
|
1291
|
-
});
|
|
1292
|
-
|
|
1293
|
-
// Generate report
|
|
1294
|
-
const report = this.createReport(startTime, results);
|
|
1295
|
-
|
|
1296
|
-
// Log metrics
|
|
1297
|
-
this.metrics.recordIngestion({
|
|
1298
|
-
filesProcessed: results.filesProcessed,
|
|
1299
|
-
recordsIngested: results.recordsIngested,
|
|
1300
|
-
failedRecords: results.failedRecords,
|
|
1301
|
-
processingTimeMs: report.totalTimeMs,
|
|
1302
|
-
avgRecordsPerSecond: report.throughput,
|
|
1303
|
-
errorRate: results.failedRecords / (results.recordsIngested || 1),
|
|
1304
|
-
});
|
|
1305
|
-
|
|
1306
|
-
this.logger.info('Pipeline execution complete', report);
|
|
1307
|
-
|
|
1308
|
-
return report;
|
|
1309
|
-
} catch (error) {
|
|
1310
|
-
this.logger.error('Pipeline execution failed', {
|
|
1311
|
-
error: error.message,
|
|
1312
|
-
stack: error.stack,
|
|
1313
|
-
});
|
|
1314
|
-
|
|
1315
|
-
await this.sendAlert('CRITICAL: Ingestion pipeline failure', error);
|
|
1316
|
-
throw error;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
/**
|
|
1321
|
-
* Process files with retry logic
|
|
1322
|
-
*/
|
|
1323
|
-
private async processFilesWithRetry(
|
|
1324
|
-
bucket: string,
|
|
1325
|
-
files: Array<{ key: string }>
|
|
1326
|
-
): Promise<ProcessingResults> {
|
|
1327
|
-
const results: ProcessingResults = {
|
|
1328
|
-
filesProcessed: 0,
|
|
1329
|
-
filesFailed: 0,
|
|
1330
|
-
recordsIngested: 0,
|
|
1331
|
-
failedRecords: 0,
|
|
1332
|
-
details: [],
|
|
1333
|
-
};
|
|
1334
|
-
|
|
1335
|
-
for (const file of files) {
|
|
1336
|
-
const maxRetries = 3;
|
|
1337
|
-
let attempt = 0;
|
|
1338
|
-
let success = false;
|
|
1339
|
-
|
|
1340
|
-
while (attempt < maxRetries && !success) {
|
|
1341
|
-
attempt++;
|
|
1342
|
-
|
|
1343
|
-
try {
|
|
1344
|
-
const fileResult = await this.processFile(bucket, file.path);
|
|
1345
|
-
|
|
1346
|
-
results.filesProcessed++;
|
|
1347
|
-
results.recordsIngested += fileResult.recordsProcessed;
|
|
1348
|
-
results.details.push({
|
|
1349
|
-
file: file.path,
|
|
1350
|
-
status: 'success',
|
|
1351
|
-
records: fileResult.recordsProcessed,
|
|
1352
|
-
});
|
|
1353
|
-
|
|
1354
|
-
success = true;
|
|
1355
|
-
|
|
1356
|
-
this.logger.info(`File processed successfully`, {
|
|
1357
|
-
file: file.path,
|
|
1358
|
-
records: fileResult.recordsProcessed,
|
|
1359
|
-
attempt,
|
|
1360
|
-
});
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
this.logger.warn(`File processing attempt ${attempt} failed`, {
|
|
1363
|
-
file: file.path,
|
|
1364
|
-
error: error.message,
|
|
1365
|
-
});
|
|
1366
|
-
|
|
1367
|
-
if (attempt < maxRetries && this.isRetryable(error)) {
|
|
1368
|
-
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
1369
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1370
|
-
} else {
|
|
1371
|
-
results.filesFailed++;
|
|
1372
|
-
results.details.push({
|
|
1373
|
-
file: file.path,
|
|
1374
|
-
status: 'failed',
|
|
1375
|
-
error: error.message,
|
|
1376
|
-
});
|
|
1377
|
-
|
|
1378
|
-
this.logger.error(`File processing failed permanently`, {
|
|
1379
|
-
file: file.path,
|
|
1380
|
-
attempts: attempt,
|
|
1381
|
-
error: error.message,
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
return results;
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
/**
|
|
1392
|
-
* Process a single file with all validations
|
|
1393
|
-
*/
|
|
1394
|
-
private async processFile(bucket: string, fileKey: string): Promise<FileProcessingResult> {
|
|
1395
|
-
this.logger.info(`Processing file: ${fileKey}`);
|
|
1396
|
-
|
|
1397
|
-
try {
|
|
1398
|
-
// Read file
|
|
1399
|
-
const fileContent = await this.s3.downloadFile(fileKey);
|
|
1400
|
-
|
|
1401
|
-
// Parse CSV
|
|
1402
|
-
const records = await this.parser.parse(fileContent);
|
|
1403
|
-
this.logger.debug(`Parsed ${records.length} records from ${fileKey}`);
|
|
1404
|
-
|
|
1405
|
-
// Data quality validation
|
|
1406
|
-
const validation = this.qualityValidator.validate(records);
|
|
1407
|
-
|
|
1408
|
-
if (validation.failed.length > 0) {
|
|
1409
|
-
this.logger.warn(`Data quality issues found`, {
|
|
1410
|
-
file: fileKey,
|
|
1411
|
-
failed: validation.failed.length,
|
|
1412
|
-
warnings: validation.warnings.length,
|
|
1413
|
-
});
|
|
1414
|
-
|
|
1415
|
-
// Save failed records to DLQ
|
|
1416
|
-
for (const { record, errors } of validation.failed) {
|
|
1417
|
-
await this.dlq.save({
|
|
1418
|
-
originalFile: fileKey,
|
|
1419
|
-
failedAt: new Date().toISOString(),
|
|
1420
|
-
errorType: 'DATA_QUALITY_ERROR',
|
|
1421
|
-
errorMessage: errors.join('; '),
|
|
1422
|
-
recordData: record,
|
|
1423
|
-
retryCount: 0,
|
|
1424
|
-
metadata: {},
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
// Proceed with passed records only
|
|
1430
|
-
if (validation.passed.length === 0) {
|
|
1431
|
-
throw new Error('No valid records after quality validation');
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// Field mapping
|
|
1435
|
-
const mappingResult = await this.mapper.map(validation.passed);
|
|
1436
|
-
|
|
1437
|
-
if (!mappingResult.success) {
|
|
1438
|
-
throw new MappingError(`Field mapping failed: ${mappingResult.errors.join(', ')}`);
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// Create job
|
|
1442
|
-
const job = await this.client.createJob({
|
|
1443
|
-
name: `Production Import - ${fileKey}`,
|
|
1444
|
-
retailerId: this.config.fluent.retailerId,
|
|
1445
|
-
metadata: {
|
|
1446
|
-
fileName: fileKey,
|
|
1447
|
-
recordCount: mappingResult.data.length,
|
|
1448
|
-
pipeline: 'production',
|
|
1449
|
-
},
|
|
1450
|
-
});
|
|
1451
|
-
|
|
1452
|
-
// Send to Batch API
|
|
1453
|
-
await this.client.sendBatch(job.id, {
|
|
1454
|
-
action: 'UPSERT',
|
|
1455
|
-
entityType: 'INVENTORY',
|
|
1456
|
-
entities: mappingResult.data,
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
// Mark as processed
|
|
1460
|
-
await this.state.markFileProcessed(fileKey, {
|
|
1461
|
-
jobId: job.id,
|
|
1462
|
-
recordCount: mappingResult.data.length,
|
|
1463
|
-
timestamp: new Date().toISOString(),
|
|
1464
|
-
});
|
|
1465
|
-
|
|
1466
|
-
// Audit log
|
|
1467
|
-
await this.logAuditEvent({
|
|
1468
|
-
operation: 'INVENTORY_INGESTION',
|
|
1469
|
-
user: 'system',
|
|
1470
|
-
resource: `s3://${bucket}/${fileKey}`,
|
|
1471
|
-
status: 'success',
|
|
1472
|
-
details: {
|
|
1473
|
-
recordsProcessed: mappingResult.data.length,
|
|
1474
|
-
jobId: job.id,
|
|
1475
|
-
},
|
|
1476
|
-
});
|
|
1477
|
-
|
|
1478
|
-
return {
|
|
1479
|
-
recordsProcessed: mappingResult.data.length,
|
|
1480
|
-
jobId: job.id,
|
|
1481
|
-
};
|
|
1482
|
-
} catch (error) {
|
|
1483
|
-
// Audit log failure
|
|
1484
|
-
await this.logAuditEvent({
|
|
1485
|
-
operation: 'INVENTORY_INGESTION',
|
|
1486
|
-
user: 'system',
|
|
1487
|
-
resource: `s3://${bucket}/${fileKey}`,
|
|
1488
|
-
status: 'failure',
|
|
1489
|
-
details: {
|
|
1490
|
-
error: error.message,
|
|
1491
|
-
},
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
|
-
throw error;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
/**
|
|
1499
|
-
* Check if error is retryable
|
|
1500
|
-
*/
|
|
1501
|
-
private isRetryable(error: any): boolean {
|
|
1502
|
-
if (error instanceof FileParsingError) {
|
|
1503
|
-
return false; // File format errors are not retryable
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
if (error instanceof BatchAPIError) {
|
|
1507
|
-
return error.isRetryable;
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// Network errors are retryable
|
|
1511
|
-
return (
|
|
1512
|
-
error.code === 'NETWORK_ERROR' ||
|
|
1513
|
-
error.message.includes('timeout') ||
|
|
1514
|
-
error.message.includes('ECONNRESET')
|
|
1515
|
-
);
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
/**
|
|
1519
|
-
* Filter unprocessed files
|
|
1520
|
-
*/
|
|
1521
|
-
private async filterUnprocessedFiles(
|
|
1522
|
-
files: FileMetadata[]
|
|
1523
|
-
): Promise<FileMetadata[]> {
|
|
1524
|
-
const unprocessed = [];
|
|
1525
|
-
|
|
1526
|
-
for (const file of files) {
|
|
1527
|
-
if (!(await this.state.isFileProcessed(this.kv, file.path))) {
|
|
1528
|
-
unprocessed.push(file);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
return unprocessed;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
/**
|
|
1536
|
-
* Create data quality validator
|
|
1537
|
-
*/
|
|
1538
|
-
private createQualityValidator(): DataQualityValidator {
|
|
1539
|
-
const validator = new DataQualityValidator();
|
|
1540
|
-
|
|
1541
|
-
// Add validation rules based on configuration
|
|
1542
|
-
validator.addRule({
|
|
1543
|
-
name: 'Required Fields',
|
|
1544
|
-
severity: 'error',
|
|
1545
|
-
validate: record => {
|
|
1546
|
-
const required = this.config.requiredFields || [];
|
|
1547
|
-
const missing = required.filter(field => !record[field]);
|
|
1548
|
-
|
|
1549
|
-
if (missing.length > 0) {
|
|
1550
|
-
return {
|
|
1551
|
-
isValid: false,
|
|
1552
|
-
error: `Missing required fields: ${missing.join(', ')}`,
|
|
1553
|
-
};
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
return { isValid: true };
|
|
1557
|
-
},
|
|
1558
|
-
});
|
|
1559
|
-
|
|
1560
|
-
return validator;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
/**
|
|
1564
|
-
* Perform system health check
|
|
1565
|
-
*/
|
|
1566
|
-
private async performHealthCheck(): Promise<HealthCheckResult> {
|
|
1567
|
-
const checks = [];
|
|
1568
|
-
|
|
1569
|
-
// Check Fluent API
|
|
1570
|
-
try {
|
|
1571
|
-
await this.client.graphql({
|
|
1572
|
-
query: '{ __typename }'
|
|
1573
|
-
});
|
|
1574
|
-
checks.push({ name: 'Fluent API', status: 'healthy' });
|
|
1575
|
-
} catch (error) {
|
|
1576
|
-
checks.push({ name: 'Fluent API', status: 'unhealthy', error: error.message });
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
// Check S3
|
|
1580
|
-
try {
|
|
1581
|
-
await this.s3.listObjects(this.config.s3Bucket, 'health-check/');
|
|
1582
|
-
checks.push({ name: 'S3', status: 'healthy' });
|
|
1583
|
-
} catch (error) {
|
|
1584
|
-
checks.push({ name: 'S3', status: 'unhealthy', error: error.message });
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
const allHealthy = checks.every(c => c.status === 'healthy');
|
|
1588
|
-
|
|
1589
|
-
return {
|
|
1590
|
-
status: allHealthy ? 'healthy' : 'degraded',
|
|
1591
|
-
checks,
|
|
1592
|
-
timestamp: new Date().toISOString(),
|
|
1593
|
-
};
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
/**
|
|
1597
|
-
* Create execution report
|
|
1598
|
-
*/
|
|
1599
|
-
private createReport(startTime: number, results: Partial<ProcessingResults>): ExecutionReport {
|
|
1600
|
-
const totalTimeMs = Date.now() - startTime;
|
|
1601
|
-
|
|
1602
|
-
return {
|
|
1603
|
-
startTime: new Date(startTime).toISOString(),
|
|
1604
|
-
endTime: new Date().toISOString(),
|
|
1605
|
-
totalTimeMs,
|
|
1606
|
-
filesProcessed: results.filesProcessed || 0,
|
|
1607
|
-
filesFailed: results.filesFailed || 0,
|
|
1608
|
-
recordsIngested: results.recordsIngested || 0,
|
|
1609
|
-
failedRecords: results.failedRecords || 0,
|
|
1610
|
-
throughput: (results.recordsIngested || 0) / (totalTimeMs / 1000),
|
|
1611
|
-
successRate:
|
|
1612
|
-
((results.filesProcessed || 0) /
|
|
1613
|
-
((results.filesProcessed || 0) + (results.filesFailed || 0))) *
|
|
1614
|
-
100,
|
|
1615
|
-
};
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
/**
|
|
1619
|
-
* Log audit event
|
|
1620
|
-
*/
|
|
1621
|
-
private async logAuditEvent(event: Omit<AuditLog, 'timestamp'>): Promise<void> {
|
|
1622
|
-
const auditLog: AuditLog = {
|
|
1623
|
-
timestamp: new Date().toISOString(),
|
|
1624
|
-
...event,
|
|
1625
|
-
};
|
|
1626
|
-
|
|
1627
|
-
this.logger.info('[AUDIT]', auditLog);
|
|
1628
|
-
|
|
1629
|
-
// Optionally save to S3 for compliance
|
|
1630
|
-
// await this.s3.putObject(auditBucket, auditKey, JSON.stringify(auditLog));
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
/**
|
|
1634
|
-
* Send alert to monitoring service
|
|
1635
|
-
*/
|
|
1636
|
-
private async sendAlert(message: string, error: any): Promise<void> {
|
|
1637
|
-
this.logger.error('[ALERT]', { message, error: error.message });
|
|
1638
|
-
|
|
1639
|
-
// Integration with alerting service (PagerDuty, Slack, etc.)
|
|
1640
|
-
// await alertService.send({ message, error });
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
// Usage
|
|
1645
|
-
const pipeline = new ProductionIngestionPipeline({
|
|
1646
|
-
fluent: {
|
|
1647
|
-
baseUrl: process.env.FLUENT_BASE_URL!,
|
|
1648
|
-
clientId: process.env.FLUENT_CLIENT_ID!,
|
|
1649
|
-
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
|
|
1650
|
-
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
1651
|
-
},
|
|
1652
|
-
s3: {
|
|
1653
|
-
region: process.env.AWS_REGION!,
|
|
1654
|
-
credentials: {
|
|
1655
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
1656
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
1657
|
-
},
|
|
1658
|
-
},
|
|
1659
|
-
s3Bucket: 'inventory-bucket',
|
|
1660
|
-
dlqBucket: 'inventory-dlq-bucket',
|
|
1661
|
-
mapping: {
|
|
1662
|
-
fields: {
|
|
1663
|
-
ref: { source: 'sku', required: true },
|
|
1664
|
-
productRef: { source: 'product_id', required: true },
|
|
1665
|
-
locationRef: { source: 'warehouse_code', required: true },
|
|
1666
|
-
qty: { source: 'quantity', resolver: 'sdk.parseInt', required: true },
|
|
1667
|
-
type: { source: 'inventory_type', default: 'ON_HAND' },
|
|
1668
|
-
status: { source: 'status', default: 'AVAILABLE' },
|
|
1669
|
-
},
|
|
1670
|
-
},
|
|
1671
|
-
requiredFields: ['sku', 'warehouse', 'quantity'],
|
|
1672
|
-
kv: openKv(),
|
|
1673
|
-
});
|
|
1674
|
-
|
|
1675
|
-
await pipeline.initialize();
|
|
1676
|
-
const report = await pipeline.run('inventory-bucket', 'data/');
|
|
1677
|
-
|
|
1678
|
-
console.log('Ingestion complete:', report);
|
|
1679
|
-
```
|
|
1680
|
-
|
|
1681
|
-
## Common Errors and Solutions
|
|
1682
|
-
|
|
1683
|
-
### Error: JOB_EXPIRED
|
|
1684
|
-
|
|
1685
|
-
**Cause:** Job exceeded 24-hour lifetime
|
|
1686
|
-
|
|
1687
|
-
**Solution:**
|
|
1688
|
-
|
|
1689
|
-
```typescript
|
|
1690
|
-
async function handleExpiredJob(originalJobId: string, remainingBatches: any[]) {
|
|
1691
|
-
// Create new job
|
|
1692
|
-
const newJob = await client.createJob({
|
|
1693
|
-
name: \`Recovery - \${new Date().toISOString()}\`,
|
|
1694
|
-
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
1695
|
-
metadata: {
|
|
1696
|
-
originalJobId,
|
|
1697
|
-
reason: 'JOB_EXPIRED'
|
|
1698
|
-
}
|
|
1699
|
-
});
|
|
1700
|
-
|
|
1701
|
-
// Resend remaining batches
|
|
1702
|
-
for (const batch of remainingBatches) {
|
|
1703
|
-
await client.sendBatch(newJob.id, batch);
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
return newJob.id;
|
|
1707
|
-
}
|
|
1708
|
-
```
|
|
1709
|
-
|
|
1710
|
-
### Error: VALIDATION_ERROR
|
|
1711
|
-
|
|
1712
|
-
**Cause:** Invalid field values or missing required fields
|
|
1713
|
-
|
|
1714
|
-
**Solution:**
|
|
1715
|
-
|
|
1716
|
-
```typescript
|
|
1717
|
-
async function handleValidationError(records: any[], error: any) {
|
|
1718
|
-
// Parse validation errors
|
|
1719
|
-
const invalidRecords = extractInvalidRecords(error);
|
|
1720
|
-
|
|
1721
|
-
// Filter out invalid records
|
|
1722
|
-
const validRecords = records.filter(r => !invalidRecords.includes(r));
|
|
1723
|
-
|
|
1724
|
-
// Log invalid records
|
|
1725
|
-
console.error('Invalid records:', invalidRecords);
|
|
1726
|
-
await saveToErrorLog(invalidRecords);
|
|
1727
|
-
|
|
1728
|
-
// Retry with valid records only
|
|
1729
|
-
return validRecords;
|
|
1730
|
-
}
|
|
1731
|
-
```
|
|
1732
|
-
|
|
1733
|
-
### Error: RATE_LIMIT_ERROR
|
|
1734
|
-
|
|
1735
|
-
**Cause:** Too many requests to Fluent API
|
|
1736
|
-
|
|
1737
|
-
**Solution:**
|
|
1738
|
-
|
|
1739
|
-
```typescript
|
|
1740
|
-
class RateLimitHandler {
|
|
1741
|
-
async handleRateLimit(operation: () => Promise<any>): Promise<any> {
|
|
1742
|
-
try {
|
|
1743
|
-
return await operation();
|
|
1744
|
-
} catch (error) {
|
|
1745
|
-
if (error.code === 'RATE_LIMIT_ERROR') {
|
|
1746
|
-
const retryAfter = error.retryAfter || 60000; // Default 60s
|
|
1747
|
-
console.log(\`Rate limited. Waiting \${retryAfter}ms\`);
|
|
1748
|
-
await sleep(retryAfter);
|
|
1749
|
-
return await operation(); // Retry once
|
|
1750
|
-
}
|
|
1751
|
-
throw error;
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
```
|
|
1756
|
-
|
|
1757
|
-
## Production Checklist
|
|
1758
|
-
|
|
1759
|
-
### Before Deployment
|
|
1760
|
-
|
|
1761
|
-
- [ ] Schema validated against Fluent Commerce API
|
|
1762
|
-
- [ ] Field mappings tested with sample data
|
|
1763
|
-
- [ ] Error handling implemented for all operations
|
|
1764
|
-
- [ ] State management configured and tested
|
|
1765
|
-
- [ ] Monitoring and alerting configured
|
|
1766
|
-
- [ ] Health checks implemented
|
|
1767
|
-
- [ ] Rate limiting configured
|
|
1768
|
-
- [ ] Dead letter queue configured
|
|
1769
|
-
- [ ] Rollback plan documented
|
|
1770
|
-
|
|
1771
|
-
### During Deployment
|
|
1772
|
-
|
|
1773
|
-
- [ ] Start with small batch sizes (100-500)
|
|
1774
|
-
- [ ] Monitor error rates closely
|
|
1775
|
-
- [ ] Test with sample files first
|
|
1776
|
-
- [ ] Gradually increase batch sizes
|
|
1777
|
-
- [ ] Verify state management works correctly
|
|
1778
|
-
|
|
1779
|
-
### After Deployment
|
|
1780
|
-
|
|
1781
|
-
- [ ] Monitor ingestion metrics daily
|
|
1782
|
-
- [ ] Review error logs regularly
|
|
1783
|
-
- [ ] Track processing times
|
|
1784
|
-
- [ ] Optimize based on metrics
|
|
1785
|
-
- [ ] Document common issues and solutions
|
|
1786
|
-
|
|
1787
|
-
## Debugging Tips
|
|
1788
|
-
|
|
1789
|
-
### Enable Debug Logging
|
|
1790
|
-
|
|
1791
|
-
```typescript
|
|
1792
|
-
const client = await createClient({
|
|
1793
|
-
config: {
|
|
1794
|
-
...config
|
|
1795
|
-
},
|
|
1796
|
-
logger: {
|
|
1797
|
-
debug: (...args) => console.log('[DEBUG]', ...args),
|
|
1798
|
-
info: (...args) => console.log('[INFO]', ...args),
|
|
1799
|
-
warn: (...args) => console.warn('[WARN]', ...args),
|
|
1800
|
-
error: (...args) => console.error('[ERROR]', ...args),
|
|
1801
|
-
},
|
|
1802
|
-
});
|
|
1803
|
-
```
|
|
1804
|
-
|
|
1805
|
-
### Trace File Processing
|
|
1806
|
-
|
|
1807
|
-
```typescript
|
|
1808
|
-
async function traceFileProcessing(fileKey: string) {
|
|
1809
|
-
console.log(\`[TRACE] Starting processing: \${fileKey}\`);
|
|
1810
|
-
|
|
1811
|
-
try {
|
|
1812
|
-
console.log('[TRACE] Reading file from S3');
|
|
1813
|
-
const data = await s3.downloadFile(fileKey);
|
|
1814
|
-
console.log(\`[TRACE] File size: \${data.length} bytes\`);
|
|
1815
|
-
|
|
1816
|
-
console.log('[TRACE] Parsing CSV');
|
|
1817
|
-
const records = await parser.parse(data);
|
|
1818
|
-
console.log(\`[TRACE] Parsed \${records.length} records\`);
|
|
1819
|
-
|
|
1820
|
-
console.log('[TRACE] Mapping fields');
|
|
1821
|
-
const result = await mapper.map(records);
|
|
1822
|
-
console.log(\`[TRACE] Mapped \${result.data.length} valid records\`);
|
|
1823
|
-
|
|
1824
|
-
console.log('[TRACE] Creating job');
|
|
1825
|
-
const job = await client.createJob({ name: \`Import - \${fileKey}\` });
|
|
1826
|
-
console.log(\`[TRACE] Job created: \${job.id}\`);
|
|
1827
|
-
|
|
1828
|
-
console.log('[TRACE] Sending batch');
|
|
1829
|
-
await client.sendBatch(job.id, { entities: result.data });
|
|
1830
|
-
console.log('[TRACE] Batch sent successfully');
|
|
1831
|
-
|
|
1832
|
-
} catch (error) {
|
|
1833
|
-
console.error(\`[TRACE] Error at: \${error.message}\`);
|
|
1834
|
-
throw error;
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
```
|
|
1838
|
-
|
|
1839
|
-
## Key Takeaways
|
|
1840
|
-
|
|
1841
|
-
- 🎯 **Always validate** - Check schema and data before sending
|
|
1842
|
-
- 🎯 **Handle errors gracefully** - Retry transient failures, log permanent ones
|
|
1843
|
-
- 🎯 **Monitor everything** - Track metrics, set up alerts
|
|
1844
|
-
- 🎯 **Use state management** - Prevent duplicates, track history
|
|
1845
|
-
- 🎯 **Test thoroughly** - Start small, scale gradually
|
|
1846
|
-
- 🎯 **Document** - Keep runbooks for common issues
|
|
1847
|
-
|
|
1848
|
-
## Congratulations!
|
|
1849
|
-
|
|
1850
|
-
You've completed the Data Ingestion Learning Path! You now know:
|
|
1851
|
-
|
|
1852
|
-
- ✅ Core ingestion concepts and architecture
|
|
1853
|
-
- ✅ How to read from multiple data sources
|
|
1854
|
-
- ✅ How to parse CSV, Parquet, XML, and JSON
|
|
1855
|
-
- ✅ How to transform data with UniversalMapper
|
|
1856
|
-
- ✅ How to use the Batch API effectively
|
|
1857
|
-
- ✅ How to implement state management
|
|
1858
|
-
- ✅ How to optimize performance
|
|
1859
|
-
- ✅ How to build production-ready ingestion workflows
|
|
1860
|
-
|
|
1861
|
-
## Next Steps
|
|
1862
|
-
|
|
1863
|
-
Congratulations! You've completed the Data Ingestion Learning Path and mastered production-ready ingestion patterns.
|
|
1864
|
-
|
|
1865
|
-
**Continue Learning:**
|
|
1866
|
-
|
|
1867
|
-
- 📖 [Data Extraction Guide](../../extraction/) - Learn reverse workflows (Fluent → S3/Parquet/CSV)
|
|
1868
|
-
- 📖 [Resolver Development Guide](../../mapping/resolvers/mapping-resolvers-readme.md) - Build custom data transformations
|
|
1869
|
-
- 📖 [Universal Mapping Guide](../../mapping/mapping-readme.md) - Master field mapping patterns
|
|
1870
|
-
- 📖 [Error Handling Guide](../../../03-PATTERN-GUIDES/error-handling/error-handling-readme.md) - Deep dive into error patterns
|
|
1871
|
-
- 📖 [Versori Platform Integration](../../../04-REFERENCE/platforms/versori/) - Deploy to production
|
|
1872
|
-
|
|
1873
|
-
**Real-World Examples:**
|
|
1874
|
-
|
|
1875
|
-
- 🔍 [Complete Connector Examples](../../../01-TEMPLATES/versori/workflows/readme.md) - Production Versori connectors
|
|
1876
|
-
- 📋 [Use Case Library](../../../01-TEMPLATES/readme.md) - Business-specific implementations
|
|
1877
|
-
- 🛠️ [CLI Tools](../../../../bin/) - Code generation and validation utilities
|
|
1878
|
-
|
|
1879
|
-
**Resources:**
|
|
1880
|
-
|
|
1881
|
-
- 📚 [Complete API Reference](../../api-reference/api-reference-readme.md)
|
|
1882
|
-
- 📖 [Quick Reference Cheat Sheet](../ingestion-quick-reference.md)
|
|
1883
|
-
- 🐛 [Troubleshooting Guide](../../../00-START-HERE/troubleshooting-quick-reference.md)
|
|
1884
|
-
|
|
1885
|
-
---
|
|
1886
|
-
|
|
1887
|
-
[← Back to Ingestion Guide](../ingestion-readme.md) | [Previous: Module 8 - Performance Optimization](./02-core-guides-ingestion-08-performance-optimization.md)
|
|
1888
|
-
|
|
1889
|
-
**Need Help?**
|
|
1890
|
-
|
|
1891
|
-
- 📧 Email: support@fluentcommerce.com
|
|
1892
|
-
- 📚 Documentation: [FC Connect SDK Docs](../../../)
|
|
1893
|
-
- 🔗 GitHub: Report issues or contribute
|
|
1
|
+
# Module 9: Best Practices
|
|
2
|
+
|
|
3
|
+
[← Back to Ingestion Guide](../ingestion-readme.md)
|
|
4
|
+
|
|
5
|
+
**Module 9 of 9** | **Level**: All Levels | **Time**: 30 minutes
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This module covers production-ready patterns for data ingestion workflows. Learn comprehensive error handling strategies, monitoring approaches, security best practices, scheduling patterns, data quality validation, and complete production deployment patterns.
|
|
12
|
+
|
|
13
|
+
New in this version:
|
|
14
|
+
|
|
15
|
+
- Preflight validation before runs (PreflightValidator)
|
|
16
|
+
- Job lifecycle tracking (JobTracker)
|
|
17
|
+
- Partial batch failure recovery (PartialBatchRecovery)
|
|
18
|
+
- Versori file-level deduplication (VersoriFileTracker)
|
|
19
|
+
|
|
20
|
+
## Learning Objectives
|
|
21
|
+
|
|
22
|
+
By the end of this module, you will:
|
|
23
|
+
|
|
24
|
+
- ✅ Implement robust error handling with retry logic and circuit breakers
|
|
25
|
+
- ✅ Set up comprehensive monitoring, logging, and alerting
|
|
26
|
+
- ✅ Apply security best practices for credentials and data
|
|
27
|
+
- ✅ Design effective scheduling and trigger strategies
|
|
28
|
+
- ✅ Validate data quality before ingestion
|
|
29
|
+
- ✅ Build production-ready ingestion pipelines
|
|
30
|
+
- ✅ Troubleshoot common issues effectively
|
|
31
|
+
- ✅ Follow deployment checklists for success
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Error Handling
|
|
36
|
+
|
|
37
|
+
### Retry Strategy with Exponential Backoff
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
async function retryWithBackoff<T>(
|
|
41
|
+
operation: () => Promise<T>,
|
|
42
|
+
maxRetries: number = 3
|
|
43
|
+
): Promise<T> {
|
|
44
|
+
let lastError: Error;
|
|
45
|
+
let delay = 1000; // Start with 1 second
|
|
46
|
+
|
|
47
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
return await operation();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
lastError = error;
|
|
52
|
+
|
|
53
|
+
if (!isRetryable(error) || attempt === maxRetries) {
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(\`Retry \${attempt}/\${maxRetries} after \${delay}ms\`);
|
|
58
|
+
await sleep(delay);
|
|
59
|
+
delay *= 2; // Exponential backoff
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw lastError;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isRetryable(error: any): boolean {
|
|
67
|
+
const retryableCodes = [
|
|
68
|
+
'RATE_LIMIT_ERROR',
|
|
69
|
+
'NETWORK_ERROR',
|
|
70
|
+
'TIMEOUT_ERROR',
|
|
71
|
+
'SERVICE_UNAVAILABLE'
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
retryableCodes.includes(error.code) ||
|
|
76
|
+
error.message.includes('timeout') ||
|
|
77
|
+
error.message.includes('ECONNRESET')
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### SDK-Specific Error Handling
|
|
83
|
+
|
|
84
|
+
The SDK throws specific error types that require different handling strategies:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import {
|
|
88
|
+
FileParsingError,
|
|
89
|
+
MappingError,
|
|
90
|
+
BatchAPIError,
|
|
91
|
+
StateError,
|
|
92
|
+
createConsoleLogger,
|
|
93
|
+
toStructuredLogger,
|
|
94
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
95
|
+
|
|
96
|
+
async function handleSDKErrors(fileKey: string) {
|
|
97
|
+
try {
|
|
98
|
+
await processFile(fileKey);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof FileParsingError) {
|
|
101
|
+
// File format issues - log and skip
|
|
102
|
+
console.error(`Invalid file format: ${fileKey}`, {
|
|
103
|
+
line: error.line,
|
|
104
|
+
column: error.column,
|
|
105
|
+
message: error.message,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Move to error bucket for manual review
|
|
109
|
+
await moveToErrorBucket(fileKey, error);
|
|
110
|
+
return { status: 'skipped', reason: 'parsing_error' };
|
|
111
|
+
} else if (error instanceof MappingError) {
|
|
112
|
+
// Field mapping failures - log details
|
|
113
|
+
console.error(`Field mapping failed: ${fileKey}`, {
|
|
114
|
+
invalidFields: error.invalidFields,
|
|
115
|
+
recordIndex: error.recordIndex,
|
|
116
|
+
errors: error.errors,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Save error report
|
|
120
|
+
await saveErrorReport(fileKey, error);
|
|
121
|
+
return { status: 'failed', reason: 'mapping_error' };
|
|
122
|
+
} else if (error instanceof BatchAPIError) {
|
|
123
|
+
// Batch API rejections - check if retryable
|
|
124
|
+
if (error.isRetryable) {
|
|
125
|
+
console.warn(`Retryable batch error: ${error.code}`);
|
|
126
|
+
throw error; // Will be caught by retry logic
|
|
127
|
+
} else {
|
|
128
|
+
console.error(`Permanent batch error: ${error.code}`, {
|
|
129
|
+
jobId: error.jobId,
|
|
130
|
+
batchId: error.batchId,
|
|
131
|
+
details: error.details,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Save to dead letter queue
|
|
135
|
+
await saveToDLQ(fileKey, error);
|
|
136
|
+
return { status: 'failed', reason: 'batch_api_error' };
|
|
137
|
+
}
|
|
138
|
+
} else if (error instanceof StateError) {
|
|
139
|
+
// State management issues - critical
|
|
140
|
+
console.error(`State management error: ${error.message}`);
|
|
141
|
+
|
|
142
|
+
// Alert operations team
|
|
143
|
+
await sendAlert('CRITICAL: State management failure', error);
|
|
144
|
+
throw error; // Don't continue if state is broken
|
|
145
|
+
} else {
|
|
146
|
+
// Unknown error - log and alert
|
|
147
|
+
console.error(`Unexpected error processing ${fileKey}:`, error);
|
|
148
|
+
await sendAlert('Unknown ingestion error', error);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Circuit Breaker Pattern
|
|
156
|
+
|
|
157
|
+
Prevent cascading failures when external services are down:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
interface CircuitBreakerConfig {
|
|
161
|
+
failureThreshold: number;
|
|
162
|
+
resetTimeoutMs: number;
|
|
163
|
+
monitoringWindowMs: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
enum CircuitState {
|
|
167
|
+
CLOSED = 'CLOSED', // Normal operation
|
|
168
|
+
OPEN = 'OPEN', // Blocking requests
|
|
169
|
+
HALF_OPEN = 'HALF_OPEN', // Testing recovery
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
class CircuitBreaker {
|
|
173
|
+
private state: CircuitState = CircuitState.CLOSED;
|
|
174
|
+
private failureCount: number = 0;
|
|
175
|
+
private lastFailureTime: number = 0;
|
|
176
|
+
private failures: number[] = [];
|
|
177
|
+
|
|
178
|
+
constructor(private config: CircuitBreakerConfig) {}
|
|
179
|
+
|
|
180
|
+
async execute<T>(operation: () => Promise<T>): Promise<T> {
|
|
181
|
+
// Check if circuit should reset
|
|
182
|
+
if (this.shouldAttemptReset()) {
|
|
183
|
+
this.state = CircuitState.HALF_OPEN;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Block if circuit is open
|
|
187
|
+
if (this.state === CircuitState.OPEN) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Circuit breaker OPEN. Last failure: ${new Date(this.lastFailureTime).toISOString()}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const result = await operation();
|
|
195
|
+
|
|
196
|
+
// Success - reset if in half-open state
|
|
197
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
198
|
+
console.log('Circuit breaker: Service recovered, closing circuit');
|
|
199
|
+
this.reset();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
this.recordFailure();
|
|
205
|
+
|
|
206
|
+
// Open circuit if threshold exceeded
|
|
207
|
+
if (this.failureCount >= this.config.failureThreshold) {
|
|
208
|
+
this.state = CircuitState.OPEN;
|
|
209
|
+
this.lastFailureTime = Date.now();
|
|
210
|
+
console.error(`Circuit breaker OPENED after ${this.failureCount} failures`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private recordFailure(): void {
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
this.failures.push(now);
|
|
220
|
+
|
|
221
|
+
// Remove old failures outside monitoring window
|
|
222
|
+
this.failures = this.failures.filter(time => now - time < this.config.monitoringWindowMs);
|
|
223
|
+
|
|
224
|
+
this.failureCount = this.failures.length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private shouldAttemptReset(): boolean {
|
|
228
|
+
if (this.state !== CircuitState.OPEN) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const timeSinceLastFailure = Date.now() - this.lastFailureTime;
|
|
233
|
+
return timeSinceLastFailure >= this.config.resetTimeoutMs;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private reset(): void {
|
|
237
|
+
this.state = CircuitState.CLOSED;
|
|
238
|
+
this.failureCount = 0;
|
|
239
|
+
this.failures = [];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getState(): CircuitState {
|
|
243
|
+
return this.state;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Usage
|
|
248
|
+
const fluentAPICircuit = new CircuitBreaker({
|
|
249
|
+
failureThreshold: 5, // Open after 5 failures
|
|
250
|
+
resetTimeoutMs: 60000, // Try again after 1 minute
|
|
251
|
+
monitoringWindowMs: 120000, // Track failures over 2 minutes
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
async function processWithCircuitBreaker(fileKey: string) {
|
|
255
|
+
try {
|
|
256
|
+
await fluentAPICircuit.execute(async () => {
|
|
257
|
+
return await processFile(fileKey);
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error.message.includes('Circuit breaker OPEN')) {
|
|
261
|
+
console.log('Service temporarily unavailable, requeueing file');
|
|
262
|
+
await requeueFile(fileKey);
|
|
263
|
+
} else {
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Dead Letter Queue Pattern
|
|
271
|
+
|
|
272
|
+
Store failed records for later processing:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
|
|
276
|
+
|
|
277
|
+
interface DLQRecord {
|
|
278
|
+
originalFile: string;
|
|
279
|
+
failedAt: string;
|
|
280
|
+
errorType: string;
|
|
281
|
+
errorMessage: string;
|
|
282
|
+
recordData: any;
|
|
283
|
+
retryCount: number;
|
|
284
|
+
metadata: Record<string, any>;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
class DeadLetterQueue {
|
|
288
|
+
constructor(
|
|
289
|
+
private s3: S3DataSource,
|
|
290
|
+
private dlqBucket: string,
|
|
291
|
+
private dlqPrefix: string = 'dlq/'
|
|
292
|
+
) {}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Save failed record to DLQ
|
|
296
|
+
*/
|
|
297
|
+
async save(record: DLQRecord): Promise<void> {
|
|
298
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
299
|
+
const key = `${this.dlqPrefix}${record.errorType}/${timestamp}-${record.originalFile}`;
|
|
300
|
+
|
|
301
|
+
await this.s3.uploadFile(key, JSON.stringify(record, null, 2));
|
|
302
|
+
|
|
303
|
+
console.log(`Saved to DLQ: ${key}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Retry processing records from DLQ
|
|
308
|
+
*/
|
|
309
|
+
async retryAll(
|
|
310
|
+
processor: (record: any) => Promise<void>
|
|
311
|
+
): Promise<{ successful: number; failed: number }> {
|
|
312
|
+
const files = await this.s3.listFiles({ prefix: this.dlqPrefix });
|
|
313
|
+
let successful = 0;
|
|
314
|
+
let failed = 0;
|
|
315
|
+
|
|
316
|
+
for (const file of files) {
|
|
317
|
+
try {
|
|
318
|
+
const content = await this.s3.downloadFile(file.path);
|
|
319
|
+
const dlqRecord: DLQRecord = JSON.parse(content);
|
|
320
|
+
|
|
321
|
+
// Attempt reprocessing
|
|
322
|
+
await processor(dlqRecord.recordData);
|
|
323
|
+
|
|
324
|
+
// Success - delete from DLQ (Note: deleteObject method may not exist - use uploadFile with empty content or check SDK)
|
|
325
|
+
// await this.s3.deleteObject(this.dlqBucket, file.path);
|
|
326
|
+
successful++;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error(`DLQ retry failed for ${file.path}:`, error);
|
|
329
|
+
failed++;
|
|
330
|
+
|
|
331
|
+
// Update retry count
|
|
332
|
+
await this.incrementRetryCount(file.path);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return { successful, failed };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Increment retry count in DLQ record
|
|
341
|
+
*/
|
|
342
|
+
private async incrementRetryCount(key: string): Promise<void> {
|
|
343
|
+
try {
|
|
344
|
+
const content = await this.s3.downloadFile(key);
|
|
345
|
+
const record: DLQRecord = JSON.parse(content);
|
|
346
|
+
|
|
347
|
+
record.retryCount = (record.retryCount || 0) + 1;
|
|
348
|
+
record.metadata.lastRetryAttempt = new Date().toISOString();
|
|
349
|
+
|
|
350
|
+
await this.s3.uploadFile(key, JSON.stringify(record, null, 2));
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error(`Failed to update retry count for ${key}:`, error);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Usage
|
|
358
|
+
const dlq = new DeadLetterQueue(s3DataSource, 'my-dlq-bucket');
|
|
359
|
+
|
|
360
|
+
async function processFileWithDLQ(fileKey: string) {
|
|
361
|
+
try {
|
|
362
|
+
const records = await parseFile(fileKey);
|
|
363
|
+
|
|
364
|
+
for (const [index, record] of records.entries()) {
|
|
365
|
+
try {
|
|
366
|
+
await processRecord(record);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
// Save failed record to DLQ
|
|
369
|
+
await dlq.save({
|
|
370
|
+
originalFile: fileKey,
|
|
371
|
+
failedAt: new Date().toISOString(),
|
|
372
|
+
errorType: error.constructor.name,
|
|
373
|
+
errorMessage: error.message,
|
|
374
|
+
recordData: record,
|
|
375
|
+
retryCount: 0,
|
|
376
|
+
metadata: {
|
|
377
|
+
recordIndex: index,
|
|
378
|
+
totalRecords: records.length,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.error(`Complete file failure: ${fileKey}`, error);
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Graceful Degradation
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
async function processFileWithFallback(fileKey: string) {
|
|
394
|
+
try {
|
|
395
|
+
// Primary processing path
|
|
396
|
+
await processFileNormally(fileKey);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error(\`Primary processing failed: \${error.message}\`);
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
// Fallback: Process with reduced batch size
|
|
402
|
+
await processFileWithSmallerBatches(fileKey);
|
|
403
|
+
} catch (fallbackError) {
|
|
404
|
+
console.error(\`Fallback processing failed: \${fallbackError.message}\`);
|
|
405
|
+
|
|
406
|
+
// Last resort: Save to dead letter queue
|
|
407
|
+
await saveToDeadLetterQueue(fileKey, error);
|
|
408
|
+
throw new Error(\`File processing failed completely: \${fileKey}\`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Validation Best Practices
|
|
415
|
+
|
|
416
|
+
### Pre-Flight Validation
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
async function validateBeforeIngestion(records: any[]): Promise<ValidationResult> {
|
|
420
|
+
const errors = [];
|
|
421
|
+
const warnings = [];
|
|
422
|
+
|
|
423
|
+
// Schema validation
|
|
424
|
+
for (const [index, record] of records.entries()) {
|
|
425
|
+
// Required fields
|
|
426
|
+
if (!record.ref) {
|
|
427
|
+
errors.push(\`Row \${index}: Missing required field 'ref'\`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!record.productRef) {
|
|
431
|
+
errors.push(\`Row \${index}: Missing required field 'productRef'\`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (!record.locationRef) {
|
|
435
|
+
errors.push(\`Row \${index}: Missing required field 'locationRef'\`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (record.qty === undefined || record.qty === null) {
|
|
439
|
+
errors.push(\`Row \${index}: Missing required field 'qty'\`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Type validation
|
|
443
|
+
if (typeof record.qty !== 'number') {
|
|
444
|
+
errors.push(\`Row \${index}: 'qty' must be a number\`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Business rules
|
|
448
|
+
if (record.qty < 0) {
|
|
449
|
+
errors.push(\`Row \${index}: Quantity cannot be negative\`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (record.qty > 1000000) {
|
|
453
|
+
warnings.push(\`Row \${index}: Unusually large quantity\`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
isValid: errors.length === 0,
|
|
459
|
+
errors,
|
|
460
|
+
warnings
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Usage
|
|
465
|
+
const validation = await validateBeforeIngestion(records);
|
|
466
|
+
|
|
467
|
+
if (!validation.isValid) {
|
|
468
|
+
console.error('Validation errors:', validation.errors);
|
|
469
|
+
throw new Error('Data validation failed');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (validation.warnings.length > 0) {
|
|
473
|
+
console.warn('Validation warnings:', validation.warnings);
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## Scheduling and Triggers
|
|
478
|
+
|
|
479
|
+
###Cron Scheduling Patterns
|
|
480
|
+
|
|
481
|
+
Different scheduling strategies for different ingestion scenarios:
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
// Daily midnight sync (00:00 UTC)
|
|
485
|
+
const dailyMidnightCron = '0 0 * * *';
|
|
486
|
+
|
|
487
|
+
// Every 6 hours
|
|
488
|
+
const every6HoursCron = '0 */6 * * *';
|
|
489
|
+
|
|
490
|
+
// Every hour during business hours (9 AM - 5 PM, Mon-Fri)
|
|
491
|
+
const businessHoursCron = '0 9-17 * * 1-5';
|
|
492
|
+
|
|
493
|
+
// Every 15 minutes
|
|
494
|
+
const every15MinutesCron = '*/15 * * * *';
|
|
495
|
+
|
|
496
|
+
// Weekly Sunday at 2 AM
|
|
497
|
+
const weeklySundayCron = '0 2 * * 0';
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Recommended schedules by use case:**
|
|
501
|
+
|
|
502
|
+
| Use Case | Schedule | Cron Expression | Rationale |
|
|
503
|
+
| ----------------------- | ------------- | ---------------- | ---------------------------- |
|
|
504
|
+
| **WMS Daily Sync** | Once daily | `0 2 * * *` | Process overnight updates |
|
|
505
|
+
| **3PL Cycle Counts** | Every 6 hours | `0 */6 * * *` | Keep inventory fresh |
|
|
506
|
+
| **Real-time Updates** | Every 15 min | `*/15 * * * *` | Near real-time sync |
|
|
507
|
+
| **Weekly Full Sync** | Sunday 2 AM | `0 2 * * 0` | Comprehensive reconciliation |
|
|
508
|
+
| **Business Hours Only** | 9 AM - 5 PM | `0 9-17 * * 1-5` | During operational hours |
|
|
509
|
+
|
|
510
|
+
### Trigger Mechanisms
|
|
511
|
+
|
|
512
|
+
Different ways to trigger ingestion workflows:
|
|
513
|
+
|
|
514
|
+
#### 1. S3 Event Triggers
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
// Versori workflow triggered by S3 event
|
|
518
|
+
export const s3TriggeredIngestion = webhook('s3-inventory-upload', {
|
|
519
|
+
response: { mode: 'sync' },
|
|
520
|
+
})
|
|
521
|
+
.then(
|
|
522
|
+
fn('parse-s3-event', async ({ data }) => {
|
|
523
|
+
const event = JSON.parse(data);
|
|
524
|
+
const bucket = event.Records[0].s3.bucket.name;
|
|
525
|
+
const key = decodeURIComponent(event.Records[0].s3.object.key);
|
|
526
|
+
|
|
527
|
+
return { bucket, key };
|
|
528
|
+
})
|
|
529
|
+
)
|
|
530
|
+
.then(
|
|
531
|
+
fn('process-file', async ({ data, connections, log, openKv }) => {
|
|
532
|
+
const client = await createClient({ connections, log, openKv });
|
|
533
|
+
|
|
534
|
+
// Initialize logger and state management
|
|
535
|
+
const logger = toStructuredLogger(log, {
|
|
536
|
+
service: 'ingestion',
|
|
537
|
+
correlationId: generateCorrelationId()
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const stateService = new StateService(logger);
|
|
541
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
542
|
+
|
|
543
|
+
const s3 = new S3DataSource(
|
|
544
|
+
{
|
|
545
|
+
type: 'S3_CSV',
|
|
546
|
+
connectionId: 's3-triggered',
|
|
547
|
+
name: 'S3 Triggered Ingestion',
|
|
548
|
+
s3Config: { region: 'us-east-1', bucket: data.bucket },
|
|
549
|
+
},
|
|
550
|
+
logger
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// Check if already processed
|
|
554
|
+
if (await stateService.isFileProcessed(kvAdapter, data.key)) {
|
|
555
|
+
log.info(`File already processed: ${data.key}`);
|
|
556
|
+
return { status: 'skipped', reason: 'already_processed' };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Process file
|
|
560
|
+
await processInventoryFile(client, s3, data.bucket, data.key);
|
|
561
|
+
|
|
562
|
+
// Mark as processed
|
|
563
|
+
await stateService.updateSyncState(kvAdapter, [{
|
|
564
|
+
fileName: data.key,
|
|
565
|
+
lastModified: new Date().toISOString(),
|
|
566
|
+
}]);
|
|
567
|
+
|
|
568
|
+
return { status: 'success', file: data.key };
|
|
569
|
+
})
|
|
570
|
+
)
|
|
571
|
+
.catch(({ data, log }) => {
|
|
572
|
+
log.error('S3 triggered ingestion failed:', data);
|
|
573
|
+
return { status: 'error', error: data };
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### 2. SFTP Polling
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
import { SftpDataSource, StateService, VersoriKVAdapter, createConsoleLogger, toStructuredLogger } from '@fluentcommerce/fc-connect-sdk';
|
|
581
|
+
|
|
582
|
+
async function pollSftpDirectory(kv: any) {
|
|
583
|
+
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
584
|
+
service: 'ingestion',
|
|
585
|
+
correlationId: generateCorrelationId()
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const stateService = new StateService(logger);
|
|
589
|
+
const kvAdapter = new VersoriKVAdapter(kv);
|
|
590
|
+
|
|
591
|
+
const sftp = new SftpDataSource({
|
|
592
|
+
type: 'SFTP_CSV',
|
|
593
|
+
connectionId: 'sftp-polling',
|
|
594
|
+
name: 'SFTP Polling',
|
|
595
|
+
settings: {
|
|
596
|
+
host: process.env.SFTP_HOST!,
|
|
597
|
+
port: 22,
|
|
598
|
+
username: process.env.SFTP_USER!,
|
|
599
|
+
privateKey: fs.readFileSync('/path/to/private/key'),
|
|
600
|
+
}
|
|
601
|
+
}, logger);
|
|
602
|
+
|
|
603
|
+
const remoteDir = '/inbound/inventory/';
|
|
604
|
+
const files = await sftp.listFiles(remoteDir);
|
|
605
|
+
|
|
606
|
+
for (const file of files) {
|
|
607
|
+
if (await stateService.isFileProcessed(kvAdapter, file.name)) {
|
|
608
|
+
continue; // Skip already processed
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Download and process
|
|
612
|
+
const localPath = `/tmp/${file.name}`;
|
|
613
|
+
await sftp.downloadFile(file.path, localPath); // Use file.path (full path) for download
|
|
614
|
+
|
|
615
|
+
await processLocalFile(localPath);
|
|
616
|
+
|
|
617
|
+
// Mark as processed
|
|
618
|
+
await stateService.updateSyncState(kvAdapter, [{
|
|
619
|
+
fileName: file.name,
|
|
620
|
+
lastModified: new Date().toISOString(),
|
|
621
|
+
}]);
|
|
622
|
+
|
|
623
|
+
// Optionally move to processed folder
|
|
624
|
+
await sftp.moveFile(`${remoteDir}${file.name}`, `/processed/${file.name}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
#### 3. Webhook Triggers
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
// Manual or external system triggered ingestion
|
|
633
|
+
export const manualIngestion = webhook('manual-trigger', {
|
|
634
|
+
response: { mode: 'sync' },
|
|
635
|
+
})
|
|
636
|
+
.then(
|
|
637
|
+
fn('validate-request', ({ data }) => {
|
|
638
|
+
const { bucket, prefix, batchSize = 2000 } = JSON.parse(data);
|
|
639
|
+
|
|
640
|
+
if (!bucket || !prefix) {
|
|
641
|
+
throw new Error('Missing required parameters: bucket, prefix');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return { bucket, prefix, batchSize };
|
|
645
|
+
})
|
|
646
|
+
)
|
|
647
|
+
.then(
|
|
648
|
+
fn('run-ingestion', async ({ data, connections, log }) => {
|
|
649
|
+
const client = await createClient(ctx); // Auto-detects Versori context
|
|
650
|
+
const s3 = new S3DataSource(
|
|
651
|
+
{
|
|
652
|
+
type: 'S3_CSV',
|
|
653
|
+
s3Config: { region: 'us-east-1' },
|
|
654
|
+
},
|
|
655
|
+
log
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
const files = await s3.listFiles({ prefix: data.prefix });
|
|
659
|
+
|
|
660
|
+
log.info(`Found ${files.length} files to process`);
|
|
661
|
+
|
|
662
|
+
const results = await processFilesInParallel(client, s3, data.bucket, files, {
|
|
663
|
+
batchSize: data.batchSize,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
filesProcessed: results.successful,
|
|
668
|
+
filesFailed: results.failed,
|
|
669
|
+
totalRecords: results.totalRecords,
|
|
670
|
+
};
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Backfill Strategies
|
|
676
|
+
|
|
677
|
+
Handle missed files or catch-up processing:
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
async function backfillMissedFiles(startDate: Date, endDate: Date, kv: any): Promise<void> {
|
|
681
|
+
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
682
|
+
service: 'backfill',
|
|
683
|
+
correlationId: generateCorrelationId()
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const stateService = new StateService(logger);
|
|
687
|
+
const kvAdapter = new VersoriKVAdapter(kv);
|
|
688
|
+
|
|
689
|
+
const s3 = new S3DataSource(
|
|
690
|
+
{
|
|
691
|
+
type: 'S3_CSV',
|
|
692
|
+
connectionId: 's3-backfill',
|
|
693
|
+
name: 'S3 Backfill',
|
|
694
|
+
s3Config: s3Config,
|
|
695
|
+
},
|
|
696
|
+
logger
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
console.log(`Backfilling files from ${startDate} to ${endDate}`);
|
|
700
|
+
|
|
701
|
+
// List all files in date range
|
|
702
|
+
const allFiles = await s3.listFiles({ prefix: 'data/' });
|
|
703
|
+
|
|
704
|
+
const filesInRange = allFiles.filter(file => {
|
|
705
|
+
const fileDate = extractDateFromKey(file.path);
|
|
706
|
+
return fileDate >= startDate && fileDate <= endDate;
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
console.log(`Found ${filesInRange.length} files in date range`);
|
|
710
|
+
|
|
711
|
+
// Check processing status
|
|
712
|
+
const unprocessedFiles = [];
|
|
713
|
+
for (const file of filesInRange) {
|
|
714
|
+
if (!(await stateService.isFileProcessed(kvAdapter, file.path))) {
|
|
715
|
+
unprocessedFiles.push(file);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
console.log(`${unprocessedFiles.length} files need processing`);
|
|
720
|
+
|
|
721
|
+
// Process with lower concurrency to avoid overwhelming system
|
|
722
|
+
await processFilesInParallel(unprocessedFiles, 2); // Only 2 concurrent
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function extractDateFromKey(key: string): Date {
|
|
726
|
+
// Extract date from key like "data/inventory-2025-01-15.csv"
|
|
727
|
+
const match = key.match(/(\d{4}-\d{2}-\d{2})/);
|
|
728
|
+
return match ? new Date(match[1]) : new Date(0);
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
## Data Quality and Validation
|
|
733
|
+
|
|
734
|
+
### Pre-Ingestion Data Quality Checks
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
interface DataQualityRule {
|
|
738
|
+
name: string;
|
|
739
|
+
validate: (record: any) => { isValid: boolean; error?: string };
|
|
740
|
+
severity: 'error' | 'warning';
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
class DataQualityValidator {
|
|
744
|
+
private rules: DataQualityRule[] = [];
|
|
745
|
+
|
|
746
|
+
addRule(rule: DataQualityRule): void {
|
|
747
|
+
this.rules.push(rule);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
validate(records: any[]): {
|
|
751
|
+
passed: any[];
|
|
752
|
+
failed: Array<{ record: any; errors: string[] }>;
|
|
753
|
+
warnings: Array<{ record: any; warnings: string[] }>;
|
|
754
|
+
} {
|
|
755
|
+
const passed: any[] = [];
|
|
756
|
+
const failed: Array<{ record: any; errors: string[] }> = [];
|
|
757
|
+
const warnings: Array<{ record: any; warnings: string[] }> = [];
|
|
758
|
+
|
|
759
|
+
for (const record of records) {
|
|
760
|
+
const errors: string[] = [];
|
|
761
|
+
const warns: string[] = [];
|
|
762
|
+
|
|
763
|
+
for (const rule of this.rules) {
|
|
764
|
+
const result = rule.validate(record);
|
|
765
|
+
|
|
766
|
+
if (!result.isValid) {
|
|
767
|
+
if (rule.severity === 'error') {
|
|
768
|
+
errors.push(`${rule.name}: ${result.error}`);
|
|
769
|
+
} else {
|
|
770
|
+
warns.push(`${rule.name}: ${result.error}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (errors.length > 0) {
|
|
776
|
+
failed.push({ record, errors });
|
|
777
|
+
} else {
|
|
778
|
+
passed.push(record);
|
|
779
|
+
|
|
780
|
+
if (warns.length > 0) {
|
|
781
|
+
warnings.push({ record, warnings: warns });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return { passed, failed, warnings };
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Define quality rules
|
|
791
|
+
const qualityValidator = new DataQualityValidator();
|
|
792
|
+
|
|
793
|
+
qualityValidator.addRule({
|
|
794
|
+
name: 'Required Fields',
|
|
795
|
+
severity: 'error',
|
|
796
|
+
validate: record => {
|
|
797
|
+
const required = ['sku', 'warehouse', 'quantity'];
|
|
798
|
+
const missing = required.filter(field => !record[field]);
|
|
799
|
+
|
|
800
|
+
if (missing.length > 0) {
|
|
801
|
+
return {
|
|
802
|
+
isValid: false,
|
|
803
|
+
error: `Missing required fields: ${missing.join(', ')}`,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return { isValid: true };
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
qualityValidator.addRule({
|
|
812
|
+
name: 'Quantity Range',
|
|
813
|
+
severity: 'error',
|
|
814
|
+
validate: record => {
|
|
815
|
+
const qty = parseInt(record.quantity, 10);
|
|
816
|
+
|
|
817
|
+
if (isNaN(qty) || qty < 0) {
|
|
818
|
+
return {
|
|
819
|
+
isValid: false,
|
|
820
|
+
error: `Invalid quantity: ${record.quantity}`,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return { isValid: true };
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
qualityValidator.addRule({
|
|
829
|
+
name: 'SKU Format',
|
|
830
|
+
severity: 'error',
|
|
831
|
+
validate: record => {
|
|
832
|
+
const skuPattern = /^SKU-[A-Z0-9]{2,10}$/;
|
|
833
|
+
|
|
834
|
+
if (!skuPattern.test(record.sku)) {
|
|
835
|
+
return {
|
|
836
|
+
isValid: false,
|
|
837
|
+
error: `Invalid SKU format: ${record.sku}`,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return { isValid: true };
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
qualityValidator.addRule({
|
|
846
|
+
name: 'Suspiciously Large Quantity',
|
|
847
|
+
severity: 'warning',
|
|
848
|
+
validate: record => {
|
|
849
|
+
const qty = parseInt(record.quantity, 10);
|
|
850
|
+
|
|
851
|
+
if (qty > 100000) {
|
|
852
|
+
return {
|
|
853
|
+
isValid: false,
|
|
854
|
+
error: `Unusually large quantity: ${qty}`,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return { isValid: true };
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// Usage
|
|
863
|
+
const records = await parseCSV(fileContent);
|
|
864
|
+
const validation = qualityValidator.validate(records);
|
|
865
|
+
|
|
866
|
+
console.log(`Passed: ${validation.passed.length}`);
|
|
867
|
+
console.log(`Failed: ${validation.failed.length}`);
|
|
868
|
+
console.log(`Warnings: ${validation.warnings.length}`);
|
|
869
|
+
|
|
870
|
+
// Log failures
|
|
871
|
+
if (validation.failed.length > 0) {
|
|
872
|
+
console.error('Data quality failures:');
|
|
873
|
+
validation.failed.forEach(({ record, errors }) => {
|
|
874
|
+
console.error(` SKU ${record.sku}:`, errors);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// Save failed records for review
|
|
878
|
+
await saveDataQualityReport(validation.failed);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Proceed with passed records only
|
|
882
|
+
await processRecords(validation.passed);
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
### Reconciliation Reports
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
interface ReconciliationReport {
|
|
889
|
+
fileKey: string;
|
|
890
|
+
processedAt: string;
|
|
891
|
+
totalRecords: number;
|
|
892
|
+
successfulRecords: number;
|
|
893
|
+
failedRecords: number;
|
|
894
|
+
duplicateRecords: number;
|
|
895
|
+
dataQualityIssues: number;
|
|
896
|
+
batchesCreated: number;
|
|
897
|
+
processingTimeMs: number;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function generateReconciliationReport(
|
|
901
|
+
fileKey: string,
|
|
902
|
+
results: ProcessingResult
|
|
903
|
+
): Promise<ReconciliationReport> {
|
|
904
|
+
return {
|
|
905
|
+
fileKey,
|
|
906
|
+
processedAt: new Date().toISOString(),
|
|
907
|
+
totalRecords: results.totalRecords,
|
|
908
|
+
successfulRecords: results.successful,
|
|
909
|
+
failedRecords: results.failed,
|
|
910
|
+
duplicateRecords: results.duplicates || 0,
|
|
911
|
+
dataQualityIssues: results.qualityIssues || 0,
|
|
912
|
+
batchesCreated: results.batches,
|
|
913
|
+
processingTimeMs: results.processingTime,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Save report to S3
|
|
918
|
+
async function saveReconciliationReport(report: ReconciliationReport): Promise<void> {
|
|
919
|
+
const s3 = new S3DataSource(
|
|
920
|
+
{
|
|
921
|
+
type: 'S3_JSON',
|
|
922
|
+
connectionId: 's3-reports',
|
|
923
|
+
name: 'S3 Reports',
|
|
924
|
+
s3Config: s3Config,
|
|
925
|
+
},
|
|
926
|
+
logger
|
|
927
|
+
);
|
|
928
|
+
const reportKey = `reports/reconciliation/${report.fileKey}-${Date.now()}.json`;
|
|
929
|
+
|
|
930
|
+
await s3.uploadFile(reportKey, JSON.stringify(report, null, 2));
|
|
931
|
+
|
|
932
|
+
console.log(`Reconciliation report saved: ${reportKey}`);
|
|
933
|
+
}
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
## Security Best Practices
|
|
937
|
+
|
|
938
|
+
### Credential Management
|
|
939
|
+
|
|
940
|
+
```typescript
|
|
941
|
+
// ✅ CORRECT - Use environment variables
|
|
942
|
+
const config = {
|
|
943
|
+
fluent: {
|
|
944
|
+
baseUrl: process.env.FLUENT_BASE_URL!,
|
|
945
|
+
clientId: process.env.FLUENT_CLIENT_ID!,
|
|
946
|
+
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
|
|
947
|
+
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
948
|
+
},
|
|
949
|
+
s3: {
|
|
950
|
+
region: process.env.AWS_REGION!,
|
|
951
|
+
credentials: {
|
|
952
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
953
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
// ❌ WRONG - Never hardcode credentials
|
|
959
|
+
const badConfig = {
|
|
960
|
+
fluent: {
|
|
961
|
+
clientId: 'hardcoded-client-id', // ❌ Don't do this
|
|
962
|
+
clientSecret: 'hardcoded-secret', // ❌ Security risk
|
|
963
|
+
},
|
|
964
|
+
};
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### Secrets Manager Integration
|
|
968
|
+
|
|
969
|
+
```typescript
|
|
970
|
+
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
|
|
971
|
+
|
|
972
|
+
async function loadSecretsFromAWS(): Promise<any> {
|
|
973
|
+
const client = new SecretsManagerClient({ region: 'us-east-1' });
|
|
974
|
+
|
|
975
|
+
const command = new GetSecretValueCommand({
|
|
976
|
+
SecretId: 'fluent-commerce/production',
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const response = await client.send(command);
|
|
980
|
+
return JSON.parse(response.SecretString!);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Usage
|
|
984
|
+
const secrets = await loadSecretsFromAWS();
|
|
985
|
+
|
|
986
|
+
const client = await createClient({
|
|
987
|
+
config: {
|
|
988
|
+
baseUrl: secrets.FLUENT_BASE_URL,
|
|
989
|
+
clientId: secrets.FLUENT_CLIENT_ID,
|
|
990
|
+
clientSecret: secrets.FLUENT_CLIENT_SECRET,
|
|
991
|
+
retailerId: secrets.FLUENT_RETAILER_ID,
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
### Data Encryption
|
|
997
|
+
|
|
998
|
+
```typescript
|
|
999
|
+
// Encrypt sensitive data before storing
|
|
1000
|
+
import * as crypto from 'crypto';
|
|
1001
|
+
|
|
1002
|
+
function encryptSensitiveData(data: string, key: string): string {
|
|
1003
|
+
const iv = crypto.randomBytes(16);
|
|
1004
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
|
|
1005
|
+
|
|
1006
|
+
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
1007
|
+
encrypted += cipher.final('hex');
|
|
1008
|
+
|
|
1009
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function decryptSensitiveData(encrypted: string, key: string): string {
|
|
1013
|
+
const parts = encrypted.split(':');
|
|
1014
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
1015
|
+
const encryptedData = parts[1];
|
|
1016
|
+
|
|
1017
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
|
|
1018
|
+
|
|
1019
|
+
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
|
1020
|
+
decrypted += decipher.final('utf8');
|
|
1021
|
+
|
|
1022
|
+
return decrypted;
|
|
1023
|
+
}
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
### Audit Logging
|
|
1027
|
+
|
|
1028
|
+
```typescript
|
|
1029
|
+
interface AuditLog {
|
|
1030
|
+
timestamp: string;
|
|
1031
|
+
operation: string;
|
|
1032
|
+
user: string;
|
|
1033
|
+
resource: string;
|
|
1034
|
+
status: 'success' | 'failure';
|
|
1035
|
+
details: Record<string, any>;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async function logAuditEvent(event: Omit<AuditLog, 'timestamp'>): Promise<void> {
|
|
1039
|
+
const auditLog: AuditLog = {
|
|
1040
|
+
timestamp: new Date().toISOString(),
|
|
1041
|
+
...event,
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
// Log to CloudWatch, Datadog, or custom logging service
|
|
1045
|
+
console.log('[AUDIT]', JSON.stringify(auditLog));
|
|
1046
|
+
|
|
1047
|
+
// Optionally save to S3 for long-term retention
|
|
1048
|
+
await saveAuditLog(auditLog);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Usage
|
|
1052
|
+
await logAuditEvent({
|
|
1053
|
+
operation: 'INVENTORY_INGESTION',
|
|
1054
|
+
user: process.env.USER || 'system',
|
|
1055
|
+
resource: `s3://bucket/file.csv`,
|
|
1056
|
+
status: 'success',
|
|
1057
|
+
details: {
|
|
1058
|
+
recordsProcessed: 1000,
|
|
1059
|
+
jobId: 'job-123',
|
|
1060
|
+
duration: 5000,
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
## Monitoring and Alerting
|
|
1066
|
+
|
|
1067
|
+
### Metrics Tracking
|
|
1068
|
+
|
|
1069
|
+
```typescript
|
|
1070
|
+
interface IngestionMetrics {
|
|
1071
|
+
timestamp: string;
|
|
1072
|
+
filesProcessed: number;
|
|
1073
|
+
recordsIngested: number;
|
|
1074
|
+
failedRecords: number;
|
|
1075
|
+
processingTimeMs: number;
|
|
1076
|
+
avgRecordsPerSecond: number;
|
|
1077
|
+
errorRate: number;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
class MetricsCollector {
|
|
1081
|
+
private metrics: IngestionMetrics[] = [];
|
|
1082
|
+
|
|
1083
|
+
recordIngestion(result: IngestionResult): void {
|
|
1084
|
+
const metrics: IngestionMetrics = {
|
|
1085
|
+
timestamp: new Date().toISOString(),
|
|
1086
|
+
filesProcessed: result.filesProcessed,
|
|
1087
|
+
recordsIngested: result.recordsIngested,
|
|
1088
|
+
failedRecords: result.failedRecords,
|
|
1089
|
+
processingTimeMs: result.processingTimeMs,
|
|
1090
|
+
avgRecordsPerSecond: result.recordsIngested / (result.processingTimeMs / 1000),
|
|
1091
|
+
errorRate: result.failedRecords / result.recordsIngested
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
this.metrics.push(metrics);
|
|
1095
|
+
|
|
1096
|
+
// Alert on high error rate
|
|
1097
|
+
if (metrics.errorRate > 0.05) { // > 5% error rate
|
|
1098
|
+
this.sendAlert('High error rate detected', metrics);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Alert on slow processing
|
|
1102
|
+
if (metrics.avgRecordsPerSecond < 10) {
|
|
1103
|
+
this.sendAlert('Slow processing speed', metrics);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
private sendAlert(message: string, metrics: IngestionMetrics): void {
|
|
1108
|
+
console.error(\`[ALERT] \${message}\`, metrics);
|
|
1109
|
+
// Send to monitoring service (e.g., Datadog, New Relic)
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
### Health Checks
|
|
1115
|
+
|
|
1116
|
+
```typescript
|
|
1117
|
+
async function performHealthCheck(): Promise<HealthCheckResult> {
|
|
1118
|
+
const checks = [];
|
|
1119
|
+
|
|
1120
|
+
// Check Fluent API connectivity
|
|
1121
|
+
try {
|
|
1122
|
+
await client.graphql({
|
|
1123
|
+
query: '{ __typename }'
|
|
1124
|
+
});
|
|
1125
|
+
checks.push({ name: 'Fluent API', status: 'healthy' });
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
checks.push({ name: 'Fluent API', status: 'unhealthy', error: error.message });
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Check S3 connectivity
|
|
1131
|
+
try {
|
|
1132
|
+
await s3.listFiles({ prefix: 'health-check/', maxKeys: 1 });
|
|
1133
|
+
checks.push({ name: 'S3', status: 'healthy' });
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
checks.push({ name: 'S3', status: 'unhealthy', error: error.message });
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Check state storage
|
|
1139
|
+
try {
|
|
1140
|
+
await state.set('health-check', Date.now());
|
|
1141
|
+
await state.get('health-check');
|
|
1142
|
+
checks.push({ name: 'State Storage', status: 'healthy' });
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
checks.push({ name: 'State Storage', status: 'unhealthy', error: error.message });
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const allHealthy = checks.every(c => c.status === 'healthy');
|
|
1148
|
+
|
|
1149
|
+
return {
|
|
1150
|
+
status: allHealthy ? 'healthy' : 'degraded',
|
|
1151
|
+
checks,
|
|
1152
|
+
timestamp: new Date().toISOString(),
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
## Complete Production Pipeline Example
|
|
1158
|
+
|
|
1159
|
+
Putting all best practices together into a production-ready ingestion pipeline:
|
|
1160
|
+
|
|
1161
|
+
```typescript
|
|
1162
|
+
import {
|
|
1163
|
+
createClient,
|
|
1164
|
+
S3DataSource,
|
|
1165
|
+
CSVParserService,
|
|
1166
|
+
UniversalMapper,
|
|
1167
|
+
StateService,
|
|
1168
|
+
VersoriKVAdapter,
|
|
1169
|
+
FluentClient,
|
|
1170
|
+
FileParsingError,
|
|
1171
|
+
MappingError,
|
|
1172
|
+
BatchAPIError,
|
|
1173
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
1174
|
+
import * as winston from 'winston';
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Production-grade ingestion pipeline with all best practices
|
|
1178
|
+
* - Error handling with retry and circuit breaker
|
|
1179
|
+
* - Comprehensive monitoring and logging
|
|
1180
|
+
* - Security best practices
|
|
1181
|
+
* - Data quality validation
|
|
1182
|
+
* - State management
|
|
1183
|
+
*/
|
|
1184
|
+
class ProductionIngestionPipeline {
|
|
1185
|
+
private client: FluentClient;
|
|
1186
|
+
private s3: S3DataSource;
|
|
1187
|
+
private parser: CSVParserService;
|
|
1188
|
+
private mapper: UniversalMapper;
|
|
1189
|
+
private state: StateService;
|
|
1190
|
+
private kv: KVStore;
|
|
1191
|
+
private logger: winston.Logger;
|
|
1192
|
+
private circuitBreaker: CircuitBreaker;
|
|
1193
|
+
private dlq: DeadLetterQueue;
|
|
1194
|
+
private metrics: MetricsCollector;
|
|
1195
|
+
private qualityValidator: DataQualityValidator;
|
|
1196
|
+
|
|
1197
|
+
constructor(private config: ProductionConfig) {}
|
|
1198
|
+
|
|
1199
|
+
async initialize(): Promise<void> {
|
|
1200
|
+
// Setup structured logging
|
|
1201
|
+
this.logger = winston.createLogger({
|
|
1202
|
+
level: 'info',
|
|
1203
|
+
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
|
1204
|
+
transports: [
|
|
1205
|
+
new winston.transports.Console(),
|
|
1206
|
+
new winston.transports.File({ filename: 'ingestion-error.log', level: 'error' }),
|
|
1207
|
+
new winston.transports.File({ filename: 'ingestion-combined.log' }),
|
|
1208
|
+
],
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
this.logger.info('Initializing production ingestion pipeline');
|
|
1212
|
+
|
|
1213
|
+
// Initialize Fluent client
|
|
1214
|
+
this.client = await createClient({ config: this.config.fluent });
|
|
1215
|
+
|
|
1216
|
+
// Initialize data sources
|
|
1217
|
+
this.s3 = new S3DataSource(
|
|
1218
|
+
{
|
|
1219
|
+
type: 'S3_CSV',
|
|
1220
|
+
connectionId: 's3-production',
|
|
1221
|
+
name: 'S3 Production',
|
|
1222
|
+
s3Config: this.config.s3,
|
|
1223
|
+
},
|
|
1224
|
+
this.logger
|
|
1225
|
+
);
|
|
1226
|
+
this.parser = new CSVParserService();
|
|
1227
|
+
|
|
1228
|
+
// Initialize field mapper with custom resolvers
|
|
1229
|
+
this.mapper = new UniversalMapper(this.config.mapping, {
|
|
1230
|
+
customResolvers: this.config.customResolvers || {},
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
// Initialize state management
|
|
1234
|
+
const logger = toStructuredLogger(this.logger, {
|
|
1235
|
+
service: 'production-pipeline',
|
|
1236
|
+
correlationId: generateCorrelationId()
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
this.kv = new VersoriKVAdapter(this.config.kv);
|
|
1240
|
+
this.stateService = new StateService(logger);
|
|
1241
|
+
|
|
1242
|
+
// Initialize circuit breaker
|
|
1243
|
+
this.circuitBreaker = new CircuitBreaker({
|
|
1244
|
+
failureThreshold: 5,
|
|
1245
|
+
resetTimeoutMs: 60000,
|
|
1246
|
+
monitoringWindowMs: 120000,
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// Initialize dead letter queue
|
|
1250
|
+
this.dlq = new DeadLetterQueue(this.s3, this.config.dlqBucket, 'dlq/');
|
|
1251
|
+
|
|
1252
|
+
// Initialize metrics collector
|
|
1253
|
+
this.metrics = new MetricsCollector();
|
|
1254
|
+
|
|
1255
|
+
// Initialize data quality validator
|
|
1256
|
+
this.qualityValidator = this.createQualityValidator();
|
|
1257
|
+
|
|
1258
|
+
this.logger.info('Pipeline initialization complete');
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Run the complete ingestion pipeline
|
|
1263
|
+
*/
|
|
1264
|
+
async run(bucket: string, prefix: string): Promise<ExecutionReport> {
|
|
1265
|
+
const startTime = Date.now();
|
|
1266
|
+
|
|
1267
|
+
try {
|
|
1268
|
+
this.logger.info('Starting ingestion pipeline', { bucket, prefix });
|
|
1269
|
+
|
|
1270
|
+
// Health check before starting
|
|
1271
|
+
const healthCheck = await this.performHealthCheck();
|
|
1272
|
+
if (healthCheck.status !== 'healthy') {
|
|
1273
|
+
throw new Error(`System unhealthy: ${JSON.stringify(healthCheck.checks)}`);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// List files
|
|
1277
|
+
const allFiles = await this.s3.listFiles({ prefix });
|
|
1278
|
+
this.logger.info(`Found ${allFiles.length} total files`);
|
|
1279
|
+
|
|
1280
|
+
// Filter unprocessed files
|
|
1281
|
+
const unprocessedFiles = await this.filterUnprocessedFiles(allFiles);
|
|
1282
|
+
this.logger.info(`${unprocessedFiles.length} files to process`);
|
|
1283
|
+
|
|
1284
|
+
if (unprocessedFiles.length === 0) {
|
|
1285
|
+
return this.createReport(startTime, { filesProcessed: 0 });
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Process files with circuit breaker
|
|
1289
|
+
const results = await this.circuitBreaker.execute(async () => {
|
|
1290
|
+
return await this.processFilesWithRetry(bucket, unprocessedFiles);
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
// Generate report
|
|
1294
|
+
const report = this.createReport(startTime, results);
|
|
1295
|
+
|
|
1296
|
+
// Log metrics
|
|
1297
|
+
this.metrics.recordIngestion({
|
|
1298
|
+
filesProcessed: results.filesProcessed,
|
|
1299
|
+
recordsIngested: results.recordsIngested,
|
|
1300
|
+
failedRecords: results.failedRecords,
|
|
1301
|
+
processingTimeMs: report.totalTimeMs,
|
|
1302
|
+
avgRecordsPerSecond: report.throughput,
|
|
1303
|
+
errorRate: results.failedRecords / (results.recordsIngested || 1),
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
this.logger.info('Pipeline execution complete', report);
|
|
1307
|
+
|
|
1308
|
+
return report;
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
this.logger.error('Pipeline execution failed', {
|
|
1311
|
+
error: error.message,
|
|
1312
|
+
stack: error.stack,
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
await this.sendAlert('CRITICAL: Ingestion pipeline failure', error);
|
|
1316
|
+
throw error;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Process files with retry logic
|
|
1322
|
+
*/
|
|
1323
|
+
private async processFilesWithRetry(
|
|
1324
|
+
bucket: string,
|
|
1325
|
+
files: Array<{ key: string }>
|
|
1326
|
+
): Promise<ProcessingResults> {
|
|
1327
|
+
const results: ProcessingResults = {
|
|
1328
|
+
filesProcessed: 0,
|
|
1329
|
+
filesFailed: 0,
|
|
1330
|
+
recordsIngested: 0,
|
|
1331
|
+
failedRecords: 0,
|
|
1332
|
+
details: [],
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
for (const file of files) {
|
|
1336
|
+
const maxRetries = 3;
|
|
1337
|
+
let attempt = 0;
|
|
1338
|
+
let success = false;
|
|
1339
|
+
|
|
1340
|
+
while (attempt < maxRetries && !success) {
|
|
1341
|
+
attempt++;
|
|
1342
|
+
|
|
1343
|
+
try {
|
|
1344
|
+
const fileResult = await this.processFile(bucket, file.path);
|
|
1345
|
+
|
|
1346
|
+
results.filesProcessed++;
|
|
1347
|
+
results.recordsIngested += fileResult.recordsProcessed;
|
|
1348
|
+
results.details.push({
|
|
1349
|
+
file: file.path,
|
|
1350
|
+
status: 'success',
|
|
1351
|
+
records: fileResult.recordsProcessed,
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
success = true;
|
|
1355
|
+
|
|
1356
|
+
this.logger.info(`File processed successfully`, {
|
|
1357
|
+
file: file.path,
|
|
1358
|
+
records: fileResult.recordsProcessed,
|
|
1359
|
+
attempt,
|
|
1360
|
+
});
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
this.logger.warn(`File processing attempt ${attempt} failed`, {
|
|
1363
|
+
file: file.path,
|
|
1364
|
+
error: error.message,
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
if (attempt < maxRetries && this.isRetryable(error)) {
|
|
1368
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
1369
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1370
|
+
} else {
|
|
1371
|
+
results.filesFailed++;
|
|
1372
|
+
results.details.push({
|
|
1373
|
+
file: file.path,
|
|
1374
|
+
status: 'failed',
|
|
1375
|
+
error: error.message,
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
this.logger.error(`File processing failed permanently`, {
|
|
1379
|
+
file: file.path,
|
|
1380
|
+
attempts: attempt,
|
|
1381
|
+
error: error.message,
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
return results;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Process a single file with all validations
|
|
1393
|
+
*/
|
|
1394
|
+
private async processFile(bucket: string, fileKey: string): Promise<FileProcessingResult> {
|
|
1395
|
+
this.logger.info(`Processing file: ${fileKey}`);
|
|
1396
|
+
|
|
1397
|
+
try {
|
|
1398
|
+
// Read file
|
|
1399
|
+
const fileContent = await this.s3.downloadFile(fileKey);
|
|
1400
|
+
|
|
1401
|
+
// Parse CSV
|
|
1402
|
+
const records = await this.parser.parse(fileContent);
|
|
1403
|
+
this.logger.debug(`Parsed ${records.length} records from ${fileKey}`);
|
|
1404
|
+
|
|
1405
|
+
// Data quality validation
|
|
1406
|
+
const validation = this.qualityValidator.validate(records);
|
|
1407
|
+
|
|
1408
|
+
if (validation.failed.length > 0) {
|
|
1409
|
+
this.logger.warn(`Data quality issues found`, {
|
|
1410
|
+
file: fileKey,
|
|
1411
|
+
failed: validation.failed.length,
|
|
1412
|
+
warnings: validation.warnings.length,
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
// Save failed records to DLQ
|
|
1416
|
+
for (const { record, errors } of validation.failed) {
|
|
1417
|
+
await this.dlq.save({
|
|
1418
|
+
originalFile: fileKey,
|
|
1419
|
+
failedAt: new Date().toISOString(),
|
|
1420
|
+
errorType: 'DATA_QUALITY_ERROR',
|
|
1421
|
+
errorMessage: errors.join('; '),
|
|
1422
|
+
recordData: record,
|
|
1423
|
+
retryCount: 0,
|
|
1424
|
+
metadata: {},
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Proceed with passed records only
|
|
1430
|
+
if (validation.passed.length === 0) {
|
|
1431
|
+
throw new Error('No valid records after quality validation');
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Field mapping
|
|
1435
|
+
const mappingResult = await this.mapper.map(validation.passed);
|
|
1436
|
+
|
|
1437
|
+
if (!mappingResult.success) {
|
|
1438
|
+
throw new MappingError(`Field mapping failed: ${mappingResult.errors.join(', ')}`);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Create job
|
|
1442
|
+
const job = await this.client.createJob({
|
|
1443
|
+
name: `Production Import - ${fileKey}`,
|
|
1444
|
+
retailerId: this.config.fluent.retailerId,
|
|
1445
|
+
metadata: {
|
|
1446
|
+
fileName: fileKey,
|
|
1447
|
+
recordCount: mappingResult.data.length,
|
|
1448
|
+
pipeline: 'production',
|
|
1449
|
+
},
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
// Send to Batch API
|
|
1453
|
+
await this.client.sendBatch(job.id, {
|
|
1454
|
+
action: 'UPSERT',
|
|
1455
|
+
entityType: 'INVENTORY',
|
|
1456
|
+
entities: mappingResult.data,
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
// Mark as processed
|
|
1460
|
+
await this.state.markFileProcessed(fileKey, {
|
|
1461
|
+
jobId: job.id,
|
|
1462
|
+
recordCount: mappingResult.data.length,
|
|
1463
|
+
timestamp: new Date().toISOString(),
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
// Audit log
|
|
1467
|
+
await this.logAuditEvent({
|
|
1468
|
+
operation: 'INVENTORY_INGESTION',
|
|
1469
|
+
user: 'system',
|
|
1470
|
+
resource: `s3://${bucket}/${fileKey}`,
|
|
1471
|
+
status: 'success',
|
|
1472
|
+
details: {
|
|
1473
|
+
recordsProcessed: mappingResult.data.length,
|
|
1474
|
+
jobId: job.id,
|
|
1475
|
+
},
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
return {
|
|
1479
|
+
recordsProcessed: mappingResult.data.length,
|
|
1480
|
+
jobId: job.id,
|
|
1481
|
+
};
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
// Audit log failure
|
|
1484
|
+
await this.logAuditEvent({
|
|
1485
|
+
operation: 'INVENTORY_INGESTION',
|
|
1486
|
+
user: 'system',
|
|
1487
|
+
resource: `s3://${bucket}/${fileKey}`,
|
|
1488
|
+
status: 'failure',
|
|
1489
|
+
details: {
|
|
1490
|
+
error: error.message,
|
|
1491
|
+
},
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
throw error;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Check if error is retryable
|
|
1500
|
+
*/
|
|
1501
|
+
private isRetryable(error: any): boolean {
|
|
1502
|
+
if (error instanceof FileParsingError) {
|
|
1503
|
+
return false; // File format errors are not retryable
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (error instanceof BatchAPIError) {
|
|
1507
|
+
return error.isRetryable;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Network errors are retryable
|
|
1511
|
+
return (
|
|
1512
|
+
error.code === 'NETWORK_ERROR' ||
|
|
1513
|
+
error.message.includes('timeout') ||
|
|
1514
|
+
error.message.includes('ECONNRESET')
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Filter unprocessed files
|
|
1520
|
+
*/
|
|
1521
|
+
private async filterUnprocessedFiles(
|
|
1522
|
+
files: FileMetadata[]
|
|
1523
|
+
): Promise<FileMetadata[]> {
|
|
1524
|
+
const unprocessed = [];
|
|
1525
|
+
|
|
1526
|
+
for (const file of files) {
|
|
1527
|
+
if (!(await this.state.isFileProcessed(this.kv, file.path))) {
|
|
1528
|
+
unprocessed.push(file);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
return unprocessed;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* Create data quality validator
|
|
1537
|
+
*/
|
|
1538
|
+
private createQualityValidator(): DataQualityValidator {
|
|
1539
|
+
const validator = new DataQualityValidator();
|
|
1540
|
+
|
|
1541
|
+
// Add validation rules based on configuration
|
|
1542
|
+
validator.addRule({
|
|
1543
|
+
name: 'Required Fields',
|
|
1544
|
+
severity: 'error',
|
|
1545
|
+
validate: record => {
|
|
1546
|
+
const required = this.config.requiredFields || [];
|
|
1547
|
+
const missing = required.filter(field => !record[field]);
|
|
1548
|
+
|
|
1549
|
+
if (missing.length > 0) {
|
|
1550
|
+
return {
|
|
1551
|
+
isValid: false,
|
|
1552
|
+
error: `Missing required fields: ${missing.join(', ')}`,
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
return { isValid: true };
|
|
1557
|
+
},
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
return validator;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Perform system health check
|
|
1565
|
+
*/
|
|
1566
|
+
private async performHealthCheck(): Promise<HealthCheckResult> {
|
|
1567
|
+
const checks = [];
|
|
1568
|
+
|
|
1569
|
+
// Check Fluent API
|
|
1570
|
+
try {
|
|
1571
|
+
await this.client.graphql({
|
|
1572
|
+
query: '{ __typename }'
|
|
1573
|
+
});
|
|
1574
|
+
checks.push({ name: 'Fluent API', status: 'healthy' });
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
checks.push({ name: 'Fluent API', status: 'unhealthy', error: error.message });
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Check S3
|
|
1580
|
+
try {
|
|
1581
|
+
await this.s3.listObjects(this.config.s3Bucket, 'health-check/');
|
|
1582
|
+
checks.push({ name: 'S3', status: 'healthy' });
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
checks.push({ name: 'S3', status: 'unhealthy', error: error.message });
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const allHealthy = checks.every(c => c.status === 'healthy');
|
|
1588
|
+
|
|
1589
|
+
return {
|
|
1590
|
+
status: allHealthy ? 'healthy' : 'degraded',
|
|
1591
|
+
checks,
|
|
1592
|
+
timestamp: new Date().toISOString(),
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Create execution report
|
|
1598
|
+
*/
|
|
1599
|
+
private createReport(startTime: number, results: Partial<ProcessingResults>): ExecutionReport {
|
|
1600
|
+
const totalTimeMs = Date.now() - startTime;
|
|
1601
|
+
|
|
1602
|
+
return {
|
|
1603
|
+
startTime: new Date(startTime).toISOString(),
|
|
1604
|
+
endTime: new Date().toISOString(),
|
|
1605
|
+
totalTimeMs,
|
|
1606
|
+
filesProcessed: results.filesProcessed || 0,
|
|
1607
|
+
filesFailed: results.filesFailed || 0,
|
|
1608
|
+
recordsIngested: results.recordsIngested || 0,
|
|
1609
|
+
failedRecords: results.failedRecords || 0,
|
|
1610
|
+
throughput: (results.recordsIngested || 0) / (totalTimeMs / 1000),
|
|
1611
|
+
successRate:
|
|
1612
|
+
((results.filesProcessed || 0) /
|
|
1613
|
+
((results.filesProcessed || 0) + (results.filesFailed || 0))) *
|
|
1614
|
+
100,
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Log audit event
|
|
1620
|
+
*/
|
|
1621
|
+
private async logAuditEvent(event: Omit<AuditLog, 'timestamp'>): Promise<void> {
|
|
1622
|
+
const auditLog: AuditLog = {
|
|
1623
|
+
timestamp: new Date().toISOString(),
|
|
1624
|
+
...event,
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
this.logger.info('[AUDIT]', auditLog);
|
|
1628
|
+
|
|
1629
|
+
// Optionally save to S3 for compliance
|
|
1630
|
+
// await this.s3.putObject(auditBucket, auditKey, JSON.stringify(auditLog));
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Send alert to monitoring service
|
|
1635
|
+
*/
|
|
1636
|
+
private async sendAlert(message: string, error: any): Promise<void> {
|
|
1637
|
+
this.logger.error('[ALERT]', { message, error: error.message });
|
|
1638
|
+
|
|
1639
|
+
// Integration with alerting service (PagerDuty, Slack, etc.)
|
|
1640
|
+
// await alertService.send({ message, error });
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Usage
|
|
1645
|
+
const pipeline = new ProductionIngestionPipeline({
|
|
1646
|
+
fluent: {
|
|
1647
|
+
baseUrl: process.env.FLUENT_BASE_URL!,
|
|
1648
|
+
clientId: process.env.FLUENT_CLIENT_ID!,
|
|
1649
|
+
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
|
|
1650
|
+
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
1651
|
+
},
|
|
1652
|
+
s3: {
|
|
1653
|
+
region: process.env.AWS_REGION!,
|
|
1654
|
+
credentials: {
|
|
1655
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
1656
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
1657
|
+
},
|
|
1658
|
+
},
|
|
1659
|
+
s3Bucket: 'inventory-bucket',
|
|
1660
|
+
dlqBucket: 'inventory-dlq-bucket',
|
|
1661
|
+
mapping: {
|
|
1662
|
+
fields: {
|
|
1663
|
+
ref: { source: 'sku', required: true },
|
|
1664
|
+
productRef: { source: 'product_id', required: true },
|
|
1665
|
+
locationRef: { source: 'warehouse_code', required: true },
|
|
1666
|
+
qty: { source: 'quantity', resolver: 'sdk.parseInt', required: true },
|
|
1667
|
+
type: { source: 'inventory_type', default: 'ON_HAND' },
|
|
1668
|
+
status: { source: 'status', default: 'AVAILABLE' },
|
|
1669
|
+
},
|
|
1670
|
+
},
|
|
1671
|
+
requiredFields: ['sku', 'warehouse', 'quantity'],
|
|
1672
|
+
kv: openKv(),
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
await pipeline.initialize();
|
|
1676
|
+
const report = await pipeline.run('inventory-bucket', 'data/');
|
|
1677
|
+
|
|
1678
|
+
console.log('Ingestion complete:', report);
|
|
1679
|
+
```
|
|
1680
|
+
|
|
1681
|
+
## Common Errors and Solutions
|
|
1682
|
+
|
|
1683
|
+
### Error: JOB_EXPIRED
|
|
1684
|
+
|
|
1685
|
+
**Cause:** Job exceeded 24-hour lifetime
|
|
1686
|
+
|
|
1687
|
+
**Solution:**
|
|
1688
|
+
|
|
1689
|
+
```typescript
|
|
1690
|
+
async function handleExpiredJob(originalJobId: string, remainingBatches: any[]) {
|
|
1691
|
+
// Create new job
|
|
1692
|
+
const newJob = await client.createJob({
|
|
1693
|
+
name: \`Recovery - \${new Date().toISOString()}\`,
|
|
1694
|
+
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
1695
|
+
metadata: {
|
|
1696
|
+
originalJobId,
|
|
1697
|
+
reason: 'JOB_EXPIRED'
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
// Resend remaining batches
|
|
1702
|
+
for (const batch of remainingBatches) {
|
|
1703
|
+
await client.sendBatch(newJob.id, batch);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
return newJob.id;
|
|
1707
|
+
}
|
|
1708
|
+
```
|
|
1709
|
+
|
|
1710
|
+
### Error: VALIDATION_ERROR
|
|
1711
|
+
|
|
1712
|
+
**Cause:** Invalid field values or missing required fields
|
|
1713
|
+
|
|
1714
|
+
**Solution:**
|
|
1715
|
+
|
|
1716
|
+
```typescript
|
|
1717
|
+
async function handleValidationError(records: any[], error: any) {
|
|
1718
|
+
// Parse validation errors
|
|
1719
|
+
const invalidRecords = extractInvalidRecords(error);
|
|
1720
|
+
|
|
1721
|
+
// Filter out invalid records
|
|
1722
|
+
const validRecords = records.filter(r => !invalidRecords.includes(r));
|
|
1723
|
+
|
|
1724
|
+
// Log invalid records
|
|
1725
|
+
console.error('Invalid records:', invalidRecords);
|
|
1726
|
+
await saveToErrorLog(invalidRecords);
|
|
1727
|
+
|
|
1728
|
+
// Retry with valid records only
|
|
1729
|
+
return validRecords;
|
|
1730
|
+
}
|
|
1731
|
+
```
|
|
1732
|
+
|
|
1733
|
+
### Error: RATE_LIMIT_ERROR
|
|
1734
|
+
|
|
1735
|
+
**Cause:** Too many requests to Fluent API
|
|
1736
|
+
|
|
1737
|
+
**Solution:**
|
|
1738
|
+
|
|
1739
|
+
```typescript
|
|
1740
|
+
class RateLimitHandler {
|
|
1741
|
+
async handleRateLimit(operation: () => Promise<any>): Promise<any> {
|
|
1742
|
+
try {
|
|
1743
|
+
return await operation();
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
if (error.code === 'RATE_LIMIT_ERROR') {
|
|
1746
|
+
const retryAfter = error.retryAfter || 60000; // Default 60s
|
|
1747
|
+
console.log(\`Rate limited. Waiting \${retryAfter}ms\`);
|
|
1748
|
+
await sleep(retryAfter);
|
|
1749
|
+
return await operation(); // Retry once
|
|
1750
|
+
}
|
|
1751
|
+
throw error;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
```
|
|
1756
|
+
|
|
1757
|
+
## Production Checklist
|
|
1758
|
+
|
|
1759
|
+
### Before Deployment
|
|
1760
|
+
|
|
1761
|
+
- [ ] Schema validated against Fluent Commerce API
|
|
1762
|
+
- [ ] Field mappings tested with sample data
|
|
1763
|
+
- [ ] Error handling implemented for all operations
|
|
1764
|
+
- [ ] State management configured and tested
|
|
1765
|
+
- [ ] Monitoring and alerting configured
|
|
1766
|
+
- [ ] Health checks implemented
|
|
1767
|
+
- [ ] Rate limiting configured
|
|
1768
|
+
- [ ] Dead letter queue configured
|
|
1769
|
+
- [ ] Rollback plan documented
|
|
1770
|
+
|
|
1771
|
+
### During Deployment
|
|
1772
|
+
|
|
1773
|
+
- [ ] Start with small batch sizes (100-500)
|
|
1774
|
+
- [ ] Monitor error rates closely
|
|
1775
|
+
- [ ] Test with sample files first
|
|
1776
|
+
- [ ] Gradually increase batch sizes
|
|
1777
|
+
- [ ] Verify state management works correctly
|
|
1778
|
+
|
|
1779
|
+
### After Deployment
|
|
1780
|
+
|
|
1781
|
+
- [ ] Monitor ingestion metrics daily
|
|
1782
|
+
- [ ] Review error logs regularly
|
|
1783
|
+
- [ ] Track processing times
|
|
1784
|
+
- [ ] Optimize based on metrics
|
|
1785
|
+
- [ ] Document common issues and solutions
|
|
1786
|
+
|
|
1787
|
+
## Debugging Tips
|
|
1788
|
+
|
|
1789
|
+
### Enable Debug Logging
|
|
1790
|
+
|
|
1791
|
+
```typescript
|
|
1792
|
+
const client = await createClient({
|
|
1793
|
+
config: {
|
|
1794
|
+
...config
|
|
1795
|
+
},
|
|
1796
|
+
logger: {
|
|
1797
|
+
debug: (...args) => console.log('[DEBUG]', ...args),
|
|
1798
|
+
info: (...args) => console.log('[INFO]', ...args),
|
|
1799
|
+
warn: (...args) => console.warn('[WARN]', ...args),
|
|
1800
|
+
error: (...args) => console.error('[ERROR]', ...args),
|
|
1801
|
+
},
|
|
1802
|
+
});
|
|
1803
|
+
```
|
|
1804
|
+
|
|
1805
|
+
### Trace File Processing
|
|
1806
|
+
|
|
1807
|
+
```typescript
|
|
1808
|
+
async function traceFileProcessing(fileKey: string) {
|
|
1809
|
+
console.log(\`[TRACE] Starting processing: \${fileKey}\`);
|
|
1810
|
+
|
|
1811
|
+
try {
|
|
1812
|
+
console.log('[TRACE] Reading file from S3');
|
|
1813
|
+
const data = await s3.downloadFile(fileKey);
|
|
1814
|
+
console.log(\`[TRACE] File size: \${data.length} bytes\`);
|
|
1815
|
+
|
|
1816
|
+
console.log('[TRACE] Parsing CSV');
|
|
1817
|
+
const records = await parser.parse(data);
|
|
1818
|
+
console.log(\`[TRACE] Parsed \${records.length} records\`);
|
|
1819
|
+
|
|
1820
|
+
console.log('[TRACE] Mapping fields');
|
|
1821
|
+
const result = await mapper.map(records);
|
|
1822
|
+
console.log(\`[TRACE] Mapped \${result.data.length} valid records\`);
|
|
1823
|
+
|
|
1824
|
+
console.log('[TRACE] Creating job');
|
|
1825
|
+
const job = await client.createJob({ name: \`Import - \${fileKey}\` });
|
|
1826
|
+
console.log(\`[TRACE] Job created: \${job.id}\`);
|
|
1827
|
+
|
|
1828
|
+
console.log('[TRACE] Sending batch');
|
|
1829
|
+
await client.sendBatch(job.id, { entities: result.data });
|
|
1830
|
+
console.log('[TRACE] Batch sent successfully');
|
|
1831
|
+
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
console.error(\`[TRACE] Error at: \${error.message}\`);
|
|
1834
|
+
throw error;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
## Key Takeaways
|
|
1840
|
+
|
|
1841
|
+
- 🎯 **Always validate** - Check schema and data before sending
|
|
1842
|
+
- 🎯 **Handle errors gracefully** - Retry transient failures, log permanent ones
|
|
1843
|
+
- 🎯 **Monitor everything** - Track metrics, set up alerts
|
|
1844
|
+
- 🎯 **Use state management** - Prevent duplicates, track history
|
|
1845
|
+
- 🎯 **Test thoroughly** - Start small, scale gradually
|
|
1846
|
+
- 🎯 **Document** - Keep runbooks for common issues
|
|
1847
|
+
|
|
1848
|
+
## Congratulations!
|
|
1849
|
+
|
|
1850
|
+
You've completed the Data Ingestion Learning Path! You now know:
|
|
1851
|
+
|
|
1852
|
+
- ✅ Core ingestion concepts and architecture
|
|
1853
|
+
- ✅ How to read from multiple data sources
|
|
1854
|
+
- ✅ How to parse CSV, Parquet, XML, and JSON
|
|
1855
|
+
- ✅ How to transform data with UniversalMapper
|
|
1856
|
+
- ✅ How to use the Batch API effectively
|
|
1857
|
+
- ✅ How to implement state management
|
|
1858
|
+
- ✅ How to optimize performance
|
|
1859
|
+
- ✅ How to build production-ready ingestion workflows
|
|
1860
|
+
|
|
1861
|
+
## Next Steps
|
|
1862
|
+
|
|
1863
|
+
Congratulations! You've completed the Data Ingestion Learning Path and mastered production-ready ingestion patterns.
|
|
1864
|
+
|
|
1865
|
+
**Continue Learning:**
|
|
1866
|
+
|
|
1867
|
+
- 📖 [Data Extraction Guide](../../extraction/) - Learn reverse workflows (Fluent → S3/Parquet/CSV)
|
|
1868
|
+
- 📖 [Resolver Development Guide](../../mapping/resolvers/mapping-resolvers-readme.md) - Build custom data transformations
|
|
1869
|
+
- 📖 [Universal Mapping Guide](../../mapping/mapping-readme.md) - Master field mapping patterns
|
|
1870
|
+
- 📖 [Error Handling Guide](../../../03-PATTERN-GUIDES/error-handling/error-handling-readme.md) - Deep dive into error patterns
|
|
1871
|
+
- 📖 [Versori Platform Integration](../../../04-REFERENCE/platforms/versori/) - Deploy to production
|
|
1872
|
+
|
|
1873
|
+
**Real-World Examples:**
|
|
1874
|
+
|
|
1875
|
+
- 🔍 [Complete Connector Examples](../../../01-TEMPLATES/versori/workflows/readme.md) - Production Versori connectors
|
|
1876
|
+
- 📋 [Use Case Library](../../../01-TEMPLATES/readme.md) - Business-specific implementations
|
|
1877
|
+
- 🛠️ [CLI Tools](../../../../bin/) - Code generation and validation utilities
|
|
1878
|
+
|
|
1879
|
+
**Resources:**
|
|
1880
|
+
|
|
1881
|
+
- 📚 [Complete API Reference](../../api-reference/api-reference-readme.md)
|
|
1882
|
+
- 📖 [Quick Reference Cheat Sheet](../ingestion-quick-reference.md)
|
|
1883
|
+
- 🐛 [Troubleshooting Guide](../../../00-START-HERE/troubleshooting-quick-reference.md)
|
|
1884
|
+
|
|
1885
|
+
---
|
|
1886
|
+
|
|
1887
|
+
[← Back to Ingestion Guide](../ingestion-readme.md) | [Previous: Module 8 - Performance Optimization](./02-core-guides-ingestion-08-performance-optimization.md)
|
|
1888
|
+
|
|
1889
|
+
**Need Help?**
|
|
1890
|
+
|
|
1891
|
+
- 📧 Email: support@fluentcommerce.com
|
|
1892
|
+
- 📚 Documentation: [FC Connect SDK Docs](../../../)
|
|
1893
|
+
- 🔗 GitHub: Report issues or contribute
|