@fluentcommerce/fc-connect-sdk 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -2
- package/README.md +39 -0
- package/dist/cjs/auth/index.d.ts +3 -0
- package/dist/cjs/auth/index.js +13 -0
- package/dist/cjs/auth/profile-loader.d.ts +18 -0
- package/dist/cjs/auth/profile-loader.js +208 -0
- package/dist/cjs/client-factory.d.ts +4 -0
- package/dist/cjs/client-factory.js +10 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/auth/index.d.ts +3 -0
- package/dist/esm/auth/index.js +2 -0
- package/dist/esm/auth/profile-loader.d.ts +18 -0
- package/dist/esm/auth/profile-loader.js +169 -0
- package/dist/esm/client-factory.d.ts +4 -0
- package/dist/esm/client-factory.js +9 -0
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/auth/index.d.ts +3 -0
- package/dist/types/auth/profile-loader.d.ts +18 -0
- package/dist/types/client-factory.d.ts +4 -0
- package/dist/types/index.d.ts +3 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -482
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
|
@@ -1,2395 +1,2395 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-ingest-s3-xml-to-product-event
|
|
3
|
-
canonical_filename: template-ingestion-s3-xml-product-event.md
|
|
4
|
-
version: 2.0.0
|
|
5
|
-
sdk_version: ^0.1.39
|
|
6
|
-
runtime: versori
|
|
7
|
-
direction: ingestion
|
|
8
|
-
source: s3-xml
|
|
9
|
-
destination: fluent-event-api
|
|
10
|
-
entity: product
|
|
11
|
-
format: xml
|
|
12
|
-
logging: versori
|
|
13
|
-
status: stable
|
|
14
|
-
features:
|
|
15
|
-
- batched-events
|
|
16
|
-
- attribute-transformation
|
|
17
|
-
- memory-management
|
|
18
|
-
- json-serialization-handling
|
|
19
|
-
- enhanced-logging
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
# Template: Ingestion - S3 XML to Product Event
|
|
23
|
-
|
|
24
|
-
**Template Version:** 2.0.0
|
|
25
|
-
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
26
|
-
**Last Updated:** 2025-01-24
|
|
27
|
-
**Deployment Target:** Versori Platform
|
|
28
|
-
|
|
29
|
-
**🆕 Version 2.0.0 Enhancements:**
|
|
30
|
-
- ✅ **Batched Events Support** - UPSERT_PRODUCTS with configurable batch sizes (10x faster for 100+ products)
|
|
31
|
-
- ✅ **Attribute Transformation** - Automatic conversion of flat attributes to Fluent Commerce array format
|
|
32
|
-
- ✅ **Memory Management** - Configurable batch processing with explicit cleanup (prevents OOM on large XML files)
|
|
33
|
-
- ✅ **JSON Serialization Handling** - Prevents 503 errors from non-serializable webhook responses
|
|
34
|
-
- ✅ **Enhanced Event Logging** - Status-prefixed logs (SUCCESS_/ERROR_) with comprehensive metrics and progress tracking
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## 📚 STEP 1: Load These Docs Into Your AI (Human Checklist)
|
|
39
|
-
|
|
40
|
-
1. REQUIRED (load all)
|
|
41
|
-
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
42
|
-
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
43
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
44
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
45
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/ingestion/
|
|
46
|
-
- [ ] fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
47
|
-
|
|
48
|
-
Copy-paste list (open these):
|
|
49
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
50
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
51
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
52
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
53
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/ingestion/
|
|
54
|
-
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## 📋 Implementation Prompt
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
I need a Versori scheduled ingestion that:
|
|
62
|
-
|
|
63
|
-
1) Lists XML files on S3 (deduplication via moveObject - NO VersoriFileTracker)
|
|
64
|
-
2) Downloads and parses XML with array normalization (CRITICAL: single object vs array)
|
|
65
|
-
3) Transforms records with UniversalMapper per mapping JSON
|
|
66
|
-
4) Sends UPSERT_PRODUCT events (async) to Fluent Commerce with per-record error handling
|
|
67
|
-
5) Moves files to processed/ or errors/ prefixes for natural deduplication
|
|
68
|
-
6) Tracks progress with JobTracker and exposes a job-status webhook
|
|
69
|
-
7) Uses native Versori log from context
|
|
70
|
-
|
|
71
|
-
Use the loaded docs to fill in SDK specifics and best practices.
|
|
72
|
-
Keep the structure identical to the template; only adapt where needed.
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## 📋 Template Overview
|
|
78
|
-
|
|
79
|
-
This connector runs on the Versori platform. Most operational settings (Fluent account/connection, S3 credentials, schedule, file patterns) are configured via activation variables. Data shape and logic (mapping JSON, XML structure, parsing rules, per-record handling) are adjusted in code as needed. It reads product data from S3 XML, transforms it, and sends events to the Fluent Commerce Event API.
|
|
80
|
-
|
|
81
|
-
### What This Template Does
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
┌─────────────────────────────────────────────────────────────────┐
|
|
85
|
-
│ INGESTION WORKFLOW │
|
|
86
|
-
└─────────────────────────────────────────────────────────────────┘
|
|
87
|
-
|
|
88
|
-
1. TRIGGER
|
|
89
|
-
├─ Scheduled (Cron): Runs automatically every hour
|
|
90
|
-
├─ Ad hoc (Webhook): Manual trigger for immediate processing
|
|
91
|
-
└─ Status Query (Webhook): Check job progress
|
|
92
|
-
|
|
93
|
-
2. DISCOVER FILES (S3DataSource)
|
|
94
|
-
├─ List objects from S3 bucket
|
|
95
|
-
├─ Filter by prefix and pattern (products/*.xml)
|
|
96
|
-
├─ NO VersoriFileTracker (S3 uses moveObject for deduplication)
|
|
97
|
-
└─ Sort by last modified
|
|
98
|
-
|
|
99
|
-
3. DOWNLOAD & PARSE (XMLParserService)
|
|
100
|
-
├─ Download object from S3
|
|
101
|
-
├─ Parse XML to JavaScript object
|
|
102
|
-
├─ ⚠️ CRITICAL: Normalize single object vs array
|
|
103
|
-
└─ Validate XML structure
|
|
104
|
-
|
|
105
|
-
4. TRANSFORM (UniversalMapper)
|
|
106
|
-
├─ Map XML fields to Fluent schema
|
|
107
|
-
├─ Apply SDK resolvers (trim, uppercase, etc.)
|
|
108
|
-
├─ Handle nested objects (price, taxType)
|
|
109
|
-
├─ Handle arrays (categoryRefs)
|
|
110
|
-
└─ Collect transformation errors
|
|
111
|
-
|
|
112
|
-
5. SEND EVENTS (Event API)
|
|
113
|
-
├─ Loop through transformed products
|
|
114
|
-
├─ Send UPSERT_PRODUCT event (async)
|
|
115
|
-
├─ Track success/failure count
|
|
116
|
-
└─ Continue on individual failures
|
|
117
|
-
|
|
118
|
-
6. ARCHIVE (S3DataSource - Natural Deduplication)
|
|
119
|
-
├─ Move object to processed/ prefix (success)
|
|
120
|
-
├─ Or move to errors/ prefix (failures)
|
|
121
|
-
├─ Generate timestamped archive name
|
|
122
|
-
└─ Files in incoming/ are removed naturally
|
|
123
|
-
|
|
124
|
-
7. TRACK JOB (JobTracker)
|
|
125
|
-
├─ Update job status at each step
|
|
126
|
-
├─ Store final result in KV
|
|
127
|
-
├─ Enable status queries via webhook
|
|
128
|
-
└─ Handle errors gracefully
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Key Features
|
|
132
|
-
|
|
133
|
-
- Job tracking with status queries
|
|
134
|
-
- Execution modes: scheduled, ad hoc, status query
|
|
135
|
-
- Uses S3DataSource, XMLParserService, UniversalMapper, JobTracker
|
|
136
|
-
- Error handling, retry logic, and S3 cleanup
|
|
137
|
-
- **S3 Deduplication:** moveObject to processed/errors prefixes (NO VersoriFileTracker)
|
|
138
|
-
- **Key Difference from SFTP:** Physical file movement vs KV state tracking
|
|
139
|
-
- **XML Array Normalization:** CRITICAL handling of single object vs array
|
|
140
|
-
- Event API: Per-record failures don't block other records
|
|
141
|
-
- S3 dispose() in finally block
|
|
142
|
-
|
|
143
|
-
Note: JobTracker persists stage/status to Versori KV for visibility, job-status webhooks, and auditing. Recommended for production multi-step flows; can be skipped for trivial single-step utilities.
|
|
144
|
-
|
|
145
|
-
### 📦 Package Information
|
|
146
|
-
|
|
147
|
-
**SDK:** [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
148
|
-
|
|
149
|
-
Use the latest SDK version:
|
|
150
|
-
|
|
151
|
-
```bash
|
|
152
|
-
npm install @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
**Templates are designed for direct deployment; customize via activation variables.**
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## ⚙️ Activation Variables
|
|
162
|
-
|
|
163
|
-
**Configuration is driven by activation variables - modify these instead of code:**
|
|
164
|
-
|
|
165
|
-
```json
|
|
166
|
-
{
|
|
167
|
-
"fluentRetailerId": "1",
|
|
168
|
-
"eventConcurrency": 1,
|
|
169
|
-
"s3Bucket": "my-product-bucket",
|
|
170
|
-
"s3Region": "us-east-1",
|
|
171
|
-
"s3AccessKeyId": "AKIA...",
|
|
172
|
-
"s3SecretAccessKey": "********",
|
|
173
|
-
"s3IncomingPrefix": "products/incoming/",
|
|
174
|
-
"s3ProcessedPrefix": "products/processed/",
|
|
175
|
-
"s3ErrorPrefix": "products/errors/",
|
|
176
|
-
"s3LogsPrefix": "products/logs/",
|
|
177
|
-
"filePattern": "products_*.xml",
|
|
178
|
-
"catalogueRef": "PC:MASTER:2",
|
|
179
|
-
"catalogueType": "MASTER",
|
|
180
|
-
"eventName": "UPSERT_PRODUCT",
|
|
181
|
-
"eventMode": "async",
|
|
182
|
-
"maxFilesPerRun": 10,
|
|
183
|
-
"processingMode": "per-file",
|
|
184
|
-
"fileChunkSize": 5,
|
|
185
|
-
"validateConnection": true,
|
|
186
|
-
"enableFileTracking": true,
|
|
187
|
-
"trackDuration": true,
|
|
188
|
-
"useBatchedEvents": false,
|
|
189
|
-
"maxProductsUnderBatchedEvent": 100,
|
|
190
|
-
"memoryBatchSize": 250
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Note: Webhook security is handled by Versori's native connection authentication. The `connection` parameter in webhook definitions ensures only authenticated requests are processed.
|
|
195
|
-
|
|
196
|
-
### Variable Explanations
|
|
197
|
-
|
|
198
|
-
| Variable | Purpose | Default | Customization Hints |
|
|
199
|
-
| ------------------- | ---------------------------- | --------------------- | ----------------------------------- |
|
|
200
|
-
| `fluentRetailerId` | Retailer ID for Event API | - | Required - Fluent retailer ID |
|
|
201
|
-
| `eventConcurrency` | Event sending concurrency | `1` | `1` = sequential, `>1` = parallel (3-10 recommended) |
|
|
202
|
-
| `s3Bucket` | S3 bucket name | - | Required - your S3 bucket |
|
|
203
|
-
| `s3Region` | S3 region | `us-east-1` | AWS region (e.g., us-west-2) |
|
|
204
|
-
| `s3AccessKeyId` | AWS access key ID | - | Required - AWS credentials |
|
|
205
|
-
| `s3SecretAccessKey` | AWS secret access key | - | Required - AWS credentials |
|
|
206
|
-
| `s3IncomingPrefix` | Incoming files prefix | `products/incoming/` | Where new files arrive |
|
|
207
|
-
| `s3ProcessedPrefix` | Processed files archive | `products/processed/` | Where files move after success |
|
|
208
|
-
| `s3ErrorPrefix` | Failed files directory | `products/errors/` | Where files move after errors |
|
|
209
|
-
| `s3LogsPrefix` | Event processing logs | `products/logs/` | Where event logs are written |
|
|
210
|
-
| `filePattern` | File name filter | `products_*.xml` | Glob pattern for matching files |
|
|
211
|
-
| `catalogueRef` | Product catalogue reference | `PC:MASTER:2` | Target catalogue in Fluent |
|
|
212
|
-
| `catalogueType` | Catalogue type | `MASTER` | Usually MASTER or STANDARD |
|
|
213
|
-
| `eventName` | Event to send | `UPSERT_PRODUCT` | Event name from Rubix |
|
|
214
|
-
| `eventMode` | Event processing mode | `async` | async (recommended) or sync |
|
|
215
|
-
| `maxFilesPerRun` | Max files per execution | `10` | Prevent timeout on large batches |
|
|
216
|
-
| `processingMode` | Processing mode | `per-file` | per-file, chunked, or batch |
|
|
217
|
-
| `fileChunkSize` | Files per chunk (chunked) | `5` | Used when processingMode=chunked |
|
|
218
|
-
| `validateConnection` | Validate S3 on initialization | `true` | Fail-fast if S3 credentials invalid |
|
|
219
|
-
| `enableFileTracking` | Enable file deduplication | `true` | Track processed files in KV store |
|
|
220
|
-
| `trackDuration` | Track processing duration | `true` | Log timing metrics for performance |
|
|
221
|
-
| `useBatchedEvents` | Use batched UPSERT_PRODUCTS | `false` | `true` = batched events (10x faster for 100+ products), `false` = individual events |
|
|
222
|
-
| `maxProductsUnderBatchedEvent` | Products per batch | `100` | Only used when `useBatchedEvents=true` (default: 100) |
|
|
223
|
-
| `memoryBatchSize` | Products per mapping batch | `250` | Processes products in batches during mapping (prevents OOM on large files) |
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
## 🔒 SDK Automatic Behaviors (v0.1.40+)
|
|
228
|
-
|
|
229
|
-
**The SDK automatically validates and retries for improved reliability:**
|
|
230
|
-
|
|
231
|
-
### retailerId Validation
|
|
232
|
-
- **SDK validates** `retailerId` before calling `sendEvent()`
|
|
233
|
-
- **Checks:** `event.retailerId || client.retailerId`
|
|
234
|
-
- **If missing:** Throws `"retailerId is required for Event API..."`
|
|
235
|
-
- **Configuration:** Set via `fluentRetailerId` activation variable (recommended)
|
|
236
|
-
|
|
237
|
-
### 401 Auth Retry
|
|
238
|
-
- **Automatic retry** for platform auth failures (3 attempts)
|
|
239
|
-
- **Delay:** Exponential backoff (1s → 2s → 4s)
|
|
240
|
-
- **Applies to:** All `sendEvent()` calls (async and sync modes)
|
|
241
|
-
- **Log:** `"[fc-connect-sdk:auth] Platform auth failure (401), retrying..."`
|
|
242
|
-
|
|
243
|
-
### 5xx Server Retry
|
|
244
|
-
- **Automatic retry** for transient server errors (3 attempts)
|
|
245
|
-
- **Delay:** Exponential backoff (1s → 2s → 4s, capped at 10s)
|
|
246
|
-
- **Protects:** Against Fluent API transient failures
|
|
247
|
-
|
|
248
|
-
### No Code Changes Required
|
|
249
|
-
- All templates remain compatible
|
|
250
|
-
- Retry logic is automatic and transparent
|
|
251
|
-
- Better error messages guide configuration
|
|
252
|
-
|
|
253
|
-
**See:** [Event API Guide](./event-api-guide.md) for complete details
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
### 📁 S3 Deduplication Pattern (NO VersoriFileTracker)
|
|
258
|
-
|
|
259
|
-
**CRITICAL: S3 uses physical file movement, NOT VersoriFileTracker**
|
|
260
|
-
|
|
261
|
-
VersoriFileTracker is **SFTP-specific** and should **NOT** be used with S3. S3 achieves deduplication through physical file movement.
|
|
262
|
-
|
|
263
|
-
#### How S3 Deduplication Works
|
|
264
|
-
|
|
265
|
-
**3-Step Process:**
|
|
266
|
-
|
|
267
|
-
1. **Discovery:** List objects from `s3IncomingPrefix` matching `filePattern`
|
|
268
|
-
2. **Process:** Download, parse, transform, and send events
|
|
269
|
-
3. **Archive:** Move object to `s3ProcessedPrefix` or `s3ErrorPrefix`
|
|
270
|
-
4. **Natural Deduplication:** Once moved, files are no longer in incoming/
|
|
271
|
-
|
|
272
|
-
#### File Lifecycle Example
|
|
273
|
-
|
|
274
|
-
```
|
|
275
|
-
INITIAL STATE:
|
|
276
|
-
s3://bucket/products/incoming/products_20250124_001.xml
|
|
277
|
-
|
|
278
|
-
↓ (workflow discovers file)
|
|
279
|
-
|
|
280
|
-
PROCESSING:
|
|
281
|
-
- List objects from "products/incoming/"
|
|
282
|
-
- Download: products_20250124_001.xml
|
|
283
|
-
- Parse XML → Normalize array → Map to events → Send to Event API
|
|
284
|
-
|
|
285
|
-
↓ (success)
|
|
286
|
-
|
|
287
|
-
ARCHIVE (moveObject):
|
|
288
|
-
s3://bucket/products/processed/products_20250124_001-20250124T183045.xml
|
|
289
|
-
|
|
290
|
-
↓ (result)
|
|
291
|
-
|
|
292
|
-
INCOMING FOLDER NOW EMPTY:
|
|
293
|
-
s3://bucket/products/incoming/ (file is gone - no duplicates possible)
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
#### Key Differences: S3 vs SFTP
|
|
297
|
-
|
|
298
|
-
| Aspect | S3 (This Template) | SFTP (Other Templates) |
|
|
299
|
-
| ------------------------ | ----------------------------- | -------------------------------- |
|
|
300
|
-
| **Deduplication Method** | Physical file movement | VersoriFileTracker (KV state) |
|
|
301
|
-
| **State Storage** | None needed | KV store tracks processed files |
|
|
302
|
-
| **File Tracking** | ❌ NO VersoriFileTracker | ✅ VersoriFileTracker required |
|
|
303
|
-
| **Reprocessing** | Move files back to incoming/ | Clear KV state or forceReprocess |
|
|
304
|
-
| **Complexity** | Simpler (no state management) | More complex (state management) |
|
|
305
|
-
|
|
306
|
-
#### Summary
|
|
307
|
-
|
|
308
|
-
✅ **DO:** Use S3 moveObject for deduplication
|
|
309
|
-
✅ **DO:** Move files to processed/ or errors/ prefixes
|
|
310
|
-
❌ **DON'T:** Import VersoriFileTracker in S3 templates
|
|
311
|
-
❌ **DON'T:** Use KV state tracking for S3 files
|
|
312
|
-
✅ **DO:** Use VersoriFileTracker for SFTP templates (different use case)
|
|
313
|
-
|
|
314
|
-
---
|
|
315
|
-
|
|
316
|
-
## When to Use Event API vs Batch API
|
|
317
|
-
|
|
318
|
-
### ✅ Use Event API (`sendEvent`) For:
|
|
319
|
-
|
|
320
|
-
| Entity Type | Use Case | Why Event API |
|
|
321
|
-
| ------------------- | -------------------------------------- | -------------------------------------------- |
|
|
322
|
-
| **Products** | Product catalog sync, variant updates | Triggers workflows, validates business rules |
|
|
323
|
-
| **Locations** | Store/warehouse setup | Requires workflow orchestration |
|
|
324
|
-
| **Customers** | Customer registration, profile updates | Needs workflow for downstream systems |
|
|
325
|
-
| **Orders** | Single order creation | Event-driven fulfillment workflows |
|
|
326
|
-
| **Custom Entities** | Any entity needing workflow triggers | Full Rubix workflow support |
|
|
327
|
-
|
|
328
|
-
### ❌ Use Batch API Instead For:
|
|
329
|
-
|
|
330
|
-
| Entity Type | Use Case | Why Batch API |
|
|
331
|
-
| ------------------ | ---------------------------------- | ----------------------------------------------- |
|
|
332
|
-
| **Inventory ONLY** | Bulk inventory updates, daily sync | Optimized for high-volume, BPP change detection |
|
|
333
|
-
|
|
334
|
-
### 🔄 Use GraphQL Mutations For:
|
|
335
|
-
|
|
336
|
-
| Scenario | Why GraphQL |
|
|
337
|
-
| --------------------- | -------------------------------------- |
|
|
338
|
-
| **Single operations** | Create one order, update one product |
|
|
339
|
-
| **Complex queries** | Fetch data with relationships |
|
|
340
|
-
| **Testing/debugging** | Direct API control, immediate feedback |
|
|
341
|
-
|
|
342
|
-
---
|
|
343
|
-
|
|
344
|
-
## XML File Format
|
|
345
|
-
|
|
346
|
-
### Sample: products.xml
|
|
347
|
-
|
|
348
|
-
Based on your `UPSERT_PRODUCT` event payload:
|
|
349
|
-
|
|
350
|
-
```xml
|
|
351
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
352
|
-
<products>
|
|
353
|
-
<product>
|
|
354
|
-
<ref>G_PROD_WITH_NO_STANDARD</ref>
|
|
355
|
-
<type>VARIANT</type>
|
|
356
|
-
<status>ACTIVE</status>
|
|
357
|
-
<gtin>MH01-XS-Orange</gtin>
|
|
358
|
-
<name>Chaz Kangeroo Hoodie-XS-Orange main</name>
|
|
359
|
-
<summary><p>test short description</p></summary>
|
|
360
|
-
<categoryRefs>
|
|
361
|
-
<ref>STANDARD_CATEGORY</ref>
|
|
362
|
-
</categoryRefs>
|
|
363
|
-
<price>
|
|
364
|
-
<item>
|
|
365
|
-
<type>DEFAULT</type>
|
|
366
|
-
<currency>USD</currency>
|
|
367
|
-
<value>52.000000</value>
|
|
368
|
-
</item>
|
|
369
|
-
<item>
|
|
370
|
-
<type>SPECIAL</type>
|
|
371
|
-
<currency>USD</currency>
|
|
372
|
-
<value>9.000000</value>
|
|
373
|
-
</item>
|
|
374
|
-
</price>
|
|
375
|
-
<taxType>
|
|
376
|
-
<country>AU</country>
|
|
377
|
-
<group>Tax Group</group>
|
|
378
|
-
<tariff>Tax Tariff</tariff>
|
|
379
|
-
</taxType>
|
|
380
|
-
<catalogue>
|
|
381
|
-
<ref>PC:MASTER:2</ref>
|
|
382
|
-
<type>MASTER</type>
|
|
383
|
-
</catalogue>
|
|
384
|
-
</product>
|
|
385
|
-
<product>
|
|
386
|
-
<ref>G_PROD_002</ref>
|
|
387
|
-
<type>VARIANT</type>
|
|
388
|
-
<status>ACTIVE</status>
|
|
389
|
-
<gtin>MH02-M-Blue</gtin>
|
|
390
|
-
<name>Kangeroo Hoodie-M-Blue</name>
|
|
391
|
-
<summary><p>Blue variant</p></summary>
|
|
392
|
-
<categoryRefs>
|
|
393
|
-
<ref>STANDARD_CATEGORY</ref>
|
|
394
|
-
<ref>SEASONAL</ref>
|
|
395
|
-
</categoryRefs>
|
|
396
|
-
<price>
|
|
397
|
-
<item>
|
|
398
|
-
<type>DEFAULT</type>
|
|
399
|
-
<currency>USD</currency>
|
|
400
|
-
<value>45.000000</value>
|
|
401
|
-
</item>
|
|
402
|
-
<item>
|
|
403
|
-
<type>SPECIAL</type>
|
|
404
|
-
<currency>USD</currency>
|
|
405
|
-
<value>35.000000</value>
|
|
406
|
-
</item>
|
|
407
|
-
</price>
|
|
408
|
-
<taxType>
|
|
409
|
-
<country>AU</country>
|
|
410
|
-
<group>Tax Group</group>
|
|
411
|
-
<tariff>Tax Tariff</tariff>
|
|
412
|
-
</taxType>
|
|
413
|
-
<catalogue>
|
|
414
|
-
<ref>PC:MASTER:2</ref>
|
|
415
|
-
<type>MASTER</type>
|
|
416
|
-
</catalogue>
|
|
417
|
-
</product>
|
|
418
|
-
</products>
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
**XML Field Mapping:**
|
|
422
|
-
|
|
423
|
-
- Nested structure for objects (e.g., `<taxType><country>AU</country></taxType>`)
|
|
424
|
-
- Arrays with repeated elements (e.g., `<categoryRefs><ref>CAT1</ref><ref>CAT2</ref></categoryRefs>`)
|
|
425
|
-
- HTML content in CDATA or escaped (e.g., `<p>description</p>`)
|
|
426
|
-
|
|
427
|
-
---
|
|
428
|
-
|
|
429
|
-
## Event Sending Configuration
|
|
430
|
-
|
|
431
|
-
**Simple Configuration:** One variable controls everything
|
|
432
|
-
|
|
433
|
-
```json
|
|
434
|
-
{
|
|
435
|
-
"eventConcurrency": 1 // 1 = sequential, 3-10 = parallel
|
|
436
|
-
}
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
**How it works:**
|
|
440
|
-
|
|
441
|
-
- `eventConcurrency: 1` → Sequential (sends events one at a time, safe default)
|
|
442
|
-
- `eventConcurrency: 3` → Parallel (sends 3 events concurrently)
|
|
443
|
-
- `eventConcurrency: 5` → Parallel (sends 5 events concurrently)
|
|
444
|
-
- `eventConcurrency: 10` → Parallel (sends 10 events concurrently)
|
|
445
|
-
|
|
446
|
-
**Configuration Guidelines:**
|
|
447
|
-
|
|
448
|
-
- **Conservative:** `1` → Sequential (safe, predictable, ~1 req/sec)
|
|
449
|
-
- **Balanced:** `3-5` → Parallel (most common, ~3-5 req/sec)
|
|
450
|
-
- **Aggressive:** `10` → Parallel (high-volume, ~10 req/sec)
|
|
451
|
-
- **Note:** Fluent API supports concurrent requests - adjust based on your needs
|
|
452
|
-
|
|
453
|
-
**Why Single Variable?**
|
|
454
|
-
|
|
455
|
-
- **Simpler:** One variable instead of two (`mode` + `concurrency`)
|
|
456
|
-
- **Clearer:** `concurrency: 1` = sequential, `concurrency > 1` = parallel
|
|
457
|
-
- **Less config:** Fewer activation variables to manage
|
|
458
|
-
- **Flexible:** Easy to tune performance (just change the number)
|
|
459
|
-
|
|
460
|
-
---
|
|
461
|
-
|
|
462
|
-
## 🔧 Production Reference Implementation
|
|
463
|
-
|
|
464
|
-
### Processing Modes
|
|
465
|
-
|
|
466
|
-
| Mode | Default | What happens | When to use |
|
|
467
|
-
| ---------- | ------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
|
|
468
|
-
| `per-file` | ✅ | Process one file end-to-end (discover → parse → Event API → archive) before moving on | Recommended for production S3 feeds and very large files |
|
|
469
|
-
| `chunked` | ➖ | Processes `fileChunkSize` files at a time, completing each chunk before the next | High-volume feeds where per-file would take too long |
|
|
470
|
-
| `batch` | ➖ | Loads every matched file into memory, produces a single Event API job, and archives when complete | Tiny datasets or smoke tests only |
|
|
471
|
-
|
|
472
|
-
Set the mode via activation variables:
|
|
473
|
-
|
|
474
|
-
```json
|
|
475
|
-
{
|
|
476
|
-
"processingMode": "per-file",
|
|
477
|
-
"fileChunkSize": "5",
|
|
478
|
-
"maxFilesPerRun": "25"
|
|
479
|
-
}
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
### Versori Workflows Structure
|
|
483
|
-
|
|
484
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
485
|
-
|
|
486
|
-
**Trigger Types:**
|
|
487
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
488
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
489
|
-
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
490
|
-
|
|
491
|
-
**Execution Steps (chained to triggers):**
|
|
492
|
-
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
493
|
-
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
494
|
-
|
|
495
|
-
### Recommended Project Structure
|
|
496
|
-
|
|
497
|
-
```
|
|
498
|
-
product-event-sync/
|
|
499
|
-
├── index.ts # Entry point - exports all workflows
|
|
500
|
-
└── src/
|
|
501
|
-
├── workflows/
|
|
502
|
-
│ ├── scheduled/
|
|
503
|
-
│ │ └── daily-product-sync.ts # Scheduled: Daily product sync
|
|
504
|
-
│ │
|
|
505
|
-
│ └── webhook/
|
|
506
|
-
│ ├── adhoc-product-sync.ts # Webhook: Manual trigger
|
|
507
|
-
│ └── job-status-check.ts # Webhook: Status query
|
|
508
|
-
│
|
|
509
|
-
├── services/
|
|
510
|
-
│ └── product-sync.service.ts # Shared orchestration logic (reusable)
|
|
511
|
-
│
|
|
512
|
-
└── types/
|
|
513
|
-
└── product.types.ts # Shared type definitions
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
**Benefits:**
|
|
517
|
-
- ✅ Clear trigger separation (`scheduled/` vs `webhook/`)
|
|
518
|
-
- ✅ Descriptive file names (easy to browse and understand)
|
|
519
|
-
- ✅ Scalable (add new workflows without cluttering)
|
|
520
|
-
- ✅ Reusable code in `services/` (DRY principle)
|
|
521
|
-
- ✅ Easy to modify individual workflows without affecting others
|
|
522
|
-
|
|
523
|
-
---
|
|
524
|
-
|
|
525
|
-
## Workflow Files
|
|
526
|
-
|
|
527
|
-
### 1. Scheduled Workflows (`src/workflows/scheduled/`)
|
|
528
|
-
|
|
529
|
-
All time-based triggers that run automatically on cron schedules.
|
|
530
|
-
|
|
531
|
-
#### `src/workflows/scheduled/daily-product-sync.ts`
|
|
532
|
-
|
|
533
|
-
**Purpose**: Automatic Daily product sync
|
|
534
|
-
**Trigger**: Cron schedule (`0 2 * * *`)
|
|
535
|
-
**Exposed as Endpoint**: ❌ NO - Runs automatically
|
|
536
|
-
|
|
537
|
-
```typescript
|
|
538
|
-
import { schedule, http } from '@versori/run';
|
|
539
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
540
|
-
import { executeProductIngestion } from '../../services/product-sync.service';
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Scheduled Workflow: Daily Product Sync
|
|
544
|
-
*
|
|
545
|
-
* Runs automatically daily at 2 AM UTC
|
|
546
|
-
* NOT exposed as HTTP endpoint - Versori executes on schedule
|
|
547
|
-
*
|
|
548
|
-
* Uses shared service: product-sync.service.ts
|
|
549
|
-
*/
|
|
550
|
-
export const dailyProductSync = schedule(
|
|
551
|
-
'product-sync-scheduled',
|
|
552
|
-
'0 2 * * *' // Daily at 2 AM UTC
|
|
553
|
-
).then(
|
|
554
|
-
http('run-product-sync', { connection: 'fluent_commerce' }, async ctx => {
|
|
555
|
-
const { log, openKv } = ctx;
|
|
556
|
-
const jobId = `product-sync-${Date.now()}`;
|
|
557
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
558
|
-
|
|
559
|
-
log.info('🚀 Starting scheduled product sync', { jobId });
|
|
560
|
-
|
|
561
|
-
await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' });
|
|
562
|
-
await tracker.updateJob(jobId, { status: 'processing' });
|
|
563
|
-
|
|
564
|
-
try {
|
|
565
|
-
// Reuse shared orchestration logic
|
|
566
|
-
log.info('⚙️ Executing product ingestion workflow', { jobId });
|
|
567
|
-
const result = await executeProductIngestion(ctx, { jobId, triggeredBy: 'schedule' }, tracker);
|
|
568
|
-
await tracker.markCompleted(jobId, result);
|
|
569
|
-
log.info('✅ Scheduled sync completed successfully', { jobId, ...result });
|
|
570
|
-
return { success: true, jobId, ...result };
|
|
571
|
-
} catch (e: any) {
|
|
572
|
-
log.error('❌ Scheduled sync failed', { jobId, error: e?.message });
|
|
573
|
-
await tracker.markFailed(jobId, e);
|
|
574
|
-
return { success: false, jobId, error: e?.message };
|
|
575
|
-
}
|
|
576
|
-
})
|
|
577
|
-
);
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
---
|
|
581
|
-
|
|
582
|
-
### 2. Webhook Workflows (`src/workflows/webhook/`)
|
|
583
|
-
|
|
584
|
-
All HTTP-based triggers that create webhook endpoints.
|
|
585
|
-
|
|
586
|
-
#### `src/workflows/webhook/adhoc-product-sync.ts`
|
|
587
|
-
|
|
588
|
-
**Purpose**: Manual product sync trigger (on-demand)
|
|
589
|
-
**Trigger**: HTTP POST
|
|
590
|
-
**Endpoint**: `POST https://{workspace}.versori.run/product-sync-adhoc`
|
|
591
|
-
**Use Cases**: Testing, priority processing, ad-hoc runs
|
|
592
|
-
|
|
593
|
-
```typescript
|
|
594
|
-
import { webhook, http } from '@versori/run';
|
|
595
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
596
|
-
import { executeProductIngestion } from '../../services/product-sync.service';
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Webhook: Manual Product Sync Trigger
|
|
600
|
-
*
|
|
601
|
-
* Endpoint: POST https://{workspace}.versori.run/product-sync-adhoc
|
|
602
|
-
* Request body (optional): { filePattern: "urgent_*.xml", maxFiles: 5 }
|
|
603
|
-
*
|
|
604
|
-
* Pattern: webhook().then(http()) - needs Fluent API access
|
|
605
|
-
* Uses shared service: product-sync.service.ts
|
|
606
|
-
*
|
|
607
|
-
* SECURITY: Authentication handled via connection parameter
|
|
608
|
-
* No manual API key validation needed - Versori manages this via connection auth
|
|
609
|
-
*/
|
|
610
|
-
export const adhocProductSync = webhook('product-sync-adhoc', {
|
|
611
|
-
response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
|
|
612
|
-
connection: 'product-sync-adhoc', // Versori validates API key
|
|
613
|
-
}).then(
|
|
614
|
-
http('run-product-sync-adhoc', { connection: 'fluent_commerce' }, async ctx => {
|
|
615
|
-
const { log, openKv, data } = ctx;
|
|
616
|
-
const jobId = `product-sync-adhoc-${Date.now()}`;
|
|
617
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
618
|
-
|
|
619
|
-
const filePattern = data?.filePattern as string;
|
|
620
|
-
const maxFiles = data?.maxFiles as number;
|
|
621
|
-
|
|
622
|
-
log.info('🚀 [WEBHOOK] Adhoc product sync triggered', {
|
|
623
|
-
jobId,
|
|
624
|
-
filePattern,
|
|
625
|
-
maxFiles,
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// Create job entry FIRST (awaited to ensure job exists in KV)
|
|
629
|
-
await tracker.createJob(jobId, {
|
|
630
|
-
triggeredBy: 'manual',
|
|
631
|
-
stage: 'initialization',
|
|
632
|
-
status: 'queued',
|
|
633
|
-
options: { filePattern, maxFiles },
|
|
634
|
-
createdAt: new Date().toISOString(),
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
// ✅ Fire-and-forget: Start background processing WITHOUT await
|
|
638
|
-
// The promise continues execution after we return the response
|
|
639
|
-
executeProductIngestion(ctx, { jobId, triggeredBy: 'manual' }, tracker)
|
|
640
|
-
.then((result) => {
|
|
641
|
-
log.info('✅ [BACKGROUND] Product sync completed successfully', {
|
|
642
|
-
jobId,
|
|
643
|
-
filesProcessed: result.filesProcessed,
|
|
644
|
-
filesFailed: result.filesFailed,
|
|
645
|
-
recordsProcessed: result.recordsProcessed,
|
|
646
|
-
});
|
|
647
|
-
return tracker.markCompleted(jobId, result);
|
|
648
|
-
})
|
|
649
|
-
.catch((error: unknown) => {
|
|
650
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
651
|
-
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
652
|
-
|
|
653
|
-
log.error('❌ [BACKGROUND] Product sync failed', {
|
|
654
|
-
jobId,
|
|
655
|
-
error: errorMessage,
|
|
656
|
-
stack: errorStack,
|
|
657
|
-
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
return tracker.markFailed(jobId, errorMessage);
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
// Return immediately with jobId (response sent with this return value)
|
|
664
|
-
return {
|
|
665
|
-
success: true,
|
|
666
|
-
jobId,
|
|
667
|
-
message: 'Product sync started in background',
|
|
668
|
-
statusEndpoint: `https://{workspace}.versori.run/product-sync-job-status`,
|
|
669
|
-
note: 'Poll the status endpoint with jobId to check progress',
|
|
670
|
-
};
|
|
671
|
-
})
|
|
672
|
-
);
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
#### `src/workflows/webhook/job-status-check.ts`
|
|
676
|
-
|
|
677
|
-
**Purpose**: Query job status
|
|
678
|
-
**Trigger**: HTTP POST
|
|
679
|
-
**Endpoint**: `POST https://{workspace}.versori.run/product-sync-job-status`
|
|
680
|
-
**Request body**: `{ "jobId": "product-sync-1234567890" }`
|
|
681
|
-
|
|
682
|
-
```typescript
|
|
683
|
-
import { webhook, fn } from '@versori/run';
|
|
684
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Webhook: Job Status Check
|
|
688
|
-
*
|
|
689
|
-
* Endpoint: POST https://{workspace}.versori.run/product-sync-job-status
|
|
690
|
-
* Request body: { "jobId": "product-sync-1234567890" }
|
|
691
|
-
*
|
|
692
|
-
* Pattern: webhook().then(fn()) - no external API needed, only KV storage
|
|
693
|
-
* Lightweight: Only queries KV store, no Fluent API calls
|
|
694
|
-
*
|
|
695
|
-
* SECURITY: Authentication handled via connection parameter
|
|
696
|
-
* No manual API key validation needed - Versori manages this via connection auth
|
|
697
|
-
*/
|
|
698
|
-
export const productSyncJobStatus = webhook('product-sync-job-status', {
|
|
699
|
-
response: { mode: 'sync' },
|
|
700
|
-
connection: 'product-sync-job-status',
|
|
701
|
-
}).then(
|
|
702
|
-
fn('status', async ctx => {
|
|
703
|
-
// ═══════════════════════════════════════════════════════════
|
|
704
|
-
// EXECUTION BOUNDARY: Job Status Query Start
|
|
705
|
-
// ═══════════════════════════════════════════════════════════
|
|
706
|
-
const { data, log, openKv } = ctx;
|
|
707
|
-
const jobId = data?.jobId as string;
|
|
708
|
-
|
|
709
|
-
log.info('🔍 Job status query received', { jobId });
|
|
710
|
-
|
|
711
|
-
if (!jobId) {
|
|
712
|
-
log.warn('⚠️ Missing jobId in request');
|
|
713
|
-
return { success: false, error: 'jobId required' };
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
717
|
-
const status = await tracker.getJob(jobId);
|
|
718
|
-
|
|
719
|
-
if (status) {
|
|
720
|
-
log.info('✅ Job status found', { jobId, status: status.status });
|
|
721
|
-
return { success: true, jobId, ...status };
|
|
722
|
-
} else {
|
|
723
|
-
log.warn('⚠️ Job not found', { jobId });
|
|
724
|
-
return { success: false, error: 'Job not found', jobId };
|
|
725
|
-
}
|
|
726
|
-
// ═══════════════════════════════════════════════════════════
|
|
727
|
-
// EXECUTION BOUNDARY: Job Status Query End
|
|
728
|
-
// ═══════════════════════════════════════════════════════════
|
|
729
|
-
})
|
|
730
|
-
);
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
---
|
|
734
|
-
|
|
735
|
-
### 3. Entry Point (`index.ts`)
|
|
736
|
-
|
|
737
|
-
**Purpose**: Register all workflows with Versori platform using MemoryInterpreter pattern
|
|
738
|
-
|
|
739
|
-
```typescript
|
|
740
|
-
/**
|
|
741
|
-
* Entry Point - Registers all workflows with Versori platform
|
|
742
|
-
*
|
|
743
|
-
* PATTERN: MemoryInterpreter for workflow registration
|
|
744
|
-
* - Workflow definitions stored in memory
|
|
745
|
-
* - Versori runtime discovers and executes via exports
|
|
746
|
-
* - No file I/O during workflow registration
|
|
747
|
-
*
|
|
748
|
-
* File Structure:
|
|
749
|
-
* - src/workflows/scheduled/ → Time-based triggers (cron)
|
|
750
|
-
* - src/workflows/webhook/ → HTTP-based triggers (webhooks)
|
|
751
|
-
*/
|
|
752
|
-
|
|
753
|
-
// Import scheduled workflows
|
|
754
|
-
import { dailyProductSync } from './src/workflows/scheduled/daily-product-sync';
|
|
755
|
-
|
|
756
|
-
// Import webhook workflows
|
|
757
|
-
import { adhocProductSync } from './src/workflows/webhook/adhoc-product-sync';
|
|
758
|
-
import { productSyncJobStatus } from './src/workflows/webhook/job-status-check';
|
|
759
|
-
|
|
760
|
-
// MemoryInterpreter Pattern: Export workflows for Versori runtime discovery
|
|
761
|
-
// Workflows are held in memory and executed when triggered by platform
|
|
762
|
-
export {
|
|
763
|
-
// Scheduled (time-based triggers)
|
|
764
|
-
dailyProductSync,
|
|
765
|
-
|
|
766
|
-
// Webhooks (HTTP-based triggers)
|
|
767
|
-
adhocProductSync,
|
|
768
|
-
productSyncJobStatus,
|
|
769
|
-
};
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
**What Gets Exposed:**
|
|
773
|
-
- ✅ `adhocProductSync` → `https://{workspace}.versori.run/product-sync-adhoc`
|
|
774
|
-
- ✅ `productSyncJobStatus` → `https://{workspace}.versori.run/product-sync-job-status`
|
|
775
|
-
- ❌ `dailyProductSync` → NOT exposed (runs automatically on cron)
|
|
776
|
-
|
|
777
|
-
---
|
|
778
|
-
|
|
779
|
-
### Adding New Workflows
|
|
780
|
-
|
|
781
|
-
**To add a scheduled workflow:**
|
|
782
|
-
1. Create file in `src/workflows/scheduled/` with descriptive name (e.g., `hourly-delta-sync.ts`)
|
|
783
|
-
2. Export the workflow from the file
|
|
784
|
-
3. Import and re-export in `index.ts`
|
|
785
|
-
|
|
786
|
-
**To add a webhook workflow:**
|
|
787
|
-
1. Create file in `src/workflows/webhook/` with descriptive name (e.g., `error-notification.ts`)
|
|
788
|
-
2. Export the workflow from the file
|
|
789
|
-
3. Import and re-export in `index.ts`
|
|
790
|
-
|
|
791
|
-
**Example - Adding hourly delta sync:**
|
|
792
|
-
|
|
793
|
-
```typescript
|
|
794
|
-
// src/workflows/scheduled/hourly-delta-sync.ts
|
|
795
|
-
export const hourlyDeltaSync = schedule(
|
|
796
|
-
'product-delta-hourly',
|
|
797
|
-
'0 * * * *' // Every hour
|
|
798
|
-
).then(
|
|
799
|
-
http('run-delta-sync', { connection: 'fluent_commerce' }, async ctx => {
|
|
800
|
-
// Delta sync logic (skip BPP)
|
|
801
|
-
const result = await runIngestion(ctx, jobId, tracker, { bppEnabled: false });
|
|
802
|
-
return result;
|
|
803
|
-
})
|
|
804
|
-
);
|
|
805
|
-
|
|
806
|
-
// index.ts (add to imports and exports)
|
|
807
|
-
import { hourlyDeltaSync } from './src/workflows/scheduled/hourly-delta-sync';
|
|
808
|
-
export { daily_product_sync, hourlyDeltaSync, ... };
|
|
809
|
-
```
|
|
810
|
-
|
|
811
|
-
---
|
|
812
|
-
## 3. Type Definitions (src/types/product-ingestion.types.ts)
|
|
813
|
-
|
|
814
|
-
```typescript
|
|
815
|
-
/**
|
|
816
|
-
* Type Definitions for Product Ingestion
|
|
817
|
-
*
|
|
818
|
-
* Centralized type definitions for product ingestion workflow
|
|
819
|
-
*/
|
|
820
|
-
|
|
821
|
-
import type { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
822
|
-
|
|
823
|
-
/**
|
|
824
|
-
* Product interface - represents transformed product data
|
|
825
|
-
*/
|
|
826
|
-
export interface Product {
|
|
827
|
-
ref: string;
|
|
828
|
-
type?: string;
|
|
829
|
-
status?: string;
|
|
830
|
-
name: string;
|
|
831
|
-
summary?: string;
|
|
832
|
-
gtin?: string;
|
|
833
|
-
catalogue?: { ref?: string };
|
|
834
|
-
attributes?: Record<string, unknown>;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
/**
|
|
838
|
-
* Event configuration
|
|
839
|
-
*/
|
|
840
|
-
export interface EventConfig {
|
|
841
|
-
eventName: string;
|
|
842
|
-
catalogueRef: string;
|
|
843
|
-
catalogueType: string;
|
|
844
|
-
eventMode: 'async' | 'sync';
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Event result - tracks success/failure counts
|
|
849
|
-
*/
|
|
850
|
-
export interface EventResult {
|
|
851
|
-
eventsSent: number;
|
|
852
|
-
eventsFailed: number;
|
|
853
|
-
errors: Array<{ productRef: string; error: string }>;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* Process file result
|
|
858
|
-
*/
|
|
859
|
-
export interface ProcessFileResult {
|
|
860
|
-
success: boolean;
|
|
861
|
-
products: Product[];
|
|
862
|
-
error?: string;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
/**
|
|
866
|
-
* Versori Context Interface
|
|
867
|
-
* Represents the Versori runtime context passed to workflow functions
|
|
868
|
-
*/
|
|
869
|
-
export interface VersoriContext {
|
|
870
|
-
log: {
|
|
871
|
-
info: (message: string, data?: Record<string, unknown>) => void;
|
|
872
|
-
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
873
|
-
error: (message: string, data?: Record<string, unknown>) => void;
|
|
874
|
-
debug?: (message: string, data?: Record<string, unknown>) => void;
|
|
875
|
-
};
|
|
876
|
-
openKv: (namespace: string) => {
|
|
877
|
-
get: (key: string) => Promise<unknown>;
|
|
878
|
-
set: (key: string, value: unknown) => Promise<void>;
|
|
879
|
-
delete: (key: string) => Promise<void>;
|
|
880
|
-
};
|
|
881
|
-
activation: {
|
|
882
|
-
getVariable: (name: string) => string | undefined;
|
|
883
|
-
connections?: Record<string, unknown>;
|
|
884
|
-
};
|
|
885
|
-
connections?: Record<string, unknown>;
|
|
886
|
-
data?: unknown;
|
|
887
|
-
fetch?: typeof fetch;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/**
|
|
891
|
-
* Parameters for ingestion workflow
|
|
892
|
-
*/
|
|
893
|
-
export interface ProductIngestionParams {
|
|
894
|
-
jobId: string;
|
|
895
|
-
triggeredBy: 'schedule' | 'webhook';
|
|
896
|
-
filePattern?: string;
|
|
897
|
-
maxFiles?: number;
|
|
898
|
-
forceReprocess?: boolean;
|
|
899
|
-
catalogueRef?: string;
|
|
900
|
-
priority?: 'normal' | 'high';
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Result from ingestion workflow
|
|
905
|
-
*/
|
|
906
|
-
export interface ProductIngestionResult {
|
|
907
|
-
success: boolean;
|
|
908
|
-
jobId: string;
|
|
909
|
-
filesProcessed: number;
|
|
910
|
-
filesFailed: number;
|
|
911
|
-
filesSkipped?: number;
|
|
912
|
-
recordsProcessed: number;
|
|
913
|
-
eventsSent: number;
|
|
914
|
-
eventsFailed: number;
|
|
915
|
-
fileResults: FileProcessingResult[];
|
|
916
|
-
error?: string;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Per-file processing result
|
|
921
|
-
*/
|
|
922
|
-
export interface FileProcessingResult {
|
|
923
|
-
fileName: string;
|
|
924
|
-
success: boolean;
|
|
925
|
-
skipped?: boolean;
|
|
926
|
-
recordsProcessed: number;
|
|
927
|
-
eventsSent: number;
|
|
928
|
-
eventsFailed: number;
|
|
929
|
-
duration: number;
|
|
930
|
-
error?: string;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Parsed XML document structure
|
|
935
|
-
*/
|
|
936
|
-
export interface ParsedProductsDocument {
|
|
937
|
-
products?: {
|
|
938
|
-
product?: Product | Product[];
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
```
|
|
942
|
-
|
|
943
|
-
---
|
|
944
|
-
|
|
945
|
-
## 4. Service: Product File Processor (`src/services/product-file-processor.service.ts`)
|
|
946
|
-
|
|
947
|
-
```typescript
|
|
948
|
-
/**
|
|
949
|
-
* Product File Processor Service
|
|
950
|
-
*
|
|
951
|
-
* Downloads XML files from S3, parses, and transforms with UniversalMapper.
|
|
952
|
-
* XML-specific: Handles array normalization (single vs multiple products).
|
|
953
|
-
*/
|
|
954
|
-
|
|
955
|
-
import {
|
|
956
|
-
S3DataSource,
|
|
957
|
-
XMLParserService,
|
|
958
|
-
UniversalMapper,
|
|
959
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
960
|
-
import type { ProcessFileResult, Product, ParsedProductsDocument } from '../types/product-ingestion.types';
|
|
961
|
-
|
|
962
|
-
/**
|
|
963
|
-
* Service for processing XML product files from S3
|
|
964
|
-
*/
|
|
965
|
-
export class ProductFileProcessorService {
|
|
966
|
-
constructor(
|
|
967
|
-
private s3: S3DataSource,
|
|
968
|
-
private xmlParser: XMLParserService,
|
|
969
|
-
private mapper: UniversalMapper,
|
|
970
|
-
private catalogueRef: string
|
|
971
|
-
) {}
|
|
972
|
-
|
|
973
|
-
/**
|
|
974
|
-
* Download XML file from S3, parse, and transform with UniversalMapper
|
|
975
|
-
*
|
|
976
|
-
* ✅ PRODUCTION ENHANCEMENT: Memory cleanup pattern
|
|
977
|
-
* - Explicit null assignments after each step
|
|
978
|
-
* - Finally block guarantees cleanup
|
|
979
|
-
* - Prevents OOM errors on large XML files
|
|
980
|
-
*/
|
|
981
|
-
async downloadParseAndTransform(objectKey: string): Promise<ProcessFileResult> {
|
|
982
|
-
// ✅ CRITICAL: Variables for cleanup tracking
|
|
983
|
-
let xmlContent: string | null = null;
|
|
984
|
-
let parsed: unknown | null = null;
|
|
985
|
-
let rawProducts: any[] | null = null;
|
|
986
|
-
|
|
987
|
-
try {
|
|
988
|
-
// STEP 1: Download XML content from S3
|
|
989
|
-
xmlContent = (await this.s3.downloadObject(objectKey, {
|
|
990
|
-
encoding: 'utf8',
|
|
991
|
-
})) as string;
|
|
992
|
-
|
|
993
|
-
// STEP 2: Parse XML with type safety
|
|
994
|
-
try {
|
|
995
|
-
parsed = await this.xmlParser.parse(xmlContent);
|
|
996
|
-
} catch (parseError: unknown) {
|
|
997
|
-
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
|
998
|
-
return {
|
|
999
|
-
success: false,
|
|
1000
|
-
products: [],
|
|
1001
|
-
error: `XML parse error: ${errorMessage}`,
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// ✅ Clear xmlContent from memory - no longer needed after parsing
|
|
1006
|
-
xmlContent = null;
|
|
1007
|
-
|
|
1008
|
-
// Type guard: Validate parsed structure
|
|
1009
|
-
if (!this.isParsedProductsDocument(parsed)) {
|
|
1010
|
-
return {
|
|
1011
|
-
success: false,
|
|
1012
|
-
products: [],
|
|
1013
|
-
error: 'Invalid XML structure: missing products element',
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// STEP 3: Normalize products array (single element → object, multiple → array)
|
|
1018
|
-
rawProducts = Array.isArray(parsed.products?.product)
|
|
1019
|
-
? parsed.products.product
|
|
1020
|
-
: parsed.products?.product
|
|
1021
|
-
? [parsed.products.product]
|
|
1022
|
-
: [];
|
|
1023
|
-
|
|
1024
|
-
if (rawProducts.length === 0) {
|
|
1025
|
-
return {
|
|
1026
|
-
success: false,
|
|
1027
|
-
products: [],
|
|
1028
|
-
error: 'No products found in XML',
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// ✅ Clear parsed data from memory - only need rawProducts now
|
|
1033
|
-
parsed = null;
|
|
1034
|
-
|
|
1035
|
-
// STEP 4: Transform with UniversalMapper
|
|
1036
|
-
// ✅ S3 XML: Merge context using spread pattern (same as SFTP XML)
|
|
1037
|
-
const sourceDataWithContext = rawProducts.map(item => ({
|
|
1038
|
-
...item,
|
|
1039
|
-
$context: { catalogueRef: this.catalogueRef },
|
|
1040
|
-
}));
|
|
1041
|
-
|
|
1042
|
-
const mappingResult = await this.mapper.map(sourceDataWithContext);
|
|
1043
|
-
|
|
1044
|
-
if (!mappingResult.success) {
|
|
1045
|
-
return {
|
|
1046
|
-
success: false,
|
|
1047
|
-
products: [],
|
|
1048
|
-
error: `Mapping validation failed: ${mappingResult.errors?.join(', ')}`,
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
1053
|
-
this.log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
1054
|
-
skippedFields: mappingResult.skippedFields,
|
|
1055
|
-
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
1056
|
-
});
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
return {
|
|
1060
|
-
success: true,
|
|
1061
|
-
products: mappingResult.data as Product[],
|
|
1062
|
-
};
|
|
1063
|
-
} catch (error: unknown) {
|
|
1064
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1065
|
-
return {
|
|
1066
|
-
success: false,
|
|
1067
|
-
products: [],
|
|
1068
|
-
error: errorMessage,
|
|
1069
|
-
};
|
|
1070
|
-
} finally {
|
|
1071
|
-
// ✅ CRITICAL: Ensure all large objects are cleared even if error occurs
|
|
1072
|
-
xmlContent = null;
|
|
1073
|
-
parsed = null;
|
|
1074
|
-
rawProducts = null;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
/**
|
|
1079
|
-
* Type guard: Check if parsed value is a valid ParsedProductsDocument
|
|
1080
|
-
*/
|
|
1081
|
-
private isParsedProductsDocument(value: unknown): value is ParsedProductsDocument {
|
|
1082
|
-
if (typeof value !== 'object' || value === null) {
|
|
1083
|
-
return false;
|
|
1084
|
-
}
|
|
1085
|
-
const doc = value as Record<string, unknown>;
|
|
1086
|
-
return 'products' in doc;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
```
|
|
1090
|
-
|
|
1091
|
-
---
|
|
1092
|
-
|
|
1093
|
-
## 5. Service: Event Sender (`src/services/event-sender.service.ts`)
|
|
1094
|
-
|
|
1095
|
-
```typescript
|
|
1096
|
-
/**
|
|
1097
|
-
* Event Sender Service
|
|
1098
|
-
*
|
|
1099
|
-
* Sends product events to Fluent Commerce Event API with per-record error handling.
|
|
1100
|
-
* Continues processing on individual failures (Event API best practice).
|
|
1101
|
-
* Supports configurable concurrency (sequential or parallel).
|
|
1102
|
-
*/
|
|
1103
|
-
|
|
1104
|
-
import type { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
1105
|
-
import type { EventResult, EventConfig, Product } from '../types/product-ingestion.types';
|
|
1106
|
-
|
|
1107
|
-
/**
|
|
1108
|
-
* Service for sending events to Fluent Commerce Event API
|
|
1109
|
-
*
|
|
1110
|
-
* ✅ PRODUCTION ENHANCEMENT: Optional logger for detailed progress tracking
|
|
1111
|
-
*/
|
|
1112
|
-
export class EventSenderService {
|
|
1113
|
-
constructor(
|
|
1114
|
-
private client: FluentClient,
|
|
1115
|
-
private log?: any // ✅ Optional logger for progress tracking
|
|
1116
|
-
) {}
|
|
1117
|
-
|
|
1118
|
-
/**
|
|
1119
|
-
* Send events to Fluent Commerce Event API with configurable concurrency
|
|
1120
|
-
*
|
|
1121
|
-
* **Performance Characteristics:**
|
|
1122
|
-
* - `concurrency: 1` → Sequential processing (safe default, ~1 event/sec)
|
|
1123
|
-
* - `concurrency: 3-5` → Balanced throughput (~3-5 events/sec, good for most cases)
|
|
1124
|
-
* - `concurrency: 10` → High-volume processing (~10 events/sec, 100+ products)
|
|
1125
|
-
*
|
|
1126
|
-
* **Implementation Strategy:**
|
|
1127
|
-
* - Concurrency = 1: Optimized sequential loop (no Promise.allSettled overhead)
|
|
1128
|
-
* - Concurrency > 1: Chunked parallel processing with bounded concurrency
|
|
1129
|
-
* - Both modes: Per-record error tracking (failures don't block other events)
|
|
1130
|
-
*
|
|
1131
|
-
* @param products - Array of products to send as events
|
|
1132
|
-
* @param eventConfig - Event configuration (name, catalogue ref, mode)
|
|
1133
|
-
* @param concurrency - Number of concurrent event requests (default: 1, min: 1)
|
|
1134
|
-
* @returns EventResult with counts (eventsSent/eventsFailed) and error details
|
|
1135
|
-
*/
|
|
1136
|
-
async sendEvents(
|
|
1137
|
-
products: Product[],
|
|
1138
|
-
eventConfig: EventConfig,
|
|
1139
|
-
concurrency: number = 1
|
|
1140
|
-
): Promise<EventResult> {
|
|
1141
|
-
// Validate concurrency (guard against invalid values)
|
|
1142
|
-
const safeConc = Math.max(1, Math.floor(concurrency));
|
|
1143
|
-
|
|
1144
|
-
// Result accumulators
|
|
1145
|
-
let eventsSent = 0;
|
|
1146
|
-
let eventsFailed = 0;
|
|
1147
|
-
const errors: Array<{ productRef: string; error: string }> = [];
|
|
1148
|
-
|
|
1149
|
-
// ✅ PRODUCTION ENHANCEMENT: Log event sending start
|
|
1150
|
-
if (this.log) {
|
|
1151
|
-
this.log.info('📤 Starting event sending', {
|
|
1152
|
-
totalProducts: products.length,
|
|
1153
|
-
concurrency: safeConc,
|
|
1154
|
-
processingMode: safeConc === 1 ? 'sequential (one at a time)' : `parallel (${safeConc} concurrently)`,
|
|
1155
|
-
eventName: eventConfig.eventName,
|
|
1156
|
-
catalogueRef: eventConfig.catalogueRef
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// Helper: Build event payload (DRY - reused in both modes)
|
|
1161
|
-
const buildPayload = (product: Product) => ({
|
|
1162
|
-
name: eventConfig.eventName,
|
|
1163
|
-
entityRef: eventConfig.catalogueRef,
|
|
1164
|
-
entityType: 'PRODUCT_CATALOGUE' as const,
|
|
1165
|
-
entitySubtype: eventConfig.catalogueType,
|
|
1166
|
-
rootEntityRef: eventConfig.catalogueRef,
|
|
1167
|
-
rootEntityType: 'PRODUCT_CATALOGUE' as const,
|
|
1168
|
-
attributes: product,
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
// ============================================================================
|
|
1172
|
-
// SEQUENTIAL MODE (concurrency === 1)
|
|
1173
|
-
// ============================================================================
|
|
1174
|
-
if (safeConc === 1) {
|
|
1175
|
-
for (let i = 0; i < products.length; i++) {
|
|
1176
|
-
const product = products[i];
|
|
1177
|
-
|
|
1178
|
-
// ✅ PRODUCTION ENHANCEMENT: Log progress every 10 products
|
|
1179
|
-
if (this.log && i % 10 === 0) {
|
|
1180
|
-
this.log.info(`📤 Sending product ${i + 1}/${products.length}`, {
|
|
1181
|
-
productRef: product.ref,
|
|
1182
|
-
progress: `${((i / products.length) * 100).toFixed(1)}%`
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
try {
|
|
1187
|
-
await this.client.sendEvent(buildPayload(product), eventConfig.eventMode);
|
|
1188
|
-
eventsSent++;
|
|
1189
|
-
} catch (err: unknown) {
|
|
1190
|
-
eventsFailed++;
|
|
1191
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1192
|
-
errors.push({ productRef: product?.ref || 'unknown', error: errorMsg });
|
|
1193
|
-
// Continue processing (failure doesn't block other products)
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// ✅ PRODUCTION ENHANCEMENT: Log completion
|
|
1198
|
-
if (this.log) {
|
|
1199
|
-
this.log.info('✅ Sequential event sending completed', {
|
|
1200
|
-
totalProducts: products.length,
|
|
1201
|
-
eventsSent,
|
|
1202
|
-
eventsFailed
|
|
1203
|
-
});
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
return { eventsSent, eventsFailed, errors };
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// ============================================================================
|
|
1210
|
-
// PARALLEL MODE (concurrency > 1)
|
|
1211
|
-
// ============================================================================
|
|
1212
|
-
const totalChunks = Math.ceil(products.length / safeConc);
|
|
1213
|
-
|
|
1214
|
-
for (let i = 0; i < products.length; i += safeConc) {
|
|
1215
|
-
const chunk = products.slice(i, i + safeConc);
|
|
1216
|
-
const chunkNumber = Math.floor(i / safeConc) + 1;
|
|
1217
|
-
|
|
1218
|
-
// ✅ PRODUCTION ENHANCEMENT: Log chunk progress
|
|
1219
|
-
if (this.log) {
|
|
1220
|
-
this.log.info(`📦 Processing chunk ${chunkNumber}/${totalChunks}`, {
|
|
1221
|
-
chunkNumber,
|
|
1222
|
-
totalChunks,
|
|
1223
|
-
productsInChunk: chunk.length,
|
|
1224
|
-
productRange: `${i + 1}-${i + chunk.length}`,
|
|
1225
|
-
totalProducts: products.length,
|
|
1226
|
-
progress: `${((i / products.length) * 100).toFixed(1)}%`
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
// Fire all requests in chunk concurrently
|
|
1231
|
-
const results = await Promise.allSettled(
|
|
1232
|
-
chunk.map(product =>
|
|
1233
|
-
this.client
|
|
1234
|
-
.sendEvent(buildPayload(product), eventConfig.eventMode)
|
|
1235
|
-
.then(() => ({ success: true as const, product }))
|
|
1236
|
-
.catch(error => ({ success: false as const, product, error }))
|
|
1237
|
-
)
|
|
1238
|
-
);
|
|
1239
|
-
|
|
1240
|
-
// Aggregate chunk results into totals
|
|
1241
|
-
let chunkSuccess = 0;
|
|
1242
|
-
let chunkFailed = 0;
|
|
1243
|
-
|
|
1244
|
-
for (const result of results) {
|
|
1245
|
-
if (result.status === 'fulfilled' && result.value.success) {
|
|
1246
|
-
eventsSent++;
|
|
1247
|
-
chunkSuccess++;
|
|
1248
|
-
} else {
|
|
1249
|
-
eventsFailed++;
|
|
1250
|
-
chunkFailed++;
|
|
1251
|
-
const error = result.status === 'fulfilled' ? result.value.error : result.reason;
|
|
1252
|
-
const product = result.status === 'fulfilled' ? result.value.product : null;
|
|
1253
|
-
errors.push({
|
|
1254
|
-
productRef: product?.ref || 'unknown',
|
|
1255
|
-
error: error?.message || String(error) || 'unknown error',
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// ✅ PRODUCTION ENHANCEMENT: Log chunk completion
|
|
1261
|
-
if (this.log) {
|
|
1262
|
-
this.log.info(`✅ Chunk ${chunkNumber}/${totalChunks} completed`, {
|
|
1263
|
-
chunkNumber,
|
|
1264
|
-
totalChunks,
|
|
1265
|
-
chunkSuccess,
|
|
1266
|
-
chunkFailed,
|
|
1267
|
-
totalSentSoFar: eventsSent,
|
|
1268
|
-
totalFailedSoFar: eventsFailed,
|
|
1269
|
-
progress: `${(((i + chunk.length) / products.length) * 100).toFixed(1)}%`
|
|
1270
|
-
});
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// ✅ PRODUCTION ENHANCEMENT: Log parallel completion
|
|
1275
|
-
if (this.log) {
|
|
1276
|
-
this.log.info('✅ Parallel event sending completed', {
|
|
1277
|
-
totalProducts: products.length,
|
|
1278
|
-
totalChunks,
|
|
1279
|
-
concurrency: safeConc,
|
|
1280
|
-
eventsSent,
|
|
1281
|
-
eventsFailed
|
|
1282
|
-
});
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
return { eventsSent, eventsFailed, errors };
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
```
|
|
1289
|
-
|
|
1290
|
-
---
|
|
1291
|
-
|
|
1292
|
-
## 6. Service: Event Logger (`src/services/event-logger.service.ts`)
|
|
1293
|
-
|
|
1294
|
-
```typescript
|
|
1295
|
-
/**
|
|
1296
|
-
* Event Logger Service
|
|
1297
|
-
*
|
|
1298
|
-
* Writes event processing logs to S3 for audit/debugging.
|
|
1299
|
-
* Optional - can be used to create rejection reports.
|
|
1300
|
-
*/
|
|
1301
|
-
|
|
1302
|
-
import { Buffer } from 'node:buffer';
|
|
1303
|
-
import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
|
|
1304
|
-
|
|
1305
|
-
/**
|
|
1306
|
-
* Service for writing event logs to S3
|
|
1307
|
-
*/
|
|
1308
|
-
export class EventLoggerService {
|
|
1309
|
-
constructor(private s3: S3DataSource) {}
|
|
1310
|
-
|
|
1311
|
-
/**
|
|
1312
|
-
* Write event log to S3
|
|
1313
|
-
*
|
|
1314
|
-
* @param objectKey - S3 object key for log file
|
|
1315
|
-
* @param logData - Log data to write (JSON format)
|
|
1316
|
-
*/
|
|
1317
|
-
async writeEventLog(objectKey: string, logData: unknown): Promise<void> {
|
|
1318
|
-
try {
|
|
1319
|
-
const logContent = JSON.stringify(logData, null, 2);
|
|
1320
|
-
await this.s3.uploadFile(objectKey, logContent);
|
|
1321
|
-
} catch (error: unknown) {
|
|
1322
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1323
|
-
throw new Error(`Failed to write event log: ${errorMessage}`);
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
```
|
|
1328
|
-
|
|
1329
|
-
## 7. Main Orchestration Service (src/services/product-ingestion.service.ts)
|
|
1330
|
-
|
|
1331
|
-
```typescript
|
|
1332
|
-
/**
|
|
1333
|
-
* MAIN INGESTION ORCHESTRATION SERVICE
|
|
1334
|
-
*
|
|
1335
|
-
* This is the heart of the ingestion workflow. It coordinates all steps:
|
|
1336
|
-
* 1. Initialize clients and services
|
|
1337
|
-
* 2. Discover files on S3
|
|
1338
|
-
* 3. Download and parse XML files
|
|
1339
|
-
* 4. Transform data with UniversalMapper
|
|
1340
|
-
* 5. Send events to Fluent Commerce
|
|
1341
|
-
* 6. Archive processed files
|
|
1342
|
-
* 7. Track file processing state (S3 uses moveObject for deduplication)
|
|
1343
|
-
* 8. Track job progress with JobTracker
|
|
1344
|
-
*
|
|
1345
|
-
* NAMING PATTERN (consistent across all use cases):
|
|
1346
|
-
* - Interface: {Entity}IngestionParams (e.g., ProductIngestionParams)
|
|
1347
|
-
* - Result: {Entity}IngestionResult (e.g., ProductIngestionResult)
|
|
1348
|
-
* - Main function: execute{Entity}Ingestion (e.g., executeProductIngestion)
|
|
1349
|
-
*
|
|
1350
|
-
* KEY DIFFERENCES FOR S3:
|
|
1351
|
-
* - S3DataSource instead of SftpDataSource
|
|
1352
|
-
* - No VersoriFileTracker (S3 uses moveObject for deduplication)
|
|
1353
|
-
* - S3 listObjects instead of SFTP listFiles
|
|
1354
|
-
* - S3 moveObject instead of SFTP moveFile
|
|
1355
|
-
* - S3 dispose() for cleanup (S3DataSource does need dispose)
|
|
1356
|
-
*/
|
|
1357
|
-
|
|
1358
|
-
import { Buffer } from 'node:buffer';
|
|
1359
|
-
import {
|
|
1360
|
-
createClient,
|
|
1361
|
-
S3DataSource,
|
|
1362
|
-
XMLParserService,
|
|
1363
|
-
UniversalMapper,
|
|
1364
|
-
JobTracker,
|
|
1365
|
-
type FluentClient,
|
|
1366
|
-
type JobStatus,
|
|
1367
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
1368
|
-
import type {
|
|
1369
|
-
ProductIngestionParams,
|
|
1370
|
-
ProductIngestionResult,
|
|
1371
|
-
FileProcessingResult,
|
|
1372
|
-
EventConfig,
|
|
1373
|
-
} from '../types/product-ingestion.types';
|
|
1374
|
-
import { ProductFileProcessorService } from './product-file-processor.service';
|
|
1375
|
-
import { EventSenderService } from './event-sender.service';
|
|
1376
|
-
import { EventLoggerService } from './event-logger.service';
|
|
1377
|
-
import { timestampedName } from '../utils/s3-path.utils';
|
|
1378
|
-
|
|
1379
|
-
import mappingConfig from '../../config/products.import.xml.json' with { type: 'json' };
|
|
1380
|
-
|
|
1381
|
-
// ✅ VERSORI PLATFORM: Use native log from context, not LoggingService
|
|
1382
|
-
|
|
1383
|
-
/**
|
|
1384
|
-
* Query job status from KV store
|
|
1385
|
-
*
|
|
1386
|
-
* NAMING: get{Entity}JobStatus or just getJobStatus (generic)
|
|
1387
|
-
*
|
|
1388
|
-
* ✅ VERSORI PLATFORM: Uses native log from context (passed as parameter)
|
|
1389
|
-
*/
|
|
1390
|
-
export async function getJobStatus(
|
|
1391
|
-
kv: ReturnType<VersoriContext['openKv']>,
|
|
1392
|
-
jobId: string,
|
|
1393
|
-
log: VersoriContext['log']
|
|
1394
|
-
): Promise<JobStatus | undefined> {
|
|
1395
|
-
try {
|
|
1396
|
-
const tracker = new JobTracker(kv, log);
|
|
1397
|
-
return await tracker.getJob(jobId); // ✅ Use getJob() not getJobStatus()
|
|
1398
|
-
} catch (error: unknown) {
|
|
1399
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1400
|
-
log.error('Failed to get job status', {
|
|
1401
|
-
jobId,
|
|
1402
|
-
message: errorMessage,
|
|
1403
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1404
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1405
|
-
});
|
|
1406
|
-
return undefined;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
/**
|
|
1411
|
-
* MAIN ORCHESTRATION FUNCTION
|
|
1412
|
-
*
|
|
1413
|
-
* NAMING: execute{Entity}Ingestion (e.g., executeProductIngestion)
|
|
1414
|
-
*
|
|
1415
|
-
* This function implements the complete ingestion workflow in 8 steps.
|
|
1416
|
-
* Each step is clearly commented for AI understanding.
|
|
1417
|
-
*/
|
|
1418
|
-
export async function executeProductIngestion(
|
|
1419
|
-
ctx,
|
|
1420
|
-
params: ProductIngestionParams
|
|
1421
|
-
): Promise<ProductIngestionResult> {
|
|
1422
|
-
|
|
1423
|
-
// ✅ VERSORI PLATFORM: Extract native log from context (LoggingService was removed - use native log)
|
|
1424
|
-
const { log, openKv, activation } = ctx;
|
|
1425
|
-
const { jobId, triggeredBy, filePattern, maxFiles, forceReprocess } = params;
|
|
1426
|
-
|
|
1427
|
-
// Open KV store for state management and job tracking
|
|
1428
|
-
// ✅ Pass raw Versori KV directly - it matches KVAdapter interface
|
|
1429
|
-
// ✅ Pass native log to JobTracker
|
|
1430
|
-
const kv = openKv(':project:');
|
|
1431
|
-
const tracker = new JobTracker(kv, log);
|
|
1432
|
-
|
|
1433
|
-
const startTime = Date.now();
|
|
1434
|
-
const fileResults: FileProcessingResult[] = [];
|
|
1435
|
-
|
|
1436
|
-
// ⚠️ CRITICAL: Declare S3 outside try block for disposal pattern
|
|
1437
|
-
let s3: S3DataSource | undefined;
|
|
1438
|
-
|
|
1439
|
-
try {
|
|
1440
|
-
//
|
|
1441
|
-
// STEP 1: Initialize Job Tracking
|
|
1442
|
-
//
|
|
1443
|
-
log.info('📋 [STEP 1/8] Initializing job tracking', { jobId });
|
|
1444
|
-
|
|
1445
|
-
await tracker.createJob(jobId, {
|
|
1446
|
-
triggeredBy,
|
|
1447
|
-
filePattern: filePattern || 'default',
|
|
1448
|
-
maxFiles: maxFiles || 'unlimited',
|
|
1449
|
-
forceReprocess: !!forceReprocess
|
|
1450
|
-
});
|
|
1451
|
-
|
|
1452
|
-
//
|
|
1453
|
-
// STEP 2: Initialize Fluent Client & S3 Connection
|
|
1454
|
-
//
|
|
1455
|
-
log.info('🔌 [STEP 2/8] Initializing Fluent Commerce client and S3', { jobId });
|
|
1456
|
-
|
|
1457
|
-
const client = await createClient(ctx);
|
|
1458
|
-
|
|
1459
|
-
if (!client) {
|
|
1460
|
-
throw new Error('Failed to create Fluent Commerce client');
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
// ✅ CRITICAL: Set retailerId for Event API calls
|
|
1464
|
-
// Event API requires retailerId - fail fast if not configured
|
|
1465
|
-
const fluentRetailerId = activation.getVariable('fluentRetailerId');
|
|
1466
|
-
if (!fluentRetailerId) {
|
|
1467
|
-
throw new Error('fluentRetailerId is required for Event API calls');
|
|
1468
|
-
}
|
|
1469
|
-
client.setRetailerId(fluentRetailerId);
|
|
1470
|
-
log.info('✅ RetailerId set for Event API', { retailerId: fluentRetailerId });
|
|
1471
|
-
|
|
1472
|
-
// Get S3 configuration from activation variables
|
|
1473
|
-
const s3Config = {
|
|
1474
|
-
bucket: activation.getVariable('s3Bucket'),
|
|
1475
|
-
region: activation.getVariable('s3Region') || 'us-east-1',
|
|
1476
|
-
accessKeyId: activation.getVariable('s3AccessKeyId'),
|
|
1477
|
-
secretAccessKey: activation.getVariable('s3SecretAccessKey'),
|
|
1478
|
-
incomingPrefix: activation.getVariable('s3IncomingPrefix') || 'products/incoming/',
|
|
1479
|
-
processedPrefix: activation.getVariable('s3ProcessedPrefix') || 'products/processed/',
|
|
1480
|
-
errorPrefix: activation.getVariable('s3ErrorPrefix') || 'products/errors/',
|
|
1481
|
-
logsPrefix: activation.getVariable('s3LogsPrefix') || 'products/logs/',
|
|
1482
|
-
filePattern: filePattern || activation.getVariable('filePattern') || 'products_*.xml'
|
|
1483
|
-
};
|
|
1484
|
-
|
|
1485
|
-
// Validate S3 config
|
|
1486
|
-
if (!s3Config.bucket) {
|
|
1487
|
-
throw new Error('S3 configuration incomplete: missing bucket');
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
if (!s3Config.accessKeyId || !s3Config.secretAccessKey) {
|
|
1491
|
-
throw new Error('S3 configuration incomplete: missing accessKeyId or secretAccessKey');
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
// Initialize S3 data source
|
|
1495
|
-
// ✅ VERSORI PLATFORM: Pass native log from context
|
|
1496
|
-
s3 = new S3DataSource(
|
|
1497
|
-
{
|
|
1498
|
-
type: 'S3_XML',
|
|
1499
|
-
connectionId: 's3-product-ingestion',
|
|
1500
|
-
name: 'Product Ingestion S3',
|
|
1501
|
-
s3Config: {
|
|
1502
|
-
bucket: s3Config.bucket,
|
|
1503
|
-
region: s3Config.region,
|
|
1504
|
-
accessKeyId: s3Config.accessKeyId,
|
|
1505
|
-
secretAccessKey: s3Config.secretAccessKey,
|
|
1506
|
-
},
|
|
1507
|
-
validateConnection: true, // ✅ Enable connection validation on init
|
|
1508
|
-
},
|
|
1509
|
-
log
|
|
1510
|
-
);
|
|
1511
|
-
|
|
1512
|
-
try {
|
|
1513
|
-
// Connection already validated during initialization
|
|
1514
|
-
// await s3.validateConnection(); // No longer needed - done in constructor
|
|
1515
|
-
log.info('✅ S3 connection validated');
|
|
1516
|
-
|
|
1517
|
-
//
|
|
1518
|
-
// STEP 3: Discover Files on S3
|
|
1519
|
-
//
|
|
1520
|
-
log.info('🔍 [STEP 3/8] Discovering files on S3', {
|
|
1521
|
-
jobId,
|
|
1522
|
-
prefix: s3Config.incomingPrefix,
|
|
1523
|
-
filePattern: s3Config.filePattern
|
|
1524
|
-
});
|
|
1525
|
-
|
|
1526
|
-
await tracker.updateJob(jobId, {
|
|
1527
|
-
status: 'processing',
|
|
1528
|
-
stage: 'file_discovery',
|
|
1529
|
-
message: 'Discovering files on S3'
|
|
1530
|
-
});
|
|
1531
|
-
|
|
1532
|
-
// List objects from S3
|
|
1533
|
-
const allFiles = await s3.listObjects({
|
|
1534
|
-
prefix: s3Config.incomingPrefix,
|
|
1535
|
-
pattern: s3Config.filePattern
|
|
1536
|
-
});
|
|
1537
|
-
|
|
1538
|
-
log.info(`📁 Discovered ${allFiles.length} files matching pattern`);
|
|
1539
|
-
|
|
1540
|
-
// Apply maxFiles limit if specified
|
|
1541
|
-
const filesToProcess = maxFiles ? allFiles.slice(0, maxFiles) : allFiles;
|
|
1542
|
-
|
|
1543
|
-
if (filesToProcess.length === 0) {
|
|
1544
|
-
log.info('No files to process');
|
|
1545
|
-
|
|
1546
|
-
await tracker.markCompleted(jobId, {
|
|
1547
|
-
filesProcessed: 0,
|
|
1548
|
-
message: 'No files found to process'
|
|
1549
|
-
});
|
|
1550
|
-
|
|
1551
|
-
return {
|
|
1552
|
-
success: true,
|
|
1553
|
-
jobId,
|
|
1554
|
-
filesProcessed: 0,
|
|
1555
|
-
filesFailed: 0,
|
|
1556
|
-
recordsProcessed: 0,
|
|
1557
|
-
eventsSent: 0,
|
|
1558
|
-
eventsFailed: 0,
|
|
1559
|
-
fileResults: []
|
|
1560
|
-
};
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
log.info(`⚙️ Processing ${filesToProcess.length} files`);
|
|
1564
|
-
|
|
1565
|
-
// Initialize services
|
|
1566
|
-
const xmlParser = new XMLParserService();
|
|
1567
|
-
const mapper = new UniversalMapper(mappingConfig);
|
|
1568
|
-
|
|
1569
|
-
// Get event configuration
|
|
1570
|
-
const eventConfig: EventConfig = {
|
|
1571
|
-
catalogueRef: params.catalogueRef || activation.getVariable('catalogueRef') || 'PC:MASTER:2',
|
|
1572
|
-
catalogueType: activation.getVariable('catalogueType') || 'MASTER',
|
|
1573
|
-
eventName: activation.getVariable('eventName') || 'UPSERT_PRODUCT',
|
|
1574
|
-
eventMode: (activation.getVariable('eventMode') || 'async') as 'async' | 'sync',
|
|
1575
|
-
};
|
|
1576
|
-
|
|
1577
|
-
// Get event sending configuration
|
|
1578
|
-
const eventConcurrency = Math.max(
|
|
1579
|
-
1,
|
|
1580
|
-
parseInt(activation.getVariable('eventConcurrency') || '1', 10)
|
|
1581
|
-
);
|
|
1582
|
-
// Validate: Ensure concurrency is at least 1 (sequential) or higher (parallel)
|
|
1583
|
-
// concurrency: 1 = sequential, concurrency > 1 = parallel
|
|
1584
|
-
|
|
1585
|
-
log.info(`Event concurrency: ${eventConcurrency}`, {
|
|
1586
|
-
mode: eventConcurrency === 1 ? 'sequential' : 'parallel',
|
|
1587
|
-
concurrentRequests: eventConcurrency === 1 ? 'N/A' : eventConcurrency,
|
|
1588
|
-
});
|
|
1589
|
-
|
|
1590
|
-
// Initialize class-based services
|
|
1591
|
-
const fileProcessor = new ProductFileProcessorService(
|
|
1592
|
-
s3,
|
|
1593
|
-
xmlParser,
|
|
1594
|
-
mapper,
|
|
1595
|
-
eventConfig.catalogueRef
|
|
1596
|
-
);
|
|
1597
|
-
// ✅ PRODUCTION ENHANCEMENT: Pass log to EventSenderService for detailed progress tracking
|
|
1598
|
-
const eventSender = new EventSenderService(client, log);
|
|
1599
|
-
const eventLogger = new EventLoggerService(s3);
|
|
1600
|
-
|
|
1601
|
-
//
|
|
1602
|
-
// STEP 4-7: Process Each File (Download → Parse → Transform → Send → Archive)
|
|
1603
|
-
//
|
|
1604
|
-
|
|
1605
|
-
for (let fileIndex = 0; fileIndex < filesToProcess.length; fileIndex++) {
|
|
1606
|
-
const file = filesToProcess[fileIndex];
|
|
1607
|
-
const fileStartTime = Date.now();
|
|
1608
|
-
|
|
1609
|
-
// Use object key from S3 listing
|
|
1610
|
-
const objectKey = file.key || (file as any).name;
|
|
1611
|
-
const baseName = objectKey.split('/').pop() || objectKey;
|
|
1612
|
-
|
|
1613
|
-
log.info(`📄 [FILE ${fileIndex + 1}/${filesToProcess.length}] Processing file: ${baseName}`);
|
|
1614
|
-
|
|
1615
|
-
try {
|
|
1616
|
-
await tracker.updateJob(jobId, {
|
|
1617
|
-
status: 'processing',
|
|
1618
|
-
stage: 'downloading',
|
|
1619
|
-
message: `Processing file ${fileIndex + 1} of ${filesToProcess.length}: ${baseName}`
|
|
1620
|
-
});
|
|
1621
|
-
|
|
1622
|
-
// ═══════════════════════════════════════════════════════════
|
|
1623
|
-
// STEP 4: Process File (Download + Parse + Map)
|
|
1624
|
-
// ═══════════════════════════════════════════════════════════
|
|
1625
|
-
log.info(`⚙️ [STEP 4/8] Processing file: ${baseName}`);
|
|
1626
|
-
|
|
1627
|
-
// Process file: download → parse → transform
|
|
1628
|
-
const fileResult = await fileProcessor.downloadParseAndTransform(objectKey);
|
|
1629
|
-
|
|
1630
|
-
if (!fileResult.success || fileResult.products.length === 0) {
|
|
1631
|
-
// Move failed file to errors and record result
|
|
1632
|
-
const archivedName = timestampedName(baseName);
|
|
1633
|
-
const errorKey = `${s3Config.errorPrefix}${archivedName}`;
|
|
1634
|
-
await s3.moveObject(objectKey, errorKey);
|
|
1635
|
-
|
|
1636
|
-
fileResults.push({
|
|
1637
|
-
fileName: baseName,
|
|
1638
|
-
success: false,
|
|
1639
|
-
recordsProcessed: fileResult.products.length,
|
|
1640
|
-
eventsSent: 0,
|
|
1641
|
-
eventsFailed: 0,
|
|
1642
|
-
duration: Date.now() - fileStartTime,
|
|
1643
|
-
error: fileResult.error || 'Processing failed'
|
|
1644
|
-
});
|
|
1645
|
-
|
|
1646
|
-
continue;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
// ═══════════════════════════════════════════════════════════
|
|
1650
|
-
// STEP 5: Send Events
|
|
1651
|
-
// ═══════════════════════════════════════════════════════════
|
|
1652
|
-
log.info(`📤 [STEP 5/8] Sending events for ${baseName}`);
|
|
1653
|
-
|
|
1654
|
-
// ? Enhanced: Extract context for progress logging
|
|
1655
|
-
const sampleProductRefs = fileResult.products.slice(0, 5).map((p: any) => p.ref || p.skuRef);
|
|
1656
|
-
const eventType = eventConfig.eventType || 'UPSERT_PRODUCT';
|
|
1657
|
-
|
|
1658
|
-
// ? Enhanced: Start logging with context
|
|
1659
|
-
log.info(`[EventSender] Sending events for file "${baseName}"`, {
|
|
1660
|
-
totalProducts: fileResult.products.length,
|
|
1661
|
-
eventType,
|
|
1662
|
-
concurrency: eventConcurrency === 1 ? 'sequential' : `parallel (${eventConcurrency})`,
|
|
1663
|
-
sampleProductRefs: sampleProductRefs.join(', '),
|
|
1664
|
-
eventMode: eventConfig.eventMode || 'async'
|
|
1665
|
-
});
|
|
1666
|
-
|
|
1667
|
-
const eventResult = await eventSender.sendEvents(
|
|
1668
|
-
fileResult.products,
|
|
1669
|
-
eventConfig,
|
|
1670
|
-
eventConcurrency
|
|
1671
|
-
);
|
|
1672
|
-
|
|
1673
|
-
const eventsSent = eventResult.eventsSent;
|
|
1674
|
-
const eventsFailed = eventResult.eventsFailed;
|
|
1675
|
-
|
|
1676
|
-
log.info(`✅ Events sent for ${baseName}`, {
|
|
1677
|
-
successful: eventsSent,
|
|
1678
|
-
failed: eventsFailed
|
|
1679
|
-
});
|
|
1680
|
-
|
|
1681
|
-
// ? Enhanced: Completion logging with summary
|
|
1682
|
-
log.info(`[EventSender] Event submission completed for file "${baseName}"`, {
|
|
1683
|
-
totalProducts: fileResult.products.length,
|
|
1684
|
-
eventsSent,
|
|
1685
|
-
eventsFailed,
|
|
1686
|
-
successRate: fileResult.products.length > 0 ? `${Math.round((eventsSent / fileResult.products.length) * 100)}%` : '0%',
|
|
1687
|
-
eventType
|
|
1688
|
-
});
|
|
1689
|
-
|
|
1690
|
-
// ═══════════════════════════════════════════════════════════
|
|
1691
|
-
// STEP 6: Archive File
|
|
1692
|
-
// ═══════════════════════════════════════════════════════════
|
|
1693
|
-
log.info(`📦 [STEP 6/8] Archiving file: ${baseName}`);
|
|
1694
|
-
|
|
1695
|
-
await tracker.updateJob(jobId, {
|
|
1696
|
-
status: 'processing',
|
|
1697
|
-
stage: 'archiving',
|
|
1698
|
-
message: `Archiving ${baseName}`
|
|
1699
|
-
});
|
|
1700
|
-
|
|
1701
|
-
// Conditionally archive: errors → errorPrefix, else → processedPrefix (timestamped)
|
|
1702
|
-
// ✅ S3: Uses moveObject for deduplication (no VersoriFileTracker needed)
|
|
1703
|
-
const archivedName = timestampedName(baseName);
|
|
1704
|
-
const targetPrefix = eventsFailed > 0 ? s3Config.errorPrefix : s3Config.processedPrefix;
|
|
1705
|
-
const targetKey = `${targetPrefix}${archivedName}`;
|
|
1706
|
-
await s3.moveObject(objectKey, targetKey);
|
|
1707
|
-
|
|
1708
|
-
log.info(`✅ File archived successfully: ${baseName}`, { targetKey });
|
|
1709
|
-
|
|
1710
|
-
// Optional: Write event log for audit/debugging
|
|
1711
|
-
if (eventsFailed > 0) {
|
|
1712
|
-
const logKey = `${s3Config.logsPrefix}${baseName.replace(/\.xml$/i, '')}-event-log.json`;
|
|
1713
|
-
await eventLogger.writeEventLog(logKey, {
|
|
1714
|
-
fileName: baseName,
|
|
1715
|
-
totalRecords: fileResult.products.length,
|
|
1716
|
-
eventsSent,
|
|
1717
|
-
eventsFailed,
|
|
1718
|
-
errors: eventResult.errors,
|
|
1719
|
-
processedAt: new Date().toISOString()
|
|
1720
|
-
});
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
// Store file result
|
|
1724
|
-
fileResults.push({
|
|
1725
|
-
fileName: baseName,
|
|
1726
|
-
success: true,
|
|
1727
|
-
recordsProcessed: fileResult.products.length,
|
|
1728
|
-
eventsSent,
|
|
1729
|
-
eventsFailed,
|
|
1730
|
-
duration: Date.now() - fileStartTime
|
|
1731
|
-
});
|
|
1732
|
-
|
|
1733
|
-
} catch (error: unknown) {
|
|
1734
|
-
// ✅ Enhanced error logging: Extract all error details for visibility
|
|
1735
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1736
|
-
const errorDetails = {
|
|
1737
|
-
message: errorMessage,
|
|
1738
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1739
|
-
fileName: (error as any)?.fileName,
|
|
1740
|
-
lineNumber: (error as any)?.lineNumber,
|
|
1741
|
-
originalError: (error as any)?.context?.originalError?.message,
|
|
1742
|
-
errorType: error instanceof Error ? error.name : 'Error',
|
|
1743
|
-
};
|
|
1744
|
-
log.error(`Error processing file ${baseName}:`, errorDetails);
|
|
1745
|
-
|
|
1746
|
-
// Best-effort: move to error archive on unexpected failure
|
|
1747
|
-
try {
|
|
1748
|
-
const archivedName = timestampedName(baseName);
|
|
1749
|
-
const errorKey = `${s3Config.errorPrefix}${archivedName}`;
|
|
1750
|
-
await s3.moveObject(objectKey, errorKey);
|
|
1751
|
-
} catch (moveError: unknown) {
|
|
1752
|
-
const moveErrorMessage = moveError instanceof Error ? moveError.message : String(moveError);
|
|
1753
|
-
log.error('Could not archive failed file', {
|
|
1754
|
-
file: baseName,
|
|
1755
|
-
message: moveErrorMessage,
|
|
1756
|
-
stack: moveError instanceof Error ? moveError.stack : undefined,
|
|
1757
|
-
errorType: moveError instanceof Error ? moveError.constructor.name : 'Error'
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
fileResults.push({
|
|
1762
|
-
fileName: baseName,
|
|
1763
|
-
success: false,
|
|
1764
|
-
recordsProcessed: 0,
|
|
1765
|
-
eventsSent: 0,
|
|
1766
|
-
eventsFailed: 0,
|
|
1767
|
-
duration: Date.now() - fileStartTime,
|
|
1768
|
-
error: errorMessage
|
|
1769
|
-
});
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
//
|
|
1774
|
-
// STEP 8: Complete Job & Calculate Totals
|
|
1775
|
-
//
|
|
1776
|
-
log.info('🏁 [STEP 8/8] Completing job and calculating totals', { jobId });
|
|
1777
|
-
|
|
1778
|
-
const filesProcessed = fileResults.filter(r => r.success).length;
|
|
1779
|
-
const filesFailed = fileResults.filter(r => !r.success).length;
|
|
1780
|
-
const totalRecordsProcessed = fileResults.reduce((sum, r) => sum + r.recordsProcessed, 0);
|
|
1781
|
-
const totalEventsSent = fileResults.reduce((sum, r) => sum + r.eventsSent, 0);
|
|
1782
|
-
const totalEventsFailed = fileResults.reduce((sum, r) => sum + r.eventsFailed, 0);
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
await tracker.markCompleted(jobId, {
|
|
1786
|
-
filesProcessed,
|
|
1787
|
-
filesFailed,
|
|
1788
|
-
recordsProcessed: totalRecordsProcessed,
|
|
1789
|
-
eventsSent: totalEventsSent,
|
|
1790
|
-
eventsFailed: totalEventsFailed,
|
|
1791
|
-
duration: Date.now() - startTime
|
|
1792
|
-
});
|
|
1793
|
-
|
|
1794
|
-
log.info('✅ Ingestion completed successfully', {
|
|
1795
|
-
filesProcessed,
|
|
1796
|
-
filesFailed,
|
|
1797
|
-
recordsProcessed: totalRecordsProcessed,
|
|
1798
|
-
eventsSent: totalEventsSent,
|
|
1799
|
-
eventsFailed: totalEventsFailed,
|
|
1800
|
-
duration: Date.now() - startTime
|
|
1801
|
-
});
|
|
1802
|
-
|
|
1803
|
-
return {
|
|
1804
|
-
success: true,
|
|
1805
|
-
jobId,
|
|
1806
|
-
filesProcessed,
|
|
1807
|
-
filesFailed,
|
|
1808
|
-
recordsProcessed: totalRecordsProcessed,
|
|
1809
|
-
eventsSent: totalEventsSent,
|
|
1810
|
-
eventsFailed: totalEventsFailed,
|
|
1811
|
-
fileResults
|
|
1812
|
-
};
|
|
1813
|
-
|
|
1814
|
-
} finally {
|
|
1815
|
-
// ❌š ï¸ CRITICAL: Always dispose S3 connection
|
|
1816
|
-
if (s3) {
|
|
1817
|
-
await s3.dispose();
|
|
1818
|
-
log.info('S3 connection disposed');
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
} catch (error: unknown) {
|
|
1823
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1824
|
-
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
1825
|
-
|
|
1826
|
-
// Generate contextual recommendations based on error type
|
|
1827
|
-
const recommendations = [];
|
|
1828
|
-
if (errorMessage.includes('S3') || errorMessage.includes('bucket')) {
|
|
1829
|
-
recommendations.push('Verify S3 credentials (accessKeyId, secretAccessKey)');
|
|
1830
|
-
recommendations.push('Check S3 bucket name and region configuration');
|
|
1831
|
-
recommendations.push('Ensure bucket permissions allow ListObjects and GetObject');
|
|
1832
|
-
}
|
|
1833
|
-
if (errorMessage.includes('XML') || errorMessage.includes('parse')) {
|
|
1834
|
-
recommendations.push('Validate XML file structure against expected schema');
|
|
1835
|
-
recommendations.push('Check for special characters or encoding issues');
|
|
1836
|
-
recommendations.push('Review XML parser configuration');
|
|
1837
|
-
}
|
|
1838
|
-
if (errorMessage.includes('event') || errorMessage.includes('API')) {
|
|
1839
|
-
recommendations.push('Verify Fluent API credentials and retailerId');
|
|
1840
|
-
recommendations.push('Check network connectivity to Fluent API');
|
|
1841
|
-
recommendations.push('Review event payload structure');
|
|
1842
|
-
}
|
|
1843
|
-
if (errorMessage.includes('mapping')) {
|
|
1844
|
-
recommendations.push('Review mapping configuration in config/*.json');
|
|
1845
|
-
recommendations.push('Check for missing required fields');
|
|
1846
|
-
recommendations.push('Validate resolver functions');
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
log.error('❌ Ingestion workflow failed', {
|
|
1850
|
-
jobId,
|
|
1851
|
-
error: errorMessage,
|
|
1852
|
-
stack: errorStack,
|
|
1853
|
-
recommendations: recommendations.length > 0 ? recommendations : ['Review logs for detailed error information'],
|
|
1854
|
-
});
|
|
1855
|
-
|
|
1856
|
-
// Try to mark job as failed, but don't let tracking errors mask workflow error
|
|
1857
|
-
try {
|
|
1858
|
-
await tracker.markFailed(jobId, error);
|
|
1859
|
-
} catch (trackingError: unknown) {
|
|
1860
|
-
const trackingErrorMessage =
|
|
1861
|
-
trackingError instanceof Error ? trackingError.message : String(trackingError);
|
|
1862
|
-
log.warn('Failed to mark job as failed in tracker', {
|
|
1863
|
-
jobId,
|
|
1864
|
-
trackingError: trackingErrorMessage,
|
|
1865
|
-
});
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
return {
|
|
1869
|
-
success: false,
|
|
1870
|
-
jobId,
|
|
1871
|
-
filesProcessed: fileResults.filter(r => r.success && !r.skipped).length,
|
|
1872
|
-
filesFailed: fileResults.filter(r => !r.success).length,
|
|
1873
|
-
filesSkipped: fileResults.filter(r => r.skipped).length,
|
|
1874
|
-
recordsProcessed: fileResults.reduce((sum, r) => sum + r.recordsProcessed, 0),
|
|
1875
|
-
eventsSent: fileResults.reduce((sum, r) => sum + r.eventsSent, 0),
|
|
1876
|
-
eventsFailed: fileResults.reduce((sum, r) => sum + r.eventsFailed, 0),
|
|
1877
|
-
fileResults,
|
|
1878
|
-
error: errorMessage,
|
|
1879
|
-
};
|
|
1880
|
-
} finally {
|
|
1881
|
-
// ⚠️ CRITICAL: Ensure S3 is disposed even if outer error occurs
|
|
1882
|
-
// This outer finally ensures disposal if error happens after S3 creation
|
|
1883
|
-
// but before inner try block (e.g., during connection validation)
|
|
1884
|
-
if (s3) {
|
|
1885
|
-
try {
|
|
1886
|
-
await s3.dispose();
|
|
1887
|
-
log.info('🔌 S3 connection disposed (outer finally)');
|
|
1888
|
-
} catch (disposeError: unknown) {
|
|
1889
|
-
const disposeErrorMessage =
|
|
1890
|
-
disposeError instanceof Error ? disposeError.message : String(disposeError);
|
|
1891
|
-
log.warn('⚠️ Error disposing S3 in outer finally', { error: disposeErrorMessage });
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
// ═══════════════════════════════════════════════════════════
|
|
1896
|
-
// EXECUTION BOUNDARY: Product Ingestion End
|
|
1897
|
-
// ═══════════════════════════════════════════════════════════
|
|
1898
|
-
}
|
|
1899
|
-
```
|
|
1900
|
-
|
|
1901
|
-
## 8. Utility Functions (src/utils/)
|
|
1902
|
-
|
|
1903
|
-
### S3 Path Helpers (src/utils/s3-path.utils.ts)
|
|
1904
|
-
|
|
1905
|
-
```typescript
|
|
1906
|
-
/**
|
|
1907
|
-
* S3 Path Utilities
|
|
1908
|
-
*
|
|
1909
|
-
* Helper functions for S3 path operations.
|
|
1910
|
-
*/
|
|
1911
|
-
|
|
1912
|
-
/**
|
|
1913
|
-
* Generate timestamped filename for archival
|
|
1914
|
-
*
|
|
1915
|
-
* Adds ISO timestamp to filename before extension for unique archival.
|
|
1916
|
-
* Handles XML files specifically but can be adapted for other formats.
|
|
1917
|
-
*
|
|
1918
|
-
* @param name - Original filename
|
|
1919
|
-
* @returns Filename with timestamp appended
|
|
1920
|
-
*
|
|
1921
|
-
* @example
|
|
1922
|
-
* ```typescript
|
|
1923
|
-
* timestampedName('products.xml')
|
|
1924
|
-
* // Returns: 'products-2025-11-01T18-30-45-123Z.xml'
|
|
1925
|
-
* ```
|
|
1926
|
-
*/
|
|
1927
|
-
export function timestampedName(name: string): string {
|
|
1928
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1929
|
-
const base = name.replace(/\.xml$/i, '');
|
|
1930
|
-
return `${base}-${ts}.xml`;
|
|
1931
|
-
}
|
|
1932
|
-
```
|
|
1933
|
-
|
|
1934
|
-
### Job ID Generator (src/utils/job-id-generator.ts)
|
|
1935
|
-
|
|
1936
|
-
```typescript
|
|
1937
|
-
/**
|
|
1938
|
-
* Job ID Generator
|
|
1939
|
-
*
|
|
1940
|
-
* Generates unique job IDs for tracking ingestion runs.
|
|
1941
|
-
* Format: {PREFIX}_{ENTITY}_{DATE}_{TIME}_{RANDOM}
|
|
1942
|
-
* Example: SCHEDULED_PROD_20250124_183045_abc123
|
|
1943
|
-
*/
|
|
1944
|
-
|
|
1945
|
-
/**
|
|
1946
|
-
* Generate unique job ID
|
|
1947
|
-
*
|
|
1948
|
-
* @param prefix - Job prefix (SCHEDULED, ADHOC, etc.)
|
|
1949
|
-
* @param entity - Entity type (PROD, INV, etc.)
|
|
1950
|
-
* @returns Unique job ID string
|
|
1951
|
-
*/
|
|
1952
|
-
export function generateJobId(prefix: string, entity: string): string {
|
|
1953
|
-
const now = new Date();
|
|
1954
|
-
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
1955
|
-
const time = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
1956
|
-
const random = Math.random().toString(36).slice(2, 8).toUpperCase();
|
|
1957
|
-
return `${prefix}_${entity}_${date}_${time}_${random}`;
|
|
1958
|
-
}
|
|
1959
|
-
```
|
|
1960
|
-
|
|
1961
|
-
## 📄 Mapping Configuration
|
|
1962
|
-
|
|
1963
|
-
**File:** `config/products.import.xml.json`
|
|
1964
|
-
|
|
1965
|
-
```json
|
|
1966
|
-
{
|
|
1967
|
-
"name": "products.import.xml",
|
|
1968
|
-
"version": "1.2.0",
|
|
1969
|
-
"description": "XML → Product Event Mapping",
|
|
1970
|
-
"fields": {
|
|
1971
|
-
"ref": {
|
|
1972
|
-
"source": "ref",
|
|
1973
|
-
"required": true,
|
|
1974
|
-
"resolver": "sdk.trim",
|
|
1975
|
-
"comment": "Product reference from XML element"
|
|
1976
|
-
},
|
|
1977
|
-
"type": {
|
|
1978
|
-
"source": "type",
|
|
1979
|
-
"required": true,
|
|
1980
|
-
"resolver": "sdk.uppercase",
|
|
1981
|
-
"comment": "Product type (STANDARD, VARIANT)"
|
|
1982
|
-
},
|
|
1983
|
-
"status": {
|
|
1984
|
-
"source": "status",
|
|
1985
|
-
"required": true,
|
|
1986
|
-
"resolver": "sdk.uppercase",
|
|
1987
|
-
"comment": "Product status (ACTIVE, INACTIVE)"
|
|
1988
|
-
},
|
|
1989
|
-
"name": {
|
|
1990
|
-
"source": "name",
|
|
1991
|
-
"required": true,
|
|
1992
|
-
"resolver": "sdk.trim",
|
|
1993
|
-
"comment": "Product name/title"
|
|
1994
|
-
},
|
|
1995
|
-
"summary": {
|
|
1996
|
-
"source": "summary",
|
|
1997
|
-
"required": false,
|
|
1998
|
-
"comment": "Product short description"
|
|
1999
|
-
},
|
|
2000
|
-
"gtin": {
|
|
2001
|
-
"source": "gtin",
|
|
2002
|
-
"required": false,
|
|
2003
|
-
"resolver": "sdk.trim",
|
|
2004
|
-
"comment": "Global Trade Item Number (barcode)"
|
|
2005
|
-
},
|
|
2006
|
-
"catalogue.ref": {
|
|
2007
|
-
"source": "$context.catalogueRef",
|
|
2008
|
-
"required": false,
|
|
2009
|
-
"comment": "Catalogue reference (injected from context, NOT in XML)"
|
|
2010
|
-
},
|
|
2011
|
-
"metadata.source": {
|
|
2012
|
-
"value": "S3_XML",
|
|
2013
|
-
"required": false,
|
|
2014
|
-
"comment": "Static value - identifies data source (NOT in XML)"
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
```
|
|
2019
|
-
|
|
2020
|
-
---
|
|
2021
|
-
|
|
2022
|
-
## 9. Package Configuration
|
|
2023
|
-
|
|
2024
|
-
### `package.json`
|
|
2025
|
-
|
|
2026
|
-
```json
|
|
2027
|
-
{
|
|
2028
|
-
"name": "s3-xml-product-event-ingestion",
|
|
2029
|
-
"version": "1.2.0",
|
|
2030
|
-
"description": "S3 XML Product Event Ingestion Connector",
|
|
2031
|
-
"type": "module",
|
|
2032
|
-
"main": "src/index.ts",
|
|
2033
|
-
"dependencies": {
|
|
2034
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
2035
|
-
"@versori/run": "^1.0.0"
|
|
2036
|
-
},
|
|
2037
|
-
"devDependencies": {
|
|
2038
|
-
"@types/node": "^20.0.0",
|
|
2039
|
-
"typescript": "^5.0.0"
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
```
|
|
2043
|
-
|
|
2044
|
-
---
|
|
2045
|
-
|
|
2046
|
-
**File:** `config/products.import.xml.json`
|
|
2047
|
-
|
|
2048
|
-
```json
|
|
2049
|
-
{
|
|
2050
|
-
"name": "products.import.xml",
|
|
2051
|
-
"version": "1.1.0",
|
|
2052
|
-
"description": "XML → Product Event Mapping",
|
|
2053
|
-
"fields": {
|
|
2054
|
-
"ref": { "source": "product.ref", "required": true, "resolver": "sdk.trim" },
|
|
2055
|
-
"type": { "source": "product.type", "resolver": "sdk.uppercase", "defaultValue": "STANDARD" },
|
|
2056
|
-
"status": { "source": "product.status", "resolver": "sdk.uppercase", "defaultValue": "ACTIVE" },
|
|
2057
|
-
"gtin": { "source": "product.gtin" },
|
|
2058
|
-
"name": { "source": "product.name", "required": true, "resolver": "sdk.trim" },
|
|
2059
|
-
"summary": { "source": "product.summary" },
|
|
2060
|
-
"categoryRefs": { "source": "product.categoryRefs.ref", "isArray": true },
|
|
2061
|
-
"price": {
|
|
2062
|
-
"source": "product.price.item",
|
|
2063
|
-
"isArray": true,
|
|
2064
|
-
"fields": {
|
|
2065
|
-
"type": { "source": "type" },
|
|
2066
|
-
"currency": { "source": "currency" },
|
|
2067
|
-
"value": { "source": "value" }
|
|
2068
|
-
}
|
|
2069
|
-
},
|
|
2070
|
-
"taxType": {
|
|
2071
|
-
"source": "product.taxType",
|
|
2072
|
-
"fields": {
|
|
2073
|
-
"country": { "source": "country" },
|
|
2074
|
-
"group": { "source": "group" },
|
|
2075
|
-
"tariff": { "source": "tariff" }
|
|
2076
|
-
}
|
|
2077
|
-
},
|
|
2078
|
-
"attributes": { "defaultValue": [] }
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
```
|
|
2082
|
-
|
|
2083
|
-
Note: Adjust fields in the JSON above as needed; prefer built-in resolvers. For advanced patterns, see SDK Universal Mapping guide.
|
|
2084
|
-
|
|
2085
|
-
---
|
|
2086
|
-
|
|
2087
|
-
## Expected XML Format
|
|
2088
|
-
|
|
2089
|
-
**Sample:** `products_20250124.xml`
|
|
2090
|
-
|
|
2091
|
-
```xml
|
|
2092
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2093
|
-
<products>
|
|
2094
|
-
<product>
|
|
2095
|
-
<ref>G_PROD_WITH_NO_STANDARD</ref>
|
|
2096
|
-
<type>VARIANT</type>
|
|
2097
|
-
<status>ACTIVE</status>
|
|
2098
|
-
<gtin>MH01-XS-Orange</gtin>
|
|
2099
|
-
<name>Chaz Kangeroo Hoodie-XS-Orange main</name>
|
|
2100
|
-
<summary><p>test short description</p></summary>
|
|
2101
|
-
<categoryRefs>
|
|
2102
|
-
<ref>STANDARD_CATEGORY</ref>
|
|
2103
|
-
</categoryRefs>
|
|
2104
|
-
<price>
|
|
2105
|
-
<item>
|
|
2106
|
-
<type>DEFAULT</type>
|
|
2107
|
-
<currency>USD</currency>
|
|
2108
|
-
<value>52.000000</value>
|
|
2109
|
-
</item>
|
|
2110
|
-
<item>
|
|
2111
|
-
<type>SPECIAL</type>
|
|
2112
|
-
<currency>USD</currency>
|
|
2113
|
-
<value>9.000000</value>
|
|
2114
|
-
</item>
|
|
2115
|
-
</price>
|
|
2116
|
-
<taxType>
|
|
2117
|
-
<country>AU</country>
|
|
2118
|
-
<group>Tax Group</group>
|
|
2119
|
-
<tariff>Tax Tariff</tariff>
|
|
2120
|
-
</taxType>
|
|
2121
|
-
<catalogue>
|
|
2122
|
-
<ref>PC:MASTER:2</ref>
|
|
2123
|
-
<type>MASTER</type>
|
|
2124
|
-
</catalogue>
|
|
2125
|
-
</product>
|
|
2126
|
-
<product>
|
|
2127
|
-
<ref>G_PROD_002</ref>
|
|
2128
|
-
<type>VARIANT</type>
|
|
2129
|
-
<status>ACTIVE</status>
|
|
2130
|
-
<gtin>MH02-M-Blue</gtin>
|
|
2131
|
-
<name>Kangeroo Hoodie-M-Blue</name>
|
|
2132
|
-
<summary><p>Blue variant medium size</p></summary>
|
|
2133
|
-
<categoryRefs>
|
|
2134
|
-
<ref>STANDARD_CATEGORY</ref>
|
|
2135
|
-
<ref>SEASONAL</ref>
|
|
2136
|
-
</categoryRefs>
|
|
2137
|
-
<price>
|
|
2138
|
-
<item>
|
|
2139
|
-
<type>DEFAULT</type>
|
|
2140
|
-
<currency>USD</currency>
|
|
2141
|
-
<value>45.000000</value>
|
|
2142
|
-
</item>
|
|
2143
|
-
<item>
|
|
2144
|
-
<type>SPECIAL</type>
|
|
2145
|
-
<currency>USD</currency>
|
|
2146
|
-
<value>35.000000</value>
|
|
2147
|
-
</item>
|
|
2148
|
-
</price>
|
|
2149
|
-
<taxType>
|
|
2150
|
-
<country>AU</country>
|
|
2151
|
-
<group>Tax Group</group>
|
|
2152
|
-
<tariff>Tax Tariff</tariff>
|
|
2153
|
-
</taxType>
|
|
2154
|
-
<catalogue>
|
|
2155
|
-
<ref>PC:MASTER:2</ref>
|
|
2156
|
-
<type>MASTER</type>
|
|
2157
|
-
</catalogue>
|
|
2158
|
-
</product>
|
|
2159
|
-
</products>
|
|
2160
|
-
```
|
|
2161
|
-
|
|
2162
|
-
**XML Field Mapping:**
|
|
2163
|
-
|
|
2164
|
-
- Nested structure for objects (e.g., `<taxType><country>AU</country></taxType>`)
|
|
2165
|
-
- Arrays with repeated elements (e.g., `<categoryRefs><ref>CAT1</ref><ref>CAT2</ref></categoryRefs>`)
|
|
2166
|
-
- HTML content in CDATA or escaped (e.g., `<p>description</p>`)
|
|
2167
|
-
|
|
2168
|
-
---
|
|
2169
|
-
|
|
2170
|
-
## 9. Package Configuration
|
|
2171
|
-
|
|
2172
|
-
### `package.json`
|
|
2173
|
-
|
|
2174
|
-
```json
|
|
2175
|
-
{
|
|
2176
|
-
"name": "s3-xml-product-event",
|
|
2177
|
-
"version": "1.1.0",
|
|
2178
|
-
"type": "module",
|
|
2179
|
-
"main": "src/index.ts",
|
|
2180
|
-
"scripts": {
|
|
2181
|
-
"dev": "versori dev",
|
|
2182
|
-
"build": "versori build",
|
|
2183
|
-
"deploy": "versori deploy"
|
|
2184
|
-
},
|
|
2185
|
-
"dependencies": {
|
|
2186
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
2187
|
-
"@versori/run": "latest"
|
|
2188
|
-
},
|
|
2189
|
-
"devDependencies": {
|
|
2190
|
-
"@types/node": "^20.0.0",
|
|
2191
|
-
"typescript": "^5.0.0"
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
```
|
|
2195
|
-
|
|
2196
|
-
### `tsconfig.json`
|
|
2197
|
-
|
|
2198
|
-
```json
|
|
2199
|
-
{
|
|
2200
|
-
"compilerOptions": {
|
|
2201
|
-
"module": "ES2022",
|
|
2202
|
-
"target": "ES2024",
|
|
2203
|
-
"moduleResolution": "node"
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
```
|
|
2207
|
-
|
|
2208
|
-
---
|
|
2209
|
-
|
|
2210
|
-
## 10. Deployment Instructions
|
|
2211
|
-
|
|
2212
|
-
### Deploy to Versori
|
|
2213
|
-
|
|
2214
|
-
```bash
|
|
2215
|
-
# 1. Install dependencies
|
|
2216
|
-
npm install
|
|
2217
|
-
|
|
2218
|
-
# 2. Test locally (if using Versori CLI)
|
|
2219
|
-
npm run dev
|
|
2220
|
-
|
|
2221
|
-
# 3. Deploy to Versori platform
|
|
2222
|
-
npm run deploy
|
|
2223
|
-
```
|
|
2224
|
-
|
|
2225
|
-
### Configure Activation Variables
|
|
2226
|
-
|
|
2227
|
-
In Versori platform settings, configure all variables listed in the Activation Variables section above.
|
|
2228
|
-
|
|
2229
|
-
---
|
|
2230
|
-
|
|
2231
|
-
## 11. Testing
|
|
2232
|
-
|
|
2233
|
-
### Test Scheduled Ingestion
|
|
2234
|
-
|
|
2235
|
-
Upload a test XML file to S3 incoming prefix and wait for the scheduled run.
|
|
2236
|
-
|
|
2237
|
-
**Check logs:**
|
|
2238
|
-
|
|
2239
|
-
```
|
|
2240
|
-
[STEP 1/8] Initializing job tracking
|
|
2241
|
-
[STEP 2/8] Initializing Fluent Commerce client and S3
|
|
2242
|
-
[STEP 3/8] Discovering files on S3
|
|
2243
|
-
[FILE 1/1] Processing file: products_20250124.xml
|
|
2244
|
-
[STEP 4/8] Downloading and parsing: products_20250124.xml
|
|
2245
|
-
[STEP 5/8] Transforming 5 products from products_20250124.xml
|
|
2246
|
-
[STEP 6/8] Sending 5 events to Fluent Commerce
|
|
2247
|
-
[STEP 7/8] Archiving file: products_20250124.xml
|
|
2248
|
-
[STEP 8/8] Completing job and calculating totals
|
|
2249
|
-
```
|
|
2250
|
-
|
|
2251
|
-
### Test Ad hoc Ingestion
|
|
2252
|
-
|
|
2253
|
-
```bash
|
|
2254
|
-
# Process all pending files
|
|
2255
|
-
curl -X POST https://api.versori.com/webhooks/product-ingestion-adhoc \
|
|
2256
|
-
-H "X-API-Key: your-secret-key" \
|
|
2257
|
-
-H "Content-Type: application/json" \
|
|
2258
|
-
-d '{}'
|
|
2259
|
-
|
|
2260
|
-
# Process specific pattern
|
|
2261
|
-
curl -X POST https://api.versori.com/webhooks/product-ingestion-adhoc \
|
|
2262
|
-
-H "X-API-Key: your-secret-key" \
|
|
2263
|
-
-H "Content-Type: application/json" \
|
|
2264
|
-
-d '{
|
|
2265
|
-
"filePattern": "urgent_*.xml", }'
|
|
2266
|
-
```
|
|
2267
|
-
|
|
2268
|
-
### Test Job Status Query
|
|
2269
|
-
|
|
2270
|
-
```bash
|
|
2271
|
-
curl -X POST https://api.versori.com/webhooks/product-ingestion-job-status \
|
|
2272
|
-
-H "X-API-Key: your-secret-key" \
|
|
2273
|
-
-H "Content-Type: application/json" \
|
|
2274
|
-
-d '{
|
|
2275
|
-
"jobId": "ADHOC_PROD_20251024_183045_abc123"
|
|
2276
|
-
}'
|
|
2277
|
-
```
|
|
2278
|
-
|
|
2279
|
-
---
|
|
2280
|
-
|
|
2281
|
-
## Monitoring
|
|
2282
|
-
|
|
2283
|
-
### Success Response
|
|
2284
|
-
|
|
2285
|
-
```json
|
|
2286
|
-
{
|
|
2287
|
-
"success": true,
|
|
2288
|
-
"filesProcessed": 1,
|
|
2289
|
-
"filesSkipped": 0,
|
|
2290
|
-
"filesFailed": 0,
|
|
2291
|
-
"totalRecords": 50,
|
|
2292
|
-
"eventsSent": 50,
|
|
2293
|
-
"eventsFailed": 0,
|
|
2294
|
-
"results": [
|
|
2295
|
-
{
|
|
2296
|
-
"file": "products_2025-01-22.xml",
|
|
2297
|
-
"success": true,
|
|
2298
|
-
"recordsProcessed": 50,
|
|
2299
|
-
"eventsSent": 50,
|
|
2300
|
-
"eventsFailed": 0
|
|
2301
|
-
}
|
|
2302
|
-
],
|
|
2303
|
-
"duration": 12345
|
|
2304
|
-
}
|
|
2305
|
-
```
|
|
2306
|
-
|
|
2307
|
-
### Partial Success Response
|
|
2308
|
-
|
|
2309
|
-
```json
|
|
2310
|
-
{
|
|
2311
|
-
"success": true,
|
|
2312
|
-
"filesProcessed": 1,
|
|
2313
|
-
"filesSkipped": 0,
|
|
2314
|
-
"filesFailed": 0,
|
|
2315
|
-
"totalRecords": 50,
|
|
2316
|
-
"eventsSent": 45,
|
|
2317
|
-
"eventsFailed": 5,
|
|
2318
|
-
"results": [
|
|
2319
|
-
{
|
|
2320
|
-
"file": "products_2025-01-22.xml",
|
|
2321
|
-
"success": true,
|
|
2322
|
-
"recordsProcessed": 50,
|
|
2323
|
-
"eventsSent": 45,
|
|
2324
|
-
"eventsFailed": 5,
|
|
2325
|
-
"errors": ["PRD-001: Invalid SKU format", "PRD-002: Missing required field"]
|
|
2326
|
-
}
|
|
2327
|
-
],
|
|
2328
|
-
"duration": 12345
|
|
2329
|
-
}
|
|
2330
|
-
```
|
|
2331
|
-
|
|
2332
|
-
### Error Response
|
|
2333
|
-
|
|
2334
|
-
```json
|
|
2335
|
-
{
|
|
2336
|
-
"success": false,
|
|
2337
|
-
"filesProcessed": 0,
|
|
2338
|
-
"filesFailed": 1,
|
|
2339
|
-
"totalRecords": 0,
|
|
2340
|
-
"eventsSent": 0,
|
|
2341
|
-
"eventsFailed": 0,
|
|
2342
|
-
"results": [
|
|
2343
|
-
{
|
|
2344
|
-
"file": "products_2025-01-22.xml",
|
|
2345
|
-
"success": false,
|
|
2346
|
-
"error": "XML parse error: Invalid structure"
|
|
2347
|
-
}
|
|
2348
|
-
],
|
|
2349
|
-
"duration": 876
|
|
2350
|
-
}
|
|
2351
|
-
```
|
|
2352
|
-
|
|
2353
|
-
### Monitoring Metrics
|
|
2354
|
-
|
|
2355
|
-
Monitor these metrics via Versori logs filtered by `jobId` or `stage`:
|
|
2356
|
-
|
|
2357
|
-
- **Files Processed** - Total files successfully processed
|
|
2358
|
-
- **Events Sent** - Total events sent to Fluent Commerce
|
|
2359
|
-
- **Events Failed** - Events that failed (check rejection reports)
|
|
2360
|
-
- **Processing Duration** - Time taken for complete workflow
|
|
2361
|
-
- **Rate Limiting** - Watch for 429 errors indicating throttling
|
|
2362
|
-
|
|
2363
|
-
Use the status webhook for dashboards and automated monitoring.
|
|
2364
|
-
|
|
2365
|
-
---
|
|
2366
|
-
|
|
2367
|
-
- **No files found:** Check `s3IncomingPrefix` and `filePattern` (object keys only)
|
|
2368
|
-
- **XML parse failed:** Move to errors/; validate XML structure and encoding
|
|
2369
|
-
- **XML array normalization:** Check if single object vs array - ALWAYS normalize
|
|
2370
|
-
- **High event failures:** Inspect logs; consider Batch API for very high volumes
|
|
2371
|
-
- **429 throttling:** Add small backoff/delay or use Batch API
|
|
2372
|
-
- **S3 connection errors:** Verify credentials, bucket, region, and permissions
|
|
2373
|
-
|
|
2374
|
-
---
|
|
2375
|
-
|
|
2376
|
-
## 13. Key Takeaways
|
|
2377
|
-
|
|
2378
|
-
- ✅ **Native Versori logs** - Use `log` from context (LoggingService removed - use native log)
|
|
2379
|
-
- ✅ **3 workflows** - Scheduled, ad hoc webhook, job status webhook
|
|
2380
|
-
- ✅ **JobTracker** - Track job lifecycle with KV persistence
|
|
2381
|
-
- ❌ **NO VersoriFileTracker** - S3 uses moveObject for deduplication (SFTP-specific only)
|
|
2382
|
-
- ✅ **S3 deduplication** - Physical file movement to processed/errors prefixes
|
|
2383
|
-
- ✅ **S3 dispose()** - Always cleanup in finally block
|
|
2384
|
-
- ✅ **S3 method names** - listObjects, downloadObject, moveObject (not \*File)
|
|
2385
|
-
- ✅ **XML array normalization** - CRITICAL: single object vs array handling
|
|
2386
|
-
- ✅ **Per-record error handling** - Continue on individual failures
|
|
2387
|
-
- ✅ **Externalized mapping** - Use JSON config file for field mappings
|
|
2388
|
-
- ✅ **File-level error handling** - Don't stop on single file failure
|
|
2389
|
-
- ✅ **S3 vs SFTP** - Different deduplication strategies (moveObject vs KV state)
|
|
2390
|
-
- ✅ **Processing modes** - per-file (default), chunked, batch
|
|
2391
|
-
- ✅ **Modular services** - orchestrator, processor, dispatcher, archive
|
|
2392
|
-
|
|
2393
|
-
---
|
|
2394
|
-
|
|
2395
|
-
[← Back to Versori Event API Templates](../../readme.md) | [Versori Platform Guide →](../../../../../04-REFERENCE/platforms/versori/platforms-versori-readme.md)
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-ingest-s3-xml-to-product-event
|
|
3
|
+
canonical_filename: template-ingestion-s3-xml-product-event.md
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
sdk_version: ^0.1.39
|
|
6
|
+
runtime: versori
|
|
7
|
+
direction: ingestion
|
|
8
|
+
source: s3-xml
|
|
9
|
+
destination: fluent-event-api
|
|
10
|
+
entity: product
|
|
11
|
+
format: xml
|
|
12
|
+
logging: versori
|
|
13
|
+
status: stable
|
|
14
|
+
features:
|
|
15
|
+
- batched-events
|
|
16
|
+
- attribute-transformation
|
|
17
|
+
- memory-management
|
|
18
|
+
- json-serialization-handling
|
|
19
|
+
- enhanced-logging
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Template: Ingestion - S3 XML to Product Event
|
|
23
|
+
|
|
24
|
+
**Template Version:** 2.0.0
|
|
25
|
+
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
26
|
+
**Last Updated:** 2025-01-24
|
|
27
|
+
**Deployment Target:** Versori Platform
|
|
28
|
+
|
|
29
|
+
**🆕 Version 2.0.0 Enhancements:**
|
|
30
|
+
- ✅ **Batched Events Support** - UPSERT_PRODUCTS with configurable batch sizes (10x faster for 100+ products)
|
|
31
|
+
- ✅ **Attribute Transformation** - Automatic conversion of flat attributes to Fluent Commerce array format
|
|
32
|
+
- ✅ **Memory Management** - Configurable batch processing with explicit cleanup (prevents OOM on large XML files)
|
|
33
|
+
- ✅ **JSON Serialization Handling** - Prevents 503 errors from non-serializable webhook responses
|
|
34
|
+
- ✅ **Enhanced Event Logging** - Status-prefixed logs (SUCCESS_/ERROR_) with comprehensive metrics and progress tracking
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 📚 STEP 1: Load These Docs Into Your AI (Human Checklist)
|
|
39
|
+
|
|
40
|
+
1. REQUIRED (load all)
|
|
41
|
+
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
42
|
+
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
43
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
44
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
45
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/ingestion/
|
|
46
|
+
- [ ] fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
47
|
+
|
|
48
|
+
Copy-paste list (open these):
|
|
49
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
50
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
51
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
52
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
53
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/ingestion/
|
|
54
|
+
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 📋 Implementation Prompt
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
I need a Versori scheduled ingestion that:
|
|
62
|
+
|
|
63
|
+
1) Lists XML files on S3 (deduplication via moveObject - NO VersoriFileTracker)
|
|
64
|
+
2) Downloads and parses XML with array normalization (CRITICAL: single object vs array)
|
|
65
|
+
3) Transforms records with UniversalMapper per mapping JSON
|
|
66
|
+
4) Sends UPSERT_PRODUCT events (async) to Fluent Commerce with per-record error handling
|
|
67
|
+
5) Moves files to processed/ or errors/ prefixes for natural deduplication
|
|
68
|
+
6) Tracks progress with JobTracker and exposes a job-status webhook
|
|
69
|
+
7) Uses native Versori log from context
|
|
70
|
+
|
|
71
|
+
Use the loaded docs to fill in SDK specifics and best practices.
|
|
72
|
+
Keep the structure identical to the template; only adapt where needed.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 📋 Template Overview
|
|
78
|
+
|
|
79
|
+
This connector runs on the Versori platform. Most operational settings (Fluent account/connection, S3 credentials, schedule, file patterns) are configured via activation variables. Data shape and logic (mapping JSON, XML structure, parsing rules, per-record handling) are adjusted in code as needed. It reads product data from S3 XML, transforms it, and sends events to the Fluent Commerce Event API.
|
|
80
|
+
|
|
81
|
+
### What This Template Does
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
85
|
+
│ INGESTION WORKFLOW │
|
|
86
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
87
|
+
|
|
88
|
+
1. TRIGGER
|
|
89
|
+
├─ Scheduled (Cron): Runs automatically every hour
|
|
90
|
+
├─ Ad hoc (Webhook): Manual trigger for immediate processing
|
|
91
|
+
└─ Status Query (Webhook): Check job progress
|
|
92
|
+
|
|
93
|
+
2. DISCOVER FILES (S3DataSource)
|
|
94
|
+
├─ List objects from S3 bucket
|
|
95
|
+
├─ Filter by prefix and pattern (products/*.xml)
|
|
96
|
+
├─ NO VersoriFileTracker (S3 uses moveObject for deduplication)
|
|
97
|
+
└─ Sort by last modified
|
|
98
|
+
|
|
99
|
+
3. DOWNLOAD & PARSE (XMLParserService)
|
|
100
|
+
├─ Download object from S3
|
|
101
|
+
├─ Parse XML to JavaScript object
|
|
102
|
+
├─ ⚠️ CRITICAL: Normalize single object vs array
|
|
103
|
+
└─ Validate XML structure
|
|
104
|
+
|
|
105
|
+
4. TRANSFORM (UniversalMapper)
|
|
106
|
+
├─ Map XML fields to Fluent schema
|
|
107
|
+
├─ Apply SDK resolvers (trim, uppercase, etc.)
|
|
108
|
+
├─ Handle nested objects (price, taxType)
|
|
109
|
+
├─ Handle arrays (categoryRefs)
|
|
110
|
+
└─ Collect transformation errors
|
|
111
|
+
|
|
112
|
+
5. SEND EVENTS (Event API)
|
|
113
|
+
├─ Loop through transformed products
|
|
114
|
+
├─ Send UPSERT_PRODUCT event (async)
|
|
115
|
+
├─ Track success/failure count
|
|
116
|
+
└─ Continue on individual failures
|
|
117
|
+
|
|
118
|
+
6. ARCHIVE (S3DataSource - Natural Deduplication)
|
|
119
|
+
├─ Move object to processed/ prefix (success)
|
|
120
|
+
├─ Or move to errors/ prefix (failures)
|
|
121
|
+
├─ Generate timestamped archive name
|
|
122
|
+
└─ Files in incoming/ are removed naturally
|
|
123
|
+
|
|
124
|
+
7. TRACK JOB (JobTracker)
|
|
125
|
+
├─ Update job status at each step
|
|
126
|
+
├─ Store final result in KV
|
|
127
|
+
├─ Enable status queries via webhook
|
|
128
|
+
└─ Handle errors gracefully
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Key Features
|
|
132
|
+
|
|
133
|
+
- Job tracking with status queries
|
|
134
|
+
- Execution modes: scheduled, ad hoc, status query
|
|
135
|
+
- Uses S3DataSource, XMLParserService, UniversalMapper, JobTracker
|
|
136
|
+
- Error handling, retry logic, and S3 cleanup
|
|
137
|
+
- **S3 Deduplication:** moveObject to processed/errors prefixes (NO VersoriFileTracker)
|
|
138
|
+
- **Key Difference from SFTP:** Physical file movement vs KV state tracking
|
|
139
|
+
- **XML Array Normalization:** CRITICAL handling of single object vs array
|
|
140
|
+
- Event API: Per-record failures don't block other records
|
|
141
|
+
- S3 dispose() in finally block
|
|
142
|
+
|
|
143
|
+
Note: JobTracker persists stage/status to Versori KV for visibility, job-status webhooks, and auditing. Recommended for production multi-step flows; can be skipped for trivial single-step utilities.
|
|
144
|
+
|
|
145
|
+
### 📦 Package Information
|
|
146
|
+
|
|
147
|
+
**SDK:** [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
148
|
+
|
|
149
|
+
Use the latest SDK version:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npm install @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
**Templates are designed for direct deployment; customize via activation variables.**
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## ⚙️ Activation Variables
|
|
162
|
+
|
|
163
|
+
**Configuration is driven by activation variables - modify these instead of code:**
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"fluentRetailerId": "1",
|
|
168
|
+
"eventConcurrency": 1,
|
|
169
|
+
"s3Bucket": "my-product-bucket",
|
|
170
|
+
"s3Region": "us-east-1",
|
|
171
|
+
"s3AccessKeyId": "AKIA...",
|
|
172
|
+
"s3SecretAccessKey": "********",
|
|
173
|
+
"s3IncomingPrefix": "products/incoming/",
|
|
174
|
+
"s3ProcessedPrefix": "products/processed/",
|
|
175
|
+
"s3ErrorPrefix": "products/errors/",
|
|
176
|
+
"s3LogsPrefix": "products/logs/",
|
|
177
|
+
"filePattern": "products_*.xml",
|
|
178
|
+
"catalogueRef": "PC:MASTER:2",
|
|
179
|
+
"catalogueType": "MASTER",
|
|
180
|
+
"eventName": "UPSERT_PRODUCT",
|
|
181
|
+
"eventMode": "async",
|
|
182
|
+
"maxFilesPerRun": 10,
|
|
183
|
+
"processingMode": "per-file",
|
|
184
|
+
"fileChunkSize": 5,
|
|
185
|
+
"validateConnection": true,
|
|
186
|
+
"enableFileTracking": true,
|
|
187
|
+
"trackDuration": true,
|
|
188
|
+
"useBatchedEvents": false,
|
|
189
|
+
"maxProductsUnderBatchedEvent": 100,
|
|
190
|
+
"memoryBatchSize": 250
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Note: Webhook security is handled by Versori's native connection authentication. The `connection` parameter in webhook definitions ensures only authenticated requests are processed.
|
|
195
|
+
|
|
196
|
+
### Variable Explanations
|
|
197
|
+
|
|
198
|
+
| Variable | Purpose | Default | Customization Hints |
|
|
199
|
+
| ------------------- | ---------------------------- | --------------------- | ----------------------------------- |
|
|
200
|
+
| `fluentRetailerId` | Retailer ID for Event API | - | Required - Fluent retailer ID |
|
|
201
|
+
| `eventConcurrency` | Event sending concurrency | `1` | `1` = sequential, `>1` = parallel (3-10 recommended) |
|
|
202
|
+
| `s3Bucket` | S3 bucket name | - | Required - your S3 bucket |
|
|
203
|
+
| `s3Region` | S3 region | `us-east-1` | AWS region (e.g., us-west-2) |
|
|
204
|
+
| `s3AccessKeyId` | AWS access key ID | - | Required - AWS credentials |
|
|
205
|
+
| `s3SecretAccessKey` | AWS secret access key | - | Required - AWS credentials |
|
|
206
|
+
| `s3IncomingPrefix` | Incoming files prefix | `products/incoming/` | Where new files arrive |
|
|
207
|
+
| `s3ProcessedPrefix` | Processed files archive | `products/processed/` | Where files move after success |
|
|
208
|
+
| `s3ErrorPrefix` | Failed files directory | `products/errors/` | Where files move after errors |
|
|
209
|
+
| `s3LogsPrefix` | Event processing logs | `products/logs/` | Where event logs are written |
|
|
210
|
+
| `filePattern` | File name filter | `products_*.xml` | Glob pattern for matching files |
|
|
211
|
+
| `catalogueRef` | Product catalogue reference | `PC:MASTER:2` | Target catalogue in Fluent |
|
|
212
|
+
| `catalogueType` | Catalogue type | `MASTER` | Usually MASTER or STANDARD |
|
|
213
|
+
| `eventName` | Event to send | `UPSERT_PRODUCT` | Event name from Rubix |
|
|
214
|
+
| `eventMode` | Event processing mode | `async` | async (recommended) or sync |
|
|
215
|
+
| `maxFilesPerRun` | Max files per execution | `10` | Prevent timeout on large batches |
|
|
216
|
+
| `processingMode` | Processing mode | `per-file` | per-file, chunked, or batch |
|
|
217
|
+
| `fileChunkSize` | Files per chunk (chunked) | `5` | Used when processingMode=chunked |
|
|
218
|
+
| `validateConnection` | Validate S3 on initialization | `true` | Fail-fast if S3 credentials invalid |
|
|
219
|
+
| `enableFileTracking` | Enable file deduplication | `true` | Track processed files in KV store |
|
|
220
|
+
| `trackDuration` | Track processing duration | `true` | Log timing metrics for performance |
|
|
221
|
+
| `useBatchedEvents` | Use batched UPSERT_PRODUCTS | `false` | `true` = batched events (10x faster for 100+ products), `false` = individual events |
|
|
222
|
+
| `maxProductsUnderBatchedEvent` | Products per batch | `100` | Only used when `useBatchedEvents=true` (default: 100) |
|
|
223
|
+
| `memoryBatchSize` | Products per mapping batch | `250` | Processes products in batches during mapping (prevents OOM on large files) |
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 🔒 SDK Automatic Behaviors (v0.1.40+)
|
|
228
|
+
|
|
229
|
+
**The SDK automatically validates and retries for improved reliability:**
|
|
230
|
+
|
|
231
|
+
### retailerId Validation
|
|
232
|
+
- **SDK validates** `retailerId` before calling `sendEvent()`
|
|
233
|
+
- **Checks:** `event.retailerId || client.retailerId`
|
|
234
|
+
- **If missing:** Throws `"retailerId is required for Event API..."`
|
|
235
|
+
- **Configuration:** Set via `fluentRetailerId` activation variable (recommended)
|
|
236
|
+
|
|
237
|
+
### 401 Auth Retry
|
|
238
|
+
- **Automatic retry** for platform auth failures (3 attempts)
|
|
239
|
+
- **Delay:** Exponential backoff (1s → 2s → 4s)
|
|
240
|
+
- **Applies to:** All `sendEvent()` calls (async and sync modes)
|
|
241
|
+
- **Log:** `"[fc-connect-sdk:auth] Platform auth failure (401), retrying..."`
|
|
242
|
+
|
|
243
|
+
### 5xx Server Retry
|
|
244
|
+
- **Automatic retry** for transient server errors (3 attempts)
|
|
245
|
+
- **Delay:** Exponential backoff (1s → 2s → 4s, capped at 10s)
|
|
246
|
+
- **Protects:** Against Fluent API transient failures
|
|
247
|
+
|
|
248
|
+
### No Code Changes Required
|
|
249
|
+
- All templates remain compatible
|
|
250
|
+
- Retry logic is automatic and transparent
|
|
251
|
+
- Better error messages guide configuration
|
|
252
|
+
|
|
253
|
+
**See:** [Event API Guide](./event-api-guide.md) for complete details
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### 📁 S3 Deduplication Pattern (NO VersoriFileTracker)
|
|
258
|
+
|
|
259
|
+
**CRITICAL: S3 uses physical file movement, NOT VersoriFileTracker**
|
|
260
|
+
|
|
261
|
+
VersoriFileTracker is **SFTP-specific** and should **NOT** be used with S3. S3 achieves deduplication through physical file movement.
|
|
262
|
+
|
|
263
|
+
#### How S3 Deduplication Works
|
|
264
|
+
|
|
265
|
+
**3-Step Process:**
|
|
266
|
+
|
|
267
|
+
1. **Discovery:** List objects from `s3IncomingPrefix` matching `filePattern`
|
|
268
|
+
2. **Process:** Download, parse, transform, and send events
|
|
269
|
+
3. **Archive:** Move object to `s3ProcessedPrefix` or `s3ErrorPrefix`
|
|
270
|
+
4. **Natural Deduplication:** Once moved, files are no longer in incoming/
|
|
271
|
+
|
|
272
|
+
#### File Lifecycle Example
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
INITIAL STATE:
|
|
276
|
+
s3://bucket/products/incoming/products_20250124_001.xml
|
|
277
|
+
|
|
278
|
+
↓ (workflow discovers file)
|
|
279
|
+
|
|
280
|
+
PROCESSING:
|
|
281
|
+
- List objects from "products/incoming/"
|
|
282
|
+
- Download: products_20250124_001.xml
|
|
283
|
+
- Parse XML → Normalize array → Map to events → Send to Event API
|
|
284
|
+
|
|
285
|
+
↓ (success)
|
|
286
|
+
|
|
287
|
+
ARCHIVE (moveObject):
|
|
288
|
+
s3://bucket/products/processed/products_20250124_001-20250124T183045.xml
|
|
289
|
+
|
|
290
|
+
↓ (result)
|
|
291
|
+
|
|
292
|
+
INCOMING FOLDER NOW EMPTY:
|
|
293
|
+
s3://bucket/products/incoming/ (file is gone - no duplicates possible)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### Key Differences: S3 vs SFTP
|
|
297
|
+
|
|
298
|
+
| Aspect | S3 (This Template) | SFTP (Other Templates) |
|
|
299
|
+
| ------------------------ | ----------------------------- | -------------------------------- |
|
|
300
|
+
| **Deduplication Method** | Physical file movement | VersoriFileTracker (KV state) |
|
|
301
|
+
| **State Storage** | None needed | KV store tracks processed files |
|
|
302
|
+
| **File Tracking** | ❌ NO VersoriFileTracker | ✅ VersoriFileTracker required |
|
|
303
|
+
| **Reprocessing** | Move files back to incoming/ | Clear KV state or forceReprocess |
|
|
304
|
+
| **Complexity** | Simpler (no state management) | More complex (state management) |
|
|
305
|
+
|
|
306
|
+
#### Summary
|
|
307
|
+
|
|
308
|
+
✅ **DO:** Use S3 moveObject for deduplication
|
|
309
|
+
✅ **DO:** Move files to processed/ or errors/ prefixes
|
|
310
|
+
❌ **DON'T:** Import VersoriFileTracker in S3 templates
|
|
311
|
+
❌ **DON'T:** Use KV state tracking for S3 files
|
|
312
|
+
✅ **DO:** Use VersoriFileTracker for SFTP templates (different use case)
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## When to Use Event API vs Batch API
|
|
317
|
+
|
|
318
|
+
### ✅ Use Event API (`sendEvent`) For:
|
|
319
|
+
|
|
320
|
+
| Entity Type | Use Case | Why Event API |
|
|
321
|
+
| ------------------- | -------------------------------------- | -------------------------------------------- |
|
|
322
|
+
| **Products** | Product catalog sync, variant updates | Triggers workflows, validates business rules |
|
|
323
|
+
| **Locations** | Store/warehouse setup | Requires workflow orchestration |
|
|
324
|
+
| **Customers** | Customer registration, profile updates | Needs workflow for downstream systems |
|
|
325
|
+
| **Orders** | Single order creation | Event-driven fulfillment workflows |
|
|
326
|
+
| **Custom Entities** | Any entity needing workflow triggers | Full Rubix workflow support |
|
|
327
|
+
|
|
328
|
+
### ❌ Use Batch API Instead For:
|
|
329
|
+
|
|
330
|
+
| Entity Type | Use Case | Why Batch API |
|
|
331
|
+
| ------------------ | ---------------------------------- | ----------------------------------------------- |
|
|
332
|
+
| **Inventory ONLY** | Bulk inventory updates, daily sync | Optimized for high-volume, BPP change detection |
|
|
333
|
+
|
|
334
|
+
### 🔄 Use GraphQL Mutations For:
|
|
335
|
+
|
|
336
|
+
| Scenario | Why GraphQL |
|
|
337
|
+
| --------------------- | -------------------------------------- |
|
|
338
|
+
| **Single operations** | Create one order, update one product |
|
|
339
|
+
| **Complex queries** | Fetch data with relationships |
|
|
340
|
+
| **Testing/debugging** | Direct API control, immediate feedback |
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## XML File Format
|
|
345
|
+
|
|
346
|
+
### Sample: products.xml
|
|
347
|
+
|
|
348
|
+
Based on your `UPSERT_PRODUCT` event payload:
|
|
349
|
+
|
|
350
|
+
```xml
|
|
351
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
352
|
+
<products>
|
|
353
|
+
<product>
|
|
354
|
+
<ref>G_PROD_WITH_NO_STANDARD</ref>
|
|
355
|
+
<type>VARIANT</type>
|
|
356
|
+
<status>ACTIVE</status>
|
|
357
|
+
<gtin>MH01-XS-Orange</gtin>
|
|
358
|
+
<name>Chaz Kangeroo Hoodie-XS-Orange main</name>
|
|
359
|
+
<summary><p>test short description</p></summary>
|
|
360
|
+
<categoryRefs>
|
|
361
|
+
<ref>STANDARD_CATEGORY</ref>
|
|
362
|
+
</categoryRefs>
|
|
363
|
+
<price>
|
|
364
|
+
<item>
|
|
365
|
+
<type>DEFAULT</type>
|
|
366
|
+
<currency>USD</currency>
|
|
367
|
+
<value>52.000000</value>
|
|
368
|
+
</item>
|
|
369
|
+
<item>
|
|
370
|
+
<type>SPECIAL</type>
|
|
371
|
+
<currency>USD</currency>
|
|
372
|
+
<value>9.000000</value>
|
|
373
|
+
</item>
|
|
374
|
+
</price>
|
|
375
|
+
<taxType>
|
|
376
|
+
<country>AU</country>
|
|
377
|
+
<group>Tax Group</group>
|
|
378
|
+
<tariff>Tax Tariff</tariff>
|
|
379
|
+
</taxType>
|
|
380
|
+
<catalogue>
|
|
381
|
+
<ref>PC:MASTER:2</ref>
|
|
382
|
+
<type>MASTER</type>
|
|
383
|
+
</catalogue>
|
|
384
|
+
</product>
|
|
385
|
+
<product>
|
|
386
|
+
<ref>G_PROD_002</ref>
|
|
387
|
+
<type>VARIANT</type>
|
|
388
|
+
<status>ACTIVE</status>
|
|
389
|
+
<gtin>MH02-M-Blue</gtin>
|
|
390
|
+
<name>Kangeroo Hoodie-M-Blue</name>
|
|
391
|
+
<summary><p>Blue variant</p></summary>
|
|
392
|
+
<categoryRefs>
|
|
393
|
+
<ref>STANDARD_CATEGORY</ref>
|
|
394
|
+
<ref>SEASONAL</ref>
|
|
395
|
+
</categoryRefs>
|
|
396
|
+
<price>
|
|
397
|
+
<item>
|
|
398
|
+
<type>DEFAULT</type>
|
|
399
|
+
<currency>USD</currency>
|
|
400
|
+
<value>45.000000</value>
|
|
401
|
+
</item>
|
|
402
|
+
<item>
|
|
403
|
+
<type>SPECIAL</type>
|
|
404
|
+
<currency>USD</currency>
|
|
405
|
+
<value>35.000000</value>
|
|
406
|
+
</item>
|
|
407
|
+
</price>
|
|
408
|
+
<taxType>
|
|
409
|
+
<country>AU</country>
|
|
410
|
+
<group>Tax Group</group>
|
|
411
|
+
<tariff>Tax Tariff</tariff>
|
|
412
|
+
</taxType>
|
|
413
|
+
<catalogue>
|
|
414
|
+
<ref>PC:MASTER:2</ref>
|
|
415
|
+
<type>MASTER</type>
|
|
416
|
+
</catalogue>
|
|
417
|
+
</product>
|
|
418
|
+
</products>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**XML Field Mapping:**
|
|
422
|
+
|
|
423
|
+
- Nested structure for objects (e.g., `<taxType><country>AU</country></taxType>`)
|
|
424
|
+
- Arrays with repeated elements (e.g., `<categoryRefs><ref>CAT1</ref><ref>CAT2</ref></categoryRefs>`)
|
|
425
|
+
- HTML content in CDATA or escaped (e.g., `<p>description</p>`)
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Event Sending Configuration
|
|
430
|
+
|
|
431
|
+
**Simple Configuration:** One variable controls everything
|
|
432
|
+
|
|
433
|
+
```json
|
|
434
|
+
{
|
|
435
|
+
"eventConcurrency": 1 // 1 = sequential, 3-10 = parallel
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**How it works:**
|
|
440
|
+
|
|
441
|
+
- `eventConcurrency: 1` → Sequential (sends events one at a time, safe default)
|
|
442
|
+
- `eventConcurrency: 3` → Parallel (sends 3 events concurrently)
|
|
443
|
+
- `eventConcurrency: 5` → Parallel (sends 5 events concurrently)
|
|
444
|
+
- `eventConcurrency: 10` → Parallel (sends 10 events concurrently)
|
|
445
|
+
|
|
446
|
+
**Configuration Guidelines:**
|
|
447
|
+
|
|
448
|
+
- **Conservative:** `1` → Sequential (safe, predictable, ~1 req/sec)
|
|
449
|
+
- **Balanced:** `3-5` → Parallel (most common, ~3-5 req/sec)
|
|
450
|
+
- **Aggressive:** `10` → Parallel (high-volume, ~10 req/sec)
|
|
451
|
+
- **Note:** Fluent API supports concurrent requests - adjust based on your needs
|
|
452
|
+
|
|
453
|
+
**Why Single Variable?**
|
|
454
|
+
|
|
455
|
+
- **Simpler:** One variable instead of two (`mode` + `concurrency`)
|
|
456
|
+
- **Clearer:** `concurrency: 1` = sequential, `concurrency > 1` = parallel
|
|
457
|
+
- **Less config:** Fewer activation variables to manage
|
|
458
|
+
- **Flexible:** Easy to tune performance (just change the number)
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## 🔧 Production Reference Implementation
|
|
463
|
+
|
|
464
|
+
### Processing Modes
|
|
465
|
+
|
|
466
|
+
| Mode | Default | What happens | When to use |
|
|
467
|
+
| ---------- | ------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
|
|
468
|
+
| `per-file` | ✅ | Process one file end-to-end (discover → parse → Event API → archive) before moving on | Recommended for production S3 feeds and very large files |
|
|
469
|
+
| `chunked` | ➖ | Processes `fileChunkSize` files at a time, completing each chunk before the next | High-volume feeds where per-file would take too long |
|
|
470
|
+
| `batch` | ➖ | Loads every matched file into memory, produces a single Event API job, and archives when complete | Tiny datasets or smoke tests only |
|
|
471
|
+
|
|
472
|
+
Set the mode via activation variables:
|
|
473
|
+
|
|
474
|
+
```json
|
|
475
|
+
{
|
|
476
|
+
"processingMode": "per-file",
|
|
477
|
+
"fileChunkSize": "5",
|
|
478
|
+
"maxFilesPerRun": "25"
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Versori Workflows Structure
|
|
483
|
+
|
|
484
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
485
|
+
|
|
486
|
+
**Trigger Types:**
|
|
487
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
488
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
489
|
+
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
490
|
+
|
|
491
|
+
**Execution Steps (chained to triggers):**
|
|
492
|
+
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
493
|
+
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
494
|
+
|
|
495
|
+
### Recommended Project Structure
|
|
496
|
+
|
|
497
|
+
```
|
|
498
|
+
product-event-sync/
|
|
499
|
+
├── index.ts # Entry point - exports all workflows
|
|
500
|
+
└── src/
|
|
501
|
+
├── workflows/
|
|
502
|
+
│ ├── scheduled/
|
|
503
|
+
│ │ └── daily-product-sync.ts # Scheduled: Daily product sync
|
|
504
|
+
│ │
|
|
505
|
+
│ └── webhook/
|
|
506
|
+
│ ├── adhoc-product-sync.ts # Webhook: Manual trigger
|
|
507
|
+
│ └── job-status-check.ts # Webhook: Status query
|
|
508
|
+
│
|
|
509
|
+
├── services/
|
|
510
|
+
│ └── product-sync.service.ts # Shared orchestration logic (reusable)
|
|
511
|
+
│
|
|
512
|
+
└── types/
|
|
513
|
+
└── product.types.ts # Shared type definitions
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**Benefits:**
|
|
517
|
+
- ✅ Clear trigger separation (`scheduled/` vs `webhook/`)
|
|
518
|
+
- ✅ Descriptive file names (easy to browse and understand)
|
|
519
|
+
- ✅ Scalable (add new workflows without cluttering)
|
|
520
|
+
- ✅ Reusable code in `services/` (DRY principle)
|
|
521
|
+
- ✅ Easy to modify individual workflows without affecting others
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Workflow Files
|
|
526
|
+
|
|
527
|
+
### 1. Scheduled Workflows (`src/workflows/scheduled/`)
|
|
528
|
+
|
|
529
|
+
All time-based triggers that run automatically on cron schedules.
|
|
530
|
+
|
|
531
|
+
#### `src/workflows/scheduled/daily-product-sync.ts`
|
|
532
|
+
|
|
533
|
+
**Purpose**: Automatic Daily product sync
|
|
534
|
+
**Trigger**: Cron schedule (`0 2 * * *`)
|
|
535
|
+
**Exposed as Endpoint**: ❌ NO - Runs automatically
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
import { schedule, http } from '@versori/run';
|
|
539
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
540
|
+
import { executeProductIngestion } from '../../services/product-sync.service';
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Scheduled Workflow: Daily Product Sync
|
|
544
|
+
*
|
|
545
|
+
* Runs automatically daily at 2 AM UTC
|
|
546
|
+
* NOT exposed as HTTP endpoint - Versori executes on schedule
|
|
547
|
+
*
|
|
548
|
+
* Uses shared service: product-sync.service.ts
|
|
549
|
+
*/
|
|
550
|
+
export const dailyProductSync = schedule(
|
|
551
|
+
'product-sync-scheduled',
|
|
552
|
+
'0 2 * * *' // Daily at 2 AM UTC
|
|
553
|
+
).then(
|
|
554
|
+
http('run-product-sync', { connection: 'fluent_commerce' }, async ctx => {
|
|
555
|
+
const { log, openKv } = ctx;
|
|
556
|
+
const jobId = `product-sync-${Date.now()}`;
|
|
557
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
558
|
+
|
|
559
|
+
log.info('🚀 Starting scheduled product sync', { jobId });
|
|
560
|
+
|
|
561
|
+
await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' });
|
|
562
|
+
await tracker.updateJob(jobId, { status: 'processing' });
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
// Reuse shared orchestration logic
|
|
566
|
+
log.info('⚙️ Executing product ingestion workflow', { jobId });
|
|
567
|
+
const result = await executeProductIngestion(ctx, { jobId, triggeredBy: 'schedule' }, tracker);
|
|
568
|
+
await tracker.markCompleted(jobId, result);
|
|
569
|
+
log.info('✅ Scheduled sync completed successfully', { jobId, ...result });
|
|
570
|
+
return { success: true, jobId, ...result };
|
|
571
|
+
} catch (e: any) {
|
|
572
|
+
log.error('❌ Scheduled sync failed', { jobId, error: e?.message });
|
|
573
|
+
await tracker.markFailed(jobId, e);
|
|
574
|
+
return { success: false, jobId, error: e?.message };
|
|
575
|
+
}
|
|
576
|
+
})
|
|
577
|
+
);
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
### 2. Webhook Workflows (`src/workflows/webhook/`)
|
|
583
|
+
|
|
584
|
+
All HTTP-based triggers that create webhook endpoints.
|
|
585
|
+
|
|
586
|
+
#### `src/workflows/webhook/adhoc-product-sync.ts`
|
|
587
|
+
|
|
588
|
+
**Purpose**: Manual product sync trigger (on-demand)
|
|
589
|
+
**Trigger**: HTTP POST
|
|
590
|
+
**Endpoint**: `POST https://{workspace}.versori.run/product-sync-adhoc`
|
|
591
|
+
**Use Cases**: Testing, priority processing, ad-hoc runs
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
import { webhook, http } from '@versori/run';
|
|
595
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
596
|
+
import { executeProductIngestion } from '../../services/product-sync.service';
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Webhook: Manual Product Sync Trigger
|
|
600
|
+
*
|
|
601
|
+
* Endpoint: POST https://{workspace}.versori.run/product-sync-adhoc
|
|
602
|
+
* Request body (optional): { filePattern: "urgent_*.xml", maxFiles: 5 }
|
|
603
|
+
*
|
|
604
|
+
* Pattern: webhook().then(http()) - needs Fluent API access
|
|
605
|
+
* Uses shared service: product-sync.service.ts
|
|
606
|
+
*
|
|
607
|
+
* SECURITY: Authentication handled via connection parameter
|
|
608
|
+
* No manual API key validation needed - Versori manages this via connection auth
|
|
609
|
+
*/
|
|
610
|
+
export const adhocProductSync = webhook('product-sync-adhoc', {
|
|
611
|
+
response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
|
|
612
|
+
connection: 'product-sync-adhoc', // Versori validates API key
|
|
613
|
+
}).then(
|
|
614
|
+
http('run-product-sync-adhoc', { connection: 'fluent_commerce' }, async ctx => {
|
|
615
|
+
const { log, openKv, data } = ctx;
|
|
616
|
+
const jobId = `product-sync-adhoc-${Date.now()}`;
|
|
617
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
618
|
+
|
|
619
|
+
const filePattern = data?.filePattern as string;
|
|
620
|
+
const maxFiles = data?.maxFiles as number;
|
|
621
|
+
|
|
622
|
+
log.info('🚀 [WEBHOOK] Adhoc product sync triggered', {
|
|
623
|
+
jobId,
|
|
624
|
+
filePattern,
|
|
625
|
+
maxFiles,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Create job entry FIRST (awaited to ensure job exists in KV)
|
|
629
|
+
await tracker.createJob(jobId, {
|
|
630
|
+
triggeredBy: 'manual',
|
|
631
|
+
stage: 'initialization',
|
|
632
|
+
status: 'queued',
|
|
633
|
+
options: { filePattern, maxFiles },
|
|
634
|
+
createdAt: new Date().toISOString(),
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// ✅ Fire-and-forget: Start background processing WITHOUT await
|
|
638
|
+
// The promise continues execution after we return the response
|
|
639
|
+
executeProductIngestion(ctx, { jobId, triggeredBy: 'manual' }, tracker)
|
|
640
|
+
.then((result) => {
|
|
641
|
+
log.info('✅ [BACKGROUND] Product sync completed successfully', {
|
|
642
|
+
jobId,
|
|
643
|
+
filesProcessed: result.filesProcessed,
|
|
644
|
+
filesFailed: result.filesFailed,
|
|
645
|
+
recordsProcessed: result.recordsProcessed,
|
|
646
|
+
});
|
|
647
|
+
return tracker.markCompleted(jobId, result);
|
|
648
|
+
})
|
|
649
|
+
.catch((error: unknown) => {
|
|
650
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
651
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
652
|
+
|
|
653
|
+
log.error('❌ [BACKGROUND] Product sync failed', {
|
|
654
|
+
jobId,
|
|
655
|
+
error: errorMessage,
|
|
656
|
+
stack: errorStack,
|
|
657
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
return tracker.markFailed(jobId, errorMessage);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// Return immediately with jobId (response sent with this return value)
|
|
664
|
+
return {
|
|
665
|
+
success: true,
|
|
666
|
+
jobId,
|
|
667
|
+
message: 'Product sync started in background',
|
|
668
|
+
statusEndpoint: `https://{workspace}.versori.run/product-sync-job-status`,
|
|
669
|
+
note: 'Poll the status endpoint with jobId to check progress',
|
|
670
|
+
};
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
#### `src/workflows/webhook/job-status-check.ts`
|
|
676
|
+
|
|
677
|
+
**Purpose**: Query job status
|
|
678
|
+
**Trigger**: HTTP POST
|
|
679
|
+
**Endpoint**: `POST https://{workspace}.versori.run/product-sync-job-status`
|
|
680
|
+
**Request body**: `{ "jobId": "product-sync-1234567890" }`
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
import { webhook, fn } from '@versori/run';
|
|
684
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Webhook: Job Status Check
|
|
688
|
+
*
|
|
689
|
+
* Endpoint: POST https://{workspace}.versori.run/product-sync-job-status
|
|
690
|
+
* Request body: { "jobId": "product-sync-1234567890" }
|
|
691
|
+
*
|
|
692
|
+
* Pattern: webhook().then(fn()) - no external API needed, only KV storage
|
|
693
|
+
* Lightweight: Only queries KV store, no Fluent API calls
|
|
694
|
+
*
|
|
695
|
+
* SECURITY: Authentication handled via connection parameter
|
|
696
|
+
* No manual API key validation needed - Versori manages this via connection auth
|
|
697
|
+
*/
|
|
698
|
+
export const productSyncJobStatus = webhook('product-sync-job-status', {
|
|
699
|
+
response: { mode: 'sync' },
|
|
700
|
+
connection: 'product-sync-job-status',
|
|
701
|
+
}).then(
|
|
702
|
+
fn('status', async ctx => {
|
|
703
|
+
// ═══════════════════════════════════════════════════════════
|
|
704
|
+
// EXECUTION BOUNDARY: Job Status Query Start
|
|
705
|
+
// ═══════════════════════════════════════════════════════════
|
|
706
|
+
const { data, log, openKv } = ctx;
|
|
707
|
+
const jobId = data?.jobId as string;
|
|
708
|
+
|
|
709
|
+
log.info('🔍 Job status query received', { jobId });
|
|
710
|
+
|
|
711
|
+
if (!jobId) {
|
|
712
|
+
log.warn('⚠️ Missing jobId in request');
|
|
713
|
+
return { success: false, error: 'jobId required' };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
717
|
+
const status = await tracker.getJob(jobId);
|
|
718
|
+
|
|
719
|
+
if (status) {
|
|
720
|
+
log.info('✅ Job status found', { jobId, status: status.status });
|
|
721
|
+
return { success: true, jobId, ...status };
|
|
722
|
+
} else {
|
|
723
|
+
log.warn('⚠️ Job not found', { jobId });
|
|
724
|
+
return { success: false, error: 'Job not found', jobId };
|
|
725
|
+
}
|
|
726
|
+
// ═══════════════════════════════════════════════════════════
|
|
727
|
+
// EXECUTION BOUNDARY: Job Status Query End
|
|
728
|
+
// ═══════════════════════════════════════════════════════════
|
|
729
|
+
})
|
|
730
|
+
);
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
### 3. Entry Point (`index.ts`)
|
|
736
|
+
|
|
737
|
+
**Purpose**: Register all workflows with Versori platform using MemoryInterpreter pattern
|
|
738
|
+
|
|
739
|
+
```typescript
|
|
740
|
+
/**
|
|
741
|
+
* Entry Point - Registers all workflows with Versori platform
|
|
742
|
+
*
|
|
743
|
+
* PATTERN: MemoryInterpreter for workflow registration
|
|
744
|
+
* - Workflow definitions stored in memory
|
|
745
|
+
* - Versori runtime discovers and executes via exports
|
|
746
|
+
* - No file I/O during workflow registration
|
|
747
|
+
*
|
|
748
|
+
* File Structure:
|
|
749
|
+
* - src/workflows/scheduled/ → Time-based triggers (cron)
|
|
750
|
+
* - src/workflows/webhook/ → HTTP-based triggers (webhooks)
|
|
751
|
+
*/
|
|
752
|
+
|
|
753
|
+
// Import scheduled workflows
|
|
754
|
+
import { dailyProductSync } from './src/workflows/scheduled/daily-product-sync';
|
|
755
|
+
|
|
756
|
+
// Import webhook workflows
|
|
757
|
+
import { adhocProductSync } from './src/workflows/webhook/adhoc-product-sync';
|
|
758
|
+
import { productSyncJobStatus } from './src/workflows/webhook/job-status-check';
|
|
759
|
+
|
|
760
|
+
// MemoryInterpreter Pattern: Export workflows for Versori runtime discovery
|
|
761
|
+
// Workflows are held in memory and executed when triggered by platform
|
|
762
|
+
export {
|
|
763
|
+
// Scheduled (time-based triggers)
|
|
764
|
+
dailyProductSync,
|
|
765
|
+
|
|
766
|
+
// Webhooks (HTTP-based triggers)
|
|
767
|
+
adhocProductSync,
|
|
768
|
+
productSyncJobStatus,
|
|
769
|
+
};
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
**What Gets Exposed:**
|
|
773
|
+
- ✅ `adhocProductSync` → `https://{workspace}.versori.run/product-sync-adhoc`
|
|
774
|
+
- ✅ `productSyncJobStatus` → `https://{workspace}.versori.run/product-sync-job-status`
|
|
775
|
+
- ❌ `dailyProductSync` → NOT exposed (runs automatically on cron)
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
### Adding New Workflows
|
|
780
|
+
|
|
781
|
+
**To add a scheduled workflow:**
|
|
782
|
+
1. Create file in `src/workflows/scheduled/` with descriptive name (e.g., `hourly-delta-sync.ts`)
|
|
783
|
+
2. Export the workflow from the file
|
|
784
|
+
3. Import and re-export in `index.ts`
|
|
785
|
+
|
|
786
|
+
**To add a webhook workflow:**
|
|
787
|
+
1. Create file in `src/workflows/webhook/` with descriptive name (e.g., `error-notification.ts`)
|
|
788
|
+
2. Export the workflow from the file
|
|
789
|
+
3. Import and re-export in `index.ts`
|
|
790
|
+
|
|
791
|
+
**Example - Adding hourly delta sync:**
|
|
792
|
+
|
|
793
|
+
```typescript
|
|
794
|
+
// src/workflows/scheduled/hourly-delta-sync.ts
|
|
795
|
+
export const hourlyDeltaSync = schedule(
|
|
796
|
+
'product-delta-hourly',
|
|
797
|
+
'0 * * * *' // Every hour
|
|
798
|
+
).then(
|
|
799
|
+
http('run-delta-sync', { connection: 'fluent_commerce' }, async ctx => {
|
|
800
|
+
// Delta sync logic (skip BPP)
|
|
801
|
+
const result = await runIngestion(ctx, jobId, tracker, { bppEnabled: false });
|
|
802
|
+
return result;
|
|
803
|
+
})
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
// index.ts (add to imports and exports)
|
|
807
|
+
import { hourlyDeltaSync } from './src/workflows/scheduled/hourly-delta-sync';
|
|
808
|
+
export { daily_product_sync, hourlyDeltaSync, ... };
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
## 3. Type Definitions (src/types/product-ingestion.types.ts)
|
|
813
|
+
|
|
814
|
+
```typescript
|
|
815
|
+
/**
|
|
816
|
+
* Type Definitions for Product Ingestion
|
|
817
|
+
*
|
|
818
|
+
* Centralized type definitions for product ingestion workflow
|
|
819
|
+
*/
|
|
820
|
+
|
|
821
|
+
import type { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Product interface - represents transformed product data
|
|
825
|
+
*/
|
|
826
|
+
export interface Product {
|
|
827
|
+
ref: string;
|
|
828
|
+
type?: string;
|
|
829
|
+
status?: string;
|
|
830
|
+
name: string;
|
|
831
|
+
summary?: string;
|
|
832
|
+
gtin?: string;
|
|
833
|
+
catalogue?: { ref?: string };
|
|
834
|
+
attributes?: Record<string, unknown>;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Event configuration
|
|
839
|
+
*/
|
|
840
|
+
export interface EventConfig {
|
|
841
|
+
eventName: string;
|
|
842
|
+
catalogueRef: string;
|
|
843
|
+
catalogueType: string;
|
|
844
|
+
eventMode: 'async' | 'sync';
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Event result - tracks success/failure counts
|
|
849
|
+
*/
|
|
850
|
+
export interface EventResult {
|
|
851
|
+
eventsSent: number;
|
|
852
|
+
eventsFailed: number;
|
|
853
|
+
errors: Array<{ productRef: string; error: string }>;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Process file result
|
|
858
|
+
*/
|
|
859
|
+
export interface ProcessFileResult {
|
|
860
|
+
success: boolean;
|
|
861
|
+
products: Product[];
|
|
862
|
+
error?: string;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Versori Context Interface
|
|
867
|
+
* Represents the Versori runtime context passed to workflow functions
|
|
868
|
+
*/
|
|
869
|
+
export interface VersoriContext {
|
|
870
|
+
log: {
|
|
871
|
+
info: (message: string, data?: Record<string, unknown>) => void;
|
|
872
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
873
|
+
error: (message: string, data?: Record<string, unknown>) => void;
|
|
874
|
+
debug?: (message: string, data?: Record<string, unknown>) => void;
|
|
875
|
+
};
|
|
876
|
+
openKv: (namespace: string) => {
|
|
877
|
+
get: (key: string) => Promise<unknown>;
|
|
878
|
+
set: (key: string, value: unknown) => Promise<void>;
|
|
879
|
+
delete: (key: string) => Promise<void>;
|
|
880
|
+
};
|
|
881
|
+
activation: {
|
|
882
|
+
getVariable: (name: string) => string | undefined;
|
|
883
|
+
connections?: Record<string, unknown>;
|
|
884
|
+
};
|
|
885
|
+
connections?: Record<string, unknown>;
|
|
886
|
+
data?: unknown;
|
|
887
|
+
fetch?: typeof fetch;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Parameters for ingestion workflow
|
|
892
|
+
*/
|
|
893
|
+
export interface ProductIngestionParams {
|
|
894
|
+
jobId: string;
|
|
895
|
+
triggeredBy: 'schedule' | 'webhook';
|
|
896
|
+
filePattern?: string;
|
|
897
|
+
maxFiles?: number;
|
|
898
|
+
forceReprocess?: boolean;
|
|
899
|
+
catalogueRef?: string;
|
|
900
|
+
priority?: 'normal' | 'high';
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Result from ingestion workflow
|
|
905
|
+
*/
|
|
906
|
+
export interface ProductIngestionResult {
|
|
907
|
+
success: boolean;
|
|
908
|
+
jobId: string;
|
|
909
|
+
filesProcessed: number;
|
|
910
|
+
filesFailed: number;
|
|
911
|
+
filesSkipped?: number;
|
|
912
|
+
recordsProcessed: number;
|
|
913
|
+
eventsSent: number;
|
|
914
|
+
eventsFailed: number;
|
|
915
|
+
fileResults: FileProcessingResult[];
|
|
916
|
+
error?: string;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Per-file processing result
|
|
921
|
+
*/
|
|
922
|
+
export interface FileProcessingResult {
|
|
923
|
+
fileName: string;
|
|
924
|
+
success: boolean;
|
|
925
|
+
skipped?: boolean;
|
|
926
|
+
recordsProcessed: number;
|
|
927
|
+
eventsSent: number;
|
|
928
|
+
eventsFailed: number;
|
|
929
|
+
duration: number;
|
|
930
|
+
error?: string;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Parsed XML document structure
|
|
935
|
+
*/
|
|
936
|
+
export interface ParsedProductsDocument {
|
|
937
|
+
products?: {
|
|
938
|
+
product?: Product | Product[];
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
---
|
|
944
|
+
|
|
945
|
+
## 4. Service: Product File Processor (`src/services/product-file-processor.service.ts`)
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
/**
|
|
949
|
+
* Product File Processor Service
|
|
950
|
+
*
|
|
951
|
+
* Downloads XML files from S3, parses, and transforms with UniversalMapper.
|
|
952
|
+
* XML-specific: Handles array normalization (single vs multiple products).
|
|
953
|
+
*/
|
|
954
|
+
|
|
955
|
+
import {
|
|
956
|
+
S3DataSource,
|
|
957
|
+
XMLParserService,
|
|
958
|
+
UniversalMapper,
|
|
959
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
960
|
+
import type { ProcessFileResult, Product, ParsedProductsDocument } from '../types/product-ingestion.types';
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Service for processing XML product files from S3
|
|
964
|
+
*/
|
|
965
|
+
export class ProductFileProcessorService {
|
|
966
|
+
constructor(
|
|
967
|
+
private s3: S3DataSource,
|
|
968
|
+
private xmlParser: XMLParserService,
|
|
969
|
+
private mapper: UniversalMapper,
|
|
970
|
+
private catalogueRef: string
|
|
971
|
+
) {}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Download XML file from S3, parse, and transform with UniversalMapper
|
|
975
|
+
*
|
|
976
|
+
* ✅ PRODUCTION ENHANCEMENT: Memory cleanup pattern
|
|
977
|
+
* - Explicit null assignments after each step
|
|
978
|
+
* - Finally block guarantees cleanup
|
|
979
|
+
* - Prevents OOM errors on large XML files
|
|
980
|
+
*/
|
|
981
|
+
async downloadParseAndTransform(objectKey: string): Promise<ProcessFileResult> {
|
|
982
|
+
// ✅ CRITICAL: Variables for cleanup tracking
|
|
983
|
+
let xmlContent: string | null = null;
|
|
984
|
+
let parsed: unknown | null = null;
|
|
985
|
+
let rawProducts: any[] | null = null;
|
|
986
|
+
|
|
987
|
+
try {
|
|
988
|
+
// STEP 1: Download XML content from S3
|
|
989
|
+
xmlContent = (await this.s3.downloadObject(objectKey, {
|
|
990
|
+
encoding: 'utf8',
|
|
991
|
+
})) as string;
|
|
992
|
+
|
|
993
|
+
// STEP 2: Parse XML with type safety
|
|
994
|
+
try {
|
|
995
|
+
parsed = await this.xmlParser.parse(xmlContent);
|
|
996
|
+
} catch (parseError: unknown) {
|
|
997
|
+
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
|
998
|
+
return {
|
|
999
|
+
success: false,
|
|
1000
|
+
products: [],
|
|
1001
|
+
error: `XML parse error: ${errorMessage}`,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// ✅ Clear xmlContent from memory - no longer needed after parsing
|
|
1006
|
+
xmlContent = null;
|
|
1007
|
+
|
|
1008
|
+
// Type guard: Validate parsed structure
|
|
1009
|
+
if (!this.isParsedProductsDocument(parsed)) {
|
|
1010
|
+
return {
|
|
1011
|
+
success: false,
|
|
1012
|
+
products: [],
|
|
1013
|
+
error: 'Invalid XML structure: missing products element',
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// STEP 3: Normalize products array (single element → object, multiple → array)
|
|
1018
|
+
rawProducts = Array.isArray(parsed.products?.product)
|
|
1019
|
+
? parsed.products.product
|
|
1020
|
+
: parsed.products?.product
|
|
1021
|
+
? [parsed.products.product]
|
|
1022
|
+
: [];
|
|
1023
|
+
|
|
1024
|
+
if (rawProducts.length === 0) {
|
|
1025
|
+
return {
|
|
1026
|
+
success: false,
|
|
1027
|
+
products: [],
|
|
1028
|
+
error: 'No products found in XML',
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// ✅ Clear parsed data from memory - only need rawProducts now
|
|
1033
|
+
parsed = null;
|
|
1034
|
+
|
|
1035
|
+
// STEP 4: Transform with UniversalMapper
|
|
1036
|
+
// ✅ S3 XML: Merge context using spread pattern (same as SFTP XML)
|
|
1037
|
+
const sourceDataWithContext = rawProducts.map(item => ({
|
|
1038
|
+
...item,
|
|
1039
|
+
$context: { catalogueRef: this.catalogueRef },
|
|
1040
|
+
}));
|
|
1041
|
+
|
|
1042
|
+
const mappingResult = await this.mapper.map(sourceDataWithContext);
|
|
1043
|
+
|
|
1044
|
+
if (!mappingResult.success) {
|
|
1045
|
+
return {
|
|
1046
|
+
success: false,
|
|
1047
|
+
products: [],
|
|
1048
|
+
error: `Mapping validation failed: ${mappingResult.errors?.join(', ')}`,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
1053
|
+
this.log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
1054
|
+
skippedFields: mappingResult.skippedFields,
|
|
1055
|
+
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return {
|
|
1060
|
+
success: true,
|
|
1061
|
+
products: mappingResult.data as Product[],
|
|
1062
|
+
};
|
|
1063
|
+
} catch (error: unknown) {
|
|
1064
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1065
|
+
return {
|
|
1066
|
+
success: false,
|
|
1067
|
+
products: [],
|
|
1068
|
+
error: errorMessage,
|
|
1069
|
+
};
|
|
1070
|
+
} finally {
|
|
1071
|
+
// ✅ CRITICAL: Ensure all large objects are cleared even if error occurs
|
|
1072
|
+
xmlContent = null;
|
|
1073
|
+
parsed = null;
|
|
1074
|
+
rawProducts = null;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Type guard: Check if parsed value is a valid ParsedProductsDocument
|
|
1080
|
+
*/
|
|
1081
|
+
private isParsedProductsDocument(value: unknown): value is ParsedProductsDocument {
|
|
1082
|
+
if (typeof value !== 'object' || value === null) {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
const doc = value as Record<string, unknown>;
|
|
1086
|
+
return 'products' in doc;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1093
|
+
## 5. Service: Event Sender (`src/services/event-sender.service.ts`)
|
|
1094
|
+
|
|
1095
|
+
```typescript
|
|
1096
|
+
/**
|
|
1097
|
+
* Event Sender Service
|
|
1098
|
+
*
|
|
1099
|
+
* Sends product events to Fluent Commerce Event API with per-record error handling.
|
|
1100
|
+
* Continues processing on individual failures (Event API best practice).
|
|
1101
|
+
* Supports configurable concurrency (sequential or parallel).
|
|
1102
|
+
*/
|
|
1103
|
+
|
|
1104
|
+
import type { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
1105
|
+
import type { EventResult, EventConfig, Product } from '../types/product-ingestion.types';
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Service for sending events to Fluent Commerce Event API
|
|
1109
|
+
*
|
|
1110
|
+
* ✅ PRODUCTION ENHANCEMENT: Optional logger for detailed progress tracking
|
|
1111
|
+
*/
|
|
1112
|
+
export class EventSenderService {
|
|
1113
|
+
constructor(
|
|
1114
|
+
private client: FluentClient,
|
|
1115
|
+
private log?: any // ✅ Optional logger for progress tracking
|
|
1116
|
+
) {}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Send events to Fluent Commerce Event API with configurable concurrency
|
|
1120
|
+
*
|
|
1121
|
+
* **Performance Characteristics:**
|
|
1122
|
+
* - `concurrency: 1` → Sequential processing (safe default, ~1 event/sec)
|
|
1123
|
+
* - `concurrency: 3-5` → Balanced throughput (~3-5 events/sec, good for most cases)
|
|
1124
|
+
* - `concurrency: 10` → High-volume processing (~10 events/sec, 100+ products)
|
|
1125
|
+
*
|
|
1126
|
+
* **Implementation Strategy:**
|
|
1127
|
+
* - Concurrency = 1: Optimized sequential loop (no Promise.allSettled overhead)
|
|
1128
|
+
* - Concurrency > 1: Chunked parallel processing with bounded concurrency
|
|
1129
|
+
* - Both modes: Per-record error tracking (failures don't block other events)
|
|
1130
|
+
*
|
|
1131
|
+
* @param products - Array of products to send as events
|
|
1132
|
+
* @param eventConfig - Event configuration (name, catalogue ref, mode)
|
|
1133
|
+
* @param concurrency - Number of concurrent event requests (default: 1, min: 1)
|
|
1134
|
+
* @returns EventResult with counts (eventsSent/eventsFailed) and error details
|
|
1135
|
+
*/
|
|
1136
|
+
async sendEvents(
|
|
1137
|
+
products: Product[],
|
|
1138
|
+
eventConfig: EventConfig,
|
|
1139
|
+
concurrency: number = 1
|
|
1140
|
+
): Promise<EventResult> {
|
|
1141
|
+
// Validate concurrency (guard against invalid values)
|
|
1142
|
+
const safeConc = Math.max(1, Math.floor(concurrency));
|
|
1143
|
+
|
|
1144
|
+
// Result accumulators
|
|
1145
|
+
let eventsSent = 0;
|
|
1146
|
+
let eventsFailed = 0;
|
|
1147
|
+
const errors: Array<{ productRef: string; error: string }> = [];
|
|
1148
|
+
|
|
1149
|
+
// ✅ PRODUCTION ENHANCEMENT: Log event sending start
|
|
1150
|
+
if (this.log) {
|
|
1151
|
+
this.log.info('📤 Starting event sending', {
|
|
1152
|
+
totalProducts: products.length,
|
|
1153
|
+
concurrency: safeConc,
|
|
1154
|
+
processingMode: safeConc === 1 ? 'sequential (one at a time)' : `parallel (${safeConc} concurrently)`,
|
|
1155
|
+
eventName: eventConfig.eventName,
|
|
1156
|
+
catalogueRef: eventConfig.catalogueRef
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Helper: Build event payload (DRY - reused in both modes)
|
|
1161
|
+
const buildPayload = (product: Product) => ({
|
|
1162
|
+
name: eventConfig.eventName,
|
|
1163
|
+
entityRef: eventConfig.catalogueRef,
|
|
1164
|
+
entityType: 'PRODUCT_CATALOGUE' as const,
|
|
1165
|
+
entitySubtype: eventConfig.catalogueType,
|
|
1166
|
+
rootEntityRef: eventConfig.catalogueRef,
|
|
1167
|
+
rootEntityType: 'PRODUCT_CATALOGUE' as const,
|
|
1168
|
+
attributes: product,
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
// ============================================================================
|
|
1172
|
+
// SEQUENTIAL MODE (concurrency === 1)
|
|
1173
|
+
// ============================================================================
|
|
1174
|
+
if (safeConc === 1) {
|
|
1175
|
+
for (let i = 0; i < products.length; i++) {
|
|
1176
|
+
const product = products[i];
|
|
1177
|
+
|
|
1178
|
+
// ✅ PRODUCTION ENHANCEMENT: Log progress every 10 products
|
|
1179
|
+
if (this.log && i % 10 === 0) {
|
|
1180
|
+
this.log.info(`📤 Sending product ${i + 1}/${products.length}`, {
|
|
1181
|
+
productRef: product.ref,
|
|
1182
|
+
progress: `${((i / products.length) * 100).toFixed(1)}%`
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
try {
|
|
1187
|
+
await this.client.sendEvent(buildPayload(product), eventConfig.eventMode);
|
|
1188
|
+
eventsSent++;
|
|
1189
|
+
} catch (err: unknown) {
|
|
1190
|
+
eventsFailed++;
|
|
1191
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1192
|
+
errors.push({ productRef: product?.ref || 'unknown', error: errorMsg });
|
|
1193
|
+
// Continue processing (failure doesn't block other products)
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// ✅ PRODUCTION ENHANCEMENT: Log completion
|
|
1198
|
+
if (this.log) {
|
|
1199
|
+
this.log.info('✅ Sequential event sending completed', {
|
|
1200
|
+
totalProducts: products.length,
|
|
1201
|
+
eventsSent,
|
|
1202
|
+
eventsFailed
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
return { eventsSent, eventsFailed, errors };
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// ============================================================================
|
|
1210
|
+
// PARALLEL MODE (concurrency > 1)
|
|
1211
|
+
// ============================================================================
|
|
1212
|
+
const totalChunks = Math.ceil(products.length / safeConc);
|
|
1213
|
+
|
|
1214
|
+
for (let i = 0; i < products.length; i += safeConc) {
|
|
1215
|
+
const chunk = products.slice(i, i + safeConc);
|
|
1216
|
+
const chunkNumber = Math.floor(i / safeConc) + 1;
|
|
1217
|
+
|
|
1218
|
+
// ✅ PRODUCTION ENHANCEMENT: Log chunk progress
|
|
1219
|
+
if (this.log) {
|
|
1220
|
+
this.log.info(`📦 Processing chunk ${chunkNumber}/${totalChunks}`, {
|
|
1221
|
+
chunkNumber,
|
|
1222
|
+
totalChunks,
|
|
1223
|
+
productsInChunk: chunk.length,
|
|
1224
|
+
productRange: `${i + 1}-${i + chunk.length}`,
|
|
1225
|
+
totalProducts: products.length,
|
|
1226
|
+
progress: `${((i / products.length) * 100).toFixed(1)}%`
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Fire all requests in chunk concurrently
|
|
1231
|
+
const results = await Promise.allSettled(
|
|
1232
|
+
chunk.map(product =>
|
|
1233
|
+
this.client
|
|
1234
|
+
.sendEvent(buildPayload(product), eventConfig.eventMode)
|
|
1235
|
+
.then(() => ({ success: true as const, product }))
|
|
1236
|
+
.catch(error => ({ success: false as const, product, error }))
|
|
1237
|
+
)
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
// Aggregate chunk results into totals
|
|
1241
|
+
let chunkSuccess = 0;
|
|
1242
|
+
let chunkFailed = 0;
|
|
1243
|
+
|
|
1244
|
+
for (const result of results) {
|
|
1245
|
+
if (result.status === 'fulfilled' && result.value.success) {
|
|
1246
|
+
eventsSent++;
|
|
1247
|
+
chunkSuccess++;
|
|
1248
|
+
} else {
|
|
1249
|
+
eventsFailed++;
|
|
1250
|
+
chunkFailed++;
|
|
1251
|
+
const error = result.status === 'fulfilled' ? result.value.error : result.reason;
|
|
1252
|
+
const product = result.status === 'fulfilled' ? result.value.product : null;
|
|
1253
|
+
errors.push({
|
|
1254
|
+
productRef: product?.ref || 'unknown',
|
|
1255
|
+
error: error?.message || String(error) || 'unknown error',
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// ✅ PRODUCTION ENHANCEMENT: Log chunk completion
|
|
1261
|
+
if (this.log) {
|
|
1262
|
+
this.log.info(`✅ Chunk ${chunkNumber}/${totalChunks} completed`, {
|
|
1263
|
+
chunkNumber,
|
|
1264
|
+
totalChunks,
|
|
1265
|
+
chunkSuccess,
|
|
1266
|
+
chunkFailed,
|
|
1267
|
+
totalSentSoFar: eventsSent,
|
|
1268
|
+
totalFailedSoFar: eventsFailed,
|
|
1269
|
+
progress: `${(((i + chunk.length) / products.length) * 100).toFixed(1)}%`
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// ✅ PRODUCTION ENHANCEMENT: Log parallel completion
|
|
1275
|
+
if (this.log) {
|
|
1276
|
+
this.log.info('✅ Parallel event sending completed', {
|
|
1277
|
+
totalProducts: products.length,
|
|
1278
|
+
totalChunks,
|
|
1279
|
+
concurrency: safeConc,
|
|
1280
|
+
eventsSent,
|
|
1281
|
+
eventsFailed
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
return { eventsSent, eventsFailed, errors };
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
---
|
|
1291
|
+
|
|
1292
|
+
## 6. Service: Event Logger (`src/services/event-logger.service.ts`)
|
|
1293
|
+
|
|
1294
|
+
```typescript
|
|
1295
|
+
/**
|
|
1296
|
+
* Event Logger Service
|
|
1297
|
+
*
|
|
1298
|
+
* Writes event processing logs to S3 for audit/debugging.
|
|
1299
|
+
* Optional - can be used to create rejection reports.
|
|
1300
|
+
*/
|
|
1301
|
+
|
|
1302
|
+
import { Buffer } from 'node:buffer';
|
|
1303
|
+
import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Service for writing event logs to S3
|
|
1307
|
+
*/
|
|
1308
|
+
export class EventLoggerService {
|
|
1309
|
+
constructor(private s3: S3DataSource) {}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Write event log to S3
|
|
1313
|
+
*
|
|
1314
|
+
* @param objectKey - S3 object key for log file
|
|
1315
|
+
* @param logData - Log data to write (JSON format)
|
|
1316
|
+
*/
|
|
1317
|
+
async writeEventLog(objectKey: string, logData: unknown): Promise<void> {
|
|
1318
|
+
try {
|
|
1319
|
+
const logContent = JSON.stringify(logData, null, 2);
|
|
1320
|
+
await this.s3.uploadFile(objectKey, logContent);
|
|
1321
|
+
} catch (error: unknown) {
|
|
1322
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1323
|
+
throw new Error(`Failed to write event log: ${errorMessage}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
## 7. Main Orchestration Service (src/services/product-ingestion.service.ts)
|
|
1330
|
+
|
|
1331
|
+
```typescript
|
|
1332
|
+
/**
|
|
1333
|
+
* MAIN INGESTION ORCHESTRATION SERVICE
|
|
1334
|
+
*
|
|
1335
|
+
* This is the heart of the ingestion workflow. It coordinates all steps:
|
|
1336
|
+
* 1. Initialize clients and services
|
|
1337
|
+
* 2. Discover files on S3
|
|
1338
|
+
* 3. Download and parse XML files
|
|
1339
|
+
* 4. Transform data with UniversalMapper
|
|
1340
|
+
* 5. Send events to Fluent Commerce
|
|
1341
|
+
* 6. Archive processed files
|
|
1342
|
+
* 7. Track file processing state (S3 uses moveObject for deduplication)
|
|
1343
|
+
* 8. Track job progress with JobTracker
|
|
1344
|
+
*
|
|
1345
|
+
* NAMING PATTERN (consistent across all use cases):
|
|
1346
|
+
* - Interface: {Entity}IngestionParams (e.g., ProductIngestionParams)
|
|
1347
|
+
* - Result: {Entity}IngestionResult (e.g., ProductIngestionResult)
|
|
1348
|
+
* - Main function: execute{Entity}Ingestion (e.g., executeProductIngestion)
|
|
1349
|
+
*
|
|
1350
|
+
* KEY DIFFERENCES FOR S3:
|
|
1351
|
+
* - S3DataSource instead of SftpDataSource
|
|
1352
|
+
* - No VersoriFileTracker (S3 uses moveObject for deduplication)
|
|
1353
|
+
* - S3 listObjects instead of SFTP listFiles
|
|
1354
|
+
* - S3 moveObject instead of SFTP moveFile
|
|
1355
|
+
* - S3 dispose() for cleanup (S3DataSource does need dispose)
|
|
1356
|
+
*/
|
|
1357
|
+
|
|
1358
|
+
import { Buffer } from 'node:buffer';
|
|
1359
|
+
import {
|
|
1360
|
+
createClient,
|
|
1361
|
+
S3DataSource,
|
|
1362
|
+
XMLParserService,
|
|
1363
|
+
UniversalMapper,
|
|
1364
|
+
JobTracker,
|
|
1365
|
+
type FluentClient,
|
|
1366
|
+
type JobStatus,
|
|
1367
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
1368
|
+
import type {
|
|
1369
|
+
ProductIngestionParams,
|
|
1370
|
+
ProductIngestionResult,
|
|
1371
|
+
FileProcessingResult,
|
|
1372
|
+
EventConfig,
|
|
1373
|
+
} from '../types/product-ingestion.types';
|
|
1374
|
+
import { ProductFileProcessorService } from './product-file-processor.service';
|
|
1375
|
+
import { EventSenderService } from './event-sender.service';
|
|
1376
|
+
import { EventLoggerService } from './event-logger.service';
|
|
1377
|
+
import { timestampedName } from '../utils/s3-path.utils';
|
|
1378
|
+
|
|
1379
|
+
import mappingConfig from '../../config/products.import.xml.json' with { type: 'json' };
|
|
1380
|
+
|
|
1381
|
+
// ✅ VERSORI PLATFORM: Use native log from context, not LoggingService
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Query job status from KV store
|
|
1385
|
+
*
|
|
1386
|
+
* NAMING: get{Entity}JobStatus or just getJobStatus (generic)
|
|
1387
|
+
*
|
|
1388
|
+
* ✅ VERSORI PLATFORM: Uses native log from context (passed as parameter)
|
|
1389
|
+
*/
|
|
1390
|
+
export async function getJobStatus(
|
|
1391
|
+
kv: ReturnType<VersoriContext['openKv']>,
|
|
1392
|
+
jobId: string,
|
|
1393
|
+
log: VersoriContext['log']
|
|
1394
|
+
): Promise<JobStatus | undefined> {
|
|
1395
|
+
try {
|
|
1396
|
+
const tracker = new JobTracker(kv, log);
|
|
1397
|
+
return await tracker.getJob(jobId); // ✅ Use getJob() not getJobStatus()
|
|
1398
|
+
} catch (error: unknown) {
|
|
1399
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1400
|
+
log.error('Failed to get job status', {
|
|
1401
|
+
jobId,
|
|
1402
|
+
message: errorMessage,
|
|
1403
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1404
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1405
|
+
});
|
|
1406
|
+
return undefined;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* MAIN ORCHESTRATION FUNCTION
|
|
1412
|
+
*
|
|
1413
|
+
* NAMING: execute{Entity}Ingestion (e.g., executeProductIngestion)
|
|
1414
|
+
*
|
|
1415
|
+
* This function implements the complete ingestion workflow in 8 steps.
|
|
1416
|
+
* Each step is clearly commented for AI understanding.
|
|
1417
|
+
*/
|
|
1418
|
+
export async function executeProductIngestion(
|
|
1419
|
+
ctx,
|
|
1420
|
+
params: ProductIngestionParams
|
|
1421
|
+
): Promise<ProductIngestionResult> {
|
|
1422
|
+
|
|
1423
|
+
// ✅ VERSORI PLATFORM: Extract native log from context (LoggingService was removed - use native log)
|
|
1424
|
+
const { log, openKv, activation } = ctx;
|
|
1425
|
+
const { jobId, triggeredBy, filePattern, maxFiles, forceReprocess } = params;
|
|
1426
|
+
|
|
1427
|
+
// Open KV store for state management and job tracking
|
|
1428
|
+
// ✅ Pass raw Versori KV directly - it matches KVAdapter interface
|
|
1429
|
+
// ✅ Pass native log to JobTracker
|
|
1430
|
+
const kv = openKv(':project:');
|
|
1431
|
+
const tracker = new JobTracker(kv, log);
|
|
1432
|
+
|
|
1433
|
+
const startTime = Date.now();
|
|
1434
|
+
const fileResults: FileProcessingResult[] = [];
|
|
1435
|
+
|
|
1436
|
+
// ⚠️ CRITICAL: Declare S3 outside try block for disposal pattern
|
|
1437
|
+
let s3: S3DataSource | undefined;
|
|
1438
|
+
|
|
1439
|
+
try {
|
|
1440
|
+
//
|
|
1441
|
+
// STEP 1: Initialize Job Tracking
|
|
1442
|
+
//
|
|
1443
|
+
log.info('📋 [STEP 1/8] Initializing job tracking', { jobId });
|
|
1444
|
+
|
|
1445
|
+
await tracker.createJob(jobId, {
|
|
1446
|
+
triggeredBy,
|
|
1447
|
+
filePattern: filePattern || 'default',
|
|
1448
|
+
maxFiles: maxFiles || 'unlimited',
|
|
1449
|
+
forceReprocess: !!forceReprocess
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
//
|
|
1453
|
+
// STEP 2: Initialize Fluent Client & S3 Connection
|
|
1454
|
+
//
|
|
1455
|
+
log.info('🔌 [STEP 2/8] Initializing Fluent Commerce client and S3', { jobId });
|
|
1456
|
+
|
|
1457
|
+
const client = await createClient(ctx);
|
|
1458
|
+
|
|
1459
|
+
if (!client) {
|
|
1460
|
+
throw new Error('Failed to create Fluent Commerce client');
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// ✅ CRITICAL: Set retailerId for Event API calls
|
|
1464
|
+
// Event API requires retailerId - fail fast if not configured
|
|
1465
|
+
const fluentRetailerId = activation.getVariable('fluentRetailerId');
|
|
1466
|
+
if (!fluentRetailerId) {
|
|
1467
|
+
throw new Error('fluentRetailerId is required for Event API calls');
|
|
1468
|
+
}
|
|
1469
|
+
client.setRetailerId(fluentRetailerId);
|
|
1470
|
+
log.info('✅ RetailerId set for Event API', { retailerId: fluentRetailerId });
|
|
1471
|
+
|
|
1472
|
+
// Get S3 configuration from activation variables
|
|
1473
|
+
const s3Config = {
|
|
1474
|
+
bucket: activation.getVariable('s3Bucket'),
|
|
1475
|
+
region: activation.getVariable('s3Region') || 'us-east-1',
|
|
1476
|
+
accessKeyId: activation.getVariable('s3AccessKeyId'),
|
|
1477
|
+
secretAccessKey: activation.getVariable('s3SecretAccessKey'),
|
|
1478
|
+
incomingPrefix: activation.getVariable('s3IncomingPrefix') || 'products/incoming/',
|
|
1479
|
+
processedPrefix: activation.getVariable('s3ProcessedPrefix') || 'products/processed/',
|
|
1480
|
+
errorPrefix: activation.getVariable('s3ErrorPrefix') || 'products/errors/',
|
|
1481
|
+
logsPrefix: activation.getVariable('s3LogsPrefix') || 'products/logs/',
|
|
1482
|
+
filePattern: filePattern || activation.getVariable('filePattern') || 'products_*.xml'
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
// Validate S3 config
|
|
1486
|
+
if (!s3Config.bucket) {
|
|
1487
|
+
throw new Error('S3 configuration incomplete: missing bucket');
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (!s3Config.accessKeyId || !s3Config.secretAccessKey) {
|
|
1491
|
+
throw new Error('S3 configuration incomplete: missing accessKeyId or secretAccessKey');
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Initialize S3 data source
|
|
1495
|
+
// ✅ VERSORI PLATFORM: Pass native log from context
|
|
1496
|
+
s3 = new S3DataSource(
|
|
1497
|
+
{
|
|
1498
|
+
type: 'S3_XML',
|
|
1499
|
+
connectionId: 's3-product-ingestion',
|
|
1500
|
+
name: 'Product Ingestion S3',
|
|
1501
|
+
s3Config: {
|
|
1502
|
+
bucket: s3Config.bucket,
|
|
1503
|
+
region: s3Config.region,
|
|
1504
|
+
accessKeyId: s3Config.accessKeyId,
|
|
1505
|
+
secretAccessKey: s3Config.secretAccessKey,
|
|
1506
|
+
},
|
|
1507
|
+
validateConnection: true, // ✅ Enable connection validation on init
|
|
1508
|
+
},
|
|
1509
|
+
log
|
|
1510
|
+
);
|
|
1511
|
+
|
|
1512
|
+
try {
|
|
1513
|
+
// Connection already validated during initialization
|
|
1514
|
+
// await s3.validateConnection(); // No longer needed - done in constructor
|
|
1515
|
+
log.info('✅ S3 connection validated');
|
|
1516
|
+
|
|
1517
|
+
//
|
|
1518
|
+
// STEP 3: Discover Files on S3
|
|
1519
|
+
//
|
|
1520
|
+
log.info('🔍 [STEP 3/8] Discovering files on S3', {
|
|
1521
|
+
jobId,
|
|
1522
|
+
prefix: s3Config.incomingPrefix,
|
|
1523
|
+
filePattern: s3Config.filePattern
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
await tracker.updateJob(jobId, {
|
|
1527
|
+
status: 'processing',
|
|
1528
|
+
stage: 'file_discovery',
|
|
1529
|
+
message: 'Discovering files on S3'
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
// List objects from S3
|
|
1533
|
+
const allFiles = await s3.listObjects({
|
|
1534
|
+
prefix: s3Config.incomingPrefix,
|
|
1535
|
+
pattern: s3Config.filePattern
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
log.info(`📁 Discovered ${allFiles.length} files matching pattern`);
|
|
1539
|
+
|
|
1540
|
+
// Apply maxFiles limit if specified
|
|
1541
|
+
const filesToProcess = maxFiles ? allFiles.slice(0, maxFiles) : allFiles;
|
|
1542
|
+
|
|
1543
|
+
if (filesToProcess.length === 0) {
|
|
1544
|
+
log.info('No files to process');
|
|
1545
|
+
|
|
1546
|
+
await tracker.markCompleted(jobId, {
|
|
1547
|
+
filesProcessed: 0,
|
|
1548
|
+
message: 'No files found to process'
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
return {
|
|
1552
|
+
success: true,
|
|
1553
|
+
jobId,
|
|
1554
|
+
filesProcessed: 0,
|
|
1555
|
+
filesFailed: 0,
|
|
1556
|
+
recordsProcessed: 0,
|
|
1557
|
+
eventsSent: 0,
|
|
1558
|
+
eventsFailed: 0,
|
|
1559
|
+
fileResults: []
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
log.info(`⚙️ Processing ${filesToProcess.length} files`);
|
|
1564
|
+
|
|
1565
|
+
// Initialize services
|
|
1566
|
+
const xmlParser = new XMLParserService();
|
|
1567
|
+
const mapper = new UniversalMapper(mappingConfig);
|
|
1568
|
+
|
|
1569
|
+
// Get event configuration
|
|
1570
|
+
const eventConfig: EventConfig = {
|
|
1571
|
+
catalogueRef: params.catalogueRef || activation.getVariable('catalogueRef') || 'PC:MASTER:2',
|
|
1572
|
+
catalogueType: activation.getVariable('catalogueType') || 'MASTER',
|
|
1573
|
+
eventName: activation.getVariable('eventName') || 'UPSERT_PRODUCT',
|
|
1574
|
+
eventMode: (activation.getVariable('eventMode') || 'async') as 'async' | 'sync',
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
// Get event sending configuration
|
|
1578
|
+
const eventConcurrency = Math.max(
|
|
1579
|
+
1,
|
|
1580
|
+
parseInt(activation.getVariable('eventConcurrency') || '1', 10)
|
|
1581
|
+
);
|
|
1582
|
+
// Validate: Ensure concurrency is at least 1 (sequential) or higher (parallel)
|
|
1583
|
+
// concurrency: 1 = sequential, concurrency > 1 = parallel
|
|
1584
|
+
|
|
1585
|
+
log.info(`Event concurrency: ${eventConcurrency}`, {
|
|
1586
|
+
mode: eventConcurrency === 1 ? 'sequential' : 'parallel',
|
|
1587
|
+
concurrentRequests: eventConcurrency === 1 ? 'N/A' : eventConcurrency,
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
// Initialize class-based services
|
|
1591
|
+
const fileProcessor = new ProductFileProcessorService(
|
|
1592
|
+
s3,
|
|
1593
|
+
xmlParser,
|
|
1594
|
+
mapper,
|
|
1595
|
+
eventConfig.catalogueRef
|
|
1596
|
+
);
|
|
1597
|
+
// ✅ PRODUCTION ENHANCEMENT: Pass log to EventSenderService for detailed progress tracking
|
|
1598
|
+
const eventSender = new EventSenderService(client, log);
|
|
1599
|
+
const eventLogger = new EventLoggerService(s3);
|
|
1600
|
+
|
|
1601
|
+
//
|
|
1602
|
+
// STEP 4-7: Process Each File (Download → Parse → Transform → Send → Archive)
|
|
1603
|
+
//
|
|
1604
|
+
|
|
1605
|
+
for (let fileIndex = 0; fileIndex < filesToProcess.length; fileIndex++) {
|
|
1606
|
+
const file = filesToProcess[fileIndex];
|
|
1607
|
+
const fileStartTime = Date.now();
|
|
1608
|
+
|
|
1609
|
+
// Use object key from S3 listing
|
|
1610
|
+
const objectKey = file.key || (file as any).name;
|
|
1611
|
+
const baseName = objectKey.split('/').pop() || objectKey;
|
|
1612
|
+
|
|
1613
|
+
log.info(`📄 [FILE ${fileIndex + 1}/${filesToProcess.length}] Processing file: ${baseName}`);
|
|
1614
|
+
|
|
1615
|
+
try {
|
|
1616
|
+
await tracker.updateJob(jobId, {
|
|
1617
|
+
status: 'processing',
|
|
1618
|
+
stage: 'downloading',
|
|
1619
|
+
message: `Processing file ${fileIndex + 1} of ${filesToProcess.length}: ${baseName}`
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
// ═══════════════════════════════════════════════════════════
|
|
1623
|
+
// STEP 4: Process File (Download + Parse + Map)
|
|
1624
|
+
// ═══════════════════════════════════════════════════════════
|
|
1625
|
+
log.info(`⚙️ [STEP 4/8] Processing file: ${baseName}`);
|
|
1626
|
+
|
|
1627
|
+
// Process file: download → parse → transform
|
|
1628
|
+
const fileResult = await fileProcessor.downloadParseAndTransform(objectKey);
|
|
1629
|
+
|
|
1630
|
+
if (!fileResult.success || fileResult.products.length === 0) {
|
|
1631
|
+
// Move failed file to errors and record result
|
|
1632
|
+
const archivedName = timestampedName(baseName);
|
|
1633
|
+
const errorKey = `${s3Config.errorPrefix}${archivedName}`;
|
|
1634
|
+
await s3.moveObject(objectKey, errorKey);
|
|
1635
|
+
|
|
1636
|
+
fileResults.push({
|
|
1637
|
+
fileName: baseName,
|
|
1638
|
+
success: false,
|
|
1639
|
+
recordsProcessed: fileResult.products.length,
|
|
1640
|
+
eventsSent: 0,
|
|
1641
|
+
eventsFailed: 0,
|
|
1642
|
+
duration: Date.now() - fileStartTime,
|
|
1643
|
+
error: fileResult.error || 'Processing failed'
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// ═══════════════════════════════════════════════════════════
|
|
1650
|
+
// STEP 5: Send Events
|
|
1651
|
+
// ═══════════════════════════════════════════════════════════
|
|
1652
|
+
log.info(`📤 [STEP 5/8] Sending events for ${baseName}`);
|
|
1653
|
+
|
|
1654
|
+
// ? Enhanced: Extract context for progress logging
|
|
1655
|
+
const sampleProductRefs = fileResult.products.slice(0, 5).map((p: any) => p.ref || p.skuRef);
|
|
1656
|
+
const eventType = eventConfig.eventType || 'UPSERT_PRODUCT';
|
|
1657
|
+
|
|
1658
|
+
// ? Enhanced: Start logging with context
|
|
1659
|
+
log.info(`[EventSender] Sending events for file "${baseName}"`, {
|
|
1660
|
+
totalProducts: fileResult.products.length,
|
|
1661
|
+
eventType,
|
|
1662
|
+
concurrency: eventConcurrency === 1 ? 'sequential' : `parallel (${eventConcurrency})`,
|
|
1663
|
+
sampleProductRefs: sampleProductRefs.join(', '),
|
|
1664
|
+
eventMode: eventConfig.eventMode || 'async'
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
const eventResult = await eventSender.sendEvents(
|
|
1668
|
+
fileResult.products,
|
|
1669
|
+
eventConfig,
|
|
1670
|
+
eventConcurrency
|
|
1671
|
+
);
|
|
1672
|
+
|
|
1673
|
+
const eventsSent = eventResult.eventsSent;
|
|
1674
|
+
const eventsFailed = eventResult.eventsFailed;
|
|
1675
|
+
|
|
1676
|
+
log.info(`✅ Events sent for ${baseName}`, {
|
|
1677
|
+
successful: eventsSent,
|
|
1678
|
+
failed: eventsFailed
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
// ? Enhanced: Completion logging with summary
|
|
1682
|
+
log.info(`[EventSender] Event submission completed for file "${baseName}"`, {
|
|
1683
|
+
totalProducts: fileResult.products.length,
|
|
1684
|
+
eventsSent,
|
|
1685
|
+
eventsFailed,
|
|
1686
|
+
successRate: fileResult.products.length > 0 ? `${Math.round((eventsSent / fileResult.products.length) * 100)}%` : '0%',
|
|
1687
|
+
eventType
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
// ═══════════════════════════════════════════════════════════
|
|
1691
|
+
// STEP 6: Archive File
|
|
1692
|
+
// ═══════════════════════════════════════════════════════════
|
|
1693
|
+
log.info(`📦 [STEP 6/8] Archiving file: ${baseName}`);
|
|
1694
|
+
|
|
1695
|
+
await tracker.updateJob(jobId, {
|
|
1696
|
+
status: 'processing',
|
|
1697
|
+
stage: 'archiving',
|
|
1698
|
+
message: `Archiving ${baseName}`
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
// Conditionally archive: errors → errorPrefix, else → processedPrefix (timestamped)
|
|
1702
|
+
// ✅ S3: Uses moveObject for deduplication (no VersoriFileTracker needed)
|
|
1703
|
+
const archivedName = timestampedName(baseName);
|
|
1704
|
+
const targetPrefix = eventsFailed > 0 ? s3Config.errorPrefix : s3Config.processedPrefix;
|
|
1705
|
+
const targetKey = `${targetPrefix}${archivedName}`;
|
|
1706
|
+
await s3.moveObject(objectKey, targetKey);
|
|
1707
|
+
|
|
1708
|
+
log.info(`✅ File archived successfully: ${baseName}`, { targetKey });
|
|
1709
|
+
|
|
1710
|
+
// Optional: Write event log for audit/debugging
|
|
1711
|
+
if (eventsFailed > 0) {
|
|
1712
|
+
const logKey = `${s3Config.logsPrefix}${baseName.replace(/\.xml$/i, '')}-event-log.json`;
|
|
1713
|
+
await eventLogger.writeEventLog(logKey, {
|
|
1714
|
+
fileName: baseName,
|
|
1715
|
+
totalRecords: fileResult.products.length,
|
|
1716
|
+
eventsSent,
|
|
1717
|
+
eventsFailed,
|
|
1718
|
+
errors: eventResult.errors,
|
|
1719
|
+
processedAt: new Date().toISOString()
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Store file result
|
|
1724
|
+
fileResults.push({
|
|
1725
|
+
fileName: baseName,
|
|
1726
|
+
success: true,
|
|
1727
|
+
recordsProcessed: fileResult.products.length,
|
|
1728
|
+
eventsSent,
|
|
1729
|
+
eventsFailed,
|
|
1730
|
+
duration: Date.now() - fileStartTime
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
} catch (error: unknown) {
|
|
1734
|
+
// ✅ Enhanced error logging: Extract all error details for visibility
|
|
1735
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1736
|
+
const errorDetails = {
|
|
1737
|
+
message: errorMessage,
|
|
1738
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1739
|
+
fileName: (error as any)?.fileName,
|
|
1740
|
+
lineNumber: (error as any)?.lineNumber,
|
|
1741
|
+
originalError: (error as any)?.context?.originalError?.message,
|
|
1742
|
+
errorType: error instanceof Error ? error.name : 'Error',
|
|
1743
|
+
};
|
|
1744
|
+
log.error(`Error processing file ${baseName}:`, errorDetails);
|
|
1745
|
+
|
|
1746
|
+
// Best-effort: move to error archive on unexpected failure
|
|
1747
|
+
try {
|
|
1748
|
+
const archivedName = timestampedName(baseName);
|
|
1749
|
+
const errorKey = `${s3Config.errorPrefix}${archivedName}`;
|
|
1750
|
+
await s3.moveObject(objectKey, errorKey);
|
|
1751
|
+
} catch (moveError: unknown) {
|
|
1752
|
+
const moveErrorMessage = moveError instanceof Error ? moveError.message : String(moveError);
|
|
1753
|
+
log.error('Could not archive failed file', {
|
|
1754
|
+
file: baseName,
|
|
1755
|
+
message: moveErrorMessage,
|
|
1756
|
+
stack: moveError instanceof Error ? moveError.stack : undefined,
|
|
1757
|
+
errorType: moveError instanceof Error ? moveError.constructor.name : 'Error'
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
fileResults.push({
|
|
1762
|
+
fileName: baseName,
|
|
1763
|
+
success: false,
|
|
1764
|
+
recordsProcessed: 0,
|
|
1765
|
+
eventsSent: 0,
|
|
1766
|
+
eventsFailed: 0,
|
|
1767
|
+
duration: Date.now() - fileStartTime,
|
|
1768
|
+
error: errorMessage
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
//
|
|
1774
|
+
// STEP 8: Complete Job & Calculate Totals
|
|
1775
|
+
//
|
|
1776
|
+
log.info('🏁 [STEP 8/8] Completing job and calculating totals', { jobId });
|
|
1777
|
+
|
|
1778
|
+
const filesProcessed = fileResults.filter(r => r.success).length;
|
|
1779
|
+
const filesFailed = fileResults.filter(r => !r.success).length;
|
|
1780
|
+
const totalRecordsProcessed = fileResults.reduce((sum, r) => sum + r.recordsProcessed, 0);
|
|
1781
|
+
const totalEventsSent = fileResults.reduce((sum, r) => sum + r.eventsSent, 0);
|
|
1782
|
+
const totalEventsFailed = fileResults.reduce((sum, r) => sum + r.eventsFailed, 0);
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
await tracker.markCompleted(jobId, {
|
|
1786
|
+
filesProcessed,
|
|
1787
|
+
filesFailed,
|
|
1788
|
+
recordsProcessed: totalRecordsProcessed,
|
|
1789
|
+
eventsSent: totalEventsSent,
|
|
1790
|
+
eventsFailed: totalEventsFailed,
|
|
1791
|
+
duration: Date.now() - startTime
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
log.info('✅ Ingestion completed successfully', {
|
|
1795
|
+
filesProcessed,
|
|
1796
|
+
filesFailed,
|
|
1797
|
+
recordsProcessed: totalRecordsProcessed,
|
|
1798
|
+
eventsSent: totalEventsSent,
|
|
1799
|
+
eventsFailed: totalEventsFailed,
|
|
1800
|
+
duration: Date.now() - startTime
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
return {
|
|
1804
|
+
success: true,
|
|
1805
|
+
jobId,
|
|
1806
|
+
filesProcessed,
|
|
1807
|
+
filesFailed,
|
|
1808
|
+
recordsProcessed: totalRecordsProcessed,
|
|
1809
|
+
eventsSent: totalEventsSent,
|
|
1810
|
+
eventsFailed: totalEventsFailed,
|
|
1811
|
+
fileResults
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
} finally {
|
|
1815
|
+
// ❌š ï¸ CRITICAL: Always dispose S3 connection
|
|
1816
|
+
if (s3) {
|
|
1817
|
+
await s3.dispose();
|
|
1818
|
+
log.info('S3 connection disposed');
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
} catch (error: unknown) {
|
|
1823
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1824
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
1825
|
+
|
|
1826
|
+
// Generate contextual recommendations based on error type
|
|
1827
|
+
const recommendations = [];
|
|
1828
|
+
if (errorMessage.includes('S3') || errorMessage.includes('bucket')) {
|
|
1829
|
+
recommendations.push('Verify S3 credentials (accessKeyId, secretAccessKey)');
|
|
1830
|
+
recommendations.push('Check S3 bucket name and region configuration');
|
|
1831
|
+
recommendations.push('Ensure bucket permissions allow ListObjects and GetObject');
|
|
1832
|
+
}
|
|
1833
|
+
if (errorMessage.includes('XML') || errorMessage.includes('parse')) {
|
|
1834
|
+
recommendations.push('Validate XML file structure against expected schema');
|
|
1835
|
+
recommendations.push('Check for special characters or encoding issues');
|
|
1836
|
+
recommendations.push('Review XML parser configuration');
|
|
1837
|
+
}
|
|
1838
|
+
if (errorMessage.includes('event') || errorMessage.includes('API')) {
|
|
1839
|
+
recommendations.push('Verify Fluent API credentials and retailerId');
|
|
1840
|
+
recommendations.push('Check network connectivity to Fluent API');
|
|
1841
|
+
recommendations.push('Review event payload structure');
|
|
1842
|
+
}
|
|
1843
|
+
if (errorMessage.includes('mapping')) {
|
|
1844
|
+
recommendations.push('Review mapping configuration in config/*.json');
|
|
1845
|
+
recommendations.push('Check for missing required fields');
|
|
1846
|
+
recommendations.push('Validate resolver functions');
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
log.error('❌ Ingestion workflow failed', {
|
|
1850
|
+
jobId,
|
|
1851
|
+
error: errorMessage,
|
|
1852
|
+
stack: errorStack,
|
|
1853
|
+
recommendations: recommendations.length > 0 ? recommendations : ['Review logs for detailed error information'],
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
// Try to mark job as failed, but don't let tracking errors mask workflow error
|
|
1857
|
+
try {
|
|
1858
|
+
await tracker.markFailed(jobId, error);
|
|
1859
|
+
} catch (trackingError: unknown) {
|
|
1860
|
+
const trackingErrorMessage =
|
|
1861
|
+
trackingError instanceof Error ? trackingError.message : String(trackingError);
|
|
1862
|
+
log.warn('Failed to mark job as failed in tracker', {
|
|
1863
|
+
jobId,
|
|
1864
|
+
trackingError: trackingErrorMessage,
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
return {
|
|
1869
|
+
success: false,
|
|
1870
|
+
jobId,
|
|
1871
|
+
filesProcessed: fileResults.filter(r => r.success && !r.skipped).length,
|
|
1872
|
+
filesFailed: fileResults.filter(r => !r.success).length,
|
|
1873
|
+
filesSkipped: fileResults.filter(r => r.skipped).length,
|
|
1874
|
+
recordsProcessed: fileResults.reduce((sum, r) => sum + r.recordsProcessed, 0),
|
|
1875
|
+
eventsSent: fileResults.reduce((sum, r) => sum + r.eventsSent, 0),
|
|
1876
|
+
eventsFailed: fileResults.reduce((sum, r) => sum + r.eventsFailed, 0),
|
|
1877
|
+
fileResults,
|
|
1878
|
+
error: errorMessage,
|
|
1879
|
+
};
|
|
1880
|
+
} finally {
|
|
1881
|
+
// ⚠️ CRITICAL: Ensure S3 is disposed even if outer error occurs
|
|
1882
|
+
// This outer finally ensures disposal if error happens after S3 creation
|
|
1883
|
+
// but before inner try block (e.g., during connection validation)
|
|
1884
|
+
if (s3) {
|
|
1885
|
+
try {
|
|
1886
|
+
await s3.dispose();
|
|
1887
|
+
log.info('🔌 S3 connection disposed (outer finally)');
|
|
1888
|
+
} catch (disposeError: unknown) {
|
|
1889
|
+
const disposeErrorMessage =
|
|
1890
|
+
disposeError instanceof Error ? disposeError.message : String(disposeError);
|
|
1891
|
+
log.warn('⚠️ Error disposing S3 in outer finally', { error: disposeErrorMessage });
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
// ═══════════════════════════════════════════════════════════
|
|
1896
|
+
// EXECUTION BOUNDARY: Product Ingestion End
|
|
1897
|
+
// ═══════════════════════════════════════════════════════════
|
|
1898
|
+
}
|
|
1899
|
+
```
|
|
1900
|
+
|
|
1901
|
+
## 8. Utility Functions (src/utils/)
|
|
1902
|
+
|
|
1903
|
+
### S3 Path Helpers (src/utils/s3-path.utils.ts)
|
|
1904
|
+
|
|
1905
|
+
```typescript
|
|
1906
|
+
/**
|
|
1907
|
+
* S3 Path Utilities
|
|
1908
|
+
*
|
|
1909
|
+
* Helper functions for S3 path operations.
|
|
1910
|
+
*/
|
|
1911
|
+
|
|
1912
|
+
/**
|
|
1913
|
+
* Generate timestamped filename for archival
|
|
1914
|
+
*
|
|
1915
|
+
* Adds ISO timestamp to filename before extension for unique archival.
|
|
1916
|
+
* Handles XML files specifically but can be adapted for other formats.
|
|
1917
|
+
*
|
|
1918
|
+
* @param name - Original filename
|
|
1919
|
+
* @returns Filename with timestamp appended
|
|
1920
|
+
*
|
|
1921
|
+
* @example
|
|
1922
|
+
* ```typescript
|
|
1923
|
+
* timestampedName('products.xml')
|
|
1924
|
+
* // Returns: 'products-2025-11-01T18-30-45-123Z.xml'
|
|
1925
|
+
* ```
|
|
1926
|
+
*/
|
|
1927
|
+
export function timestampedName(name: string): string {
|
|
1928
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1929
|
+
const base = name.replace(/\.xml$/i, '');
|
|
1930
|
+
return `${base}-${ts}.xml`;
|
|
1931
|
+
}
|
|
1932
|
+
```
|
|
1933
|
+
|
|
1934
|
+
### Job ID Generator (src/utils/job-id-generator.ts)
|
|
1935
|
+
|
|
1936
|
+
```typescript
|
|
1937
|
+
/**
|
|
1938
|
+
* Job ID Generator
|
|
1939
|
+
*
|
|
1940
|
+
* Generates unique job IDs for tracking ingestion runs.
|
|
1941
|
+
* Format: {PREFIX}_{ENTITY}_{DATE}_{TIME}_{RANDOM}
|
|
1942
|
+
* Example: SCHEDULED_PROD_20250124_183045_abc123
|
|
1943
|
+
*/
|
|
1944
|
+
|
|
1945
|
+
/**
|
|
1946
|
+
* Generate unique job ID
|
|
1947
|
+
*
|
|
1948
|
+
* @param prefix - Job prefix (SCHEDULED, ADHOC, etc.)
|
|
1949
|
+
* @param entity - Entity type (PROD, INV, etc.)
|
|
1950
|
+
* @returns Unique job ID string
|
|
1951
|
+
*/
|
|
1952
|
+
export function generateJobId(prefix: string, entity: string): string {
|
|
1953
|
+
const now = new Date();
|
|
1954
|
+
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
1955
|
+
const time = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
1956
|
+
const random = Math.random().toString(36).slice(2, 8).toUpperCase();
|
|
1957
|
+
return `${prefix}_${entity}_${date}_${time}_${random}`;
|
|
1958
|
+
}
|
|
1959
|
+
```
|
|
1960
|
+
|
|
1961
|
+
## 📄 Mapping Configuration
|
|
1962
|
+
|
|
1963
|
+
**File:** `config/products.import.xml.json`
|
|
1964
|
+
|
|
1965
|
+
```json
|
|
1966
|
+
{
|
|
1967
|
+
"name": "products.import.xml",
|
|
1968
|
+
"version": "1.2.0",
|
|
1969
|
+
"description": "XML → Product Event Mapping",
|
|
1970
|
+
"fields": {
|
|
1971
|
+
"ref": {
|
|
1972
|
+
"source": "ref",
|
|
1973
|
+
"required": true,
|
|
1974
|
+
"resolver": "sdk.trim",
|
|
1975
|
+
"comment": "Product reference from XML element"
|
|
1976
|
+
},
|
|
1977
|
+
"type": {
|
|
1978
|
+
"source": "type",
|
|
1979
|
+
"required": true,
|
|
1980
|
+
"resolver": "sdk.uppercase",
|
|
1981
|
+
"comment": "Product type (STANDARD, VARIANT)"
|
|
1982
|
+
},
|
|
1983
|
+
"status": {
|
|
1984
|
+
"source": "status",
|
|
1985
|
+
"required": true,
|
|
1986
|
+
"resolver": "sdk.uppercase",
|
|
1987
|
+
"comment": "Product status (ACTIVE, INACTIVE)"
|
|
1988
|
+
},
|
|
1989
|
+
"name": {
|
|
1990
|
+
"source": "name",
|
|
1991
|
+
"required": true,
|
|
1992
|
+
"resolver": "sdk.trim",
|
|
1993
|
+
"comment": "Product name/title"
|
|
1994
|
+
},
|
|
1995
|
+
"summary": {
|
|
1996
|
+
"source": "summary",
|
|
1997
|
+
"required": false,
|
|
1998
|
+
"comment": "Product short description"
|
|
1999
|
+
},
|
|
2000
|
+
"gtin": {
|
|
2001
|
+
"source": "gtin",
|
|
2002
|
+
"required": false,
|
|
2003
|
+
"resolver": "sdk.trim",
|
|
2004
|
+
"comment": "Global Trade Item Number (barcode)"
|
|
2005
|
+
},
|
|
2006
|
+
"catalogue.ref": {
|
|
2007
|
+
"source": "$context.catalogueRef",
|
|
2008
|
+
"required": false,
|
|
2009
|
+
"comment": "Catalogue reference (injected from context, NOT in XML)"
|
|
2010
|
+
},
|
|
2011
|
+
"metadata.source": {
|
|
2012
|
+
"value": "S3_XML",
|
|
2013
|
+
"required": false,
|
|
2014
|
+
"comment": "Static value - identifies data source (NOT in XML)"
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
```
|
|
2019
|
+
|
|
2020
|
+
---
|
|
2021
|
+
|
|
2022
|
+
## 9. Package Configuration
|
|
2023
|
+
|
|
2024
|
+
### `package.json`
|
|
2025
|
+
|
|
2026
|
+
```json
|
|
2027
|
+
{
|
|
2028
|
+
"name": "s3-xml-product-event-ingestion",
|
|
2029
|
+
"version": "1.2.0",
|
|
2030
|
+
"description": "S3 XML Product Event Ingestion Connector",
|
|
2031
|
+
"type": "module",
|
|
2032
|
+
"main": "src/index.ts",
|
|
2033
|
+
"dependencies": {
|
|
2034
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
2035
|
+
"@versori/run": "^1.0.0"
|
|
2036
|
+
},
|
|
2037
|
+
"devDependencies": {
|
|
2038
|
+
"@types/node": "^20.0.0",
|
|
2039
|
+
"typescript": "^5.0.0"
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
```
|
|
2043
|
+
|
|
2044
|
+
---
|
|
2045
|
+
|
|
2046
|
+
**File:** `config/products.import.xml.json`
|
|
2047
|
+
|
|
2048
|
+
```json
|
|
2049
|
+
{
|
|
2050
|
+
"name": "products.import.xml",
|
|
2051
|
+
"version": "1.1.0",
|
|
2052
|
+
"description": "XML → Product Event Mapping",
|
|
2053
|
+
"fields": {
|
|
2054
|
+
"ref": { "source": "product.ref", "required": true, "resolver": "sdk.trim" },
|
|
2055
|
+
"type": { "source": "product.type", "resolver": "sdk.uppercase", "defaultValue": "STANDARD" },
|
|
2056
|
+
"status": { "source": "product.status", "resolver": "sdk.uppercase", "defaultValue": "ACTIVE" },
|
|
2057
|
+
"gtin": { "source": "product.gtin" },
|
|
2058
|
+
"name": { "source": "product.name", "required": true, "resolver": "sdk.trim" },
|
|
2059
|
+
"summary": { "source": "product.summary" },
|
|
2060
|
+
"categoryRefs": { "source": "product.categoryRefs.ref", "isArray": true },
|
|
2061
|
+
"price": {
|
|
2062
|
+
"source": "product.price.item",
|
|
2063
|
+
"isArray": true,
|
|
2064
|
+
"fields": {
|
|
2065
|
+
"type": { "source": "type" },
|
|
2066
|
+
"currency": { "source": "currency" },
|
|
2067
|
+
"value": { "source": "value" }
|
|
2068
|
+
}
|
|
2069
|
+
},
|
|
2070
|
+
"taxType": {
|
|
2071
|
+
"source": "product.taxType",
|
|
2072
|
+
"fields": {
|
|
2073
|
+
"country": { "source": "country" },
|
|
2074
|
+
"group": { "source": "group" },
|
|
2075
|
+
"tariff": { "source": "tariff" }
|
|
2076
|
+
}
|
|
2077
|
+
},
|
|
2078
|
+
"attributes": { "defaultValue": [] }
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
```
|
|
2082
|
+
|
|
2083
|
+
Note: Adjust fields in the JSON above as needed; prefer built-in resolvers. For advanced patterns, see SDK Universal Mapping guide.
|
|
2084
|
+
|
|
2085
|
+
---
|
|
2086
|
+
|
|
2087
|
+
## Expected XML Format
|
|
2088
|
+
|
|
2089
|
+
**Sample:** `products_20250124.xml`
|
|
2090
|
+
|
|
2091
|
+
```xml
|
|
2092
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2093
|
+
<products>
|
|
2094
|
+
<product>
|
|
2095
|
+
<ref>G_PROD_WITH_NO_STANDARD</ref>
|
|
2096
|
+
<type>VARIANT</type>
|
|
2097
|
+
<status>ACTIVE</status>
|
|
2098
|
+
<gtin>MH01-XS-Orange</gtin>
|
|
2099
|
+
<name>Chaz Kangeroo Hoodie-XS-Orange main</name>
|
|
2100
|
+
<summary><p>test short description</p></summary>
|
|
2101
|
+
<categoryRefs>
|
|
2102
|
+
<ref>STANDARD_CATEGORY</ref>
|
|
2103
|
+
</categoryRefs>
|
|
2104
|
+
<price>
|
|
2105
|
+
<item>
|
|
2106
|
+
<type>DEFAULT</type>
|
|
2107
|
+
<currency>USD</currency>
|
|
2108
|
+
<value>52.000000</value>
|
|
2109
|
+
</item>
|
|
2110
|
+
<item>
|
|
2111
|
+
<type>SPECIAL</type>
|
|
2112
|
+
<currency>USD</currency>
|
|
2113
|
+
<value>9.000000</value>
|
|
2114
|
+
</item>
|
|
2115
|
+
</price>
|
|
2116
|
+
<taxType>
|
|
2117
|
+
<country>AU</country>
|
|
2118
|
+
<group>Tax Group</group>
|
|
2119
|
+
<tariff>Tax Tariff</tariff>
|
|
2120
|
+
</taxType>
|
|
2121
|
+
<catalogue>
|
|
2122
|
+
<ref>PC:MASTER:2</ref>
|
|
2123
|
+
<type>MASTER</type>
|
|
2124
|
+
</catalogue>
|
|
2125
|
+
</product>
|
|
2126
|
+
<product>
|
|
2127
|
+
<ref>G_PROD_002</ref>
|
|
2128
|
+
<type>VARIANT</type>
|
|
2129
|
+
<status>ACTIVE</status>
|
|
2130
|
+
<gtin>MH02-M-Blue</gtin>
|
|
2131
|
+
<name>Kangeroo Hoodie-M-Blue</name>
|
|
2132
|
+
<summary><p>Blue variant medium size</p></summary>
|
|
2133
|
+
<categoryRefs>
|
|
2134
|
+
<ref>STANDARD_CATEGORY</ref>
|
|
2135
|
+
<ref>SEASONAL</ref>
|
|
2136
|
+
</categoryRefs>
|
|
2137
|
+
<price>
|
|
2138
|
+
<item>
|
|
2139
|
+
<type>DEFAULT</type>
|
|
2140
|
+
<currency>USD</currency>
|
|
2141
|
+
<value>45.000000</value>
|
|
2142
|
+
</item>
|
|
2143
|
+
<item>
|
|
2144
|
+
<type>SPECIAL</type>
|
|
2145
|
+
<currency>USD</currency>
|
|
2146
|
+
<value>35.000000</value>
|
|
2147
|
+
</item>
|
|
2148
|
+
</price>
|
|
2149
|
+
<taxType>
|
|
2150
|
+
<country>AU</country>
|
|
2151
|
+
<group>Tax Group</group>
|
|
2152
|
+
<tariff>Tax Tariff</tariff>
|
|
2153
|
+
</taxType>
|
|
2154
|
+
<catalogue>
|
|
2155
|
+
<ref>PC:MASTER:2</ref>
|
|
2156
|
+
<type>MASTER</type>
|
|
2157
|
+
</catalogue>
|
|
2158
|
+
</product>
|
|
2159
|
+
</products>
|
|
2160
|
+
```
|
|
2161
|
+
|
|
2162
|
+
**XML Field Mapping:**
|
|
2163
|
+
|
|
2164
|
+
- Nested structure for objects (e.g., `<taxType><country>AU</country></taxType>`)
|
|
2165
|
+
- Arrays with repeated elements (e.g., `<categoryRefs><ref>CAT1</ref><ref>CAT2</ref></categoryRefs>`)
|
|
2166
|
+
- HTML content in CDATA or escaped (e.g., `<p>description</p>`)
|
|
2167
|
+
|
|
2168
|
+
---
|
|
2169
|
+
|
|
2170
|
+
## 9. Package Configuration
|
|
2171
|
+
|
|
2172
|
+
### `package.json`
|
|
2173
|
+
|
|
2174
|
+
```json
|
|
2175
|
+
{
|
|
2176
|
+
"name": "s3-xml-product-event",
|
|
2177
|
+
"version": "1.1.0",
|
|
2178
|
+
"type": "module",
|
|
2179
|
+
"main": "src/index.ts",
|
|
2180
|
+
"scripts": {
|
|
2181
|
+
"dev": "versori dev",
|
|
2182
|
+
"build": "versori build",
|
|
2183
|
+
"deploy": "versori deploy"
|
|
2184
|
+
},
|
|
2185
|
+
"dependencies": {
|
|
2186
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
2187
|
+
"@versori/run": "latest"
|
|
2188
|
+
},
|
|
2189
|
+
"devDependencies": {
|
|
2190
|
+
"@types/node": "^20.0.0",
|
|
2191
|
+
"typescript": "^5.0.0"
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
```
|
|
2195
|
+
|
|
2196
|
+
### `tsconfig.json`
|
|
2197
|
+
|
|
2198
|
+
```json
|
|
2199
|
+
{
|
|
2200
|
+
"compilerOptions": {
|
|
2201
|
+
"module": "ES2022",
|
|
2202
|
+
"target": "ES2024",
|
|
2203
|
+
"moduleResolution": "node"
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
```
|
|
2207
|
+
|
|
2208
|
+
---
|
|
2209
|
+
|
|
2210
|
+
## 10. Deployment Instructions
|
|
2211
|
+
|
|
2212
|
+
### Deploy to Versori
|
|
2213
|
+
|
|
2214
|
+
```bash
|
|
2215
|
+
# 1. Install dependencies
|
|
2216
|
+
npm install
|
|
2217
|
+
|
|
2218
|
+
# 2. Test locally (if using Versori CLI)
|
|
2219
|
+
npm run dev
|
|
2220
|
+
|
|
2221
|
+
# 3. Deploy to Versori platform
|
|
2222
|
+
npm run deploy
|
|
2223
|
+
```
|
|
2224
|
+
|
|
2225
|
+
### Configure Activation Variables
|
|
2226
|
+
|
|
2227
|
+
In Versori platform settings, configure all variables listed in the Activation Variables section above.
|
|
2228
|
+
|
|
2229
|
+
---
|
|
2230
|
+
|
|
2231
|
+
## 11. Testing
|
|
2232
|
+
|
|
2233
|
+
### Test Scheduled Ingestion
|
|
2234
|
+
|
|
2235
|
+
Upload a test XML file to S3 incoming prefix and wait for the scheduled run.
|
|
2236
|
+
|
|
2237
|
+
**Check logs:**
|
|
2238
|
+
|
|
2239
|
+
```
|
|
2240
|
+
[STEP 1/8] Initializing job tracking
|
|
2241
|
+
[STEP 2/8] Initializing Fluent Commerce client and S3
|
|
2242
|
+
[STEP 3/8] Discovering files on S3
|
|
2243
|
+
[FILE 1/1] Processing file: products_20250124.xml
|
|
2244
|
+
[STEP 4/8] Downloading and parsing: products_20250124.xml
|
|
2245
|
+
[STEP 5/8] Transforming 5 products from products_20250124.xml
|
|
2246
|
+
[STEP 6/8] Sending 5 events to Fluent Commerce
|
|
2247
|
+
[STEP 7/8] Archiving file: products_20250124.xml
|
|
2248
|
+
[STEP 8/8] Completing job and calculating totals
|
|
2249
|
+
```
|
|
2250
|
+
|
|
2251
|
+
### Test Ad hoc Ingestion
|
|
2252
|
+
|
|
2253
|
+
```bash
|
|
2254
|
+
# Process all pending files
|
|
2255
|
+
curl -X POST https://api.versori.com/webhooks/product-ingestion-adhoc \
|
|
2256
|
+
-H "X-API-Key: your-secret-key" \
|
|
2257
|
+
-H "Content-Type: application/json" \
|
|
2258
|
+
-d '{}'
|
|
2259
|
+
|
|
2260
|
+
# Process specific pattern
|
|
2261
|
+
curl -X POST https://api.versori.com/webhooks/product-ingestion-adhoc \
|
|
2262
|
+
-H "X-API-Key: your-secret-key" \
|
|
2263
|
+
-H "Content-Type: application/json" \
|
|
2264
|
+
-d '{
|
|
2265
|
+
"filePattern": "urgent_*.xml", }'
|
|
2266
|
+
```
|
|
2267
|
+
|
|
2268
|
+
### Test Job Status Query
|
|
2269
|
+
|
|
2270
|
+
```bash
|
|
2271
|
+
curl -X POST https://api.versori.com/webhooks/product-ingestion-job-status \
|
|
2272
|
+
-H "X-API-Key: your-secret-key" \
|
|
2273
|
+
-H "Content-Type: application/json" \
|
|
2274
|
+
-d '{
|
|
2275
|
+
"jobId": "ADHOC_PROD_20251024_183045_abc123"
|
|
2276
|
+
}'
|
|
2277
|
+
```
|
|
2278
|
+
|
|
2279
|
+
---
|
|
2280
|
+
|
|
2281
|
+
## Monitoring
|
|
2282
|
+
|
|
2283
|
+
### Success Response
|
|
2284
|
+
|
|
2285
|
+
```json
|
|
2286
|
+
{
|
|
2287
|
+
"success": true,
|
|
2288
|
+
"filesProcessed": 1,
|
|
2289
|
+
"filesSkipped": 0,
|
|
2290
|
+
"filesFailed": 0,
|
|
2291
|
+
"totalRecords": 50,
|
|
2292
|
+
"eventsSent": 50,
|
|
2293
|
+
"eventsFailed": 0,
|
|
2294
|
+
"results": [
|
|
2295
|
+
{
|
|
2296
|
+
"file": "products_2025-01-22.xml",
|
|
2297
|
+
"success": true,
|
|
2298
|
+
"recordsProcessed": 50,
|
|
2299
|
+
"eventsSent": 50,
|
|
2300
|
+
"eventsFailed": 0
|
|
2301
|
+
}
|
|
2302
|
+
],
|
|
2303
|
+
"duration": 12345
|
|
2304
|
+
}
|
|
2305
|
+
```
|
|
2306
|
+
|
|
2307
|
+
### Partial Success Response
|
|
2308
|
+
|
|
2309
|
+
```json
|
|
2310
|
+
{
|
|
2311
|
+
"success": true,
|
|
2312
|
+
"filesProcessed": 1,
|
|
2313
|
+
"filesSkipped": 0,
|
|
2314
|
+
"filesFailed": 0,
|
|
2315
|
+
"totalRecords": 50,
|
|
2316
|
+
"eventsSent": 45,
|
|
2317
|
+
"eventsFailed": 5,
|
|
2318
|
+
"results": [
|
|
2319
|
+
{
|
|
2320
|
+
"file": "products_2025-01-22.xml",
|
|
2321
|
+
"success": true,
|
|
2322
|
+
"recordsProcessed": 50,
|
|
2323
|
+
"eventsSent": 45,
|
|
2324
|
+
"eventsFailed": 5,
|
|
2325
|
+
"errors": ["PRD-001: Invalid SKU format", "PRD-002: Missing required field"]
|
|
2326
|
+
}
|
|
2327
|
+
],
|
|
2328
|
+
"duration": 12345
|
|
2329
|
+
}
|
|
2330
|
+
```
|
|
2331
|
+
|
|
2332
|
+
### Error Response
|
|
2333
|
+
|
|
2334
|
+
```json
|
|
2335
|
+
{
|
|
2336
|
+
"success": false,
|
|
2337
|
+
"filesProcessed": 0,
|
|
2338
|
+
"filesFailed": 1,
|
|
2339
|
+
"totalRecords": 0,
|
|
2340
|
+
"eventsSent": 0,
|
|
2341
|
+
"eventsFailed": 0,
|
|
2342
|
+
"results": [
|
|
2343
|
+
{
|
|
2344
|
+
"file": "products_2025-01-22.xml",
|
|
2345
|
+
"success": false,
|
|
2346
|
+
"error": "XML parse error: Invalid structure"
|
|
2347
|
+
}
|
|
2348
|
+
],
|
|
2349
|
+
"duration": 876
|
|
2350
|
+
}
|
|
2351
|
+
```
|
|
2352
|
+
|
|
2353
|
+
### Monitoring Metrics
|
|
2354
|
+
|
|
2355
|
+
Monitor these metrics via Versori logs filtered by `jobId` or `stage`:
|
|
2356
|
+
|
|
2357
|
+
- **Files Processed** - Total files successfully processed
|
|
2358
|
+
- **Events Sent** - Total events sent to Fluent Commerce
|
|
2359
|
+
- **Events Failed** - Events that failed (check rejection reports)
|
|
2360
|
+
- **Processing Duration** - Time taken for complete workflow
|
|
2361
|
+
- **Rate Limiting** - Watch for 429 errors indicating throttling
|
|
2362
|
+
|
|
2363
|
+
Use the status webhook for dashboards and automated monitoring.
|
|
2364
|
+
|
|
2365
|
+
---
|
|
2366
|
+
|
|
2367
|
+
- **No files found:** Check `s3IncomingPrefix` and `filePattern` (object keys only)
|
|
2368
|
+
- **XML parse failed:** Move to errors/; validate XML structure and encoding
|
|
2369
|
+
- **XML array normalization:** Check if single object vs array - ALWAYS normalize
|
|
2370
|
+
- **High event failures:** Inspect logs; consider Batch API for very high volumes
|
|
2371
|
+
- **429 throttling:** Add small backoff/delay or use Batch API
|
|
2372
|
+
- **S3 connection errors:** Verify credentials, bucket, region, and permissions
|
|
2373
|
+
|
|
2374
|
+
---
|
|
2375
|
+
|
|
2376
|
+
## 13. Key Takeaways
|
|
2377
|
+
|
|
2378
|
+
- ✅ **Native Versori logs** - Use `log` from context (LoggingService removed - use native log)
|
|
2379
|
+
- ✅ **3 workflows** - Scheduled, ad hoc webhook, job status webhook
|
|
2380
|
+
- ✅ **JobTracker** - Track job lifecycle with KV persistence
|
|
2381
|
+
- ❌ **NO VersoriFileTracker** - S3 uses moveObject for deduplication (SFTP-specific only)
|
|
2382
|
+
- ✅ **S3 deduplication** - Physical file movement to processed/errors prefixes
|
|
2383
|
+
- ✅ **S3 dispose()** - Always cleanup in finally block
|
|
2384
|
+
- ✅ **S3 method names** - listObjects, downloadObject, moveObject (not \*File)
|
|
2385
|
+
- ✅ **XML array normalization** - CRITICAL: single object vs array handling
|
|
2386
|
+
- ✅ **Per-record error handling** - Continue on individual failures
|
|
2387
|
+
- ✅ **Externalized mapping** - Use JSON config file for field mappings
|
|
2388
|
+
- ✅ **File-level error handling** - Don't stop on single file failure
|
|
2389
|
+
- ✅ **S3 vs SFTP** - Different deduplication strategies (moveObject vs KV state)
|
|
2390
|
+
- ✅ **Processing modes** - per-file (default), chunked, batch
|
|
2391
|
+
- ✅ **Modular services** - orchestrator, processor, dispatcher, archive
|
|
2392
|
+
|
|
2393
|
+
---
|
|
2394
|
+
|
|
2395
|
+
[← Back to Versori Event API Templates](../../readme.md) | [Versori Platform Guide →](../../../../../04-REFERENCE/platforms/versori/platforms-versori-readme.md)
|