@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +11 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
|
@@ -1,1547 +1,1547 @@
|
|
|
1
|
-
# Module 6: Advanced Integration Services
|
|
2
|
-
|
|
3
|
-
> **Learning Objective:** Master advanced SDK services for webhook validation, partial batch recovery, job tracking, and pre-flight validation.
|
|
4
|
-
>
|
|
5
|
-
> **Level:** Advanced
|
|
6
|
-
|
|
7
|
-
## Table of Contents
|
|
8
|
-
|
|
9
|
-
1. [Overview](#overview)
|
|
10
|
-
2. [Webhook Validation Service](#webhook-validation-service)
|
|
11
|
-
3. [Partial Batch Recovery](#partial-batch-recovery)
|
|
12
|
-
4. [Job Tracker](#job-tracker)
|
|
13
|
-
5. [Preflight Validator](#preflight-validator)
|
|
14
|
-
6. [Integration Patterns](#integration-patterns)
|
|
15
|
-
7. [Production Examples](#production-examples)
|
|
16
|
-
8. [Summary](#summary)
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## Overview
|
|
21
|
-
|
|
22
|
-
### What These Services Solve
|
|
23
|
-
|
|
24
|
-
| Service | Problem | Solution | Benefits |
|
|
25
|
-
|---------|---------|----------|----------|
|
|
26
|
-
| **Webhook Validation** | Unverified webhooks | Cryptographic signature validation | Security, authenticity |
|
|
27
|
-
| **Partial Batch Recovery** | Entire batch fails when one record fails | Retry only failed records | Efficiency, data integrity |
|
|
28
|
-
| **Job Tracker** | No visibility into job status | Track job lifecycle in KV store | Observability, debugging |
|
|
29
|
-
| **Preflight Validator** | API quota wasted on invalid data | Validate before API calls | Cost savings, faster feedback |
|
|
30
|
-
|
|
31
|
-
### When to Use
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
Production Integration Checklist:
|
|
35
|
-
✅ Webhook Validation - ALL webhook endpoints (security)
|
|
36
|
-
✅ Partial Batch Recovery - Batch API ingestion (reliability)
|
|
37
|
-
✅ Job Tracker - Async scheduled workflows (observability)
|
|
38
|
-
✅ Preflight Validator - Large batch operations (cost optimization)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## Webhook Validation Service
|
|
44
|
-
|
|
45
|
-
### What It Does
|
|
46
|
-
|
|
47
|
-
**Validates webhook signatures from Fluent Commerce Rubix workflows** using RSA-based cryptographic verification.
|
|
48
|
-
|
|
49
|
-
> ⚠️ **IMPORTANT:** This service is **ONLY for Fluent Commerce webhooks**. Do not use for Shopify, GitHub, Stripe, or other third-party webhooks. For those systems, implement manual HMAC validation (see Module 4: Webhook Patterns).
|
|
50
|
-
|
|
51
|
-
### Key Features
|
|
52
|
-
|
|
53
|
-
- ✅ **SHA512withRSA** (fluent-signature) - Recommended for Fluent Commerce webhooks
|
|
54
|
-
- ✅ **MD5withRSA** (flex.signature) - Support for older Fluent Commerce webhooks
|
|
55
|
-
- ✅ Automatic algorithm detection from Fluent Commerce headers
|
|
56
|
-
- ✅ Public key caching for performance
|
|
57
|
-
- ✅ Fallback algorithm support for Fluent Commerce
|
|
58
|
-
- ✅ Integrated with FluentClient.validateWebhook()
|
|
59
|
-
- ❌ **NOT for Shopify/GitHub/Stripe webhooks** (use manual HMAC - see Module 4)
|
|
60
|
-
|
|
61
|
-
### Basic Usage
|
|
62
|
-
|
|
63
|
-
#### Using FluentClient (Recommended)
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
import { createClient } from '@fluentcommerce/fc-connect-sdk';
|
|
67
|
-
|
|
68
|
-
const client = await createClient({
|
|
69
|
-
baseUrl: 'https://api.fluentcommerce.com',
|
|
70
|
-
clientId: process.env.FLUENT_CLIENT_ID,
|
|
71
|
-
clientSecret: process.env.FLUENT_CLIENT_SECRET,
|
|
72
|
-
username: process.env.FLUENT_USERNAME,
|
|
73
|
-
password: process.env.FLUENT_PASSWORD,
|
|
74
|
-
retailerId: process.env.FLUENT_RETAILER_ID,
|
|
75
|
-
publicKey: process.env.FLUENT_WEBHOOK_PUBLIC_KEY // For webhook validation
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// In webhook handler
|
|
79
|
-
export async function handleWebhook(request: Request) {
|
|
80
|
-
const rawBody = await request.text(); // CRITICAL: Must be raw body
|
|
81
|
-
const payload = JSON.parse(rawBody);
|
|
82
|
-
const signature = request.headers.get('fluent-signature');
|
|
83
|
-
|
|
84
|
-
// Validate webhook signature
|
|
85
|
-
const isValid = await client.validateWebhook(
|
|
86
|
-
payload,
|
|
87
|
-
signature,
|
|
88
|
-
rawBody // Pass raw string, not parsed JSON
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
if (!isValid) {
|
|
92
|
-
return new Response('Invalid signature', { status: 401 });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Process webhook...
|
|
96
|
-
return new Response('OK', { status: 200 });
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
#### Using WebhookValidationService Directly
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
import {
|
|
104
|
-
WebhookValidationService,
|
|
105
|
-
SignatureAlgorithm,
|
|
106
|
-
createConsoleLogger
|
|
107
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
108
|
-
|
|
109
|
-
const logger = createConsoleLogger();
|
|
110
|
-
const validator = new WebhookValidationService({
|
|
111
|
-
algorithm: SignatureAlgorithm.SHA512_WITH_RSA,
|
|
112
|
-
strictValidation: true // Throw on validation failure
|
|
113
|
-
}, logger);
|
|
114
|
-
|
|
115
|
-
// Validate webhook
|
|
116
|
-
const result = await validator.validateWebhookSignature(
|
|
117
|
-
rawPayload, // Raw request body as string
|
|
118
|
-
headers['fluent-signature'], // Signature from header
|
|
119
|
-
publicKey, // Public key from env
|
|
120
|
-
SignatureAlgorithm.SHA512_WITH_RSA
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
if (!result.isValid) {
|
|
124
|
-
console.error('Validation failed:', result.error);
|
|
125
|
-
throw new Error('Invalid webhook signature');
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Versori Platform Integration
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
import { webhook, fn } from '@versori/run';
|
|
133
|
-
import { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
134
|
-
|
|
135
|
-
export const fluentWebhook = webhook('fluent-order', {
|
|
136
|
-
response: {
|
|
137
|
-
mode: 'sync',
|
|
138
|
-
onSuccess: (ctx) => new Response(JSON.stringify({ success: true }), {
|
|
139
|
-
status: 200,
|
|
140
|
-
headers: { 'Content-Type': 'application/json' }
|
|
141
|
-
}),
|
|
142
|
-
onError: (ctx) => new Response(JSON.stringify({ error: ctx.error.message }), {
|
|
143
|
-
status: 500,
|
|
144
|
-
headers: { 'Content-Type': 'application/json' }
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
})
|
|
148
|
-
.then(fn('validate', async (ctx) => {
|
|
149
|
-
const { request, vars, log } = ctx;
|
|
150
|
-
|
|
151
|
-
// Get raw body (Versori v0.2.29+)
|
|
152
|
-
const rawBody = await request.text();
|
|
153
|
-
const payload = JSON.parse(rawBody);
|
|
154
|
-
|
|
155
|
-
// Get signature from headers
|
|
156
|
-
const signature = request.headers.get('fluent-signature') ||
|
|
157
|
-
request.headers.get('x-fluent-signature');
|
|
158
|
-
|
|
159
|
-
if (!signature) {
|
|
160
|
-
throw new Error('Missing webhook signature header');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Validate using FluentClient
|
|
164
|
-
const client = new FluentClient({
|
|
165
|
-
baseUrl: vars.FLUENT_BASE_URL,
|
|
166
|
-
clientId: vars.FLUENT_CLIENT_ID,
|
|
167
|
-
clientSecret: vars.FLUENT_CLIENT_SECRET,
|
|
168
|
-
username: vars.FLUENT_USERNAME,
|
|
169
|
-
password: vars.FLUENT_PASSWORD,
|
|
170
|
-
retailerId: vars.FLUENT_RETAILER_ID,
|
|
171
|
-
publicKey: vars.FLUENT_WEBHOOK_PUBLIC_KEY
|
|
172
|
-
}, log);
|
|
173
|
-
|
|
174
|
-
const isValid = await client.validateWebhook(payload, signature, rawBody);
|
|
175
|
-
|
|
176
|
-
if (!isValid) {
|
|
177
|
-
throw new Error('Invalid webhook signature - not from Fluent Commerce');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
log.info('Webhook signature validated', { eventName: payload.name });
|
|
181
|
-
|
|
182
|
-
return payload;
|
|
183
|
-
}))
|
|
184
|
-
.then(fn('process', async ({ data, log }) => {
|
|
185
|
-
// Process validated webhook...
|
|
186
|
-
log.info('Processing webhook', { orderId: data.entityRef });
|
|
187
|
-
|
|
188
|
-
return { success: true, orderId: data.entityRef };
|
|
189
|
-
}));
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Signature Header Detection
|
|
193
|
-
|
|
194
|
-
The service automatically detects signature algorithm:
|
|
195
|
-
|
|
196
|
-
| Header | Algorithm | Status |
|
|
197
|
-
|--------|-----------|--------|
|
|
198
|
-
| `fluent-signature` | SHA512withRSA | **Recommended** |
|
|
199
|
-
| `x-fluent-signature` | SHA512withRSA | Supported |
|
|
200
|
-
| `flex.signature` | MD5withRSA | **Older algorithm** |
|
|
201
|
-
| `flex-signature` | MD5withRSA | Supported |
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
// Auto-detection (recommended)
|
|
205
|
-
const result = await validator.validateWebhook(
|
|
206
|
-
rawPayload,
|
|
207
|
-
headers, // Pass all headers
|
|
208
|
-
publicKey
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// Manual algorithm selection
|
|
212
|
-
const result = await validator.validateWebhookSignature(
|
|
213
|
-
rawPayload,
|
|
214
|
-
headers['fluent-signature'],
|
|
215
|
-
publicKey,
|
|
216
|
-
SignatureAlgorithm.SHA512_WITH_RSA
|
|
217
|
-
);
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Common Pitfalls
|
|
221
|
-
|
|
222
|
-
#### ❌ WRONG: Validating parsed JSON
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
// ❌ This will FAIL - signature is for raw body
|
|
226
|
-
const payload = await request.json();
|
|
227
|
-
const isValid = await validator.validateWebhookSignature(
|
|
228
|
-
JSON.stringify(payload), // Serialization changes order/whitespace
|
|
229
|
-
signature,
|
|
230
|
-
publicKey
|
|
231
|
-
);
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
#### ✅ CORRECT: Validate raw body
|
|
235
|
-
|
|
236
|
-
```typescript
|
|
237
|
-
// ✅ Correct - validate raw body
|
|
238
|
-
const rawBody = await request.text();
|
|
239
|
-
const isValid = await validator.validateWebhookSignature(
|
|
240
|
-
rawBody, // Exact bytes received
|
|
241
|
-
signature,
|
|
242
|
-
publicKey
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
// Then parse for processing
|
|
246
|
-
const payload = JSON.parse(rawBody);
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### Production Factory Methods
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
import { WebhookValidationFactory } from '@fluentcommerce/fc-connect-sdk';
|
|
253
|
-
|
|
254
|
-
// Production: strict validation, throws on failure
|
|
255
|
-
const validator = WebhookValidationFactory.createProduction(logger);
|
|
256
|
-
|
|
257
|
-
// Development: lenient validation, logs but doesn't throw
|
|
258
|
-
const validator = WebhookValidationFactory.createDevelopment(logger);
|
|
259
|
-
|
|
260
|
-
// With fallback: try SHA512, fallback to MD5
|
|
261
|
-
const validator = WebhookValidationFactory.createWithFallback(logger);
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
---
|
|
265
|
-
|
|
266
|
-
## Partial Batch Recovery
|
|
267
|
-
|
|
268
|
-
### What It Does
|
|
269
|
-
|
|
270
|
-
**Tracks per-record success/failure** in batch operations and enables:
|
|
271
|
-
- Retrying only failed records instead of entire batch
|
|
272
|
-
- Checkpoint/resume functionality
|
|
273
|
-
- Detailed error reporting per record
|
|
274
|
-
- Progress tracking
|
|
275
|
-
|
|
276
|
-
### Key Features
|
|
277
|
-
|
|
278
|
-
- ✅ **Per-record tracking** - Know exactly which records failed
|
|
279
|
-
- ✅ **Selective retry** - Retry only failures, not successes
|
|
280
|
-
- ✅ **Checkpoint support** - Resume from failure point
|
|
281
|
-
- ✅ **Exponential backoff** - Configurable retry delays
|
|
282
|
-
- ✅ **Custom retry logic** - Override retry decisions
|
|
283
|
-
|
|
284
|
-
### Basic Usage
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
import { PartialBatchRecovery } from '@fluentcommerce/fc-connect-sdk';
|
|
288
|
-
|
|
289
|
-
const recovery = new PartialBatchRecovery(logger);
|
|
290
|
-
|
|
291
|
-
// Process batch with automatic recovery
|
|
292
|
-
const result = await recovery.processBatchWithRecovery(
|
|
293
|
-
records,
|
|
294
|
-
async (batch) => {
|
|
295
|
-
// Your batch processing logic
|
|
296
|
-
return await client.sendBatch(jobId, {
|
|
297
|
-
action: 'UPSERT',
|
|
298
|
-
entityType: 'INVENTORY',
|
|
299
|
-
entities: batch
|
|
300
|
-
});
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
maxRetries: 3,
|
|
304
|
-
retryOnlyFailed: true, // Only retry failed records
|
|
305
|
-
retryDelayMs: 1000, // Start with 1 second
|
|
306
|
-
retryBatchSize: 100, // Process 100 at a time
|
|
307
|
-
checkpointKey: 'inventory-sync-2025-01-24'
|
|
308
|
-
}
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
console.log(`✓ Success: ${result.successCount}/${result.totalRecords}`);
|
|
312
|
-
console.log(`✗ Failed: ${result.failedCount} records`);
|
|
313
|
-
|
|
314
|
-
if (result.failedCount > 0) {
|
|
315
|
-
console.error('Failed records:', result.failedRecords);
|
|
316
|
-
console.log(`Checkpoint saved: ${result.checkpointId}`);
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Integration with Batch API
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
import {
|
|
324
|
-
createClient,
|
|
325
|
-
PartialBatchRecovery
|
|
326
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
327
|
-
|
|
328
|
-
async function batchIngestionWithRecovery(records: any[]) {
|
|
329
|
-
const client = await createClient({ config });
|
|
330
|
-
const recovery = new PartialBatchRecovery(logger);
|
|
331
|
-
|
|
332
|
-
// Create job
|
|
333
|
-
const job = await client.createJob({
|
|
334
|
-
name: 'Inventory Ingestion with Recovery',
|
|
335
|
-
retailerId: 'my-retailer'
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// Process with recovery
|
|
339
|
-
const result = await recovery.processBatchWithRecovery(
|
|
340
|
-
records,
|
|
341
|
-
async (batch) => {
|
|
342
|
-
const response = await client.sendBatch(job.id, {
|
|
343
|
-
action: 'UPSERT',
|
|
344
|
-
entityType: 'INVENTORY',
|
|
345
|
-
entities: batch
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
logger.info('Batch sent', {
|
|
349
|
-
batchId: response.id,
|
|
350
|
-
recordCount: batch.length
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
return response;
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
maxRetries: 3,
|
|
357
|
-
retryOnlyFailed: true,
|
|
358
|
-
checkpointKey: `job-${job.id}`
|
|
359
|
-
}
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
// Check job status
|
|
363
|
-
const status = await client.getJobStatus(job.id);
|
|
364
|
-
|
|
365
|
-
return {
|
|
366
|
-
jobId: job.id,
|
|
367
|
-
jobStatus: status.status,
|
|
368
|
-
...result
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
### Checkpoint and Resume
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
// Process batch and save checkpoint
|
|
377
|
-
const result = await recovery.processBatchWithRecovery(
|
|
378
|
-
records,
|
|
379
|
-
processBatch,
|
|
380
|
-
{
|
|
381
|
-
maxRetries: 3,
|
|
382
|
-
checkpointKey: 'daily-inventory-sync'
|
|
383
|
-
}
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
if (result.failedCount > 0) {
|
|
387
|
-
console.log(`Checkpoint created: ${result.checkpointId}`);
|
|
388
|
-
console.log(`Failed records saved for later retry`);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Later: Resume from checkpoint
|
|
392
|
-
const checkpointId = result.checkpointId;
|
|
393
|
-
const resumeResult = await recovery.resumeFromCheckpoint(
|
|
394
|
-
checkpointId,
|
|
395
|
-
processBatch,
|
|
396
|
-
{
|
|
397
|
-
maxRetries: 5 // More retries on resume
|
|
398
|
-
}
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
console.log(`Resume: ${resumeResult.successCount} recovered`);
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### Custom Retry Logic
|
|
405
|
-
|
|
406
|
-
```typescript
|
|
407
|
-
const result = await recovery.processBatchWithRecovery(
|
|
408
|
-
records,
|
|
409
|
-
processBatch,
|
|
410
|
-
{
|
|
411
|
-
maxRetries: 5,
|
|
412
|
-
retryDelayMs: 2000,
|
|
413
|
-
// Custom retry decision
|
|
414
|
-
shouldRetry: (error, attemptCount) => {
|
|
415
|
-
// Don't retry validation errors
|
|
416
|
-
if (error.message.includes('validation')) {
|
|
417
|
-
return false;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Don't retry after 3 attempts for rate limits
|
|
421
|
-
if (error.message.includes('rate limit') && attemptCount > 3) {
|
|
422
|
-
return false;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Retry all other errors
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
);
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
### Record Failure Details
|
|
433
|
-
|
|
434
|
-
```typescript
|
|
435
|
-
// Access detailed failure information
|
|
436
|
-
if (result.failedCount > 0) {
|
|
437
|
-
result.failedRecords.forEach(failure => {
|
|
438
|
-
console.error(`Record ${failure.index} failed:`, {
|
|
439
|
-
record: failure.record,
|
|
440
|
-
error: failure.error.message,
|
|
441
|
-
attempts: failure.attemptCount,
|
|
442
|
-
timestamp: failure.timestamp
|
|
443
|
-
});
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// Export failures for manual review
|
|
447
|
-
await fs.writeFile(
|
|
448
|
-
'failed-records.json',
|
|
449
|
-
JSON.stringify(result.failedRecords, null, 2)
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
---
|
|
455
|
-
|
|
456
|
-
## Job Tracker
|
|
457
|
-
|
|
458
|
-
### What It Does
|
|
459
|
-
|
|
460
|
-
**Tracks job status** in Versori KV store for async workflows:
|
|
461
|
-
- Job lifecycle tracking (queued → processing → completed/failed)
|
|
462
|
-
- Stage-based progress tracking
|
|
463
|
-
- Error capture with stack traces
|
|
464
|
-
- Automatic timestamp management
|
|
465
|
-
- TTL-based cleanup
|
|
466
|
-
|
|
467
|
-
### Key Features
|
|
468
|
-
|
|
469
|
-
- ✅ **Lifecycle tracking** - queued, processing, completed, failed
|
|
470
|
-
- ✅ **Stage tracking** - Custom stages for your workflow
|
|
471
|
-
- ✅ **Metadata storage** - Store job-specific context
|
|
472
|
-
- ✅ **TTL support** - Automatic cleanup after 7 days
|
|
473
|
-
- ✅ **Error capture** - Store errors with stack traces
|
|
474
|
-
|
|
475
|
-
### Basic Usage
|
|
476
|
-
|
|
477
|
-
```typescript
|
|
478
|
-
import { JobTracker, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
479
|
-
// ✅ CORRECT: Access openKv from Versori context
|
|
480
|
-
// import { openKv } from '@versori/run'; // ❌ WRONG - Not a direct export
|
|
481
|
-
|
|
482
|
-
// In Versori workflow handler:
|
|
483
|
-
const { openKv } = ctx;
|
|
484
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
485
|
-
const tracker = new JobTracker(kvAdapter, logger);
|
|
486
|
-
|
|
487
|
-
// Create job
|
|
488
|
-
const jobId = `scheduled_${Date.now()}`;
|
|
489
|
-
|
|
490
|
-
await tracker.createJob(jobId, {
|
|
491
|
-
triggeredBy: 'schedule',
|
|
492
|
-
stage: 'initialization',
|
|
493
|
-
details: {
|
|
494
|
-
catalogueRef: 'DEFAULT:1',
|
|
495
|
-
fileName: 'inventory.csv'
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
// Update progress
|
|
500
|
-
await tracker.updateJob(jobId, {
|
|
501
|
-
status: 'processing',
|
|
502
|
-
stage: 'extraction',
|
|
503
|
-
message: 'Extracting records from S3'
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
await tracker.updateJob(jobId, {
|
|
507
|
-
stage: 'transformation',
|
|
508
|
-
message: 'Mapping 1000 records',
|
|
509
|
-
details: { recordCount: 1000 }
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
// Mark as completed
|
|
513
|
-
await tracker.markCompleted(jobId, {
|
|
514
|
-
recordCount: 1000,
|
|
515
|
-
successCount: 998,
|
|
516
|
-
failedCount: 2
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// Or mark as failed
|
|
520
|
-
try {
|
|
521
|
-
// ... job logic ...
|
|
522
|
-
} catch (error) {
|
|
523
|
-
await tracker.markFailed(jobId, error);
|
|
524
|
-
}
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
### Scheduled Workflow Integration
|
|
528
|
-
|
|
529
|
-
```typescript
|
|
530
|
-
import { schedule } from '@versori/run';
|
|
531
|
-
import { JobTracker, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
532
|
-
|
|
533
|
-
export const dailyInventorySync = schedule('daily-inventory', '0 2 * * *')
|
|
534
|
-
.execute(async ({ log, connections, vars, kv }) => {
|
|
535
|
-
const jobId = `inventory_${Date.now()}`;
|
|
536
|
-
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
537
|
-
|
|
538
|
-
try {
|
|
539
|
-
// Create job
|
|
540
|
-
await tracker.createJob(jobId, {
|
|
541
|
-
triggeredBy: 'schedule',
|
|
542
|
-
stage: 'start',
|
|
543
|
-
details: { schedule: 'daily 2am' }
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
// Stage 1: Extraction
|
|
547
|
-
await tracker.updateJob(jobId, {
|
|
548
|
-
status: 'processing',
|
|
549
|
-
stage: 'extraction',
|
|
550
|
-
message: 'Querying virtual positions'
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
const data = await extractFromFluent();
|
|
554
|
-
|
|
555
|
-
// Stage 2: Transformation
|
|
556
|
-
await tracker.updateJob(jobId, {
|
|
557
|
-
stage: 'transformation',
|
|
558
|
-
message: `Processing ${data.length} records`
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
const transformed = await transformData(data);
|
|
562
|
-
|
|
563
|
-
// Stage 3: Upload
|
|
564
|
-
await tracker.updateJob(jobId, {
|
|
565
|
-
stage: 'upload',
|
|
566
|
-
message: 'Uploading to SFTP'
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
await uploadToSFTP(transformed);
|
|
570
|
-
|
|
571
|
-
// Completed
|
|
572
|
-
await tracker.markCompleted(jobId, {
|
|
573
|
-
recordCount: data.length,
|
|
574
|
-
fileName: `inventory_${jobId}.xml`
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
log.info('Job completed successfully', { jobId });
|
|
578
|
-
|
|
579
|
-
} catch (error) {
|
|
580
|
-
await tracker.markFailed(jobId, error);
|
|
581
|
-
log.error('Job failed', error);
|
|
582
|
-
throw error;
|
|
583
|
-
}
|
|
584
|
-
});
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
### Webhook Workflow Integration
|
|
588
|
-
|
|
589
|
-
```typescript
|
|
590
|
-
import { webhook, fn } from '@versori/run';
|
|
591
|
-
import { JobTracker, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
592
|
-
|
|
593
|
-
export const orderWebhook = webhook('order-webhook')
|
|
594
|
-
.then(fn('track-start', async ({ request, kv, log }) => {
|
|
595
|
-
const jobId = request.headers.get('x-request-id') || crypto.randomUUID();
|
|
596
|
-
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
597
|
-
|
|
598
|
-
await tracker.createJob(jobId, {
|
|
599
|
-
triggeredBy: 'webhook',
|
|
600
|
-
stage: 'validation',
|
|
601
|
-
details: {
|
|
602
|
-
source: request.headers.get('user-agent')
|
|
603
|
-
}
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
return { jobId };
|
|
607
|
-
}))
|
|
608
|
-
.then(fn('process', async ({ data, kv, log }) => {
|
|
609
|
-
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
610
|
-
|
|
611
|
-
await tracker.updateJob(data.jobId, {
|
|
612
|
-
status: 'processing',
|
|
613
|
-
stage: 'transformation'
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
// Process order...
|
|
617
|
-
const result = await processOrder(data);
|
|
618
|
-
|
|
619
|
-
await tracker.markCompleted(data.jobId, {
|
|
620
|
-
orderId: result.orderId
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
return result;
|
|
624
|
-
}))
|
|
625
|
-
.catch(async ({ error, data, kv, log }) => {
|
|
626
|
-
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
627
|
-
|
|
628
|
-
if (data?.jobId) {
|
|
629
|
-
await tracker.markFailed(data.jobId, error);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
throw error;
|
|
633
|
-
});
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
### Querying Job Status
|
|
637
|
-
|
|
638
|
-
```typescript
|
|
639
|
-
// Get job status
|
|
640
|
-
const status = await tracker.getJob(jobId);
|
|
641
|
-
|
|
642
|
-
if (status) {
|
|
643
|
-
console.log(`Job ${jobId}:`, {
|
|
644
|
-
status: status.status,
|
|
645
|
-
stage: status.stage,
|
|
646
|
-
message: status.message,
|
|
647
|
-
createdAt: status.createdAt,
|
|
648
|
-
completedAt: status.completedAt
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Check if job is still running
|
|
653
|
-
if (status.status === 'processing') {
|
|
654
|
-
console.log(`Job in progress: ${status.stage}`);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Check for errors
|
|
658
|
-
if (status.status === 'failed') {
|
|
659
|
-
console.error('Job failed:', {
|
|
660
|
-
error: status.error,
|
|
661
|
-
stack: status.errorStack
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
### Custom TTL Configuration
|
|
667
|
-
|
|
668
|
-
```typescript
|
|
669
|
-
// Default TTL: 7 days
|
|
670
|
-
const tracker = new JobTracker(kvAdapter, logger);
|
|
671
|
-
|
|
672
|
-
// Custom TTL: 24 hours
|
|
673
|
-
const shortTracker = new JobTracker(
|
|
674
|
-
kvAdapter,
|
|
675
|
-
logger,
|
|
676
|
-
86400 // 24 hours in seconds
|
|
677
|
-
);
|
|
678
|
-
|
|
679
|
-
// Custom TTL: 30 days
|
|
680
|
-
const longTracker = new JobTracker(
|
|
681
|
-
kvAdapter,
|
|
682
|
-
logger,
|
|
683
|
-
2592000 // 30 days in seconds
|
|
684
|
-
);
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
---
|
|
688
|
-
|
|
689
|
-
## Preflight Validator
|
|
690
|
-
|
|
691
|
-
### What It Does
|
|
692
|
-
|
|
693
|
-
**Validates data BEFORE sending to Fluent API** to:
|
|
694
|
-
- Catch errors early and save API quota
|
|
695
|
-
- Validate against GraphQL schema requirements
|
|
696
|
-
- Check for duplicates and data quality issues
|
|
697
|
-
- Provide actionable error messages
|
|
698
|
-
|
|
699
|
-
### Key Features
|
|
700
|
-
|
|
701
|
-
- ✅ **Required field validation** - Ensure mandatory fields present
|
|
702
|
-
- ✅ **Type validation** - Check field types (string, number, etc.)
|
|
703
|
-
- ✅ **Duplicate detection** - Find duplicate refs in batch
|
|
704
|
-
- ✅ **Batch size limits** - Enforce max batch size
|
|
705
|
-
- ✅ **Custom validators** - Add domain-specific rules
|
|
706
|
-
- ✅ **Detailed error reports** - Per-record error details
|
|
707
|
-
|
|
708
|
-
### Basic Usage
|
|
709
|
-
|
|
710
|
-
```typescript
|
|
711
|
-
import { PreflightValidator } from '@fluentcommerce/fc-connect-sdk';
|
|
712
|
-
|
|
713
|
-
const validator = new PreflightValidator(logger);
|
|
714
|
-
|
|
715
|
-
// Validate batch before sending
|
|
716
|
-
const result = await validator.validateBatch(records, {
|
|
717
|
-
entityType: 'INVENTORY',
|
|
718
|
-
requiredFields: ['ref', 'qty', 'productRef', 'locationRef'],
|
|
719
|
-
checkDuplicates: true,
|
|
720
|
-
maxBatchSize: 5000,
|
|
721
|
-
validateTypes: true
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
if (!result.isValid) {
|
|
725
|
-
console.error(`Validation failed: ${result.errors.length} errors`);
|
|
726
|
-
|
|
727
|
-
// Log first 10 errors
|
|
728
|
-
result.errors.slice(0, 10).forEach(err => {
|
|
729
|
-
console.error(`Record ${err.index}: ${err.message}`, {
|
|
730
|
-
field: err.field,
|
|
731
|
-
value: err.value
|
|
732
|
-
});
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
// Don't send to API - save quota
|
|
736
|
-
throw new Error(`Validation failed: ${result.summary}`);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
console.log(`✓ Validation passed: ${result.summary}`);
|
|
740
|
-
|
|
741
|
-
// Safe to send to API
|
|
742
|
-
await sendToFluentAPI(records);
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
### Integration with Batch API
|
|
746
|
-
|
|
747
|
-
```typescript
|
|
748
|
-
import {
|
|
749
|
-
createClient,
|
|
750
|
-
PreflightValidator
|
|
751
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
752
|
-
|
|
753
|
-
async function batchIngestionWithValidation(records: any[]) {
|
|
754
|
-
const validator = new PreflightValidator(logger);
|
|
755
|
-
const client = await createClient({ config });
|
|
756
|
-
|
|
757
|
-
// Step 1: Preflight validation
|
|
758
|
-
logger.info('Running preflight validation', { recordCount: records.length });
|
|
759
|
-
|
|
760
|
-
const validationResult = await validator.validateBatch(records, {
|
|
761
|
-
entityType: 'INVENTORY',
|
|
762
|
-
requiredFields: ['ref', 'qty', 'productRef', 'locationRef'],
|
|
763
|
-
checkDuplicates: true,
|
|
764
|
-
maxBatchSize: 5000,
|
|
765
|
-
validateTypes: true
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
if (!validationResult.isValid) {
|
|
769
|
-
logger.error('Preflight validation failed', {
|
|
770
|
-
errorCount: validationResult.errors.length,
|
|
771
|
-
validRecords: validationResult.validRecords
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
// Export validation errors
|
|
775
|
-
await fs.writeFile(
|
|
776
|
-
'validation-errors.json',
|
|
777
|
-
JSON.stringify(validationResult.errors, null, 2)
|
|
778
|
-
);
|
|
779
|
-
|
|
780
|
-
throw new Error(`Validation failed: ${validationResult.summary}`);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
logger.info('Preflight validation passed', {
|
|
784
|
-
validRecords: validationResult.validRecords,
|
|
785
|
-
warnings: validationResult.warnings.length
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
// Step 2: Send to API (data is valid)
|
|
789
|
-
const job = await client.createJob({
|
|
790
|
-
name: 'Validated Inventory Ingestion',
|
|
791
|
-
retailerId: 'my-retailer'
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
await client.sendBatch(job.id, {
|
|
795
|
-
action: 'UPSERT',
|
|
796
|
-
entityType: 'INVENTORY',
|
|
797
|
-
entities: records
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
return { jobId: job.id, validationResult };
|
|
801
|
-
}
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
### Custom Validation Rules
|
|
805
|
-
|
|
806
|
-
```typescript
|
|
807
|
-
const result = await validator.validateBatch(records, {
|
|
808
|
-
entityType: 'INVENTORY',
|
|
809
|
-
requiredFields: ['ref', 'qty'],
|
|
810
|
-
validateTypes: true,
|
|
811
|
-
// Custom validation function
|
|
812
|
-
customValidator: (record, index) => {
|
|
813
|
-
// Business rule: qty must be >= 0
|
|
814
|
-
if (record.qty < 0) {
|
|
815
|
-
return `Quantity cannot be negative (got ${record.qty})`;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Business rule: ref must follow pattern
|
|
819
|
-
if (!/^[A-Z0-9-]+$/.test(record.ref)) {
|
|
820
|
-
return `Invalid ref format (must be alphanumeric with hyphens)`;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Business rule: locationRef must be valid
|
|
824
|
-
if (!isValidLocation(record.locationRef)) {
|
|
825
|
-
return `Invalid locationRef: ${record.locationRef}`;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
return null; // Validation passed
|
|
829
|
-
}
|
|
830
|
-
});
|
|
831
|
-
```
|
|
832
|
-
|
|
833
|
-
### Quick Validation (Fast Mode)
|
|
834
|
-
|
|
835
|
-
```typescript
|
|
836
|
-
// Quick validation - checks first record only
|
|
837
|
-
const quickResult = await validator.validateQuick(records, {
|
|
838
|
-
entityType: 'INVENTORY',
|
|
839
|
-
requiredFields: ['ref', 'qty'],
|
|
840
|
-
maxBatchSize: 5000
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
if (!quickResult.isValid) {
|
|
844
|
-
console.error('Quick validation failed:', quickResult.error);
|
|
845
|
-
throw new Error(`Data format invalid: ${quickResult.error}`);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// Optionally do full validation
|
|
849
|
-
const fullResult = await validator.validateBatch(records, options);
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
### Duplicate Detection
|
|
853
|
-
|
|
854
|
-
```typescript
|
|
855
|
-
const result = await validator.validateBatch(records, {
|
|
856
|
-
entityType: 'INVENTORY',
|
|
857
|
-
requiredFields: ['ref'],
|
|
858
|
-
checkDuplicates: true // Enable duplicate detection
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
if (result.warnings.length > 0) {
|
|
862
|
-
console.warn('Validation warnings:', result.warnings);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
if (result.duplicates && result.duplicates.length > 0) {
|
|
866
|
-
console.warn('Duplicate refs found:', result.duplicates);
|
|
867
|
-
|
|
868
|
-
// Option 1: Remove duplicates
|
|
869
|
-
const uniqueRecords = records.filter((r, i, arr) =>
|
|
870
|
-
arr.findIndex(x => x.ref === r.ref) === i
|
|
871
|
-
);
|
|
872
|
-
|
|
873
|
-
// Option 2: Fail on duplicates
|
|
874
|
-
if (result.duplicates.length > 10) {
|
|
875
|
-
throw new Error(`Too many duplicates: ${result.duplicates.length}`);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
### Error Reporting
|
|
881
|
-
|
|
882
|
-
```typescript
|
|
883
|
-
const result = await validator.validateBatch(records, options);
|
|
884
|
-
|
|
885
|
-
if (!result.isValid) {
|
|
886
|
-
// Generate detailed error report
|
|
887
|
-
const report = {
|
|
888
|
-
summary: result.summary,
|
|
889
|
-
totalRecords: result.totalRecords,
|
|
890
|
-
validRecords: result.validRecords,
|
|
891
|
-
errorCount: result.errors.length,
|
|
892
|
-
errors: result.errors.map(err => ({
|
|
893
|
-
record: err.index,
|
|
894
|
-
field: err.field,
|
|
895
|
-
message: err.message,
|
|
896
|
-
value: err.value,
|
|
897
|
-
severity: err.severity
|
|
898
|
-
}))
|
|
899
|
-
};
|
|
900
|
-
|
|
901
|
-
// Export to file
|
|
902
|
-
await fs.writeFile(
|
|
903
|
-
`validation-report-${Date.now()}.json`,
|
|
904
|
-
JSON.stringify(report, null, 2)
|
|
905
|
-
);
|
|
906
|
-
|
|
907
|
-
// Send to monitoring
|
|
908
|
-
logger.error('Validation failed', report);
|
|
909
|
-
}
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
---
|
|
913
|
-
|
|
914
|
-
## Integration Patterns
|
|
915
|
-
|
|
916
|
-
### Pattern 1: Secure Webhook Endpoint
|
|
917
|
-
|
|
918
|
-
**Webhook validation + Job tracking + Error handling**
|
|
919
|
-
|
|
920
|
-
```typescript
|
|
921
|
-
import {
|
|
922
|
-
webhook,
|
|
923
|
-
fn
|
|
924
|
-
} from '@versori/run';
|
|
925
|
-
import {
|
|
926
|
-
FluentClient,
|
|
927
|
-
JobTracker,
|
|
928
|
-
VersoriKVAdapter
|
|
929
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
930
|
-
|
|
931
|
-
export const secureWebhook = webhook('secure-order', {
|
|
932
|
-
response: {
|
|
933
|
-
mode: 'sync',
|
|
934
|
-
onSuccess: (ctx) => new Response(JSON.stringify({ success: true }), {
|
|
935
|
-
status: 200,
|
|
936
|
-
headers: { 'Content-Type': 'application/json' }
|
|
937
|
-
}),
|
|
938
|
-
onError: (ctx) => new Response(JSON.stringify({
|
|
939
|
-
error: ctx.error.message
|
|
940
|
-
}), {
|
|
941
|
-
status: ctx.error.message.includes('signature') ? 401 : 500,
|
|
942
|
-
headers: { 'Content-Type': 'application/json' }
|
|
943
|
-
})
|
|
944
|
-
}
|
|
945
|
-
})
|
|
946
|
-
.then(fn('validate-signature', async ({ request, vars, log, kv }) => {
|
|
947
|
-
const jobId = request.headers.get('x-request-id') || crypto.randomUUID();
|
|
948
|
-
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
949
|
-
|
|
950
|
-
// Track job start
|
|
951
|
-
await tracker.createJob(jobId, {
|
|
952
|
-
triggeredBy: 'webhook',
|
|
953
|
-
stage: 'validation'
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
// Get raw body
|
|
957
|
-
const rawBody = await request.text();
|
|
958
|
-
const payload = JSON.parse(rawBody);
|
|
959
|
-
const signature = request.headers.get('fluent-signature');
|
|
960
|
-
|
|
961
|
-
if (!signature) {
|
|
962
|
-
await tracker.markFailed(jobId, new Error('Missing signature'));
|
|
963
|
-
throw new Error('Missing fluent-signature header');
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// Validate signature
|
|
967
|
-
const client = new FluentClient({
|
|
968
|
-
baseUrl: vars.FLUENT_BASE_URL,
|
|
969
|
-
clientId: vars.FLUENT_CLIENT_ID,
|
|
970
|
-
clientSecret: vars.FLUENT_CLIENT_SECRET,
|
|
971
|
-
username: vars.FLUENT_USERNAME,
|
|
972
|
-
password: vars.FLUENT_PASSWORD,
|
|
973
|
-
retailerId: vars.FLUENT_RETAILER_ID,
|
|
974
|
-
publicKey: vars.FLUENT_WEBHOOK_PUBLIC_KEY
|
|
975
|
-
}, log);
|
|
976
|
-
|
|
977
|
-
const isValid = await client.validateWebhook(payload, signature, rawBody);
|
|
978
|
-
|
|
979
|
-
if (!isValid) {
|
|
980
|
-
await tracker.markFailed(jobId, new Error('Invalid signature'));
|
|
981
|
-
throw new Error('Invalid webhook signature');
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
await tracker.updateJob(jobId, {
|
|
985
|
-
status: 'processing',
|
|
986
|
-
stage: 'processing',
|
|
987
|
-
message: 'Signature validated'
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
return { jobId, payload };
|
|
991
|
-
}))
|
|
992
|
-
.then(fn('process-order', async ({ data, kv, log }) => {
|
|
993
|
-
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
994
|
-
|
|
995
|
-
await tracker.updateJob(data.jobId, {
|
|
996
|
-
stage: 'transformation',
|
|
997
|
-
message: `Processing order ${data.payload.entityRef}`
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
// Process order...
|
|
1001
|
-
const result = await processOrder(data.payload);
|
|
1002
|
-
|
|
1003
|
-
await tracker.markCompleted(data.jobId, {
|
|
1004
|
-
orderId: result.orderId
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
return { success: true, orderId: result.orderId };
|
|
1008
|
-
}));
|
|
1009
|
-
```
|
|
1010
|
-
|
|
1011
|
-
### Pattern 2: Validated Batch Ingestion with Recovery
|
|
1012
|
-
|
|
1013
|
-
**Preflight validation + Partial batch recovery + Job tracking**
|
|
1014
|
-
|
|
1015
|
-
```typescript
|
|
1016
|
-
import {
|
|
1017
|
-
createClient,
|
|
1018
|
-
PreflightValidator,
|
|
1019
|
-
PartialBatchRecovery,
|
|
1020
|
-
JobTracker,
|
|
1021
|
-
VersoriKVAdapter
|
|
1022
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
1023
|
-
|
|
1024
|
-
async function validatedBatchIngestion(
|
|
1025
|
-
records: any[],
|
|
1026
|
-
config: any,
|
|
1027
|
-
logger: any,
|
|
1028
|
-
kv: any
|
|
1029
|
-
) {
|
|
1030
|
-
const client = await createClient({ config });
|
|
1031
|
-
const validator = new PreflightValidator(logger);
|
|
1032
|
-
const recovery = new PartialBatchRecovery(logger);
|
|
1033
|
-
const tracker = new JobTracker(new VersoriKVAdapter(kv), logger);
|
|
1034
|
-
|
|
1035
|
-
const jobId = `batch_${Date.now()}`;
|
|
1036
|
-
|
|
1037
|
-
try {
|
|
1038
|
-
// Track job
|
|
1039
|
-
await tracker.createJob(jobId, {
|
|
1040
|
-
triggeredBy: 'schedule',
|
|
1041
|
-
stage: 'validation',
|
|
1042
|
-
details: { recordCount: records.length }
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
// Step 1: Preflight validation
|
|
1046
|
-
await tracker.updateJob(jobId, {
|
|
1047
|
-
status: 'processing',
|
|
1048
|
-
stage: 'preflight-validation',
|
|
1049
|
-
message: `Validating ${records.length} records`
|
|
1050
|
-
});
|
|
1051
|
-
|
|
1052
|
-
const validationResult = await validator.validateBatch(records, {
|
|
1053
|
-
entityType: 'INVENTORY',
|
|
1054
|
-
requiredFields: ['ref', 'qty', 'productRef', 'locationRef'],
|
|
1055
|
-
checkDuplicates: true,
|
|
1056
|
-
maxBatchSize: 5000,
|
|
1057
|
-
validateTypes: true,
|
|
1058
|
-
customValidator: (record) => {
|
|
1059
|
-
if (record.qty < 0) return 'Quantity cannot be negative';
|
|
1060
|
-
return null;
|
|
1061
|
-
}
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
if (!validationResult.isValid) {
|
|
1065
|
-
await tracker.markFailed(jobId, new Error(validationResult.summary));
|
|
1066
|
-
throw new Error(`Validation failed: ${validationResult.summary}`);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// Step 2: Create Fluent job
|
|
1070
|
-
await tracker.updateJob(jobId, {
|
|
1071
|
-
stage: 'batch-creation',
|
|
1072
|
-
message: 'Creating Fluent batch job'
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
const fluentJob = await client.createJob({
|
|
1076
|
-
name: `Validated Ingestion ${jobId}`,
|
|
1077
|
-
retailerId: config.retailerId
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
// Step 3: Send with recovery
|
|
1081
|
-
await tracker.updateJob(jobId, {
|
|
1082
|
-
stage: 'batch-processing',
|
|
1083
|
-
message: `Sending ${records.length} records with recovery`
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
const recoveryResult = await recovery.processBatchWithRecovery(
|
|
1087
|
-
records,
|
|
1088
|
-
async (batch) => {
|
|
1089
|
-
return await client.sendBatch(fluentJob.id, {
|
|
1090
|
-
action: 'UPSERT',
|
|
1091
|
-
entityType: 'INVENTORY',
|
|
1092
|
-
entities: batch
|
|
1093
|
-
});
|
|
1094
|
-
},
|
|
1095
|
-
{
|
|
1096
|
-
maxRetries: 3,
|
|
1097
|
-
retryOnlyFailed: true,
|
|
1098
|
-
checkpointKey: jobId,
|
|
1099
|
-
retryBatchSize: 100
|
|
1100
|
-
}
|
|
1101
|
-
);
|
|
1102
|
-
|
|
1103
|
-
// Step 4: Complete
|
|
1104
|
-
await tracker.markCompleted(jobId, {
|
|
1105
|
-
fluentJobId: fluentJob.id,
|
|
1106
|
-
totalRecords: recoveryResult.totalRecords,
|
|
1107
|
-
successCount: recoveryResult.successCount,
|
|
1108
|
-
failedCount: recoveryResult.failedCount,
|
|
1109
|
-
checkpointId: recoveryResult.checkpointId
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
return {
|
|
1113
|
-
jobId,
|
|
1114
|
-
fluentJobId: fluentJob.id,
|
|
1115
|
-
validation: validationResult,
|
|
1116
|
-
recovery: recoveryResult
|
|
1117
|
-
};
|
|
1118
|
-
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
await tracker.markFailed(jobId, error);
|
|
1121
|
-
throw error;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
```
|
|
1125
|
-
|
|
1126
|
-
### Pattern 3: Scheduled Extraction with Tracking
|
|
1127
|
-
|
|
1128
|
-
**Job tracking + Error handling**
|
|
1129
|
-
|
|
1130
|
-
```typescript
|
|
1131
|
-
import { schedule } from '@versori/run';
|
|
1132
|
-
import {
|
|
1133
|
-
createClient,
|
|
1134
|
-
ExtractionOrchestrator,
|
|
1135
|
-
JobTracker,
|
|
1136
|
-
VersoriKVAdapter,
|
|
1137
|
-
SftpDataSource
|
|
1138
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
1139
|
-
|
|
1140
|
-
export const dailyExtraction = schedule('daily-extraction', '0 3 * * *')
|
|
1141
|
-
.execute(async ({ log, connections, vars, kv }) => {
|
|
1142
|
-
const jobId = `extraction_${Date.now()}`;
|
|
1143
|
-
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
1144
|
-
|
|
1145
|
-
try {
|
|
1146
|
-
await tracker.createJob(jobId, {
|
|
1147
|
-
triggeredBy: 'schedule',
|
|
1148
|
-
stage: 'initialization',
|
|
1149
|
-
details: { schedule: 'daily 3am' }
|
|
1150
|
-
});
|
|
1151
|
-
|
|
1152
|
-
// Stage 1: Extraction
|
|
1153
|
-
await tracker.updateJob(jobId, {
|
|
1154
|
-
status: 'processing',
|
|
1155
|
-
stage: 'extraction',
|
|
1156
|
-
message: 'Querying Fluent API'
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
const client = await createClient(ctx); // Auto-detects Versori context
|
|
1160
|
-
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
1161
|
-
|
|
1162
|
-
const data = await orchestrator.extract({
|
|
1163
|
-
entityType: 'virtualPositions',
|
|
1164
|
-
fields: ['ref', 'type', 'quantity', 'productRef', 'groupRef'],
|
|
1165
|
-
pagination: { pageSize: 1000 }
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
// Stage 2: Transformation
|
|
1169
|
-
await tracker.updateJob(jobId, {
|
|
1170
|
-
stage: 'transformation',
|
|
1171
|
-
message: `Transforming ${data.length} records`
|
|
1172
|
-
});
|
|
1173
|
-
|
|
1174
|
-
const xml = transformToXML(data);
|
|
1175
|
-
|
|
1176
|
-
// Stage 3: Upload
|
|
1177
|
-
await tracker.updateJob(jobId, {
|
|
1178
|
-
stage: 'upload',
|
|
1179
|
-
message: 'Uploading to SFTP'
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
const sftp = new SftpDataSource({
|
|
1183
|
-
host: vars.SFTP_HOST,
|
|
1184
|
-
username: vars.SFTP_USERNAME,
|
|
1185
|
-
privateKey: vars.SFTP_PRIVATE_KEY
|
|
1186
|
-
}, log);
|
|
1187
|
-
|
|
1188
|
-
const fileName = `inventory_${jobId}.xml`;
|
|
1189
|
-
await sftp.writeFile(`/uploads/${fileName}`, xml);
|
|
1190
|
-
|
|
1191
|
-
// Complete
|
|
1192
|
-
await tracker.markCompleted(jobId, {
|
|
1193
|
-
recordCount: data.length,
|
|
1194
|
-
fileName,
|
|
1195
|
-
fileSize: xml.length
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
log.info('Extraction completed', { jobId, recordCount: data.length });
|
|
1199
|
-
|
|
1200
|
-
} catch (error) {
|
|
1201
|
-
await tracker.markFailed(jobId, error);
|
|
1202
|
-
log.error('Extraction failed', error);
|
|
1203
|
-
throw error;
|
|
1204
|
-
}
|
|
1205
|
-
});
|
|
1206
|
-
```
|
|
1207
|
-
|
|
1208
|
-
---
|
|
1209
|
-
|
|
1210
|
-
## Production Examples
|
|
1211
|
-
|
|
1212
|
-
### Complete Production Workflow
|
|
1213
|
-
|
|
1214
|
-
```typescript
|
|
1215
|
-
/**
|
|
1216
|
-
* Production-ready inventory ingestion with all advanced services
|
|
1217
|
-
*/
|
|
1218
|
-
|
|
1219
|
-
import { schedule } from '@versori/run';
|
|
1220
|
-
import {
|
|
1221
|
-
createClient,
|
|
1222
|
-
PreflightValidator,
|
|
1223
|
-
PartialBatchRecovery,
|
|
1224
|
-
JobTracker,
|
|
1225
|
-
VersoriKVAdapter,
|
|
1226
|
-
S3DataSource,
|
|
1227
|
-
CSVParserService,
|
|
1228
|
-
UniversalMapper
|
|
1229
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
1230
|
-
|
|
1231
|
-
export const productionInventorySync = schedule('inventory-sync', '0 */6 * * *')
|
|
1232
|
-
.execute(async ({ log, connections, vars, kv }) => {
|
|
1233
|
-
const jobId = `inventory_${Date.now()}`;
|
|
1234
|
-
const tracker = new JobTracker(new VersoriKVAdapter(kv), log, 2592000); // 30 day TTL
|
|
1235
|
-
const validator = new PreflightValidator(log);
|
|
1236
|
-
const recovery = new PartialBatchRecovery(log);
|
|
1237
|
-
|
|
1238
|
-
try {
|
|
1239
|
-
// ========================================
|
|
1240
|
-
// Stage 1: Job Initialization
|
|
1241
|
-
// ========================================
|
|
1242
|
-
await tracker.createJob(jobId, {
|
|
1243
|
-
triggeredBy: 'schedule',
|
|
1244
|
-
stage: 'initialization',
|
|
1245
|
-
details: {
|
|
1246
|
-
schedule: 'every 6 hours',
|
|
1247
|
-
bucket: vars.S3_BUCKET
|
|
1248
|
-
}
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
log.info('Starting production inventory sync', { jobId });
|
|
1252
|
-
|
|
1253
|
-
// ========================================
|
|
1254
|
-
// Stage 2: Data Extraction
|
|
1255
|
-
// ========================================
|
|
1256
|
-
await tracker.updateJob(jobId, {
|
|
1257
|
-
status: 'processing',
|
|
1258
|
-
stage: 'extraction',
|
|
1259
|
-
message: 'Reading CSV from S3'
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
const s3 = new S3DataSource({
|
|
1263
|
-
region: vars.AWS_REGION,
|
|
1264
|
-
accessKeyId: vars.AWS_ACCESS_KEY_ID,
|
|
1265
|
-
secretAccessKey: vars.AWS_SECRET_ACCESS_KEY
|
|
1266
|
-
}, log);
|
|
1267
|
-
|
|
1268
|
-
const csvContent = await s3.readFile(vars.S3_BUCKET, 'inventory/latest.csv');
|
|
1269
|
-
|
|
1270
|
-
const parser = new CSVParserService({ hasHeaders: true }, log);
|
|
1271
|
-
const rawRecords = await parser.parseToArray(csvContent);
|
|
1272
|
-
|
|
1273
|
-
log.info('CSV parsed', { recordCount: rawRecords.length });
|
|
1274
|
-
|
|
1275
|
-
// ========================================
|
|
1276
|
-
// Stage 3: Data Transformation
|
|
1277
|
-
// ========================================
|
|
1278
|
-
await tracker.updateJob(jobId, {
|
|
1279
|
-
stage: 'transformation',
|
|
1280
|
-
message: `Mapping ${rawRecords.length} records`
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
const mapper = new UniversalMapper({
|
|
1284
|
-
fields: {
|
|
1285
|
-
ref: { source: 'sku', required: true },
|
|
1286
|
-
type: { resolver: () => 'INVENTORY_POSITION' },
|
|
1287
|
-
qty: { source: 'quantity', resolver: 'sdk.parseInt', required: true },
|
|
1288
|
-
productRef: { source: 'product_id', required: true },
|
|
1289
|
-
locationRef: { source: 'location_id', required: true },
|
|
1290
|
-
condition: { source: 'condition', defaultValue: 'NEW' }
|
|
1291
|
-
}
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
const mappedRecords = [];
|
|
1295
|
-
const mappingErrors = [];
|
|
1296
|
-
|
|
1297
|
-
for (let i = 0; i < rawRecords.length; i++) {
|
|
1298
|
-
const result = await mapper.map(rawRecords[i]);
|
|
1299
|
-
if (result.success) {
|
|
1300
|
-
mappedRecords.push(result.data);
|
|
1301
|
-
} else {
|
|
1302
|
-
mappingErrors.push({ index: i, errors: result.errors });
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
if (mappingErrors.length > 0) {
|
|
1307
|
-
log.warn('Mapping errors', {
|
|
1308
|
-
errorCount: mappingErrors.length,
|
|
1309
|
-
totalRecords: rawRecords.length
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// ========================================
|
|
1314
|
-
// Stage 4: Preflight Validation
|
|
1315
|
-
// ========================================
|
|
1316
|
-
await tracker.updateJob(jobId, {
|
|
1317
|
-
stage: 'validation',
|
|
1318
|
-
message: `Validating ${mappedRecords.length} records`
|
|
1319
|
-
});
|
|
1320
|
-
|
|
1321
|
-
const validationResult = await validator.validateBatch(mappedRecords, {
|
|
1322
|
-
entityType: 'INVENTORY',
|
|
1323
|
-
requiredFields: ['ref', 'qty', 'productRef', 'locationRef'],
|
|
1324
|
-
checkDuplicates: true,
|
|
1325
|
-
maxBatchSize: 5000,
|
|
1326
|
-
validateTypes: true,
|
|
1327
|
-
customValidator: (record) => {
|
|
1328
|
-
if (record.qty < 0) {
|
|
1329
|
-
return 'Quantity cannot be negative';
|
|
1330
|
-
}
|
|
1331
|
-
if (!record.ref.match(/^[A-Z0-9-]+$/)) {
|
|
1332
|
-
return 'Invalid ref format';
|
|
1333
|
-
}
|
|
1334
|
-
return null;
|
|
1335
|
-
}
|
|
1336
|
-
});
|
|
1337
|
-
|
|
1338
|
-
if (!validationResult.isValid) {
|
|
1339
|
-
await tracker.markFailed(jobId, new Error(validationResult.summary));
|
|
1340
|
-
|
|
1341
|
-
// Export validation errors
|
|
1342
|
-
await s3.writeFile(
|
|
1343
|
-
vars.S3_BUCKET,
|
|
1344
|
-
`errors/validation-${jobId}.json`,
|
|
1345
|
-
JSON.stringify(validationResult.errors, null, 2)
|
|
1346
|
-
);
|
|
1347
|
-
|
|
1348
|
-
throw new Error(`Validation failed: ${validationResult.summary}`);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
log.info('Validation passed', {
|
|
1352
|
-
validRecords: validationResult.validRecords,
|
|
1353
|
-
warnings: validationResult.warnings.length
|
|
1354
|
-
});
|
|
1355
|
-
|
|
1356
|
-
// ========================================
|
|
1357
|
-
// Stage 5: Fluent API Ingestion
|
|
1358
|
-
// ========================================
|
|
1359
|
-
await tracker.updateJob(jobId, {
|
|
1360
|
-
stage: 'ingestion',
|
|
1361
|
-
message: 'Creating Fluent batch job'
|
|
1362
|
-
});
|
|
1363
|
-
|
|
1364
|
-
const client = await createClient({
|
|
1365
|
-
connection: connections.fluent_commerce,
|
|
1366
|
-
logger: log
|
|
1367
|
-
});
|
|
1368
|
-
|
|
1369
|
-
const fluentJob = await client.createJob({
|
|
1370
|
-
name: `Production Inventory Sync ${jobId}`,
|
|
1371
|
-
retailerId: vars.FLUENT_RETAILER_ID,
|
|
1372
|
-
meta: {
|
|
1373
|
-
preprocessing: 'default', // Enable BPP
|
|
1374
|
-
source: 's3-csv',
|
|
1375
|
-
jobId
|
|
1376
|
-
}
|
|
1377
|
-
});
|
|
1378
|
-
|
|
1379
|
-
log.info('Fluent job created', { fluentJobId: fluentJob.id });
|
|
1380
|
-
|
|
1381
|
-
// ========================================
|
|
1382
|
-
// Stage 6: Batch Processing with Recovery
|
|
1383
|
-
// ========================================
|
|
1384
|
-
await tracker.updateJob(jobId, {
|
|
1385
|
-
stage: 'batch-processing',
|
|
1386
|
-
message: `Sending ${mappedRecords.length} records`
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
const recoveryResult = await recovery.processBatchWithRecovery(
|
|
1390
|
-
mappedRecords,
|
|
1391
|
-
async (batch) => {
|
|
1392
|
-
log.info('Sending batch', { batchSize: batch.length });
|
|
1393
|
-
|
|
1394
|
-
return await client.sendBatch(fluentJob.id, {
|
|
1395
|
-
action: 'UPSERT',
|
|
1396
|
-
entityType: 'INVENTORY',
|
|
1397
|
-
entities: batch
|
|
1398
|
-
});
|
|
1399
|
-
},
|
|
1400
|
-
{
|
|
1401
|
-
maxRetries: 5,
|
|
1402
|
-
retryOnlyFailed: true,
|
|
1403
|
-
retryDelayMs: 2000,
|
|
1404
|
-
retryBatchSize: 100,
|
|
1405
|
-
checkpointKey: jobId,
|
|
1406
|
-
shouldRetry: (error, attemptCount) => {
|
|
1407
|
-
// Don't retry validation errors
|
|
1408
|
-
if (error.message.includes('validation')) {
|
|
1409
|
-
return false;
|
|
1410
|
-
}
|
|
1411
|
-
// Max 5 attempts
|
|
1412
|
-
return attemptCount <= 5;
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
);
|
|
1416
|
-
|
|
1417
|
-
log.info('Batch processing complete', {
|
|
1418
|
-
successCount: recoveryResult.successCount,
|
|
1419
|
-
failedCount: recoveryResult.failedCount,
|
|
1420
|
-
checkpointId: recoveryResult.checkpointId
|
|
1421
|
-
});
|
|
1422
|
-
|
|
1423
|
-
// ========================================
|
|
1424
|
-
// Stage 7: Job Status Verification
|
|
1425
|
-
// ========================================
|
|
1426
|
-
await tracker.updateJob(jobId, {
|
|
1427
|
-
stage: 'verification',
|
|
1428
|
-
message: 'Verifying Fluent job status'
|
|
1429
|
-
});
|
|
1430
|
-
|
|
1431
|
-
// Poll for completion
|
|
1432
|
-
let attempts = 0;
|
|
1433
|
-
const maxAttempts = 30;
|
|
1434
|
-
let fluentStatus;
|
|
1435
|
-
|
|
1436
|
-
while (attempts < maxAttempts) {
|
|
1437
|
-
fluentStatus = await client.getJobStatus(fluentJob.id);
|
|
1438
|
-
|
|
1439
|
-
if (fluentStatus.status === 'COMPLETED' || fluentStatus.status === 'FAILED') {
|
|
1440
|
-
break;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10s
|
|
1444
|
-
attempts++;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
log.info('Fluent job status', {
|
|
1448
|
-
status: fluentStatus.status,
|
|
1449
|
-
errorSummary: fluentStatus.errorSummary
|
|
1450
|
-
});
|
|
1451
|
-
|
|
1452
|
-
// ========================================
|
|
1453
|
-
// Stage 8: Completion
|
|
1454
|
-
// ========================================
|
|
1455
|
-
const finalDetails = {
|
|
1456
|
-
fluentJobId: fluentJob.id,
|
|
1457
|
-
fluentJobStatus: fluentStatus.status,
|
|
1458
|
-
sourceRecords: rawRecords.length,
|
|
1459
|
-
mappedRecords: mappedRecords.length,
|
|
1460
|
-
mappingErrors: mappingErrors.length,
|
|
1461
|
-
validationResult: {
|
|
1462
|
-
validRecords: validationResult.validRecords,
|
|
1463
|
-
warnings: validationResult.warnings.length
|
|
1464
|
-
},
|
|
1465
|
-
recoveryResult: {
|
|
1466
|
-
successCount: recoveryResult.successCount,
|
|
1467
|
-
failedCount: recoveryResult.failedCount,
|
|
1468
|
-
checkpointId: recoveryResult.checkpointId
|
|
1469
|
-
},
|
|
1470
|
-
fluentErrors: fluentStatus.errorSummary?.totalErrors || 0
|
|
1471
|
-
};
|
|
1472
|
-
|
|
1473
|
-
if (recoveryResult.failedCount > 0 || (fluentStatus.errorSummary?.totalErrors || 0) > 0) {
|
|
1474
|
-
await tracker.updateJob(jobId, {
|
|
1475
|
-
status: 'completed',
|
|
1476
|
-
stage: 'completed-with-errors',
|
|
1477
|
-
message: `Completed with ${recoveryResult.failedCount} recovery errors, ${fluentStatus.errorSummary?.totalErrors || 0} Fluent errors`,
|
|
1478
|
-
details: finalDetails
|
|
1479
|
-
});
|
|
1480
|
-
} else {
|
|
1481
|
-
await tracker.markCompleted(jobId, finalDetails);
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
return {
|
|
1485
|
-
success: true,
|
|
1486
|
-
jobId,
|
|
1487
|
-
...finalDetails
|
|
1488
|
-
};
|
|
1489
|
-
|
|
1490
|
-
} catch (error) {
|
|
1491
|
-
await tracker.markFailed(jobId, error);
|
|
1492
|
-
log.error('Production sync failed', error);
|
|
1493
|
-
throw error;
|
|
1494
|
-
}
|
|
1495
|
-
});
|
|
1496
|
-
```
|
|
1497
|
-
|
|
1498
|
-
---
|
|
1499
|
-
|
|
1500
|
-
## Summary
|
|
1501
|
-
|
|
1502
|
-
### Key Takeaways
|
|
1503
|
-
|
|
1504
|
-
1. **Webhook Validation** - ALWAYS validate Fluent Commerce webhook signatures
|
|
1505
|
-
2. **Partial Batch Recovery** - Retry only failed records, not entire batch
|
|
1506
|
-
3. **Job Tracker** - Track async workflow progress in KV store
|
|
1507
|
-
4. **Preflight Validator** - Validate before API calls to save quota
|
|
1508
|
-
|
|
1509
|
-
### Service Comparison
|
|
1510
|
-
|
|
1511
|
-
| Service | Primary Benefit | When to Use | Complexity |
|
|
1512
|
-
|---------|----------------|-------------|------------|
|
|
1513
|
-
| **Webhook Validation** | Security | All webhook endpoints | Low |
|
|
1514
|
-
| **Partial Batch Recovery** | Reliability | Batch ingestion | Medium |
|
|
1515
|
-
| **Job Tracker** | Observability | Scheduled workflows | Low |
|
|
1516
|
-
| **Preflight Validator** | Cost savings | Large batch operations | Low |
|
|
1517
|
-
|
|
1518
|
-
### Integration Checklist
|
|
1519
|
-
|
|
1520
|
-
Production integration should include:
|
|
1521
|
-
|
|
1522
|
-
- [ ] **Webhook Validation** - Verify all incoming webhooks
|
|
1523
|
-
- [ ] **Preflight Validator** - Validate before API submission
|
|
1524
|
-
- [ ] **Partial Batch Recovery** - Handle batch failures gracefully
|
|
1525
|
-
- [ ] **Job Tracker** - Track workflow lifecycle
|
|
1526
|
-
- [ ] **Structured Logging** - Log all stages with context
|
|
1527
|
-
- [ ] **Error Handling** - Catch and log all errors
|
|
1528
|
-
- [ ] **Monitoring** - Track metrics and job status
|
|
1529
|
-
|
|
1530
|
-
---
|
|
1531
|
-
|
|
1532
|
-
## Next Steps
|
|
1533
|
-
|
|
1534
|
-
Continue your learning journey:
|
|
1535
|
-
|
|
1536
|
-
- [Quick Reference](../integration-patterns-quick-reference.md) - All patterns at a glance
|
|
1537
|
-
- [Use Cases](../../../01-TEMPLATES/readme.md) - Complete production examples
|
|
1538
|
-
- [Error Handling](./integration-patterns-05-error-handling.md) - Comprehensive error patterns
|
|
1539
|
-
|
|
1540
|
-
Or explore related guides:
|
|
1541
|
-
- [Auto-Pagination](../../../02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md)
|
|
1542
|
-
- [Universal Mapping](../../../02-CORE-GUIDES/mapping/mapping-readme.md)
|
|
1543
|
-
- [Batch API Guide](../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md)
|
|
1544
|
-
|
|
1545
|
-
---
|
|
1546
|
-
|
|
1547
|
-
[← Back to Index](../integration-patterns-readme.md) | [Previous: Error Handling](./integration-patterns-05-error-handling.md)
|
|
1
|
+
# Module 6: Advanced Integration Services
|
|
2
|
+
|
|
3
|
+
> **Learning Objective:** Master advanced SDK services for webhook validation, partial batch recovery, job tracking, and pre-flight validation.
|
|
4
|
+
>
|
|
5
|
+
> **Level:** Advanced
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Overview](#overview)
|
|
10
|
+
2. [Webhook Validation Service](#webhook-validation-service)
|
|
11
|
+
3. [Partial Batch Recovery](#partial-batch-recovery)
|
|
12
|
+
4. [Job Tracker](#job-tracker)
|
|
13
|
+
5. [Preflight Validator](#preflight-validator)
|
|
14
|
+
6. [Integration Patterns](#integration-patterns)
|
|
15
|
+
7. [Production Examples](#production-examples)
|
|
16
|
+
8. [Summary](#summary)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Overview
|
|
21
|
+
|
|
22
|
+
### What These Services Solve
|
|
23
|
+
|
|
24
|
+
| Service | Problem | Solution | Benefits |
|
|
25
|
+
|---------|---------|----------|----------|
|
|
26
|
+
| **Webhook Validation** | Unverified webhooks | Cryptographic signature validation | Security, authenticity |
|
|
27
|
+
| **Partial Batch Recovery** | Entire batch fails when one record fails | Retry only failed records | Efficiency, data integrity |
|
|
28
|
+
| **Job Tracker** | No visibility into job status | Track job lifecycle in KV store | Observability, debugging |
|
|
29
|
+
| **Preflight Validator** | API quota wasted on invalid data | Validate before API calls | Cost savings, faster feedback |
|
|
30
|
+
|
|
31
|
+
### When to Use
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Production Integration Checklist:
|
|
35
|
+
✅ Webhook Validation - ALL webhook endpoints (security)
|
|
36
|
+
✅ Partial Batch Recovery - Batch API ingestion (reliability)
|
|
37
|
+
✅ Job Tracker - Async scheduled workflows (observability)
|
|
38
|
+
✅ Preflight Validator - Large batch operations (cost optimization)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Webhook Validation Service
|
|
44
|
+
|
|
45
|
+
### What It Does
|
|
46
|
+
|
|
47
|
+
**Validates webhook signatures from Fluent Commerce Rubix workflows** using RSA-based cryptographic verification.
|
|
48
|
+
|
|
49
|
+
> ⚠️ **IMPORTANT:** This service is **ONLY for Fluent Commerce webhooks**. Do not use for Shopify, GitHub, Stripe, or other third-party webhooks. For those systems, implement manual HMAC validation (see Module 4: Webhook Patterns).
|
|
50
|
+
|
|
51
|
+
### Key Features
|
|
52
|
+
|
|
53
|
+
- ✅ **SHA512withRSA** (fluent-signature) - Recommended for Fluent Commerce webhooks
|
|
54
|
+
- ✅ **MD5withRSA** (flex.signature) - Support for older Fluent Commerce webhooks
|
|
55
|
+
- ✅ Automatic algorithm detection from Fluent Commerce headers
|
|
56
|
+
- ✅ Public key caching for performance
|
|
57
|
+
- ✅ Fallback algorithm support for Fluent Commerce
|
|
58
|
+
- ✅ Integrated with FluentClient.validateWebhook()
|
|
59
|
+
- ❌ **NOT for Shopify/GitHub/Stripe webhooks** (use manual HMAC - see Module 4)
|
|
60
|
+
|
|
61
|
+
### Basic Usage
|
|
62
|
+
|
|
63
|
+
#### Using FluentClient (Recommended)
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { createClient } from '@fluentcommerce/fc-connect-sdk';
|
|
67
|
+
|
|
68
|
+
const client = await createClient({
|
|
69
|
+
baseUrl: 'https://api.fluentcommerce.com',
|
|
70
|
+
clientId: process.env.FLUENT_CLIENT_ID,
|
|
71
|
+
clientSecret: process.env.FLUENT_CLIENT_SECRET,
|
|
72
|
+
username: process.env.FLUENT_USERNAME,
|
|
73
|
+
password: process.env.FLUENT_PASSWORD,
|
|
74
|
+
retailerId: process.env.FLUENT_RETAILER_ID,
|
|
75
|
+
publicKey: process.env.FLUENT_WEBHOOK_PUBLIC_KEY // For webhook validation
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// In webhook handler
|
|
79
|
+
export async function handleWebhook(request: Request) {
|
|
80
|
+
const rawBody = await request.text(); // CRITICAL: Must be raw body
|
|
81
|
+
const payload = JSON.parse(rawBody);
|
|
82
|
+
const signature = request.headers.get('fluent-signature');
|
|
83
|
+
|
|
84
|
+
// Validate webhook signature
|
|
85
|
+
const isValid = await client.validateWebhook(
|
|
86
|
+
payload,
|
|
87
|
+
signature,
|
|
88
|
+
rawBody // Pass raw string, not parsed JSON
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!isValid) {
|
|
92
|
+
return new Response('Invalid signature', { status: 401 });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Process webhook...
|
|
96
|
+
return new Response('OK', { status: 200 });
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Using WebhookValidationService Directly
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import {
|
|
104
|
+
WebhookValidationService,
|
|
105
|
+
SignatureAlgorithm,
|
|
106
|
+
createConsoleLogger
|
|
107
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
108
|
+
|
|
109
|
+
const logger = createConsoleLogger();
|
|
110
|
+
const validator = new WebhookValidationService({
|
|
111
|
+
algorithm: SignatureAlgorithm.SHA512_WITH_RSA,
|
|
112
|
+
strictValidation: true // Throw on validation failure
|
|
113
|
+
}, logger);
|
|
114
|
+
|
|
115
|
+
// Validate webhook
|
|
116
|
+
const result = await validator.validateWebhookSignature(
|
|
117
|
+
rawPayload, // Raw request body as string
|
|
118
|
+
headers['fluent-signature'], // Signature from header
|
|
119
|
+
publicKey, // Public key from env
|
|
120
|
+
SignatureAlgorithm.SHA512_WITH_RSA
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (!result.isValid) {
|
|
124
|
+
console.error('Validation failed:', result.error);
|
|
125
|
+
throw new Error('Invalid webhook signature');
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Versori Platform Integration
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { webhook, fn } from '@versori/run';
|
|
133
|
+
import { FluentClient } from '@fluentcommerce/fc-connect-sdk';
|
|
134
|
+
|
|
135
|
+
export const fluentWebhook = webhook('fluent-order', {
|
|
136
|
+
response: {
|
|
137
|
+
mode: 'sync',
|
|
138
|
+
onSuccess: (ctx) => new Response(JSON.stringify({ success: true }), {
|
|
139
|
+
status: 200,
|
|
140
|
+
headers: { 'Content-Type': 'application/json' }
|
|
141
|
+
}),
|
|
142
|
+
onError: (ctx) => new Response(JSON.stringify({ error: ctx.error.message }), {
|
|
143
|
+
status: 500,
|
|
144
|
+
headers: { 'Content-Type': 'application/json' }
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
.then(fn('validate', async (ctx) => {
|
|
149
|
+
const { request, vars, log } = ctx;
|
|
150
|
+
|
|
151
|
+
// Get raw body (Versori v0.2.29+)
|
|
152
|
+
const rawBody = await request.text();
|
|
153
|
+
const payload = JSON.parse(rawBody);
|
|
154
|
+
|
|
155
|
+
// Get signature from headers
|
|
156
|
+
const signature = request.headers.get('fluent-signature') ||
|
|
157
|
+
request.headers.get('x-fluent-signature');
|
|
158
|
+
|
|
159
|
+
if (!signature) {
|
|
160
|
+
throw new Error('Missing webhook signature header');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate using FluentClient
|
|
164
|
+
const client = new FluentClient({
|
|
165
|
+
baseUrl: vars.FLUENT_BASE_URL,
|
|
166
|
+
clientId: vars.FLUENT_CLIENT_ID,
|
|
167
|
+
clientSecret: vars.FLUENT_CLIENT_SECRET,
|
|
168
|
+
username: vars.FLUENT_USERNAME,
|
|
169
|
+
password: vars.FLUENT_PASSWORD,
|
|
170
|
+
retailerId: vars.FLUENT_RETAILER_ID,
|
|
171
|
+
publicKey: vars.FLUENT_WEBHOOK_PUBLIC_KEY
|
|
172
|
+
}, log);
|
|
173
|
+
|
|
174
|
+
const isValid = await client.validateWebhook(payload, signature, rawBody);
|
|
175
|
+
|
|
176
|
+
if (!isValid) {
|
|
177
|
+
throw new Error('Invalid webhook signature - not from Fluent Commerce');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
log.info('Webhook signature validated', { eventName: payload.name });
|
|
181
|
+
|
|
182
|
+
return payload;
|
|
183
|
+
}))
|
|
184
|
+
.then(fn('process', async ({ data, log }) => {
|
|
185
|
+
// Process validated webhook...
|
|
186
|
+
log.info('Processing webhook', { orderId: data.entityRef });
|
|
187
|
+
|
|
188
|
+
return { success: true, orderId: data.entityRef };
|
|
189
|
+
}));
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Signature Header Detection
|
|
193
|
+
|
|
194
|
+
The service automatically detects signature algorithm:
|
|
195
|
+
|
|
196
|
+
| Header | Algorithm | Status |
|
|
197
|
+
|--------|-----------|--------|
|
|
198
|
+
| `fluent-signature` | SHA512withRSA | **Recommended** |
|
|
199
|
+
| `x-fluent-signature` | SHA512withRSA | Supported |
|
|
200
|
+
| `flex.signature` | MD5withRSA | **Older algorithm** |
|
|
201
|
+
| `flex-signature` | MD5withRSA | Supported |
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Auto-detection (recommended)
|
|
205
|
+
const result = await validator.validateWebhook(
|
|
206
|
+
rawPayload,
|
|
207
|
+
headers, // Pass all headers
|
|
208
|
+
publicKey
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Manual algorithm selection
|
|
212
|
+
const result = await validator.validateWebhookSignature(
|
|
213
|
+
rawPayload,
|
|
214
|
+
headers['fluent-signature'],
|
|
215
|
+
publicKey,
|
|
216
|
+
SignatureAlgorithm.SHA512_WITH_RSA
|
|
217
|
+
);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Common Pitfalls
|
|
221
|
+
|
|
222
|
+
#### ❌ WRONG: Validating parsed JSON
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// ❌ This will FAIL - signature is for raw body
|
|
226
|
+
const payload = await request.json();
|
|
227
|
+
const isValid = await validator.validateWebhookSignature(
|
|
228
|
+
JSON.stringify(payload), // Serialization changes order/whitespace
|
|
229
|
+
signature,
|
|
230
|
+
publicKey
|
|
231
|
+
);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### ✅ CORRECT: Validate raw body
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// ✅ Correct - validate raw body
|
|
238
|
+
const rawBody = await request.text();
|
|
239
|
+
const isValid = await validator.validateWebhookSignature(
|
|
240
|
+
rawBody, // Exact bytes received
|
|
241
|
+
signature,
|
|
242
|
+
publicKey
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Then parse for processing
|
|
246
|
+
const payload = JSON.parse(rawBody);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Production Factory Methods
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { WebhookValidationFactory } from '@fluentcommerce/fc-connect-sdk';
|
|
253
|
+
|
|
254
|
+
// Production: strict validation, throws on failure
|
|
255
|
+
const validator = WebhookValidationFactory.createProduction(logger);
|
|
256
|
+
|
|
257
|
+
// Development: lenient validation, logs but doesn't throw
|
|
258
|
+
const validator = WebhookValidationFactory.createDevelopment(logger);
|
|
259
|
+
|
|
260
|
+
// With fallback: try SHA512, fallback to MD5
|
|
261
|
+
const validator = WebhookValidationFactory.createWithFallback(logger);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Partial Batch Recovery
|
|
267
|
+
|
|
268
|
+
### What It Does
|
|
269
|
+
|
|
270
|
+
**Tracks per-record success/failure** in batch operations and enables:
|
|
271
|
+
- Retrying only failed records instead of entire batch
|
|
272
|
+
- Checkpoint/resume functionality
|
|
273
|
+
- Detailed error reporting per record
|
|
274
|
+
- Progress tracking
|
|
275
|
+
|
|
276
|
+
### Key Features
|
|
277
|
+
|
|
278
|
+
- ✅ **Per-record tracking** - Know exactly which records failed
|
|
279
|
+
- ✅ **Selective retry** - Retry only failures, not successes
|
|
280
|
+
- ✅ **Checkpoint support** - Resume from failure point
|
|
281
|
+
- ✅ **Exponential backoff** - Configurable retry delays
|
|
282
|
+
- ✅ **Custom retry logic** - Override retry decisions
|
|
283
|
+
|
|
284
|
+
### Basic Usage
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { PartialBatchRecovery } from '@fluentcommerce/fc-connect-sdk';
|
|
288
|
+
|
|
289
|
+
const recovery = new PartialBatchRecovery(logger);
|
|
290
|
+
|
|
291
|
+
// Process batch with automatic recovery
|
|
292
|
+
const result = await recovery.processBatchWithRecovery(
|
|
293
|
+
records,
|
|
294
|
+
async (batch) => {
|
|
295
|
+
// Your batch processing logic
|
|
296
|
+
return await client.sendBatch(jobId, {
|
|
297
|
+
action: 'UPSERT',
|
|
298
|
+
entityType: 'INVENTORY',
|
|
299
|
+
entities: batch
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
maxRetries: 3,
|
|
304
|
+
retryOnlyFailed: true, // Only retry failed records
|
|
305
|
+
retryDelayMs: 1000, // Start with 1 second
|
|
306
|
+
retryBatchSize: 100, // Process 100 at a time
|
|
307
|
+
checkpointKey: 'inventory-sync-2025-01-24'
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
console.log(`✓ Success: ${result.successCount}/${result.totalRecords}`);
|
|
312
|
+
console.log(`✗ Failed: ${result.failedCount} records`);
|
|
313
|
+
|
|
314
|
+
if (result.failedCount > 0) {
|
|
315
|
+
console.error('Failed records:', result.failedRecords);
|
|
316
|
+
console.log(`Checkpoint saved: ${result.checkpointId}`);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Integration with Batch API
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import {
|
|
324
|
+
createClient,
|
|
325
|
+
PartialBatchRecovery
|
|
326
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
327
|
+
|
|
328
|
+
async function batchIngestionWithRecovery(records: any[]) {
|
|
329
|
+
const client = await createClient({ config });
|
|
330
|
+
const recovery = new PartialBatchRecovery(logger);
|
|
331
|
+
|
|
332
|
+
// Create job
|
|
333
|
+
const job = await client.createJob({
|
|
334
|
+
name: 'Inventory Ingestion with Recovery',
|
|
335
|
+
retailerId: 'my-retailer'
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Process with recovery
|
|
339
|
+
const result = await recovery.processBatchWithRecovery(
|
|
340
|
+
records,
|
|
341
|
+
async (batch) => {
|
|
342
|
+
const response = await client.sendBatch(job.id, {
|
|
343
|
+
action: 'UPSERT',
|
|
344
|
+
entityType: 'INVENTORY',
|
|
345
|
+
entities: batch
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
logger.info('Batch sent', {
|
|
349
|
+
batchId: response.id,
|
|
350
|
+
recordCount: batch.length
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return response;
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
maxRetries: 3,
|
|
357
|
+
retryOnlyFailed: true,
|
|
358
|
+
checkpointKey: `job-${job.id}`
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Check job status
|
|
363
|
+
const status = await client.getJobStatus(job.id);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
jobId: job.id,
|
|
367
|
+
jobStatus: status.status,
|
|
368
|
+
...result
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Checkpoint and Resume
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// Process batch and save checkpoint
|
|
377
|
+
const result = await recovery.processBatchWithRecovery(
|
|
378
|
+
records,
|
|
379
|
+
processBatch,
|
|
380
|
+
{
|
|
381
|
+
maxRetries: 3,
|
|
382
|
+
checkpointKey: 'daily-inventory-sync'
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (result.failedCount > 0) {
|
|
387
|
+
console.log(`Checkpoint created: ${result.checkpointId}`);
|
|
388
|
+
console.log(`Failed records saved for later retry`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Later: Resume from checkpoint
|
|
392
|
+
const checkpointId = result.checkpointId;
|
|
393
|
+
const resumeResult = await recovery.resumeFromCheckpoint(
|
|
394
|
+
checkpointId,
|
|
395
|
+
processBatch,
|
|
396
|
+
{
|
|
397
|
+
maxRetries: 5 // More retries on resume
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
console.log(`Resume: ${resumeResult.successCount} recovered`);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Custom Retry Logic
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
const result = await recovery.processBatchWithRecovery(
|
|
408
|
+
records,
|
|
409
|
+
processBatch,
|
|
410
|
+
{
|
|
411
|
+
maxRetries: 5,
|
|
412
|
+
retryDelayMs: 2000,
|
|
413
|
+
// Custom retry decision
|
|
414
|
+
shouldRetry: (error, attemptCount) => {
|
|
415
|
+
// Don't retry validation errors
|
|
416
|
+
if (error.message.includes('validation')) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Don't retry after 3 attempts for rate limits
|
|
421
|
+
if (error.message.includes('rate limit') && attemptCount > 3) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Retry all other errors
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Record Failure Details
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// Access detailed failure information
|
|
436
|
+
if (result.failedCount > 0) {
|
|
437
|
+
result.failedRecords.forEach(failure => {
|
|
438
|
+
console.error(`Record ${failure.index} failed:`, {
|
|
439
|
+
record: failure.record,
|
|
440
|
+
error: failure.error.message,
|
|
441
|
+
attempts: failure.attemptCount,
|
|
442
|
+
timestamp: failure.timestamp
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Export failures for manual review
|
|
447
|
+
await fs.writeFile(
|
|
448
|
+
'failed-records.json',
|
|
449
|
+
JSON.stringify(result.failedRecords, null, 2)
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Job Tracker
|
|
457
|
+
|
|
458
|
+
### What It Does
|
|
459
|
+
|
|
460
|
+
**Tracks job status** in Versori KV store for async workflows:
|
|
461
|
+
- Job lifecycle tracking (queued → processing → completed/failed)
|
|
462
|
+
- Stage-based progress tracking
|
|
463
|
+
- Error capture with stack traces
|
|
464
|
+
- Automatic timestamp management
|
|
465
|
+
- TTL-based cleanup
|
|
466
|
+
|
|
467
|
+
### Key Features
|
|
468
|
+
|
|
469
|
+
- ✅ **Lifecycle tracking** - queued, processing, completed, failed
|
|
470
|
+
- ✅ **Stage tracking** - Custom stages for your workflow
|
|
471
|
+
- ✅ **Metadata storage** - Store job-specific context
|
|
472
|
+
- ✅ **TTL support** - Automatic cleanup after 7 days
|
|
473
|
+
- ✅ **Error capture** - Store errors with stack traces
|
|
474
|
+
|
|
475
|
+
### Basic Usage
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
import { JobTracker, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
479
|
+
// ✅ CORRECT: Access openKv from Versori context
|
|
480
|
+
// import { openKv } from '@versori/run'; // ❌ WRONG - Not a direct export
|
|
481
|
+
|
|
482
|
+
// In Versori workflow handler:
|
|
483
|
+
const { openKv } = ctx;
|
|
484
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
485
|
+
const tracker = new JobTracker(kvAdapter, logger);
|
|
486
|
+
|
|
487
|
+
// Create job
|
|
488
|
+
const jobId = `scheduled_${Date.now()}`;
|
|
489
|
+
|
|
490
|
+
await tracker.createJob(jobId, {
|
|
491
|
+
triggeredBy: 'schedule',
|
|
492
|
+
stage: 'initialization',
|
|
493
|
+
details: {
|
|
494
|
+
catalogueRef: 'DEFAULT:1',
|
|
495
|
+
fileName: 'inventory.csv'
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Update progress
|
|
500
|
+
await tracker.updateJob(jobId, {
|
|
501
|
+
status: 'processing',
|
|
502
|
+
stage: 'extraction',
|
|
503
|
+
message: 'Extracting records from S3'
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
await tracker.updateJob(jobId, {
|
|
507
|
+
stage: 'transformation',
|
|
508
|
+
message: 'Mapping 1000 records',
|
|
509
|
+
details: { recordCount: 1000 }
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Mark as completed
|
|
513
|
+
await tracker.markCompleted(jobId, {
|
|
514
|
+
recordCount: 1000,
|
|
515
|
+
successCount: 998,
|
|
516
|
+
failedCount: 2
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Or mark as failed
|
|
520
|
+
try {
|
|
521
|
+
// ... job logic ...
|
|
522
|
+
} catch (error) {
|
|
523
|
+
await tracker.markFailed(jobId, error);
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Scheduled Workflow Integration
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
import { schedule } from '@versori/run';
|
|
531
|
+
import { JobTracker, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
532
|
+
|
|
533
|
+
export const dailyInventorySync = schedule('daily-inventory', '0 2 * * *')
|
|
534
|
+
.execute(async ({ log, connections, vars, kv }) => {
|
|
535
|
+
const jobId = `inventory_${Date.now()}`;
|
|
536
|
+
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
// Create job
|
|
540
|
+
await tracker.createJob(jobId, {
|
|
541
|
+
triggeredBy: 'schedule',
|
|
542
|
+
stage: 'start',
|
|
543
|
+
details: { schedule: 'daily 2am' }
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Stage 1: Extraction
|
|
547
|
+
await tracker.updateJob(jobId, {
|
|
548
|
+
status: 'processing',
|
|
549
|
+
stage: 'extraction',
|
|
550
|
+
message: 'Querying virtual positions'
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const data = await extractFromFluent();
|
|
554
|
+
|
|
555
|
+
// Stage 2: Transformation
|
|
556
|
+
await tracker.updateJob(jobId, {
|
|
557
|
+
stage: 'transformation',
|
|
558
|
+
message: `Processing ${data.length} records`
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const transformed = await transformData(data);
|
|
562
|
+
|
|
563
|
+
// Stage 3: Upload
|
|
564
|
+
await tracker.updateJob(jobId, {
|
|
565
|
+
stage: 'upload',
|
|
566
|
+
message: 'Uploading to SFTP'
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
await uploadToSFTP(transformed);
|
|
570
|
+
|
|
571
|
+
// Completed
|
|
572
|
+
await tracker.markCompleted(jobId, {
|
|
573
|
+
recordCount: data.length,
|
|
574
|
+
fileName: `inventory_${jobId}.xml`
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
log.info('Job completed successfully', { jobId });
|
|
578
|
+
|
|
579
|
+
} catch (error) {
|
|
580
|
+
await tracker.markFailed(jobId, error);
|
|
581
|
+
log.error('Job failed', error);
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Webhook Workflow Integration
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
import { webhook, fn } from '@versori/run';
|
|
591
|
+
import { JobTracker, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
592
|
+
|
|
593
|
+
export const orderWebhook = webhook('order-webhook')
|
|
594
|
+
.then(fn('track-start', async ({ request, kv, log }) => {
|
|
595
|
+
const jobId = request.headers.get('x-request-id') || crypto.randomUUID();
|
|
596
|
+
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
597
|
+
|
|
598
|
+
await tracker.createJob(jobId, {
|
|
599
|
+
triggeredBy: 'webhook',
|
|
600
|
+
stage: 'validation',
|
|
601
|
+
details: {
|
|
602
|
+
source: request.headers.get('user-agent')
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
return { jobId };
|
|
607
|
+
}))
|
|
608
|
+
.then(fn('process', async ({ data, kv, log }) => {
|
|
609
|
+
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
610
|
+
|
|
611
|
+
await tracker.updateJob(data.jobId, {
|
|
612
|
+
status: 'processing',
|
|
613
|
+
stage: 'transformation'
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Process order...
|
|
617
|
+
const result = await processOrder(data);
|
|
618
|
+
|
|
619
|
+
await tracker.markCompleted(data.jobId, {
|
|
620
|
+
orderId: result.orderId
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
return result;
|
|
624
|
+
}))
|
|
625
|
+
.catch(async ({ error, data, kv, log }) => {
|
|
626
|
+
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
627
|
+
|
|
628
|
+
if (data?.jobId) {
|
|
629
|
+
await tracker.markFailed(data.jobId, error);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
throw error;
|
|
633
|
+
});
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Querying Job Status
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
// Get job status
|
|
640
|
+
const status = await tracker.getJob(jobId);
|
|
641
|
+
|
|
642
|
+
if (status) {
|
|
643
|
+
console.log(`Job ${jobId}:`, {
|
|
644
|
+
status: status.status,
|
|
645
|
+
stage: status.stage,
|
|
646
|
+
message: status.message,
|
|
647
|
+
createdAt: status.createdAt,
|
|
648
|
+
completedAt: status.completedAt
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Check if job is still running
|
|
653
|
+
if (status.status === 'processing') {
|
|
654
|
+
console.log(`Job in progress: ${status.stage}`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Check for errors
|
|
658
|
+
if (status.status === 'failed') {
|
|
659
|
+
console.error('Job failed:', {
|
|
660
|
+
error: status.error,
|
|
661
|
+
stack: status.errorStack
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Custom TTL Configuration
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
// Default TTL: 7 days
|
|
670
|
+
const tracker = new JobTracker(kvAdapter, logger);
|
|
671
|
+
|
|
672
|
+
// Custom TTL: 24 hours
|
|
673
|
+
const shortTracker = new JobTracker(
|
|
674
|
+
kvAdapter,
|
|
675
|
+
logger,
|
|
676
|
+
86400 // 24 hours in seconds
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
// Custom TTL: 30 days
|
|
680
|
+
const longTracker = new JobTracker(
|
|
681
|
+
kvAdapter,
|
|
682
|
+
logger,
|
|
683
|
+
2592000 // 30 days in seconds
|
|
684
|
+
);
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## Preflight Validator
|
|
690
|
+
|
|
691
|
+
### What It Does
|
|
692
|
+
|
|
693
|
+
**Validates data BEFORE sending to Fluent API** to:
|
|
694
|
+
- Catch errors early and save API quota
|
|
695
|
+
- Validate against GraphQL schema requirements
|
|
696
|
+
- Check for duplicates and data quality issues
|
|
697
|
+
- Provide actionable error messages
|
|
698
|
+
|
|
699
|
+
### Key Features
|
|
700
|
+
|
|
701
|
+
- ✅ **Required field validation** - Ensure mandatory fields present
|
|
702
|
+
- ✅ **Type validation** - Check field types (string, number, etc.)
|
|
703
|
+
- ✅ **Duplicate detection** - Find duplicate refs in batch
|
|
704
|
+
- ✅ **Batch size limits** - Enforce max batch size
|
|
705
|
+
- ✅ **Custom validators** - Add domain-specific rules
|
|
706
|
+
- ✅ **Detailed error reports** - Per-record error details
|
|
707
|
+
|
|
708
|
+
### Basic Usage
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
import { PreflightValidator } from '@fluentcommerce/fc-connect-sdk';
|
|
712
|
+
|
|
713
|
+
const validator = new PreflightValidator(logger);
|
|
714
|
+
|
|
715
|
+
// Validate batch before sending
|
|
716
|
+
const result = await validator.validateBatch(records, {
|
|
717
|
+
entityType: 'INVENTORY',
|
|
718
|
+
requiredFields: ['ref', 'qty', 'productRef', 'locationRef'],
|
|
719
|
+
checkDuplicates: true,
|
|
720
|
+
maxBatchSize: 5000,
|
|
721
|
+
validateTypes: true
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
if (!result.isValid) {
|
|
725
|
+
console.error(`Validation failed: ${result.errors.length} errors`);
|
|
726
|
+
|
|
727
|
+
// Log first 10 errors
|
|
728
|
+
result.errors.slice(0, 10).forEach(err => {
|
|
729
|
+
console.error(`Record ${err.index}: ${err.message}`, {
|
|
730
|
+
field: err.field,
|
|
731
|
+
value: err.value
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Don't send to API - save quota
|
|
736
|
+
throw new Error(`Validation failed: ${result.summary}`);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
console.log(`✓ Validation passed: ${result.summary}`);
|
|
740
|
+
|
|
741
|
+
// Safe to send to API
|
|
742
|
+
await sendToFluentAPI(records);
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Integration with Batch API
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
import {
|
|
749
|
+
createClient,
|
|
750
|
+
PreflightValidator
|
|
751
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
752
|
+
|
|
753
|
+
async function batchIngestionWithValidation(records: any[]) {
|
|
754
|
+
const validator = new PreflightValidator(logger);
|
|
755
|
+
const client = await createClient({ config });
|
|
756
|
+
|
|
757
|
+
// Step 1: Preflight validation
|
|
758
|
+
logger.info('Running preflight validation', { recordCount: records.length });
|
|
759
|
+
|
|
760
|
+
const validationResult = await validator.validateBatch(records, {
|
|
761
|
+
entityType: 'INVENTORY',
|
|
762
|
+
requiredFields: ['ref', 'qty', 'productRef', 'locationRef'],
|
|
763
|
+
checkDuplicates: true,
|
|
764
|
+
maxBatchSize: 5000,
|
|
765
|
+
validateTypes: true
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
if (!validationResult.isValid) {
|
|
769
|
+
logger.error('Preflight validation failed', {
|
|
770
|
+
errorCount: validationResult.errors.length,
|
|
771
|
+
validRecords: validationResult.validRecords
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// Export validation errors
|
|
775
|
+
await fs.writeFile(
|
|
776
|
+
'validation-errors.json',
|
|
777
|
+
JSON.stringify(validationResult.errors, null, 2)
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
throw new Error(`Validation failed: ${validationResult.summary}`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
logger.info('Preflight validation passed', {
|
|
784
|
+
validRecords: validationResult.validRecords,
|
|
785
|
+
warnings: validationResult.warnings.length
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// Step 2: Send to API (data is valid)
|
|
789
|
+
const job = await client.createJob({
|
|
790
|
+
name: 'Validated Inventory Ingestion',
|
|
791
|
+
retailerId: 'my-retailer'
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
await client.sendBatch(job.id, {
|
|
795
|
+
action: 'UPSERT',
|
|
796
|
+
entityType: 'INVENTORY',
|
|
797
|
+
entities: records
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
return { jobId: job.id, validationResult };
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Custom Validation Rules
|
|
805
|
+
|
|
806
|
+
```typescript
|
|
807
|
+
const result = await validator.validateBatch(records, {
|
|
808
|
+
entityType: 'INVENTORY',
|
|
809
|
+
requiredFields: ['ref', 'qty'],
|
|
810
|
+
validateTypes: true,
|
|
811
|
+
// Custom validation function
|
|
812
|
+
customValidator: (record, index) => {
|
|
813
|
+
// Business rule: qty must be >= 0
|
|
814
|
+
if (record.qty < 0) {
|
|
815
|
+
return `Quantity cannot be negative (got ${record.qty})`;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Business rule: ref must follow pattern
|
|
819
|
+
if (!/^[A-Z0-9-]+$/.test(record.ref)) {
|
|
820
|
+
return `Invalid ref format (must be alphanumeric with hyphens)`;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Business rule: locationRef must be valid
|
|
824
|
+
if (!isValidLocation(record.locationRef)) {
|
|
825
|
+
return `Invalid locationRef: ${record.locationRef}`;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return null; // Validation passed
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Quick Validation (Fast Mode)
|
|
834
|
+
|
|
835
|
+
```typescript
|
|
836
|
+
// Quick validation - checks first record only
|
|
837
|
+
const quickResult = await validator.validateQuick(records, {
|
|
838
|
+
entityType: 'INVENTORY',
|
|
839
|
+
requiredFields: ['ref', 'qty'],
|
|
840
|
+
maxBatchSize: 5000
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
if (!quickResult.isValid) {
|
|
844
|
+
console.error('Quick validation failed:', quickResult.error);
|
|
845
|
+
throw new Error(`Data format invalid: ${quickResult.error}`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Optionally do full validation
|
|
849
|
+
const fullResult = await validator.validateBatch(records, options);
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### Duplicate Detection
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
const result = await validator.validateBatch(records, {
|
|
856
|
+
entityType: 'INVENTORY',
|
|
857
|
+
requiredFields: ['ref'],
|
|
858
|
+
checkDuplicates: true // Enable duplicate detection
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
if (result.warnings.length > 0) {
|
|
862
|
+
console.warn('Validation warnings:', result.warnings);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (result.duplicates && result.duplicates.length > 0) {
|
|
866
|
+
console.warn('Duplicate refs found:', result.duplicates);
|
|
867
|
+
|
|
868
|
+
// Option 1: Remove duplicates
|
|
869
|
+
const uniqueRecords = records.filter((r, i, arr) =>
|
|
870
|
+
arr.findIndex(x => x.ref === r.ref) === i
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
// Option 2: Fail on duplicates
|
|
874
|
+
if (result.duplicates.length > 10) {
|
|
875
|
+
throw new Error(`Too many duplicates: ${result.duplicates.length}`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### Error Reporting
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
const result = await validator.validateBatch(records, options);
|
|
884
|
+
|
|
885
|
+
if (!result.isValid) {
|
|
886
|
+
// Generate detailed error report
|
|
887
|
+
const report = {
|
|
888
|
+
summary: result.summary,
|
|
889
|
+
totalRecords: result.totalRecords,
|
|
890
|
+
validRecords: result.validRecords,
|
|
891
|
+
errorCount: result.errors.length,
|
|
892
|
+
errors: result.errors.map(err => ({
|
|
893
|
+
record: err.index,
|
|
894
|
+
field: err.field,
|
|
895
|
+
message: err.message,
|
|
896
|
+
value: err.value,
|
|
897
|
+
severity: err.severity
|
|
898
|
+
}))
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// Export to file
|
|
902
|
+
await fs.writeFile(
|
|
903
|
+
`validation-report-${Date.now()}.json`,
|
|
904
|
+
JSON.stringify(report, null, 2)
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
// Send to monitoring
|
|
908
|
+
logger.error('Validation failed', report);
|
|
909
|
+
}
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
## Integration Patterns
|
|
915
|
+
|
|
916
|
+
### Pattern 1: Secure Webhook Endpoint
|
|
917
|
+
|
|
918
|
+
**Webhook validation + Job tracking + Error handling**
|
|
919
|
+
|
|
920
|
+
```typescript
|
|
921
|
+
import {
|
|
922
|
+
webhook,
|
|
923
|
+
fn
|
|
924
|
+
} from '@versori/run';
|
|
925
|
+
import {
|
|
926
|
+
FluentClient,
|
|
927
|
+
JobTracker,
|
|
928
|
+
VersoriKVAdapter
|
|
929
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
930
|
+
|
|
931
|
+
export const secureWebhook = webhook('secure-order', {
|
|
932
|
+
response: {
|
|
933
|
+
mode: 'sync',
|
|
934
|
+
onSuccess: (ctx) => new Response(JSON.stringify({ success: true }), {
|
|
935
|
+
status: 200,
|
|
936
|
+
headers: { 'Content-Type': 'application/json' }
|
|
937
|
+
}),
|
|
938
|
+
onError: (ctx) => new Response(JSON.stringify({
|
|
939
|
+
error: ctx.error.message
|
|
940
|
+
}), {
|
|
941
|
+
status: ctx.error.message.includes('signature') ? 401 : 500,
|
|
942
|
+
headers: { 'Content-Type': 'application/json' }
|
|
943
|
+
})
|
|
944
|
+
}
|
|
945
|
+
})
|
|
946
|
+
.then(fn('validate-signature', async ({ request, vars, log, kv }) => {
|
|
947
|
+
const jobId = request.headers.get('x-request-id') || crypto.randomUUID();
|
|
948
|
+
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
949
|
+
|
|
950
|
+
// Track job start
|
|
951
|
+
await tracker.createJob(jobId, {
|
|
952
|
+
triggeredBy: 'webhook',
|
|
953
|
+
stage: 'validation'
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
// Get raw body
|
|
957
|
+
const rawBody = await request.text();
|
|
958
|
+
const payload = JSON.parse(rawBody);
|
|
959
|
+
const signature = request.headers.get('fluent-signature');
|
|
960
|
+
|
|
961
|
+
if (!signature) {
|
|
962
|
+
await tracker.markFailed(jobId, new Error('Missing signature'));
|
|
963
|
+
throw new Error('Missing fluent-signature header');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Validate signature
|
|
967
|
+
const client = new FluentClient({
|
|
968
|
+
baseUrl: vars.FLUENT_BASE_URL,
|
|
969
|
+
clientId: vars.FLUENT_CLIENT_ID,
|
|
970
|
+
clientSecret: vars.FLUENT_CLIENT_SECRET,
|
|
971
|
+
username: vars.FLUENT_USERNAME,
|
|
972
|
+
password: vars.FLUENT_PASSWORD,
|
|
973
|
+
retailerId: vars.FLUENT_RETAILER_ID,
|
|
974
|
+
publicKey: vars.FLUENT_WEBHOOK_PUBLIC_KEY
|
|
975
|
+
}, log);
|
|
976
|
+
|
|
977
|
+
const isValid = await client.validateWebhook(payload, signature, rawBody);
|
|
978
|
+
|
|
979
|
+
if (!isValid) {
|
|
980
|
+
await tracker.markFailed(jobId, new Error('Invalid signature'));
|
|
981
|
+
throw new Error('Invalid webhook signature');
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
await tracker.updateJob(jobId, {
|
|
985
|
+
status: 'processing',
|
|
986
|
+
stage: 'processing',
|
|
987
|
+
message: 'Signature validated'
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
return { jobId, payload };
|
|
991
|
+
}))
|
|
992
|
+
.then(fn('process-order', async ({ data, kv, log }) => {
|
|
993
|
+
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
994
|
+
|
|
995
|
+
await tracker.updateJob(data.jobId, {
|
|
996
|
+
stage: 'transformation',
|
|
997
|
+
message: `Processing order ${data.payload.entityRef}`
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// Process order...
|
|
1001
|
+
const result = await processOrder(data.payload);
|
|
1002
|
+
|
|
1003
|
+
await tracker.markCompleted(data.jobId, {
|
|
1004
|
+
orderId: result.orderId
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
return { success: true, orderId: result.orderId };
|
|
1008
|
+
}));
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### Pattern 2: Validated Batch Ingestion with Recovery
|
|
1012
|
+
|
|
1013
|
+
**Preflight validation + Partial batch recovery + Job tracking**
|
|
1014
|
+
|
|
1015
|
+
```typescript
|
|
1016
|
+
import {
|
|
1017
|
+
createClient,
|
|
1018
|
+
PreflightValidator,
|
|
1019
|
+
PartialBatchRecovery,
|
|
1020
|
+
JobTracker,
|
|
1021
|
+
VersoriKVAdapter
|
|
1022
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
1023
|
+
|
|
1024
|
+
async function validatedBatchIngestion(
|
|
1025
|
+
records: any[],
|
|
1026
|
+
config: any,
|
|
1027
|
+
logger: any,
|
|
1028
|
+
kv: any
|
|
1029
|
+
) {
|
|
1030
|
+
const client = await createClient({ config });
|
|
1031
|
+
const validator = new PreflightValidator(logger);
|
|
1032
|
+
const recovery = new PartialBatchRecovery(logger);
|
|
1033
|
+
const tracker = new JobTracker(new VersoriKVAdapter(kv), logger);
|
|
1034
|
+
|
|
1035
|
+
const jobId = `batch_${Date.now()}`;
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
// Track job
|
|
1039
|
+
await tracker.createJob(jobId, {
|
|
1040
|
+
triggeredBy: 'schedule',
|
|
1041
|
+
stage: 'validation',
|
|
1042
|
+
details: { recordCount: records.length }
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// Step 1: Preflight validation
|
|
1046
|
+
await tracker.updateJob(jobId, {
|
|
1047
|
+
status: 'processing',
|
|
1048
|
+
stage: 'preflight-validation',
|
|
1049
|
+
message: `Validating ${records.length} records`
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
const validationResult = await validator.validateBatch(records, {
|
|
1053
|
+
entityType: 'INVENTORY',
|
|
1054
|
+
requiredFields: ['ref', 'qty', 'productRef', 'locationRef'],
|
|
1055
|
+
checkDuplicates: true,
|
|
1056
|
+
maxBatchSize: 5000,
|
|
1057
|
+
validateTypes: true,
|
|
1058
|
+
customValidator: (record) => {
|
|
1059
|
+
if (record.qty < 0) return 'Quantity cannot be negative';
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
if (!validationResult.isValid) {
|
|
1065
|
+
await tracker.markFailed(jobId, new Error(validationResult.summary));
|
|
1066
|
+
throw new Error(`Validation failed: ${validationResult.summary}`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Step 2: Create Fluent job
|
|
1070
|
+
await tracker.updateJob(jobId, {
|
|
1071
|
+
stage: 'batch-creation',
|
|
1072
|
+
message: 'Creating Fluent batch job'
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
const fluentJob = await client.createJob({
|
|
1076
|
+
name: `Validated Ingestion ${jobId}`,
|
|
1077
|
+
retailerId: config.retailerId
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// Step 3: Send with recovery
|
|
1081
|
+
await tracker.updateJob(jobId, {
|
|
1082
|
+
stage: 'batch-processing',
|
|
1083
|
+
message: `Sending ${records.length} records with recovery`
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
const recoveryResult = await recovery.processBatchWithRecovery(
|
|
1087
|
+
records,
|
|
1088
|
+
async (batch) => {
|
|
1089
|
+
return await client.sendBatch(fluentJob.id, {
|
|
1090
|
+
action: 'UPSERT',
|
|
1091
|
+
entityType: 'INVENTORY',
|
|
1092
|
+
entities: batch
|
|
1093
|
+
});
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
maxRetries: 3,
|
|
1097
|
+
retryOnlyFailed: true,
|
|
1098
|
+
checkpointKey: jobId,
|
|
1099
|
+
retryBatchSize: 100
|
|
1100
|
+
}
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
// Step 4: Complete
|
|
1104
|
+
await tracker.markCompleted(jobId, {
|
|
1105
|
+
fluentJobId: fluentJob.id,
|
|
1106
|
+
totalRecords: recoveryResult.totalRecords,
|
|
1107
|
+
successCount: recoveryResult.successCount,
|
|
1108
|
+
failedCount: recoveryResult.failedCount,
|
|
1109
|
+
checkpointId: recoveryResult.checkpointId
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
return {
|
|
1113
|
+
jobId,
|
|
1114
|
+
fluentJobId: fluentJob.id,
|
|
1115
|
+
validation: validationResult,
|
|
1116
|
+
recovery: recoveryResult
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
await tracker.markFailed(jobId, error);
|
|
1121
|
+
throw error;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
### Pattern 3: Scheduled Extraction with Tracking
|
|
1127
|
+
|
|
1128
|
+
**Job tracking + Error handling**
|
|
1129
|
+
|
|
1130
|
+
```typescript
|
|
1131
|
+
import { schedule } from '@versori/run';
|
|
1132
|
+
import {
|
|
1133
|
+
createClient,
|
|
1134
|
+
ExtractionOrchestrator,
|
|
1135
|
+
JobTracker,
|
|
1136
|
+
VersoriKVAdapter,
|
|
1137
|
+
SftpDataSource
|
|
1138
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
1139
|
+
|
|
1140
|
+
export const dailyExtraction = schedule('daily-extraction', '0 3 * * *')
|
|
1141
|
+
.execute(async ({ log, connections, vars, kv }) => {
|
|
1142
|
+
const jobId = `extraction_${Date.now()}`;
|
|
1143
|
+
const tracker = new JobTracker(new VersoriKVAdapter(kv), log);
|
|
1144
|
+
|
|
1145
|
+
try {
|
|
1146
|
+
await tracker.createJob(jobId, {
|
|
1147
|
+
triggeredBy: 'schedule',
|
|
1148
|
+
stage: 'initialization',
|
|
1149
|
+
details: { schedule: 'daily 3am' }
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// Stage 1: Extraction
|
|
1153
|
+
await tracker.updateJob(jobId, {
|
|
1154
|
+
status: 'processing',
|
|
1155
|
+
stage: 'extraction',
|
|
1156
|
+
message: 'Querying Fluent API'
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
const client = await createClient(ctx); // Auto-detects Versori context
|
|
1160
|
+
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
1161
|
+
|
|
1162
|
+
const data = await orchestrator.extract({
|
|
1163
|
+
entityType: 'virtualPositions',
|
|
1164
|
+
fields: ['ref', 'type', 'quantity', 'productRef', 'groupRef'],
|
|
1165
|
+
pagination: { pageSize: 1000 }
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
// Stage 2: Transformation
|
|
1169
|
+
await tracker.updateJob(jobId, {
|
|
1170
|
+
stage: 'transformation',
|
|
1171
|
+
message: `Transforming ${data.length} records`
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
const xml = transformToXML(data);
|
|
1175
|
+
|
|
1176
|
+
// Stage 3: Upload
|
|
1177
|
+
await tracker.updateJob(jobId, {
|
|
1178
|
+
stage: 'upload',
|
|
1179
|
+
message: 'Uploading to SFTP'
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
const sftp = new SftpDataSource({
|
|
1183
|
+
host: vars.SFTP_HOST,
|
|
1184
|
+
username: vars.SFTP_USERNAME,
|
|
1185
|
+
privateKey: vars.SFTP_PRIVATE_KEY
|
|
1186
|
+
}, log);
|
|
1187
|
+
|
|
1188
|
+
const fileName = `inventory_${jobId}.xml`;
|
|
1189
|
+
await sftp.writeFile(`/uploads/${fileName}`, xml);
|
|
1190
|
+
|
|
1191
|
+
// Complete
|
|
1192
|
+
await tracker.markCompleted(jobId, {
|
|
1193
|
+
recordCount: data.length,
|
|
1194
|
+
fileName,
|
|
1195
|
+
fileSize: xml.length
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
log.info('Extraction completed', { jobId, recordCount: data.length });
|
|
1199
|
+
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
await tracker.markFailed(jobId, error);
|
|
1202
|
+
log.error('Extraction failed', error);
|
|
1203
|
+
throw error;
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
---
|
|
1209
|
+
|
|
1210
|
+
## Production Examples
|
|
1211
|
+
|
|
1212
|
+
### Complete Production Workflow
|
|
1213
|
+
|
|
1214
|
+
```typescript
|
|
1215
|
+
/**
|
|
1216
|
+
* Production-ready inventory ingestion with all advanced services
|
|
1217
|
+
*/
|
|
1218
|
+
|
|
1219
|
+
import { schedule } from '@versori/run';
|
|
1220
|
+
import {
|
|
1221
|
+
createClient,
|
|
1222
|
+
PreflightValidator,
|
|
1223
|
+
PartialBatchRecovery,
|
|
1224
|
+
JobTracker,
|
|
1225
|
+
VersoriKVAdapter,
|
|
1226
|
+
S3DataSource,
|
|
1227
|
+
CSVParserService,
|
|
1228
|
+
UniversalMapper
|
|
1229
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
1230
|
+
|
|
1231
|
+
export const productionInventorySync = schedule('inventory-sync', '0 */6 * * *')
|
|
1232
|
+
.execute(async ({ log, connections, vars, kv }) => {
|
|
1233
|
+
const jobId = `inventory_${Date.now()}`;
|
|
1234
|
+
const tracker = new JobTracker(new VersoriKVAdapter(kv), log, 2592000); // 30 day TTL
|
|
1235
|
+
const validator = new PreflightValidator(log);
|
|
1236
|
+
const recovery = new PartialBatchRecovery(log);
|
|
1237
|
+
|
|
1238
|
+
try {
|
|
1239
|
+
// ========================================
|
|
1240
|
+
// Stage 1: Job Initialization
|
|
1241
|
+
// ========================================
|
|
1242
|
+
await tracker.createJob(jobId, {
|
|
1243
|
+
triggeredBy: 'schedule',
|
|
1244
|
+
stage: 'initialization',
|
|
1245
|
+
details: {
|
|
1246
|
+
schedule: 'every 6 hours',
|
|
1247
|
+
bucket: vars.S3_BUCKET
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
log.info('Starting production inventory sync', { jobId });
|
|
1252
|
+
|
|
1253
|
+
// ========================================
|
|
1254
|
+
// Stage 2: Data Extraction
|
|
1255
|
+
// ========================================
|
|
1256
|
+
await tracker.updateJob(jobId, {
|
|
1257
|
+
status: 'processing',
|
|
1258
|
+
stage: 'extraction',
|
|
1259
|
+
message: 'Reading CSV from S3'
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
const s3 = new S3DataSource({
|
|
1263
|
+
region: vars.AWS_REGION,
|
|
1264
|
+
accessKeyId: vars.AWS_ACCESS_KEY_ID,
|
|
1265
|
+
secretAccessKey: vars.AWS_SECRET_ACCESS_KEY
|
|
1266
|
+
}, log);
|
|
1267
|
+
|
|
1268
|
+
const csvContent = await s3.readFile(vars.S3_BUCKET, 'inventory/latest.csv');
|
|
1269
|
+
|
|
1270
|
+
const parser = new CSVParserService({ hasHeaders: true }, log);
|
|
1271
|
+
const rawRecords = await parser.parseToArray(csvContent);
|
|
1272
|
+
|
|
1273
|
+
log.info('CSV parsed', { recordCount: rawRecords.length });
|
|
1274
|
+
|
|
1275
|
+
// ========================================
|
|
1276
|
+
// Stage 3: Data Transformation
|
|
1277
|
+
// ========================================
|
|
1278
|
+
await tracker.updateJob(jobId, {
|
|
1279
|
+
stage: 'transformation',
|
|
1280
|
+
message: `Mapping ${rawRecords.length} records`
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
const mapper = new UniversalMapper({
|
|
1284
|
+
fields: {
|
|
1285
|
+
ref: { source: 'sku', required: true },
|
|
1286
|
+
type: { resolver: () => 'INVENTORY_POSITION' },
|
|
1287
|
+
qty: { source: 'quantity', resolver: 'sdk.parseInt', required: true },
|
|
1288
|
+
productRef: { source: 'product_id', required: true },
|
|
1289
|
+
locationRef: { source: 'location_id', required: true },
|
|
1290
|
+
condition: { source: 'condition', defaultValue: 'NEW' }
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
const mappedRecords = [];
|
|
1295
|
+
const mappingErrors = [];
|
|
1296
|
+
|
|
1297
|
+
for (let i = 0; i < rawRecords.length; i++) {
|
|
1298
|
+
const result = await mapper.map(rawRecords[i]);
|
|
1299
|
+
if (result.success) {
|
|
1300
|
+
mappedRecords.push(result.data);
|
|
1301
|
+
} else {
|
|
1302
|
+
mappingErrors.push({ index: i, errors: result.errors });
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (mappingErrors.length > 0) {
|
|
1307
|
+
log.warn('Mapping errors', {
|
|
1308
|
+
errorCount: mappingErrors.length,
|
|
1309
|
+
totalRecords: rawRecords.length
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ========================================
|
|
1314
|
+
// Stage 4: Preflight Validation
|
|
1315
|
+
// ========================================
|
|
1316
|
+
await tracker.updateJob(jobId, {
|
|
1317
|
+
stage: 'validation',
|
|
1318
|
+
message: `Validating ${mappedRecords.length} records`
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
const validationResult = await validator.validateBatch(mappedRecords, {
|
|
1322
|
+
entityType: 'INVENTORY',
|
|
1323
|
+
requiredFields: ['ref', 'qty', 'productRef', 'locationRef'],
|
|
1324
|
+
checkDuplicates: true,
|
|
1325
|
+
maxBatchSize: 5000,
|
|
1326
|
+
validateTypes: true,
|
|
1327
|
+
customValidator: (record) => {
|
|
1328
|
+
if (record.qty < 0) {
|
|
1329
|
+
return 'Quantity cannot be negative';
|
|
1330
|
+
}
|
|
1331
|
+
if (!record.ref.match(/^[A-Z0-9-]+$/)) {
|
|
1332
|
+
return 'Invalid ref format';
|
|
1333
|
+
}
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
if (!validationResult.isValid) {
|
|
1339
|
+
await tracker.markFailed(jobId, new Error(validationResult.summary));
|
|
1340
|
+
|
|
1341
|
+
// Export validation errors
|
|
1342
|
+
await s3.writeFile(
|
|
1343
|
+
vars.S3_BUCKET,
|
|
1344
|
+
`errors/validation-${jobId}.json`,
|
|
1345
|
+
JSON.stringify(validationResult.errors, null, 2)
|
|
1346
|
+
);
|
|
1347
|
+
|
|
1348
|
+
throw new Error(`Validation failed: ${validationResult.summary}`);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
log.info('Validation passed', {
|
|
1352
|
+
validRecords: validationResult.validRecords,
|
|
1353
|
+
warnings: validationResult.warnings.length
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
// ========================================
|
|
1357
|
+
// Stage 5: Fluent API Ingestion
|
|
1358
|
+
// ========================================
|
|
1359
|
+
await tracker.updateJob(jobId, {
|
|
1360
|
+
stage: 'ingestion',
|
|
1361
|
+
message: 'Creating Fluent batch job'
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
const client = await createClient({
|
|
1365
|
+
connection: connections.fluent_commerce,
|
|
1366
|
+
logger: log
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
const fluentJob = await client.createJob({
|
|
1370
|
+
name: `Production Inventory Sync ${jobId}`,
|
|
1371
|
+
retailerId: vars.FLUENT_RETAILER_ID,
|
|
1372
|
+
meta: {
|
|
1373
|
+
preprocessing: 'default', // Enable BPP
|
|
1374
|
+
source: 's3-csv',
|
|
1375
|
+
jobId
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
log.info('Fluent job created', { fluentJobId: fluentJob.id });
|
|
1380
|
+
|
|
1381
|
+
// ========================================
|
|
1382
|
+
// Stage 6: Batch Processing with Recovery
|
|
1383
|
+
// ========================================
|
|
1384
|
+
await tracker.updateJob(jobId, {
|
|
1385
|
+
stage: 'batch-processing',
|
|
1386
|
+
message: `Sending ${mappedRecords.length} records`
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
const recoveryResult = await recovery.processBatchWithRecovery(
|
|
1390
|
+
mappedRecords,
|
|
1391
|
+
async (batch) => {
|
|
1392
|
+
log.info('Sending batch', { batchSize: batch.length });
|
|
1393
|
+
|
|
1394
|
+
return await client.sendBatch(fluentJob.id, {
|
|
1395
|
+
action: 'UPSERT',
|
|
1396
|
+
entityType: 'INVENTORY',
|
|
1397
|
+
entities: batch
|
|
1398
|
+
});
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
maxRetries: 5,
|
|
1402
|
+
retryOnlyFailed: true,
|
|
1403
|
+
retryDelayMs: 2000,
|
|
1404
|
+
retryBatchSize: 100,
|
|
1405
|
+
checkpointKey: jobId,
|
|
1406
|
+
shouldRetry: (error, attemptCount) => {
|
|
1407
|
+
// Don't retry validation errors
|
|
1408
|
+
if (error.message.includes('validation')) {
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
// Max 5 attempts
|
|
1412
|
+
return attemptCount <= 5;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
);
|
|
1416
|
+
|
|
1417
|
+
log.info('Batch processing complete', {
|
|
1418
|
+
successCount: recoveryResult.successCount,
|
|
1419
|
+
failedCount: recoveryResult.failedCount,
|
|
1420
|
+
checkpointId: recoveryResult.checkpointId
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
// ========================================
|
|
1424
|
+
// Stage 7: Job Status Verification
|
|
1425
|
+
// ========================================
|
|
1426
|
+
await tracker.updateJob(jobId, {
|
|
1427
|
+
stage: 'verification',
|
|
1428
|
+
message: 'Verifying Fluent job status'
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
// Poll for completion
|
|
1432
|
+
let attempts = 0;
|
|
1433
|
+
const maxAttempts = 30;
|
|
1434
|
+
let fluentStatus;
|
|
1435
|
+
|
|
1436
|
+
while (attempts < maxAttempts) {
|
|
1437
|
+
fluentStatus = await client.getJobStatus(fluentJob.id);
|
|
1438
|
+
|
|
1439
|
+
if (fluentStatus.status === 'COMPLETED' || fluentStatus.status === 'FAILED') {
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10s
|
|
1444
|
+
attempts++;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
log.info('Fluent job status', {
|
|
1448
|
+
status: fluentStatus.status,
|
|
1449
|
+
errorSummary: fluentStatus.errorSummary
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
// ========================================
|
|
1453
|
+
// Stage 8: Completion
|
|
1454
|
+
// ========================================
|
|
1455
|
+
const finalDetails = {
|
|
1456
|
+
fluentJobId: fluentJob.id,
|
|
1457
|
+
fluentJobStatus: fluentStatus.status,
|
|
1458
|
+
sourceRecords: rawRecords.length,
|
|
1459
|
+
mappedRecords: mappedRecords.length,
|
|
1460
|
+
mappingErrors: mappingErrors.length,
|
|
1461
|
+
validationResult: {
|
|
1462
|
+
validRecords: validationResult.validRecords,
|
|
1463
|
+
warnings: validationResult.warnings.length
|
|
1464
|
+
},
|
|
1465
|
+
recoveryResult: {
|
|
1466
|
+
successCount: recoveryResult.successCount,
|
|
1467
|
+
failedCount: recoveryResult.failedCount,
|
|
1468
|
+
checkpointId: recoveryResult.checkpointId
|
|
1469
|
+
},
|
|
1470
|
+
fluentErrors: fluentStatus.errorSummary?.totalErrors || 0
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
if (recoveryResult.failedCount > 0 || (fluentStatus.errorSummary?.totalErrors || 0) > 0) {
|
|
1474
|
+
await tracker.updateJob(jobId, {
|
|
1475
|
+
status: 'completed',
|
|
1476
|
+
stage: 'completed-with-errors',
|
|
1477
|
+
message: `Completed with ${recoveryResult.failedCount} recovery errors, ${fluentStatus.errorSummary?.totalErrors || 0} Fluent errors`,
|
|
1478
|
+
details: finalDetails
|
|
1479
|
+
});
|
|
1480
|
+
} else {
|
|
1481
|
+
await tracker.markCompleted(jobId, finalDetails);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return {
|
|
1485
|
+
success: true,
|
|
1486
|
+
jobId,
|
|
1487
|
+
...finalDetails
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
await tracker.markFailed(jobId, error);
|
|
1492
|
+
log.error('Production sync failed', error);
|
|
1493
|
+
throw error;
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
```
|
|
1497
|
+
|
|
1498
|
+
---
|
|
1499
|
+
|
|
1500
|
+
## Summary
|
|
1501
|
+
|
|
1502
|
+
### Key Takeaways
|
|
1503
|
+
|
|
1504
|
+
1. **Webhook Validation** - ALWAYS validate Fluent Commerce webhook signatures
|
|
1505
|
+
2. **Partial Batch Recovery** - Retry only failed records, not entire batch
|
|
1506
|
+
3. **Job Tracker** - Track async workflow progress in KV store
|
|
1507
|
+
4. **Preflight Validator** - Validate before API calls to save quota
|
|
1508
|
+
|
|
1509
|
+
### Service Comparison
|
|
1510
|
+
|
|
1511
|
+
| Service | Primary Benefit | When to Use | Complexity |
|
|
1512
|
+
|---------|----------------|-------------|------------|
|
|
1513
|
+
| **Webhook Validation** | Security | All webhook endpoints | Low |
|
|
1514
|
+
| **Partial Batch Recovery** | Reliability | Batch ingestion | Medium |
|
|
1515
|
+
| **Job Tracker** | Observability | Scheduled workflows | Low |
|
|
1516
|
+
| **Preflight Validator** | Cost savings | Large batch operations | Low |
|
|
1517
|
+
|
|
1518
|
+
### Integration Checklist
|
|
1519
|
+
|
|
1520
|
+
Production integration should include:
|
|
1521
|
+
|
|
1522
|
+
- [ ] **Webhook Validation** - Verify all incoming webhooks
|
|
1523
|
+
- [ ] **Preflight Validator** - Validate before API submission
|
|
1524
|
+
- [ ] **Partial Batch Recovery** - Handle batch failures gracefully
|
|
1525
|
+
- [ ] **Job Tracker** - Track workflow lifecycle
|
|
1526
|
+
- [ ] **Structured Logging** - Log all stages with context
|
|
1527
|
+
- [ ] **Error Handling** - Catch and log all errors
|
|
1528
|
+
- [ ] **Monitoring** - Track metrics and job status
|
|
1529
|
+
|
|
1530
|
+
---
|
|
1531
|
+
|
|
1532
|
+
## Next Steps
|
|
1533
|
+
|
|
1534
|
+
Continue your learning journey:
|
|
1535
|
+
|
|
1536
|
+
- [Quick Reference](../integration-patterns-quick-reference.md) - All patterns at a glance
|
|
1537
|
+
- [Use Cases](../../../01-TEMPLATES/readme.md) - Complete production examples
|
|
1538
|
+
- [Error Handling](./integration-patterns-05-error-handling.md) - Comprehensive error patterns
|
|
1539
|
+
|
|
1540
|
+
Or explore related guides:
|
|
1541
|
+
- [Auto-Pagination](../../../02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md)
|
|
1542
|
+
- [Universal Mapping](../../../02-CORE-GUIDES/mapping/mapping-readme.md)
|
|
1543
|
+
- [Batch API Guide](../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md)
|
|
1544
|
+
|
|
1545
|
+
---
|
|
1546
|
+
|
|
1547
|
+
[← Back to Index](../integration-patterns-readme.md) | [Previous: Error Handling](./integration-patterns-05-error-handling.md)
|