@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
|
@@ -1,1952 +1,1952 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-ingest-s3-json-inventory-batch
|
|
3
|
-
canonical_filename: template-ingestion-s3-json-inventory-batch.md
|
|
4
|
-
version: 2.0.0
|
|
5
|
-
sdk_version: ^0.1.39
|
|
6
|
-
runtime: versori
|
|
7
|
-
direction: ingestion
|
|
8
|
-
source: s3-json
|
|
9
|
-
destination: fluent-batch-api
|
|
10
|
-
entity: inventory
|
|
11
|
-
format: json
|
|
12
|
-
logging: versori
|
|
13
|
-
status: stable
|
|
14
|
-
features:
|
|
15
|
-
- batch-api-integration
|
|
16
|
-
- memory-management
|
|
17
|
-
- enhanced-logging
|
|
18
|
-
- attribute-transformation
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
# Template: Ingestion - S3 JSON Inventory to Batch API
|
|
22
|
-
|
|
23
|
-
**FC Connect SDK Use Case Guide**
|
|
24
|
-
|
|
25
|
-
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
26
|
-
> **Version**: @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
27
|
-
|
|
28
|
-
**🆕 Production Code Enhancements (Applied):**
|
|
29
|
-
1. ✅ Batch API with retry logic and BPP change detection
|
|
30
|
-
2. ✅ Memory management (clearing large arrays after batch processing)
|
|
31
|
-
3. ✅ Enhanced logging with emoji progress tracking (📦 batch creation, 📤 batch sending, ✅ completion)
|
|
32
|
-
4. ✅ Attribute transformation with nested field support
|
|
33
|
-
|
|
34
|
-
**Template Version:** 2.0.0
|
|
35
|
-
**Last Updated:** 2025-01-24
|
|
36
|
-
|
|
37
|
-
**Context**: Versori scheduled workflow that reads inventory JSON files from S3, transforms data with UniversalMapper, and sends bulk inventory updates to Fluent Commerce Batch API with BPP change detection
|
|
38
|
-
|
|
39
|
-
**Complexity**: Medium
|
|
40
|
-
|
|
41
|
-
**Runtime**: Versori Platform
|
|
42
|
-
|
|
43
|
-
**Estimated Lines**: ~520 lines
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## STEP 1: Understand This Template
|
|
48
|
-
|
|
49
|
-
**What This Template Does:**
|
|
50
|
-
|
|
51
|
-
- Scheduled Versori workflow for bulk inventory ingestion from S3 JSON files
|
|
52
|
-
- Reads JSON files from S3 with retry logic and streaming support
|
|
53
|
-
- Parses JSON with format detection (standard JSON vs JSON Lines)
|
|
54
|
-
- Transforms data using UniversalMapper with direct field access (no special notation)
|
|
55
|
-
- Sends bulk updates to Fluent Commerce Batch API with chunking
|
|
56
|
-
- Uses BPP (Batch Pre-Processing) for change detection
|
|
57
|
-
- Tracks processing state with VersoriFileTracker to prevent duplicates
|
|
58
|
-
- Archives processed files and handles errors gracefully
|
|
59
|
-
|
|
60
|
-
**Key SDK Components:**
|
|
61
|
-
|
|
62
|
-
- `createClient()` - Universal client factory (auto-detects Versori context)
|
|
63
|
-
- `S3DataSource` - S3 operations with streaming (NEW: enhanced retry logic)
|
|
64
|
-
- `JSONParserService` - JSON parsing with format auto-detection (JSON vs JSON Lines)
|
|
65
|
-
- `UniversalMapper` - Field transformation with SDK resolvers
|
|
66
|
-
- `VersoriFileTracker` - State management (prevent duplicate processing)
|
|
67
|
-
- `JobTracker` - Job lifecycle tracking
|
|
68
|
-
- Native Versori `log` - Use `log` from context
|
|
69
|
-
|
|
70
|
-
**Entity Type:**
|
|
71
|
-
|
|
72
|
-
- **InventoryQuantity** - Fluent entity for inventory positions and quantities
|
|
73
|
-
- **EntityType: 'INVENTORY'** - Used in Batch API `sendBatch()` call
|
|
74
|
-
- **Batch API Method** - Uses `createJob()` and `sendBatch()` (not Event API)
|
|
75
|
-
|
|
76
|
-
**Critical Patterns:**
|
|
77
|
-
|
|
78
|
-
- **JSON Format Support**: Standard JSON objects and JSON Lines (one record per line)
|
|
79
|
-
- **Direct Field Access**: Use simple paths like `"locationRef"` (no `@` prefix needed for JSON)
|
|
80
|
-
- **Safe S3 Paths**: Use absolute paths in S3DataSource config
|
|
81
|
-
- **Always Dispose**: Call `s3.dispose()` in `finally` block to release connections
|
|
82
|
-
- **BPP Configuration**: Enabled by default for full snapshots, skip for delta feeds
|
|
83
|
-
- **Progress Logging**: Enhanced logging with context (sample SKUs, locations)
|
|
84
|
-
- **Detailed Logging Toggle**: Optional detailed payload logging for debugging (default: disabled)
|
|
85
|
-
- **File Tracking Toggle**: Optional file tracking to prevent duplicates (default: enabled)
|
|
86
|
-
- **Retry Logic**: Enhanced S3 operations with exponential backoff
|
|
87
|
-
|
|
88
|
-
**When to Use This Template:**
|
|
89
|
-
|
|
90
|
-
- ✅ Daily/hourly full inventory snapshots from S3 JSON files
|
|
91
|
-
- ✅ Bulk inventory updates with BPP change detection
|
|
92
|
-
- ✅ JSON or JSON Lines format data
|
|
93
|
-
- ✅ Need state management to prevent duplicate processing
|
|
94
|
-
- ✅ Files should be archived after processing
|
|
95
|
-
|
|
96
|
-
**When NOT to Use:**
|
|
97
|
-
|
|
98
|
-
- ❌ Single inventory updates (use GraphQL mutation instead)
|
|
99
|
-
- ❌ Products, Locations, Customers (use Event API templates)
|
|
100
|
-
- ❌ Real-time inventory events (use Event API)
|
|
101
|
-
- ❌ XML/CSV/Parquet formats (use appropriate format template)
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## STEP 2: AI Prompt
|
|
106
|
-
|
|
107
|
-
**Copy this prompt to generate the complete implementation:**
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
Create a Versori scheduled workflow for S3 JSON inventory ingestion to Fluent Commerce Batch API.
|
|
111
|
-
|
|
112
|
-
REQUIREMENTS:
|
|
113
|
-
1. Runtime: Versori Platform (scheduled workflow)
|
|
114
|
-
2. Source: S3 JSON files from s3://bucket/inventory/
|
|
115
|
-
3. Destination: Fluent Commerce Batch API (InventoryQuantity entity)
|
|
116
|
-
4. Format: JSON (standard JSON objects and JSON Lines support)
|
|
117
|
-
5. Entity: InventoryQuantity (EntityType: 'INVENTORY')
|
|
118
|
-
|
|
119
|
-
KEY FEATURES:
|
|
120
|
-
- S3 JSON file discovery with VersoriFileTracker for state management
|
|
121
|
-
- JSON parsing with format auto-detection (JSON vs JSON Lines)
|
|
122
|
-
- Direct field mapping using UniversalMapper (no special notation needed)
|
|
123
|
-
- Batch API with chunking (1000 records per batch) and BPP change detection
|
|
124
|
-
- Safe S3 paths with absolute path requirements
|
|
125
|
-
- S3 dispose() in finally block
|
|
126
|
-
- Job lifecycle tracking with JobTracker
|
|
127
|
-
- File archival on S3 after successful processing
|
|
128
|
-
- Error handling with exponential backoff retry
|
|
129
|
-
|
|
130
|
-
CRITICAL REQUIREMENTS:
|
|
131
|
-
1. JSON Parser: JSONParserService with format auto-detection
|
|
132
|
-
2. Array Extraction: Handle both standard JSON arrays and JSON Lines
|
|
133
|
-
3. Mapping Config: Use direct paths like "locationRef" (no @ prefix)
|
|
134
|
-
4. Entity Type: 'INVENTORY' (for InventoryQuantity)
|
|
135
|
-
5. BPP: Enabled by default (full snapshots), skip for delta feeds
|
|
136
|
-
6. S3 Config: Include proper AWS credentials configuration
|
|
137
|
-
7. S3 Dispose: Always call s3.dispose() in finally block
|
|
138
|
-
8. Native Logging: Use log from context
|
|
139
|
-
9. Retry Logic: Enhanced S3 operations with exponential backoff
|
|
140
|
-
|
|
141
|
-
SDK METHODS TO USE:
|
|
142
|
-
- createClient(ctx) - Pass entire Versori context, auto-detects platform
|
|
143
|
-
- client.setRetailerId(retailerId) - REQUIRED after createClient for Batch API operations
|
|
144
|
-
- new S3DataSource(config, log) - Config includes AWS credentials
|
|
145
|
-
- await s3.listFiles({ prefix, maxKeys })
|
|
146
|
-
- await s3.downloadFile(key, { encoding: 'utf8' })
|
|
147
|
-
- await s3.moveFile(sourceKey, destKey)
|
|
148
|
-
- await s3.dispose() - MUST call in finally block
|
|
149
|
-
- new JSONParserService()
|
|
150
|
-
- await parser.parse(jsonContent, { format: 'json' | 'jsonl' })
|
|
151
|
-
- new UniversalMapper(mappingConfig)
|
|
152
|
-
- await mapper.map(records)
|
|
153
|
-
- new VersoriFileTracker(ctx.openKv(':project:'), 'prefix')
|
|
154
|
-
- await fileTracker.wasFileProcessed(fileName)
|
|
155
|
-
- await fileTracker.markFileProcessed(fileName, metadata)
|
|
156
|
-
- new JobTracker(ctx.openKv(':project:'), log)
|
|
157
|
-
- await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' })
|
|
158
|
-
- await tracker.updateJob(jobId, { status: 'processing' })
|
|
159
|
-
- await tracker.markCompleted(jobId, details)
|
|
160
|
-
- await tracker.markFailed(jobId, error)
|
|
161
|
-
- await client.createJob({ name, meta: { preprocessing: 'skip' } })
|
|
162
|
-
- await client.sendBatch(jobId, { action: 'UPSERT', entityType: 'INVENTORY', entities })
|
|
163
|
-
- Fire-and-forget batch submission (no polling)
|
|
164
|
-
|
|
165
|
-
FORBIDDEN PATTERNS:
|
|
166
|
-
- ❌ LoggingService (removed - use native log on Versori)
|
|
167
|
-
- ❌ Don't use @ prefix for JSON fields (that's for XML attributes only)
|
|
168
|
-
- ❌ Don't forget to wrap records in proper structure for mapping
|
|
169
|
-
- ❌ Don't use Event API (use Batch API)
|
|
170
|
-
- ❌ Don't forget to call s3.dispose() in finally block
|
|
171
|
-
- ❌ Don't use relative S3 paths (use absolute paths)
|
|
172
|
-
|
|
173
|
-
MAPPING CONFIGURATION FILE: config/inventory.batch.json
|
|
174
|
-
Structure:
|
|
175
|
-
{
|
|
176
|
-
"name": "inventory.batch.json",
|
|
177
|
-
"version": "1.0.0",
|
|
178
|
-
"description": "JSON inventory to Fluent Commerce Batch API mapping",
|
|
179
|
-
"fields": {
|
|
180
|
-
"locationRef": { "source": "locationRef", "required": true, "resolver": "sdk.trim" },
|
|
181
|
-
"skuRef": { "source": "skuRef", "required": true, "resolver": "sdk.trim" },
|
|
182
|
-
"qty": { "source": "qty", "required": true, "resolver": "sdk.parseInt" },
|
|
183
|
-
"type": { "source": "type", "required": true, "resolver": "sdk.uppercase" },
|
|
184
|
-
"status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
|
|
185
|
-
"expectedOn": { "source": "expectedOn", "required": false, "resolver": "sdk.formatDate" },
|
|
186
|
-
"attributes.expiryDate": { "source": "attributes.expiryDate", "required": false, "resolver": "sdk.formatDate" },
|
|
187
|
-
"attributes.batchNumber": { "source": "attributes.batchNumber", "required": false },
|
|
188
|
-
"attributes.condition": { "source": "attributes.condition", "required": false, "defaultValue": "NEW", "resolver": "sdk.uppercase" },
|
|
189
|
-
"attributes.storageZone": { "source": "attributes.storageZone", "required": false }
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
GENERATE:
|
|
194
|
-
1. package.json with dependencies
|
|
195
|
-
2. index.ts (workflow entry point with scheduled trigger)
|
|
196
|
-
3. src/workflows/scheduled/daily-inventory-sync.ts (scheduled workflow)
|
|
197
|
-
4. src/workflows/webhook/adhoc-inventory-sync.ts (webhook workflow)
|
|
198
|
-
5. src/workflows/webhook/job-status-check.ts (status webhook)
|
|
199
|
-
6. src/services/inventory-sync.service.ts (shared orchestration logic)
|
|
200
|
-
7. src/types/inventory.types.ts (TypeScript type definitions)
|
|
201
|
-
8. config/inventory.batch.json (mapping configuration - external JSON file)
|
|
202
|
-
9. .env.example (environment variables)
|
|
203
|
-
|
|
204
|
-
NOTE: Use external JSON files for mapping configuration (not TypeScript .config files)
|
|
205
|
-
|
|
206
|
-
Ensure all code is production-ready with proper error handling, S3 dispose() in finally block. Polling is intentionally omitted (fire-and-forget).
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## What You'll Build
|
|
212
|
-
|
|
213
|
-
### Project Structure
|
|
214
|
-
|
|
215
|
-
```
|
|
216
|
-
s3-json-inventory-batch-sync/
|
|
217
|
-
├── package.json
|
|
218
|
-
├── index.ts # Workflow entry point
|
|
219
|
-
└── src/
|
|
220
|
-
├── workflows/
|
|
221
|
-
│ ├── scheduled/
|
|
222
|
-
│ │ └── daily-inventory-sync.ts # Scheduled: Daily inventory sync
|
|
223
|
-
│ │
|
|
224
|
-
│ └── webhook/
|
|
225
|
-
│ ├── adhoc-inventory-sync.ts # Webhook: Manual trigger
|
|
226
|
-
│ └── job-status-check.ts # Webhook: Status query
|
|
227
|
-
│
|
|
228
|
-
├── services/
|
|
229
|
-
│ └── inventory-sync.service.ts # Shared orchestration logic (reusable)
|
|
230
|
-
│
|
|
231
|
-
├── config/
|
|
232
|
-
│ └── inventory.batch.json # Mapping configuration (external JSON)
|
|
233
|
-
│
|
|
234
|
-
└── types/
|
|
235
|
-
└── inventory.types.ts # TypeScript interfaces
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### Features
|
|
239
|
-
|
|
240
|
-
- ✅ Scheduled Versori workflow (daily/hourly inventory sync)
|
|
241
|
-
- ✅ S3 file download with enhanced retry logic
|
|
242
|
-
- ✅ JSON parsing with format auto-detection (standard JSON vs JSON Lines)
|
|
243
|
-
- ✅ Direct field mapping (no special notation needed for JSON)
|
|
244
|
-
- ✅ Field mapping with UniversalMapper and external JSON config
|
|
245
|
-
- ✅ Batch API job creation and chunked processing (1000 records/batch)
|
|
246
|
-
- ✅ BPP (Batch Pre-Processing) change detection
|
|
247
|
-
- ✅ Fire-and-forget batch submission with audit logging
|
|
248
|
-
- ✅ State management (VersoriFileTracker + JobTracker prevent duplicates)
|
|
249
|
-
- ✅ File archival on S3 (success → processed/, failure → errors/)
|
|
250
|
-
- ✅ Comprehensive error handling with exponential backoff retry
|
|
251
|
-
- ✅ Modular architecture (reusable services, easy to test)
|
|
252
|
-
|
|
253
|
-
---
|
|
254
|
-
|
|
255
|
-
## Versori Workflows Structure
|
|
256
|
-
|
|
257
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
258
|
-
|
|
259
|
-
**Trigger Types:**
|
|
260
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
261
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
262
|
-
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
263
|
-
|
|
264
|
-
**Execution Steps (chained to triggers):**
|
|
265
|
-
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
266
|
-
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
267
|
-
|
|
268
|
-
### Recommended Project Structure
|
|
269
|
-
|
|
270
|
-
```
|
|
271
|
-
s3-json-inventory-batch-sync/
|
|
272
|
-
├── index.ts # Entry point - exports all workflows
|
|
273
|
-
└── src/
|
|
274
|
-
├── workflows/
|
|
275
|
-
│ ├── scheduled/
|
|
276
|
-
│ │ └── daily-inventory-sync.ts # Scheduled: Daily inventory sync
|
|
277
|
-
│ │
|
|
278
|
-
│ └── webhook/
|
|
279
|
-
│ ├── adhoc-inventory-sync.ts # Webhook: Manual trigger
|
|
280
|
-
│ └── job-status-check.ts # Webhook: Status query
|
|
281
|
-
│
|
|
282
|
-
├── services/
|
|
283
|
-
│ └── inventory-sync.service.ts # Shared orchestration logic (reusable)
|
|
284
|
-
│
|
|
285
|
-
└── types/
|
|
286
|
-
└── inventory.types.ts # Shared type definitions
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
**Benefits:**
|
|
290
|
-
- ✅ Clear trigger separation (`scheduled/` vs `webhook/`)
|
|
291
|
-
- ✅ Descriptive file names (easy to browse and understand)
|
|
292
|
-
- ✅ Scalable (add new workflows without cluttering)
|
|
293
|
-
- ✅ Reusable code in `services/` (DRY principle)
|
|
294
|
-
- ✅ Easy to modify individual workflows without affecting others
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
|
-
## Workflow Files
|
|
299
|
-
|
|
300
|
-
### 1. Scheduled Workflows (`src/workflows/scheduled/`)
|
|
301
|
-
|
|
302
|
-
All time-based triggers that run automatically on cron schedules.
|
|
303
|
-
|
|
304
|
-
#### `src/workflows/scheduled/daily-inventory-sync.ts`
|
|
305
|
-
|
|
306
|
-
**Purpose**: Automatic daily inventory sync
|
|
307
|
-
**Trigger**: Cron schedule (`0 2 * * *`)
|
|
308
|
-
**Exposed as Endpoint**: ❌ NO - Runs automatically
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
import { schedule, http } from '@versori/run';
|
|
312
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
313
|
-
import { runIngestion } from '../../services/inventory-sync.service';
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Scheduled Workflow: Daily Inventory Sync
|
|
317
|
-
*
|
|
318
|
-
* Runs automatically daily at 2 AM UTC
|
|
319
|
-
* NOT exposed as HTTP endpoint - Versori executes on schedule
|
|
320
|
-
*
|
|
321
|
-
* Uses shared service: inventory-sync.service.ts
|
|
322
|
-
*/
|
|
323
|
-
export const dailyInventorySync = schedule(
|
|
324
|
-
'inventory-batch-scheduled',
|
|
325
|
-
'0 2 * * *' // Daily at 2 AM UTC
|
|
326
|
-
).then(
|
|
327
|
-
http('run-inventory-batch', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
328
|
-
const { log, openKv } = ctx;
|
|
329
|
-
const executionStartTime = Date.now();
|
|
330
|
-
const jobId = `inventory-batch-${Date.now()}`;
|
|
331
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
332
|
-
|
|
333
|
-
log.info('═══════════════════════════════════════════════════════════════');
|
|
334
|
-
log.info('🚀 [WORKFLOW] Starting scheduled inventory sync', { jobId });
|
|
335
|
-
log.info('═══════════════════════════════════════════════════════════════');
|
|
336
|
-
|
|
337
|
-
await tracker.createJob(jobId, {
|
|
338
|
-
triggeredBy: 'schedule',
|
|
339
|
-
stage: 'initialization',
|
|
340
|
-
startTime: executionStartTime,
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
await tracker.updateJob(jobId, { status: 'processing' });
|
|
344
|
-
|
|
345
|
-
try {
|
|
346
|
-
// Reuse shared orchestration logic
|
|
347
|
-
const result = await runIngestion(ctx, jobId, tracker);
|
|
348
|
-
|
|
349
|
-
if (result.success) {
|
|
350
|
-
const duration = Date.now() - executionStartTime;
|
|
351
|
-
await tracker.markCompleted(jobId, { ...result, duration });
|
|
352
|
-
log.info('✅ [WORKFLOW] Inventory sync completed successfully', {
|
|
353
|
-
jobId,
|
|
354
|
-
filesProcessed: result.filesProcessed,
|
|
355
|
-
duration: `${duration}ms (${(duration / 1000).toFixed(2)}s)`,
|
|
356
|
-
});
|
|
357
|
-
} else {
|
|
358
|
-
await tracker.markFailed(jobId, result.error || 'Unknown error');
|
|
359
|
-
log.error('❌ [WORKFLOW] Inventory sync failed', {
|
|
360
|
-
jobId,
|
|
361
|
-
error: result.error,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
log.info('═══════════════════════════════════════════════════════════════');
|
|
366
|
-
log.info('🏁 [WORKFLOW] Execution complete', {
|
|
367
|
-
jobId,
|
|
368
|
-
success: result.success,
|
|
369
|
-
duration: `${Date.now() - executionStartTime}ms`,
|
|
370
|
-
});
|
|
371
|
-
log.info('═══════════════════════════════════════════════════════════════');
|
|
372
|
-
|
|
373
|
-
return { success: true, jobId, ...result };
|
|
374
|
-
} catch (e: any) {
|
|
375
|
-
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
376
|
-
await tracker.markFailed(jobId, errorMessage);
|
|
377
|
-
log.error('❌ [WORKFLOW] Inventory sync failed with exception', {
|
|
378
|
-
jobId,
|
|
379
|
-
error: errorMessage,
|
|
380
|
-
stack: e instanceof Error ? e.stack : undefined,
|
|
381
|
-
recommendation: 'Check SFTP credentials, network connectivity, and file permissions',
|
|
382
|
-
});
|
|
383
|
-
return { success: false, jobId, error: errorMessage };
|
|
384
|
-
}
|
|
385
|
-
})
|
|
386
|
-
);
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
---
|
|
390
|
-
|
|
391
|
-
### 2. Webhook Workflows (`src/workflows/webhook/`)
|
|
392
|
-
|
|
393
|
-
All HTTP-based triggers that create webhook endpoints.
|
|
394
|
-
|
|
395
|
-
#### `src/workflows/webhook/adhoc-inventory-sync.ts`
|
|
396
|
-
|
|
397
|
-
**Purpose**: Manual inventory sync trigger (on-demand)
|
|
398
|
-
**Trigger**: HTTP POST
|
|
399
|
-
**Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-adhoc`
|
|
400
|
-
**Use Cases**: Testing, priority processing, ad-hoc runs
|
|
401
|
-
|
|
402
|
-
```typescript
|
|
403
|
-
import { webhook, http } from '@versori/run';
|
|
404
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
405
|
-
import { runIngestion } from '../../services/inventory-sync.service';
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Webhook: Manual Inventory Sync Trigger
|
|
409
|
-
*
|
|
410
|
-
* Endpoint: POST https://{workspace}.versori.run/inventory-batch-adhoc
|
|
411
|
-
* Request body (optional): { filePattern: "urgent_*.json", maxFiles: 5 }
|
|
412
|
-
*
|
|
413
|
-
* Pattern: Sync mode + fire-and-forget
|
|
414
|
-
* - Returns jobId immediately
|
|
415
|
-
* - Background processing continues without blocking response
|
|
416
|
-
* - ✅ Works because Versori keeps execution context alive for unawaited promises
|
|
417
|
-
*
|
|
418
|
-
* Uses shared service: inventory-sync.service.ts
|
|
419
|
-
*/
|
|
420
|
-
export const adhocInventorySync = webhook('inventory-batch-adhoc', {
|
|
421
|
-
response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
|
|
422
|
-
connection: 'inventory-batch-adhoc', // Versori validates API key
|
|
423
|
-
}).then(
|
|
424
|
-
http('run-inventory-batch-adhoc', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
425
|
-
const { log, openKv, data } = ctx;
|
|
426
|
-
const executionStartTime = Date.now();
|
|
427
|
-
const jobId = `inventory-batch-adhoc-${Date.now()}`;
|
|
428
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
429
|
-
|
|
430
|
-
const filePattern = data?.filePattern as string || '*.json';
|
|
431
|
-
const maxFiles = data?.maxFiles as number;
|
|
432
|
-
|
|
433
|
-
log.info('🚀 [WEBHOOK] Adhoc inventory sync triggered', {
|
|
434
|
-
jobId,
|
|
435
|
-
filePattern,
|
|
436
|
-
maxFiles,
|
|
437
|
-
requestData: data,
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
// Create job entry FIRST (awaited to ensure job exists in KV)
|
|
441
|
-
await tracker.createJob(jobId, {
|
|
442
|
-
triggeredBy: 'manual',
|
|
443
|
-
stage: 'initialization',
|
|
444
|
-
status: 'queued',
|
|
445
|
-
startTime: executionStartTime,
|
|
446
|
-
options: { filePattern, maxFiles },
|
|
447
|
-
createdAt: new Date().toISOString(),
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
// ✅ Fire-and-forget: Start background processing WITHOUT await
|
|
451
|
-
// The promise continues execution after we return the response
|
|
452
|
-
runIngestion(ctx, jobId, tracker)
|
|
453
|
-
.then((result) => {
|
|
454
|
-
const duration = Date.now() - executionStartTime;
|
|
455
|
-
if (result.success) {
|
|
456
|
-
log.info('✅ [BACKGROUND] Inventory sync completed successfully', {
|
|
457
|
-
jobId,
|
|
458
|
-
filesProcessed: result.filesProcessed,
|
|
459
|
-
filesFailed: result.filesFailed,
|
|
460
|
-
recordsProcessed: result.recordsProcessed,
|
|
461
|
-
duration: `${duration}ms (${(duration / 1000).toFixed(2)}s)`,
|
|
462
|
-
});
|
|
463
|
-
return tracker.markCompleted(jobId, { ...result, duration });
|
|
464
|
-
} else {
|
|
465
|
-
log.error('❌ [BACKGROUND] Inventory sync failed', {
|
|
466
|
-
jobId,
|
|
467
|
-
error: result.error,
|
|
468
|
-
});
|
|
469
|
-
return tracker.markFailed(jobId, result.error || 'Unknown error');
|
|
470
|
-
}
|
|
471
|
-
})
|
|
472
|
-
.catch((error: unknown) => {
|
|
473
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
474
|
-
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
475
|
-
|
|
476
|
-
log.error('❌ [BACKGROUND] Inventory sync failed with exception', {
|
|
477
|
-
jobId,
|
|
478
|
-
error: errorMessage,
|
|
479
|
-
stack: errorStack,
|
|
480
|
-
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
481
|
-
recommendation: 'Check S3 credentials, bucket permissions, and network connectivity',
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
return tracker.markFailed(jobId, errorMessage);
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
// Return immediately with jobId (response sent with this return value)
|
|
488
|
-
return {
|
|
489
|
-
success: true,
|
|
490
|
-
jobId,
|
|
491
|
-
message: 'Inventory sync started in background',
|
|
492
|
-
statusEndpoint: `https://{workspace}.versori.run/inventory-batch-job-status`,
|
|
493
|
-
note: 'Poll the status endpoint with jobId to check progress',
|
|
494
|
-
};
|
|
495
|
-
})
|
|
496
|
-
);
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
---
|
|
500
|
-
|
|
501
|
-
#### `src/workflows/webhook/job-status-check.ts`
|
|
502
|
-
|
|
503
|
-
**Purpose**: Query job status and progress
|
|
504
|
-
**Trigger**: HTTP POST/GET
|
|
505
|
-
**Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-job-status`
|
|
506
|
-
**Request Body**: `{ "jobId": "inventory-batch-1234567890" }`
|
|
507
|
-
|
|
508
|
-
```typescript
|
|
509
|
-
import { webhook, fn } from '@versori/run';
|
|
510
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Webhook: Job Status Check
|
|
514
|
-
*
|
|
515
|
-
* Endpoint: POST https://{workspace}.versori.run/inventory-batch-job-status
|
|
516
|
-
* Request body: { "jobId": "inventory-batch-1234567890" }
|
|
517
|
-
*
|
|
518
|
-
* Pattern: webhook().then(fn()) - no external API needed, only KV storage
|
|
519
|
-
* Lightweight: Only queries KV store, no Fluent API calls
|
|
520
|
-
*/
|
|
521
|
-
export const jobStatusCheck = webhook('inventory-batch-job-status', {
|
|
522
|
-
response: { mode: 'sync' },
|
|
523
|
-
connection: 'inventory-batch-job-status',
|
|
524
|
-
}).then(
|
|
525
|
-
fn('status', async ctx => {
|
|
526
|
-
const { data, log, openKv } = ctx;
|
|
527
|
-
const jobId = data?.jobId as string;
|
|
528
|
-
|
|
529
|
-
if (!jobId) {
|
|
530
|
-
return { success: false, error: 'jobId required' };
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
534
|
-
const status = await tracker.getJob(jobId);
|
|
535
|
-
|
|
536
|
-
return status
|
|
537
|
-
? { success: true, jobId, ...status }
|
|
538
|
-
: { success: false, error: 'Job not found', jobId };
|
|
539
|
-
})
|
|
540
|
-
);
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
---
|
|
544
|
-
|
|
545
|
-
### 3. Entry Point (`index.ts`)
|
|
546
|
-
|
|
547
|
-
**Purpose**: Register all workflows with Versori platform
|
|
548
|
-
|
|
549
|
-
```typescript
|
|
550
|
-
/**
|
|
551
|
-
* Entry Point - Registers all workflows with Versori platform
|
|
552
|
-
*
|
|
553
|
-
* Versori automatically discovers and registers exported workflows
|
|
554
|
-
*
|
|
555
|
-
* File Structure:
|
|
556
|
-
* - src/workflows/scheduled/ → Time-based triggers (cron)
|
|
557
|
-
* - src/workflows/webhook/ → HTTP-based triggers (webhooks)
|
|
558
|
-
*/
|
|
559
|
-
|
|
560
|
-
// Scheduled workflows
|
|
561
|
-
export { dailyInventorySync } from './src/workflows/scheduled/daily-inventory-sync';
|
|
562
|
-
|
|
563
|
-
// Webhook workflows
|
|
564
|
-
export { adhocInventorySync } from './src/workflows/webhook/adhoc-inventory-sync';
|
|
565
|
-
export { jobStatusCheck } from './src/workflows/webhook/job-status-check';
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
**What Gets Exposed:**
|
|
569
|
-
- ✅ `adhocInventorySync` → `https://{workspace}.versori.run/inventory-batch-adhoc`
|
|
570
|
-
- ✅ `jobStatusCheck` → `https://{workspace}.versori.run/inventory-batch-job-status`
|
|
571
|
-
- ❌ `dailyInventorySync` → NOT exposed (runs automatically on cron)
|
|
572
|
-
|
|
573
|
-
## When to Use Batch API vs Event API
|
|
574
|
-
|
|
575
|
-
### ✅ Use Batch API (`createJob`/`sendBatch`) For:
|
|
576
|
-
|
|
577
|
-
| Entity Type | Use Case | Why Batch API |
|
|
578
|
-
| --------------------- | ---------------------------------- | ----------------------------------------------- |
|
|
579
|
-
| **Inventory** | Bulk inventory updates, daily sync | Optimized for high-volume, BPP change detection |
|
|
580
|
-
| **InventoryQuantity** | Inventory positions and quantities | Native Batch API entity type |
|
|
581
|
-
|
|
582
|
-
### ❌ Use Event API Instead For:
|
|
583
|
-
|
|
584
|
-
| Entity Type | Use Case | Why Event API / GraphQL |
|
|
585
|
-
| ------------------- | ------------------------------------- | --------------------------------------------- |
|
|
586
|
-
| **Products** | Product catalog sync, variant updates | Triggers workflows, validates business rules |
|
|
587
|
-
| **Locations** | Store/warehouse setup | Requires workflow orchestration |
|
|
588
|
-
| **Customers** | Customer registration/profile updates | Prefer GraphQL mutations (no Rubix support) |
|
|
589
|
-
| **Orders** | Updates/events (not creation) | Events fine for updates; creation via GraphQL |
|
|
590
|
-
| **Custom Entities** | Any entity needing workflow triggers | Full Rubix workflow support |
|
|
591
|
-
|
|
592
|
-
### 🔍 Use GraphQL Mutations For:
|
|
593
|
-
|
|
594
|
-
| Scenario | Why GraphQL |
|
|
595
|
-
| --------------------- | -------------------------------------- |
|
|
596
|
-
| **Single operations** | Create one record, update one record |
|
|
597
|
-
| **Complex queries** | Fetch data with relationships |
|
|
598
|
-
| **Testing/debugging** | Direct API control, immediate feedback |
|
|
599
|
-
|
|
600
|
-
Note:
|
|
601
|
-
|
|
602
|
-
- Orders: Use `createOrder` GraphQL mutation for order creation (this triggers the Order CREATED event in Rubix). Order updates/events are fine via Event API.
|
|
603
|
-
- Customers: Use GraphQL mutations (no Rubix workflow support for customers).
|
|
604
|
-
|
|
605
|
-
---
|
|
606
|
-
|
|
607
|
-
## Understanding BPP (Batch Pre-Processing)
|
|
608
|
-
|
|
609
|
-
**BPP** is Fluent's change detection system that filters out unchanged records before workflow processing.
|
|
610
|
-
|
|
611
|
-
Note on defaults and control:
|
|
612
|
-
|
|
613
|
-
- BPP is a Fluent platform feature. The default on/off behavior is controlled at your Fluent account level, not by the SDK.
|
|
614
|
-
- If you omit `meta.preprocessing` in `createJob()`, the account-level default applies.
|
|
615
|
-
- To force behavior per job, set `meta.preprocessing` explicitly (`'skip'` to disable for delta feeds).
|
|
616
|
-
|
|
617
|
-
### When to Use BPP (Default - Enabled)
|
|
618
|
-
|
|
619
|
-
**✅ Full Inventory Snapshots:**
|
|
620
|
-
|
|
621
|
-
- Daily complete inventory dumps
|
|
622
|
-
- Entire warehouse stock files
|
|
623
|
-
- Records may be identical to previous run
|
|
624
|
-
|
|
625
|
-
**Example:**
|
|
626
|
-
|
|
627
|
-
```typescript
|
|
628
|
-
// BPP enabled by default - filters unchanged records
|
|
629
|
-
const job = await client.createJob({
|
|
630
|
-
name: 'Daily Full Inventory',
|
|
631
|
-
retailerId: 'my-retailer',
|
|
632
|
-
// BPP automatically enabled - no meta needed
|
|
633
|
-
});
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
**What BPP does:**
|
|
637
|
-
|
|
638
|
-
- Compares incoming records with existing records in Fluent
|
|
639
|
-
- Filters out records with no changes
|
|
640
|
-
- Only passes changed/new records to workflows
|
|
641
|
-
- Significantly reduces workflow processing load
|
|
642
|
-
|
|
643
|
-
### When to Skip BPP
|
|
644
|
-
|
|
645
|
-
**✅ Delta Feeds (Pre-Filtered Data):**
|
|
646
|
-
|
|
647
|
-
- Hourly change files (only updates since last run)
|
|
648
|
-
- Event-driven inventory changes
|
|
649
|
-
- Pre-filtered data from source system
|
|
650
|
-
- All records are guaranteed to be changes
|
|
651
|
-
|
|
652
|
-
**Example:**
|
|
653
|
-
|
|
654
|
-
```typescript
|
|
655
|
-
// Skip BPP for delta feeds - all records are changes
|
|
656
|
-
const job = await client.createJob({
|
|
657
|
-
name: 'Hourly Delta Inventory',
|
|
658
|
-
retailerId: 'my-retailer',
|
|
659
|
-
meta: {
|
|
660
|
-
preprocessing: 'skip', // All records already filtered
|
|
661
|
-
},
|
|
662
|
-
});
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
**Performance Impact:**
|
|
666
|
-
|
|
667
|
-
- **Full snapshot + BPP**: 10,000 records → 500 changes → Fast
|
|
668
|
-
- **Full snapshot, no BPP**: 10,000 records → 10,000 processed → Slow
|
|
669
|
-
- **Delta feed + BPP**: 500 records → 500 changes → Fast (but BPP overhead wasted)
|
|
670
|
-
- **Delta feed, no BPP**: 500 records → 500 processed → Fastest
|
|
671
|
-
|
|
672
|
-
### Decision Guide
|
|
673
|
-
|
|
674
|
-
| Source Data Type | BPP Setting | Reason |
|
|
675
|
-
| ---------------------- | ----------------------- | ----------------------------------------------- |
|
|
676
|
-
| **Daily full dump** | Enabled (default) | Most records unchanged, BPP filters efficiently |
|
|
677
|
-
| **Hourly delta feed** | `preprocessing: 'skip'` | All records are changes, BPP overhead wasted |
|
|
678
|
-
| **Initial load** | Enabled (default) | No previous data, but good practice |
|
|
679
|
-
| **Manual corrections** | Enabled (default) | Unknown change ratio, let BPP filter |
|
|
680
|
-
| **Real-time events** | `preprocessing: 'skip'` | Each record is a change event |
|
|
681
|
-
|
|
682
|
-
---
|
|
683
|
-
|
|
684
|
-
## Processing Modes
|
|
685
|
-
|
|
686
|
-
**⚠️ IMPORTANT:** Choose ONE processing mode per connector. These are alternative patterns, not features to use together.
|
|
687
|
-
|
|
688
|
-
The SDK supports three processing modes for handling multiple files. **Select the mode that best fits your use case** and configure your connector accordingly:
|
|
689
|
-
|
|
690
|
-
### Mode 1: Per-File Processing (Recommended Default) ✅ IMPLEMENTED
|
|
691
|
-
|
|
692
|
-
**When to use:**
|
|
693
|
-
|
|
694
|
-
- Multiple large files that shouldn't be in memory together
|
|
695
|
-
- Need file-level consistency (file 3 fails → files 1-2 already archived)
|
|
696
|
-
- Memory constraints
|
|
697
|
-
- Fault isolation (one file failure doesn't affect others)
|
|
698
|
-
|
|
699
|
-
**Flow:**
|
|
700
|
-
|
|
701
|
-
```
|
|
702
|
-
FOR EACH FILE:
|
|
703
|
-
1. Download file
|
|
704
|
-
2. Parse JSON
|
|
705
|
-
3. Map records
|
|
706
|
-
4. Create Batch API job
|
|
707
|
-
5. Send batches
|
|
708
|
-
6. Write log
|
|
709
|
-
7. Archive file
|
|
710
|
-
8. Mark file processed
|
|
711
|
-
|
|
712
|
-
IF FILE FAILS:
|
|
713
|
-
- Move to /errors/
|
|
714
|
-
- Continue to next file
|
|
715
|
-
- Other files unaffected
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
**Configuration:**
|
|
719
|
-
|
|
720
|
-
```json
|
|
721
|
-
{
|
|
722
|
-
"processingMode": "per-file",
|
|
723
|
-
"maxFilesPerRun": 10
|
|
724
|
-
}
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
**Benefits:**
|
|
728
|
-
|
|
729
|
-
- ✅ File-level atomicity (each file processed independently)
|
|
730
|
-
- ✅ Low memory footprint (1 file at a time)
|
|
731
|
-
- ✅ Clear error isolation (failed file doesn't block others)
|
|
732
|
-
- ✅ Incremental progress (files archived as completed)
|
|
733
|
-
|
|
734
|
-
**Example implementation (see code section below)**
|
|
735
|
-
|
|
736
|
-
### Mode 2: Batch Processing (All Files at Once) ⚠️ OPTIONAL - Choose if needed
|
|
737
|
-
|
|
738
|
-
**When to use:**
|
|
739
|
-
|
|
740
|
-
- Small files (all fit in memory comfortably)
|
|
741
|
-
- Need one Batch API job for all files
|
|
742
|
-
- Atomic processing (all files or none)
|
|
743
|
-
|
|
744
|
-
**Flow:**
|
|
745
|
-
|
|
746
|
-
```
|
|
747
|
-
1. Download ALL files
|
|
748
|
-
2. Parse ALL files
|
|
749
|
-
3. Map ALL records
|
|
750
|
-
4. Create ONE Batch API job
|
|
751
|
-
5. Send ALL batches
|
|
752
|
-
6. Write logs
|
|
753
|
-
7. Archive ALL files
|
|
754
|
-
8. Mark ALL processed
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
**Configuration:**
|
|
758
|
-
|
|
759
|
-
```json
|
|
760
|
-
{
|
|
761
|
-
"processingMode": "batch",
|
|
762
|
-
"maxFilesPerRun": 10
|
|
763
|
-
}
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
**Benefits:**
|
|
767
|
-
|
|
768
|
-
- ✅ Single job for all files (simplifies tracking)
|
|
769
|
-
- ✅ Faster for many small files (parallel parsing possible)
|
|
770
|
-
|
|
771
|
-
**Drawbacks:**
|
|
772
|
-
|
|
773
|
-
- ❌ High memory usage (all files in memory)
|
|
774
|
-
- ❌ All-or-nothing (one failure affects all)
|
|
775
|
-
- ❌ No incremental progress
|
|
776
|
-
|
|
777
|
-
### Mode 3: Chunked Per-File Processing (Balanced) ⚠️ OPTIONAL - Choose if needed
|
|
778
|
-
|
|
779
|
-
**When to use:**
|
|
780
|
-
|
|
781
|
-
- Many files (e.g., 100+ files)
|
|
782
|
-
- Process N files at a time
|
|
783
|
-
- Balance between memory and parallelism
|
|
784
|
-
|
|
785
|
-
**Flow:**
|
|
786
|
-
|
|
787
|
-
```
|
|
788
|
-
CHUNK files into groups of N (e.g., 5 files per chunk):
|
|
789
|
-
|
|
790
|
-
FOR EACH CHUNK:
|
|
791
|
-
FOR EACH FILE in chunk:
|
|
792
|
-
1. Download file
|
|
793
|
-
2. Parse JSON
|
|
794
|
-
3. Map records
|
|
795
|
-
4. Create Batch API job
|
|
796
|
-
5. Send batches
|
|
797
|
-
6. Archive file
|
|
798
|
-
|
|
799
|
-
Wait for chunk to complete before next chunk
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
**Configuration:**
|
|
803
|
-
|
|
804
|
-
```json
|
|
805
|
-
{
|
|
806
|
-
"processingMode": "chunked",
|
|
807
|
-
"fileChunkSize": 5,
|
|
808
|
-
"maxFilesPerRun": 100
|
|
809
|
-
}
|
|
810
|
-
```
|
|
811
|
-
|
|
812
|
-
**Benefits:**
|
|
813
|
-
|
|
814
|
-
- ✅ Bounded memory usage (N files at a time)
|
|
815
|
-
- ✅ Better throughput than per-file (parallel processing within chunk)
|
|
816
|
-
- ✅ Incremental progress (per-chunk)
|
|
817
|
-
|
|
818
|
-
**Use cases:**
|
|
819
|
-
|
|
820
|
-
- High-volume file ingestion (100+ files per run)
|
|
821
|
-
- Rate limiting (control API load)
|
|
822
|
-
- Resource-constrained environments
|
|
823
|
-
|
|
824
|
-
### Comparison Matrix
|
|
825
|
-
|
|
826
|
-
| Aspect | Per-File | Batch | Chunked |
|
|
827
|
-
| ------------------- | ---------------------------------- | ----------------------------- | --------------------------------------- |
|
|
828
|
-
| **Memory Usage** | Low (1 file at a time) | High (all files) | Medium (N files) |
|
|
829
|
-
| **Consistency** | Per-file atomic | All-or-nothing | Per-chunk atomic |
|
|
830
|
-
| **Fault Isolation** | ✅ Best (1 file fails → others OK) | ❌ Worst (1 fails → all fail) | ✅ Good (chunk fails → other chunks OK) |
|
|
831
|
-
| **Performance** | Slower (sequential) | Fastest (parallel parsing) | Balanced |
|
|
832
|
-
| **Batch API Jobs** | N jobs (1 per file) | 1 job (all files) | N jobs (1 per file) |
|
|
833
|
-
| **Use Case** | Large files, strict consistency | Small files, fast processing | Many files, balanced approach |
|
|
834
|
-
| **Recommended For** | Production (safest) | Testing, small datasets | High-volume production |
|
|
835
|
-
|
|
836
|
-
> **📋 This Template Implementation:**
|
|
837
|
-
>
|
|
838
|
-
> **✅ This template implements ALL THREE modes** and selects based on `PROCESSING_MODE` variable.
|
|
839
|
-
>
|
|
840
|
-
> **Choose ONE mode per connector:**
|
|
841
|
-
> - **Default:** `PROCESSING_MODE=per-file` (recommended for production)
|
|
842
|
-
> - **Alternative:** `PROCESSING_MODE=batch` (for small files, atomic processing)
|
|
843
|
-
> - **Alternative:** `PROCESSING_MODE=chunked` (for high-volume scenarios)
|
|
844
|
-
>
|
|
845
|
-
> **Important:** Do NOT use multiple modes in the same connector. Each connector should use ONE consistent pattern.
|
|
846
|
-
|
|
847
|
-
### Decision Guide
|
|
848
|
-
|
|
849
|
-
| Source Data | File Count | File Size | Recommended Mode | Reason |
|
|
850
|
-
| -------------- | ---------- | ----------- | ---------------- | --------------------------------------- |
|
|
851
|
-
| Daily snapshot | 1-10 | >10MB each | **Per-File** | Memory efficient, fault isolation |
|
|
852
|
-
| Hourly deltas | 10-50 | <1MB each | **Batch** | Fast processing, small memory footprint |
|
|
853
|
-
| Real-time feed | 100+ | <100KB each | **Chunked** | Balanced throughput + memory |
|
|
854
|
-
| Initial load | 1 | >100MB | **Per-File** | Memory safety |
|
|
855
|
-
| Testing | 1-5 | Any | **Batch** | Simplicity |
|
|
856
|
-
|
|
857
|
-
---
|
|
858
|
-
|
|
859
|
-
## JSON File Format
|
|
860
|
-
|
|
861
|
-
### Sample: inventory.json (Standard JSON)
|
|
862
|
-
|
|
863
|
-
```json
|
|
864
|
-
{
|
|
865
|
-
"inventory": [
|
|
866
|
-
{
|
|
867
|
-
"locationRef": "LOC001",
|
|
868
|
-
"skuRef": "SKU-12345",
|
|
869
|
-
"qty": 100,
|
|
870
|
-
"type": "LAST_ON_HAND",
|
|
871
|
-
"status": "ACTIVE",
|
|
872
|
-
"expectedOn": "2025-01-25",
|
|
873
|
-
"attributes": {
|
|
874
|
-
"expiryDate": "2026-12-31",
|
|
875
|
-
"batchNumber": "BATCH-A001",
|
|
876
|
-
"condition": "NEW",
|
|
877
|
-
"storageZone": "ZONE-A"
|
|
878
|
-
}
|
|
879
|
-
},
|
|
880
|
-
{
|
|
881
|
-
"locationRef": "LOC001",
|
|
882
|
-
"skuRef": "SKU-67890",
|
|
883
|
-
"qty": 50,
|
|
884
|
-
"type": "LAST_ON_HAND",
|
|
885
|
-
"status": "ACTIVE",
|
|
886
|
-
"expectedOn": "2025-01-25",
|
|
887
|
-
"attributes": {
|
|
888
|
-
"expiryDate": "2026-06-30",
|
|
889
|
-
"batchNumber": "BATCH-A002",
|
|
890
|
-
"condition": "NEW",
|
|
891
|
-
"storageZone": "ZONE-B"
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
]
|
|
895
|
-
}
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
### Sample: inventory.jsonl (JSON Lines)
|
|
899
|
-
|
|
900
|
-
```jsonl
|
|
901
|
-
{"locationRef":"LOC001","skuRef":"SKU-12345","qty":100,"type":"LAST_ON_HAND","status":"ACTIVE","expectedOn":"2025-01-25","attributes":{"expiryDate":"2026-12-31","batchNumber":"BATCH-A001","condition":"NEW","storageZone":"ZONE-A"}}
|
|
902
|
-
{"locationRef":"LOC001","skuRef":"SKU-67890","qty":50,"type":"LAST_ON_HAND","status":"ACTIVE","expectedOn":"2025-01-25","attributes":{"expiryDate":"2026-06-30","batchNumber":"BATCH-A002","condition":"NEW","storageZone":"ZONE-B"}}
|
|
903
|
-
```
|
|
904
|
-
|
|
905
|
-
**JSON Structure:**
|
|
906
|
-
|
|
907
|
-
- Standard JSON: Root object with nested arrays (`{ "inventory": [...] }`)
|
|
908
|
-
- JSON Lines: One JSON object per line (streaming-friendly for large files)
|
|
909
|
-
- JSONParserService auto-detects format based on content
|
|
910
|
-
- Both formats support nested objects with dot notation
|
|
911
|
-
|
|
912
|
-
**Field Descriptions:**
|
|
913
|
-
|
|
914
|
-
- `locationRef`: Fluent location reference
|
|
915
|
-
- `skuRef`: Fluent SKU/product reference
|
|
916
|
-
- `qty`: Inventory quantity (integer)
|
|
917
|
-
- `type`: Inventory type (LAST_ON_HAND for full snapshots, DELTA for incremental changes)
|
|
918
|
-
- `status`: Record status (ACTIVE, INACTIVE)
|
|
919
|
-
- `expectedOn`: Expected date (ISO 8601 or parseable format)
|
|
920
|
-
- `attributes.expiryDate`: Product expiry date (optional)
|
|
921
|
-
- `attributes.batchNumber`: Manufacturing batch/lot number (optional)
|
|
922
|
-
- `attributes.condition`: Product condition (NEW, USED, DAMAGED)
|
|
923
|
-
- `attributes.storageZone`: Warehouse zone identifier (optional)
|
|
924
|
-
|
|
925
|
-
### JSON Field Mapping
|
|
926
|
-
|
|
927
|
-
**IMPORTANT**: JSON uses direct field access (no special prefix needed):
|
|
928
|
-
|
|
929
|
-
```json
|
|
930
|
-
{
|
|
931
|
-
"fields": {
|
|
932
|
-
"locationRef": { "source": "locationRef", "required": true },
|
|
933
|
-
"skuRef": { "source": "skuRef", "required": true }
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
```
|
|
937
|
-
|
|
938
|
-
**Why**: Unlike XML attributes (which need `@` prefix), JSON fields are accessed directly by name.
|
|
939
|
-
|
|
940
|
-
### Array Handling (Standard JSON vs JSON Lines)
|
|
941
|
-
|
|
942
|
-
**JSONParserService behavior:**
|
|
943
|
-
|
|
944
|
-
- **Standard JSON**: Returns object or array based on structure
|
|
945
|
-
- **JSON Lines**: Returns array of objects (one per line)
|
|
946
|
-
|
|
947
|
-
**Solution**: Always normalize to array after parsing:
|
|
948
|
-
|
|
949
|
-
```typescript
|
|
950
|
-
const parsed = await parser.parse(jsonContent, { format: 'json' | 'jsonl' });
|
|
951
|
-
|
|
952
|
-
// Extract inventory array - handle different JSON structures
|
|
953
|
-
let records: any[] = [];
|
|
954
|
-
if (format === 'jsonl') {
|
|
955
|
-
records = Array.isArray(parsed) ? parsed : [parsed];
|
|
956
|
-
} else {
|
|
957
|
-
if (Array.isArray(parsed)) {
|
|
958
|
-
records = parsed; // Root level array
|
|
959
|
-
} else if (parsed?.inventory && Array.isArray(parsed.inventory)) {
|
|
960
|
-
records = parsed.inventory; // { "inventory": [...] }
|
|
961
|
-
} else if (parsed?.records && Array.isArray(parsed.records)) {
|
|
962
|
-
records = parsed.records; // { "records": [...] }
|
|
963
|
-
} else {
|
|
964
|
-
records = [parsed]; // Single object
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
```
|
|
968
|
-
|
|
969
|
-
---
|
|
970
|
-
|
|
971
|
-
## Service Implementation
|
|
972
|
-
|
|
973
|
-
### File: `src/services/inventory-sync.service.ts`
|
|
974
|
-
|
|
975
|
-
**Complete implementation showing all SDK patterns:**
|
|
976
|
-
|
|
977
|
-
```typescript
|
|
978
|
-
import { Buffer } from 'node:buffer'; // ✅ Required for Versori/Deno runtime
|
|
979
|
-
import {
|
|
980
|
-
createClient,
|
|
981
|
-
S3DataSource,
|
|
982
|
-
JSONParserService,
|
|
983
|
-
UniversalMapper,
|
|
984
|
-
VersoriFileTracker,
|
|
985
|
-
JobTracker,
|
|
986
|
-
extractFileName,
|
|
987
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
988
|
-
import type { FileMetadata } from '@fluentcommerce/fc-connect-sdk';
|
|
989
|
-
import inventoryMapping from '../config/inventory.batch.json' with { type: 'json' }; // ✅ External JSON import
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* Shared inventory ingestion orchestration logic
|
|
993
|
-
* Used by both scheduled and webhook workflows
|
|
994
|
-
*
|
|
995
|
-
* @param ctx - Versori HTTP context
|
|
996
|
-
* @param jobId - Unique job identifier
|
|
997
|
-
* @param tracker - JobTracker instance
|
|
998
|
-
*/
|
|
999
|
-
export async function runIngestion(
|
|
1000
|
-
ctx: any,
|
|
1001
|
-
jobId: string,
|
|
1002
|
-
tracker: JobTracker
|
|
1003
|
-
) {
|
|
1004
|
-
const { log, activation, openKv } = ctx;
|
|
1005
|
-
const startTime = Date.now();
|
|
1006
|
-
let s3: S3DataSource | null = null;
|
|
1007
|
-
|
|
1008
|
-
// Optional feature toggles
|
|
1009
|
-
const enableFileTracking = activation.getVariable('enableFileTracking') !== 'false';
|
|
1010
|
-
const detailedLogging = activation.getVariable('detailedLogging') === 'true';
|
|
1011
|
-
|
|
1012
|
-
try {
|
|
1013
|
-
// ========================================
|
|
1014
|
-
// CLIENT INITIALIZATION (with retailerId and validation)
|
|
1015
|
-
// ========================================
|
|
1016
|
-
log.info('🔧 [InventorySync] Initializing Fluent Commerce client...');
|
|
1017
|
-
|
|
1018
|
-
const client = await createClient({
|
|
1019
|
-
...ctx,
|
|
1020
|
-
validateConnection: true, // ✅ Validate connection on creation
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
// ✅ CRITICAL: Set retailerId for Batch API
|
|
1024
|
-
const retailerId = activation.getVariable('fluentRetailerId');
|
|
1025
|
-
if (!retailerId) {
|
|
1026
|
-
log.error('❌ [InventorySync] Missing required fluentRetailerId activation variable');
|
|
1027
|
-
throw new Error('fluentRetailerId activation variable is required for Batch API');
|
|
1028
|
-
}
|
|
1029
|
-
client.setRetailerId(retailerId);
|
|
1030
|
-
|
|
1031
|
-
log.info('✅ [InventorySync] Client initialized and validated', { retailerId });
|
|
1032
|
-
|
|
1033
|
-
// ========================================
|
|
1034
|
-
// S3 DATA SOURCE INITIALIZATION
|
|
1035
|
-
// ========================================
|
|
1036
|
-
log.info('🔧 [InventorySync] Initializing S3 data source...');
|
|
1037
|
-
|
|
1038
|
-
const s3Config = {
|
|
1039
|
-
bucket: activation.getVariable('s3BucketName'),
|
|
1040
|
-
region: activation.getVariable('awsRegion') || 'us-east-1',
|
|
1041
|
-
accessKeyId: activation.getVariable('awsAccessKeyId'),
|
|
1042
|
-
secretAccessKey: activation.getVariable('awsSecretAccessKey'),
|
|
1043
|
-
prefix: activation.getVariable('s3Prefix') || 'inventory/',
|
|
1044
|
-
};
|
|
1045
|
-
|
|
1046
|
-
s3 = new S3DataSource(
|
|
1047
|
-
{
|
|
1048
|
-
type: 'S3_JSON',
|
|
1049
|
-
connectionId: 's3-inventory-sync',
|
|
1050
|
-
name: 'inventory-sync',
|
|
1051
|
-
s3Config,
|
|
1052
|
-
},
|
|
1053
|
-
log
|
|
1054
|
-
);
|
|
1055
|
-
|
|
1056
|
-
log.info('✅ [InventorySync] S3 data source initialized', {
|
|
1057
|
-
bucket: s3Config.bucket,
|
|
1058
|
-
region: s3Config.region,
|
|
1059
|
-
prefix: s3Config.prefix,
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
// ========================================
|
|
1063
|
-
// SERVICE INITIALIZATION
|
|
1064
|
-
// ========================================
|
|
1065
|
-
const parser = new JSONParserService();
|
|
1066
|
-
const mapper = new UniversalMapper(inventoryMapping);
|
|
1067
|
-
const kv = openKv(':project:');
|
|
1068
|
-
const fileTracker = enableFileTracking
|
|
1069
|
-
? new VersoriFileTracker(kv, 's3-json-inventory-sync')
|
|
1070
|
-
: null;
|
|
1071
|
-
const bppEnabled = activation.getVariable('bppEnabled') !== 'false';
|
|
1072
|
-
|
|
1073
|
-
if (detailedLogging) {
|
|
1074
|
-
log.info('🔍 [InventorySync] Configuration loaded', {
|
|
1075
|
-
bppEnabled,
|
|
1076
|
-
enableFileTracking,
|
|
1077
|
-
detailedLogging,
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// ========================================
|
|
1082
|
-
// FILE DISCOVERY
|
|
1083
|
-
// ========================================
|
|
1084
|
-
log.info('🔍 [InventorySync] Discovering files on S3...');
|
|
1085
|
-
|
|
1086
|
-
const files = await s3.listFiles({ prefix: s3Config.prefix });
|
|
1087
|
-
log.info('📄 [InventorySync] File discovery complete', { count: files.length });
|
|
1088
|
-
|
|
1089
|
-
if (files.length === 0) {
|
|
1090
|
-
log.info('⚠️ [InventorySync] No files found to process');
|
|
1091
|
-
return { success: true, message: 'No files to process', filesProcessed: 0 };
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// ========================================
|
|
1095
|
-
// FILE PROCESSING
|
|
1096
|
-
// ========================================
|
|
1097
|
-
const results = [];
|
|
1098
|
-
let fileIndex = 0;
|
|
1099
|
-
|
|
1100
|
-
for (const file of files) {
|
|
1101
|
-
fileIndex++;
|
|
1102
|
-
const fileStartTime = Date.now();
|
|
1103
|
-
const fileName = extractFileName(file.path);
|
|
1104
|
-
|
|
1105
|
-
log.info(`📄 [FILE ${fileIndex}/${files.length}] Processing: ${fileName}`);
|
|
1106
|
-
|
|
1107
|
-
try {
|
|
1108
|
-
// Check if already processed
|
|
1109
|
-
if (fileTracker && (await fileTracker.wasFileProcessed(file.name))) {
|
|
1110
|
-
log.info(`⏭️ [FILE ${fileIndex}/${files.length}] Skipping already processed: ${fileName}`);
|
|
1111
|
-
results.push({ fileName, success: true, skipped: true });
|
|
1112
|
-
continue;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// Download and parse
|
|
1116
|
-
if (detailedLogging) {
|
|
1117
|
-
log.info(`🔍 [FILE ${fileIndex}/${files.length}] Downloading: ${fileName}`);
|
|
1118
|
-
}
|
|
1119
|
-
const jsonContent = (await s3.downloadFile(file.path, { encoding: 'utf8' })) as string;
|
|
1120
|
-
const parsed = await parser.parse(jsonContent);
|
|
1121
|
-
|
|
1122
|
-
// Extract inventory array
|
|
1123
|
-
const records = Array.isArray(parsed) ? parsed :
|
|
1124
|
-
parsed?.inventory ? parsed.inventory :
|
|
1125
|
-
[parsed];
|
|
1126
|
-
|
|
1127
|
-
if (records.length === 0) {
|
|
1128
|
-
log.warn(`⚠️ [FILE ${fileIndex}/${files.length}] No records found in: ${fileName}`);
|
|
1129
|
-
continue;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
log.info(`🔄 [FILE ${fileIndex}/${files.length}] Transforming ${records.length} records`);
|
|
1133
|
-
|
|
1134
|
-
// Transform records
|
|
1135
|
-
const mappedRecords = [];
|
|
1136
|
-
const aggregatedSkippedFields: string[] = [];
|
|
1137
|
-
for (const rec of records) {
|
|
1138
|
-
const sourceDataWithContext = { ...rec, $context: { retailerId } };
|
|
1139
|
-
const mappingResult = await mapper.map(sourceDataWithContext);
|
|
1140
|
-
if (mappingResult.success) {
|
|
1141
|
-
mappedRecords.push(mappingResult.data);
|
|
1142
|
-
}
|
|
1143
|
-
// Aggregate skipped fields
|
|
1144
|
-
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
1145
|
-
for (const fieldName of mappingResult.skippedFields) {
|
|
1146
|
-
if (!aggregatedSkippedFields.includes(fieldName)) {
|
|
1147
|
-
aggregatedSkippedFields.push(fieldName);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
if (aggregatedSkippedFields.length > 0) {
|
|
1154
|
-
log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
1155
|
-
file: fileName,
|
|
1156
|
-
skippedFields: aggregatedSkippedFields,
|
|
1157
|
-
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
1158
|
-
});
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
if (mappedRecords.length === 0) {
|
|
1162
|
-
log.warn(`⚠️ [FILE ${fileIndex}/${files.length}] No valid records after mapping: ${fileName}`, {
|
|
1163
|
-
recommendation: 'Check mapping configuration and required fields',
|
|
1164
|
-
});
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Create job and send batches
|
|
1169
|
-
log.info(`📦 [FILE ${fileIndex}/${files.length}] Creating Batch API job for ${mappedRecords.length} records`);
|
|
1170
|
-
|
|
1171
|
-
const job = await client.createJob({
|
|
1172
|
-
name: `s3-json-${fileName}-${Date.now()}`,
|
|
1173
|
-
meta: bppEnabled ? undefined : { preprocessing: 'skip' },
|
|
1174
|
-
});
|
|
1175
|
-
|
|
1176
|
-
await tracker.updateJob(jobId, {
|
|
1177
|
-
status: 'processing',
|
|
1178
|
-
details: { fileName, recordCount: mappedRecords.length },
|
|
1179
|
-
});
|
|
1180
|
-
|
|
1181
|
-
// ? Enhanced: Extract context for progress logging
|
|
1182
|
-
const uniqueLocations = [...new Set(mappedRecords.map((r: any) => r.locationRef))];
|
|
1183
|
-
const sampleSKUs = mappedRecords.slice(0, 5).map((r: any) => r.skuRef);
|
|
1184
|
-
|
|
1185
|
-
// ? Enhanced: Start logging with context
|
|
1186
|
-
log.info(`📤 [BatchProcessor] Sending batch for file "${fileName}"`, {
|
|
1187
|
-
totalRecords: mappedRecords.length,
|
|
1188
|
-
locations: uniqueLocations.join(', '),
|
|
1189
|
-
sampleSKUs: sampleSKUs.join(', '),
|
|
1190
|
-
jobId: job.id
|
|
1191
|
-
});
|
|
1192
|
-
|
|
1193
|
-
// Send batches (fire-and-forget)
|
|
1194
|
-
await client.sendBatch(job.id, {
|
|
1195
|
-
action: 'UPSERT',
|
|
1196
|
-
entityType: 'INVENTORY',
|
|
1197
|
-
entities: mappedRecords,
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
// ? Enhanced: Completion logging
|
|
1201
|
-
log.info(`✅ [BatchProcessor] Batch sent successfully for file "${fileName}"`, {
|
|
1202
|
-
totalRecords: mappedRecords.length,
|
|
1203
|
-
jobId: job.id,
|
|
1204
|
-
duration: Date.now() - fileStartTime
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
// ? Enhanced: Clear large arrays after processing to free memory
|
|
1208
|
-
mappedRecords.length = 0;
|
|
1209
|
-
|
|
1210
|
-
// Mark as processed
|
|
1211
|
-
if (fileTracker) {
|
|
1212
|
-
await fileTracker.markFileProcessed(file.name, {
|
|
1213
|
-
recordCount: mappedRecords.length,
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const fileDuration = Date.now() - fileStartTime;
|
|
1218
|
-
results.push({
|
|
1219
|
-
fileName,
|
|
1220
|
-
success: true,
|
|
1221
|
-
recordCount: mappedRecords.length,
|
|
1222
|
-
duration: fileDuration,
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
log.info(`✅ [FILE ${fileIndex}/${files.length}] Successfully processed: ${fileName}`, {
|
|
1226
|
-
records: mappedRecords.length,
|
|
1227
|
-
jobId: job.id,
|
|
1228
|
-
duration: `${fileDuration}ms (${(fileDuration / 1000).toFixed(2)}s)`,
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
|
-
} catch (error: any) {
|
|
1232
|
-
const fileDuration = Date.now() - fileStartTime;
|
|
1233
|
-
log.error(`❌ [FILE ${fileIndex}/${files.length}] Processing failed: ${fileName}`, {
|
|
1234
|
-
error: error?.message,
|
|
1235
|
-
stack: error?.stack,
|
|
1236
|
-
duration: `${fileDuration}ms`,
|
|
1237
|
-
recommendation: 'Check S3 permissions, file format, and mapping configuration',
|
|
1238
|
-
});
|
|
1239
|
-
results.push({ fileName, success: false, error: error?.message, duration: fileDuration });
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
const totalDuration = Date.now() - startTime;
|
|
1244
|
-
const successCount = results.filter((r) => r.success && !r.skipped).length;
|
|
1245
|
-
|
|
1246
|
-
log.info('✅ [InventorySync] Batch processing complete', {
|
|
1247
|
-
total: files.length,
|
|
1248
|
-
processed: successCount,
|
|
1249
|
-
skipped: results.filter((r) => r.skipped).length,
|
|
1250
|
-
failed: results.filter((r) => !r.success).length,
|
|
1251
|
-
duration: `${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)`,
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
return {
|
|
1255
|
-
success: true,
|
|
1256
|
-
results,
|
|
1257
|
-
filesProcessed: successCount,
|
|
1258
|
-
duration: totalDuration,
|
|
1259
|
-
};
|
|
1260
|
-
|
|
1261
|
-
} catch (error: any) {
|
|
1262
|
-
const totalDuration = Date.now() - startTime;
|
|
1263
|
-
log.error('❌ [InventorySync] Fatal error during ingestion', {
|
|
1264
|
-
message: error?.message,
|
|
1265
|
-
stack: error?.stack,
|
|
1266
|
-
duration: `${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)`,
|
|
1267
|
-
recommendation: 'Check activation variables, S3 credentials, and Fluent API connectivity',
|
|
1268
|
-
});
|
|
1269
|
-
throw error; // Re-throw for workflow error handling
|
|
1270
|
-
} finally {
|
|
1271
|
-
// ✅ CRITICAL: Always dispose S3 connection
|
|
1272
|
-
if (s3) {
|
|
1273
|
-
try {
|
|
1274
|
-
await s3.dispose();
|
|
1275
|
-
log.info('✅ [InventorySync] S3 connection disposed successfully');
|
|
1276
|
-
} catch (disposeError: any) {
|
|
1277
|
-
log.error('⚠️ [InventorySync] Failed to dispose S3 connection', {
|
|
1278
|
-
error: disposeError?.message,
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
```
|
|
1285
|
-
|
|
1286
|
-
**Key Patterns Demonstrated:**
|
|
1287
|
-
- ✅ Buffer import for Versori/Deno compatibility
|
|
1288
|
-
- ✅ External JSON mapping import with `{ type: 'json' }`
|
|
1289
|
-
- ✅ `validateConnection: true` for client initialization
|
|
1290
|
-
- ✅ `setRetailerId()` call after client creation (REQUIRED for Batch API)
|
|
1291
|
-
- ✅ Emoji-based logging (🚀 ✅ ❌ ⚠️ 🔍 📄) for visual log scanning
|
|
1292
|
-
- ✅ Execution boundaries with ═══ separators
|
|
1293
|
-
- ✅ `extractFileName()` usage for clean file names
|
|
1294
|
-
- ✅ Optional feature toggles (enableFileTracking, detailedLogging)
|
|
1295
|
-
- ✅ Duration tracking at file and workflow levels (ms and seconds)
|
|
1296
|
-
- ✅ Recommendations in error logs for actionable debugging
|
|
1297
|
-
- ✅ S3 `dispose()` in finally block with error handling
|
|
1298
|
-
- ✅ VersoriFileTracker for state management (optional via toggle)
|
|
1299
|
-
- ✅ JobTracker for job lifecycle tracking
|
|
1300
|
-
|
|
1301
|
-
---
|
|
1302
|
-
|
|
1303
|
-
## Code Flow Explanation
|
|
1304
|
-
|
|
1305
|
-
### Initialization Phase
|
|
1306
|
-
|
|
1307
|
-
**Logger Setup:**
|
|
1308
|
-
|
|
1309
|
-
```typescript
|
|
1310
|
-
// ✅ CORRECT: Use native Versori log from context
|
|
1311
|
-
const { log } = ctx;
|
|
1312
|
-
log.info('Starting workflow');
|
|
1313
|
-
|
|
1314
|
-
// ❌ WRONG: LoggingService (removed - use native log on Versori)
|
|
1315
|
-
// import { LoggingService } from '@fluentcommerce/fc-connect-sdk';
|
|
1316
|
-
// const logging = new LoggingService();
|
|
1317
|
-
// const log = logging.createLogger({ logLevel: 'info' });
|
|
1318
|
-
```
|
|
1319
|
-
|
|
1320
|
-
- Versori provides `log` in the context - use it directly
|
|
1321
|
-
|
|
1322
|
-
**Client Creation:**
|
|
1323
|
-
|
|
1324
|
-
```typescript
|
|
1325
|
-
// Pass entire Versori context object
|
|
1326
|
-
const client = await createClient(ctx);
|
|
1327
|
-
```
|
|
1328
|
-
|
|
1329
|
-
- `createClient(ctx)` accepts entire Versori context (fetch, connections, log, activation)
|
|
1330
|
-
- Auto-detects Versori platform from context
|
|
1331
|
-
- Returns `FluentClient` configured with OAuth2 from connections.fluent_commerce
|
|
1332
|
-
- OAuth2 authentication handled automatically
|
|
1333
|
-
- This is the **CORRECT** pattern for Versori workflows
|
|
1334
|
-
|
|
1335
|
-
**S3 Data Source:**
|
|
1336
|
-
|
|
1337
|
-
```typescript
|
|
1338
|
-
const s3 = new S3DataSource(
|
|
1339
|
-
{
|
|
1340
|
-
type: 'S3_JSON', // Type indicates JSON files (metadata only)
|
|
1341
|
-
connectionId: 's3-inventory-sync', // Unique identifier (required)
|
|
1342
|
-
name: 'inventory-sync', // Human-readable name (required)
|
|
1343
|
-
s3Config: {
|
|
1344
|
-
bucket: 'my-inventory-bucket',
|
|
1345
|
-
region: 'us-east-1',
|
|
1346
|
-
accessKeyId: 'AKIAXXXX',
|
|
1347
|
-
secretAccessKey: 'xxxxxxxx',
|
|
1348
|
-
},
|
|
1349
|
-
},
|
|
1350
|
-
log
|
|
1351
|
-
);
|
|
1352
|
-
```
|
|
1353
|
-
|
|
1354
|
-
- **Constructor params:** `(config: S3DataSourceConfig, log)` - Versori native log, no explicit typing needed
|
|
1355
|
-
- `connectionId` and `name` are required in config
|
|
1356
|
-
- `type` must be `'S3_JSON'` for JSON files
|
|
1357
|
-
- Enhanced retry logic with exponential backoff
|
|
1358
|
-
|
|
1359
|
-
### File Discovery Phase
|
|
1360
|
-
|
|
1361
|
-
**List Files:**
|
|
1362
|
-
|
|
1363
|
-
```typescript
|
|
1364
|
-
const files = await s3.listFiles({
|
|
1365
|
-
prefix: config.s3.prefix, // Override config (optional)
|
|
1366
|
-
maxKeys: 1000, // Maximum files to retrieve
|
|
1367
|
-
});
|
|
1368
|
-
```
|
|
1369
|
-
|
|
1370
|
-
- Returns `FileMetadata[]` with: `name`, `lastModified`, `size`, `path`, `source`
|
|
1371
|
-
- Files sorted by modification time (newest first)
|
|
1372
|
-
- Directories excluded automatically
|
|
1373
|
-
|
|
1374
|
-
**State Check:**
|
|
1375
|
-
|
|
1376
|
-
```typescript
|
|
1377
|
-
const alreadyProcessed = await fileTracker.wasFileProcessed(file.name);
|
|
1378
|
-
if (alreadyProcessed) {
|
|
1379
|
-
continue; // Skip already processed files
|
|
1380
|
-
}
|
|
1381
|
-
```
|
|
1382
|
-
|
|
1383
|
-
- Uses Versori KV store (`openKv()`) for distributed state
|
|
1384
|
-
- Prevents duplicate processing across workflow runs
|
|
1385
|
-
- State persists between executions
|
|
1386
|
-
|
|
1387
|
-
### File Processing Phase
|
|
1388
|
-
|
|
1389
|
-
**Download File:**
|
|
1390
|
-
|
|
1391
|
-
```typescript
|
|
1392
|
-
const jsonContent = (await s3.downloadFile(file.path, {
|
|
1393
|
-
encoding: 'utf8',
|
|
1394
|
-
})) as string;
|
|
1395
|
-
```
|
|
1396
|
-
|
|
1397
|
-
- **Important:** Cast to `string` when `encoding` is specified
|
|
1398
|
-
- Without encoding, returns `Buffer`
|
|
1399
|
-
- Supports streaming for large files
|
|
1400
|
-
|
|
1401
|
-
**Parse JSON:**
|
|
1402
|
-
|
|
1403
|
-
```typescript
|
|
1404
|
-
const jsonFormat = activation.getVariable('jsonFormat') || 'json';
|
|
1405
|
-
const parsed = await parser.parse(jsonContent, { format: jsonFormat });
|
|
1406
|
-
|
|
1407
|
-
// Extract inventory array - handle different JSON structures
|
|
1408
|
-
let records: any[] = [];
|
|
1409
|
-
if (jsonFormat === 'jsonl') {
|
|
1410
|
-
records = Array.isArray(parsed) ? parsed : [parsed];
|
|
1411
|
-
} else {
|
|
1412
|
-
if (Array.isArray(parsed)) {
|
|
1413
|
-
records = parsed; // Root level array
|
|
1414
|
-
} else if (parsed?.inventory && Array.isArray(parsed.inventory)) {
|
|
1415
|
-
records = parsed.inventory; // { "inventory": [...] }
|
|
1416
|
-
} else {
|
|
1417
|
-
records = [parsed]; // Single object
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
```
|
|
1421
|
-
|
|
1422
|
-
- Parses JSON into JavaScript object or array
|
|
1423
|
-
- **Format detection**: Auto-detects standard JSON vs JSON Lines
|
|
1424
|
-
- Always normalize to array for consistent processing
|
|
1425
|
-
- Throws `ParsingError` on invalid JSON
|
|
1426
|
-
|
|
1427
|
-
**JSON Field Handling:**
|
|
1428
|
-
|
|
1429
|
-
- Direct field access by name (no special prefix needed)
|
|
1430
|
-
- Example: `{ "locationRef": "LOC001" }` → `{ locationRef: 'LOC001' }`
|
|
1431
|
-
- Use simple paths like `"locationRef"` in mapping config
|
|
1432
|
-
|
|
1433
|
-
**Transform Data:**
|
|
1434
|
-
|
|
1435
|
-
```typescript
|
|
1436
|
-
const mapper = new UniversalMapper(inventoryMapping);
|
|
1437
|
-
|
|
1438
|
-
// Map each record directly (no wrapper needed for JSON)
|
|
1439
|
-
const mappingResult = await mapper.map(rec);
|
|
1440
|
-
|
|
1441
|
-
if (!mappingResult.success) {
|
|
1442
|
-
// Mapping validation failed for required fields
|
|
1443
|
-
log.warn('Mapping failed:', mappingResult.errors);
|
|
1444
|
-
continue; // Skip invalid records
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
mappedRecords.push(mappingResult.data);
|
|
1448
|
-
```
|
|
1449
|
-
|
|
1450
|
-
- Direct mapping (no need to wrap in object like XML)
|
|
1451
|
-
- `MappingResult`: `{ success: boolean, data: any, errors?: string[] }`
|
|
1452
|
-
- `success: false` only if required fields fail
|
|
1453
|
-
- Optional field errors reported but don't fail mapping
|
|
1454
|
-
|
|
1455
|
-
### Batch API Phase
|
|
1456
|
-
|
|
1457
|
-
**Create Job:**
|
|
1458
|
-
|
|
1459
|
-
```typescript
|
|
1460
|
-
// Full snapshot (BPP enabled by default)
|
|
1461
|
-
const job = await client.createJob({
|
|
1462
|
-
name: 'Daily Full Inventory',
|
|
1463
|
-
retailerId: 'my-retailer',
|
|
1464
|
-
// BPP automatically enabled - filters unchanged records
|
|
1465
|
-
});
|
|
1466
|
-
|
|
1467
|
-
// Delta feed (skip BPP)
|
|
1468
|
-
const job = await client.createJob({
|
|
1469
|
-
name: 'Hourly Deltas',
|
|
1470
|
-
retailerId: 'my-retailer',
|
|
1471
|
-
meta: {
|
|
1472
|
-
preprocessing: 'skip', // All records are changes
|
|
1473
|
-
},
|
|
1474
|
-
});
|
|
1475
|
-
```
|
|
1476
|
-
|
|
1477
|
-
- Returns job object with `id` and metadata
|
|
1478
|
-
- `retailerId` required for tenant isolation
|
|
1479
|
-
- BPP enabled by default, skip with `meta: { preprocessing: 'skip' }`
|
|
1480
|
-
|
|
1481
|
-
**Send Batches:**
|
|
1482
|
-
|
|
1483
|
-
```typescript
|
|
1484
|
-
const batch = await client.sendBatch(jobId, {
|
|
1485
|
-
action: 'UPSERT', // Most common: create or update
|
|
1486
|
-
entityType: 'INVENTORY', // Entity type for inventory
|
|
1487
|
-
source: 'S3_JSON', // Data source identifier (optional)
|
|
1488
|
-
event: 'InventoryChanged', // Valid Rubix event (optional, defaults to InventoryChanged)
|
|
1489
|
-
entities: chunk, // Array of inventory records
|
|
1490
|
-
});
|
|
1491
|
-
```
|
|
1492
|
-
|
|
1493
|
-
- `action`: `'UPSERT'` (currently the only action supported by Fluent Batch API)
|
|
1494
|
-
- `entityType`: `'INVENTORY'` for inventory records
|
|
1495
|
-
- `entities`: Array of transformed inventory objects
|
|
1496
|
-
- Returns batch object with `id` for tracking
|
|
1497
|
-
|
|
1498
|
-
**Fire-and-Forget Pattern:**
|
|
1499
|
-
|
|
1500
|
-
```typescript
|
|
1501
|
-
const batch = await client.sendBatch(jobId, { ... });
|
|
1502
|
-
log.info('Batch sent (fire-and-forget)', { batchId: batch.id });
|
|
1503
|
-
|
|
1504
|
-
// Record batch details for audit trail
|
|
1505
|
-
result.batches.push({
|
|
1506
|
-
batchId: batch.id,
|
|
1507
|
-
recordCount: chunk.length,
|
|
1508
|
-
timestamp: new Date().toISOString(),
|
|
1509
|
-
status: 'SENT',
|
|
1510
|
-
});
|
|
1511
|
-
```
|
|
1512
|
-
|
|
1513
|
-
- No polling - batches sent and tracked immediately
|
|
1514
|
-
- Batch details recorded for audit logging
|
|
1515
|
-
- Log files written to S3 for tracking (optional, configurable)
|
|
1516
|
-
|
|
1517
|
-
### Cleanup Phase
|
|
1518
|
-
|
|
1519
|
-
**Archive File:**
|
|
1520
|
-
|
|
1521
|
-
```typescript
|
|
1522
|
-
await s3.moveFile(
|
|
1523
|
-
file.path,
|
|
1524
|
-
`${config.s3.archivePrefix}${file.name}`
|
|
1525
|
-
);
|
|
1526
|
-
```
|
|
1527
|
-
|
|
1528
|
-
- Moves file on S3 (atomic operation)
|
|
1529
|
-
- Creates destination prefix if needed
|
|
1530
|
-
- Original file removed after successful move
|
|
1531
|
-
|
|
1532
|
-
**Mark Processed:**
|
|
1533
|
-
|
|
1534
|
-
```typescript
|
|
1535
|
-
await fileTracker.markFileProcessed(file.name, {
|
|
1536
|
-
recordCount: batchResults.totalSent,
|
|
1537
|
-
batchCount: batchResults.batchCount,
|
|
1538
|
-
jobId: job.id,
|
|
1539
|
-
});
|
|
1540
|
-
```
|
|
1541
|
-
|
|
1542
|
-
- Stores metadata in Versori KV store
|
|
1543
|
-
- Metadata optional but useful for audit trails
|
|
1544
|
-
- Prevents reprocessing in future runs
|
|
1545
|
-
|
|
1546
|
-
**Dispose Resources:**
|
|
1547
|
-
|
|
1548
|
-
```typescript
|
|
1549
|
-
await s3.dispose();
|
|
1550
|
-
```
|
|
1551
|
-
|
|
1552
|
-
- Releases S3 connection pool
|
|
1553
|
-
- Should be called at end of workflow in `finally` block
|
|
1554
|
-
- Ensures proper cleanup
|
|
1555
|
-
|
|
1556
|
-
---
|
|
1557
|
-
|
|
1558
|
-
## Versori Activation Variables
|
|
1559
|
-
|
|
1560
|
-
Configure in Versori platform settings:
|
|
1561
|
-
|
|
1562
|
-
```bash
|
|
1563
|
-
# S3 Configuration
|
|
1564
|
-
S3_BUCKET_NAME=my-inventory-bucket
|
|
1565
|
-
AWS_REGION=us-east-1
|
|
1566
|
-
AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXX
|
|
1567
|
-
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
1568
|
-
|
|
1569
|
-
# S3 Paths
|
|
1570
|
-
S3_PREFIX=inventory/
|
|
1571
|
-
ARCHIVE_PREFIX=processed/
|
|
1572
|
-
ERROR_PREFIX=errors/
|
|
1573
|
-
LOG_PREFIX=logs/
|
|
1574
|
-
|
|
1575
|
-
# File Processing
|
|
1576
|
-
FILE_PATTERN=.json
|
|
1577
|
-
JSON_FORMAT=json # or 'jsonl' for JSON Lines
|
|
1578
|
-
MAX_FILES=10
|
|
1579
|
-
|
|
1580
|
-
# Processing Mode Configuration
|
|
1581
|
-
# ⚠️ IMPORTANT: Choose ONE mode per connector (these are alternatives, not used together)
|
|
1582
|
-
# Options: "per-file" (default, recommended), "batch", "chunked"
|
|
1583
|
-
PROCESSING_MODE=per-file
|
|
1584
|
-
# For chunked mode only: number of files to process per chunk (default: 5)
|
|
1585
|
-
FILE_CHUNK_SIZE=5
|
|
1586
|
-
|
|
1587
|
-
# Batch Log Configuration
|
|
1588
|
-
LOG_ENABLED=true # Enable/disable batch log writing (default: true)
|
|
1589
|
-
LOG_FORMAT=json # Log format: json or text (default: json)
|
|
1590
|
-
|
|
1591
|
-
# Fluent Configuration (via Versori connection)
|
|
1592
|
-
# Connection: fluent_commerce (OAuth2)
|
|
1593
|
-
FLUENT_RETAILER_ID=my-retailer
|
|
1594
|
-
|
|
1595
|
-
# Batch Processing
|
|
1596
|
-
BATCH_SIZE=1000
|
|
1597
|
-
|
|
1598
|
-
# BPP Configuration
|
|
1599
|
-
# true = Full snapshot (BPP filters unchanged records - DEFAULT)
|
|
1600
|
-
# false = Delta feed (skip BPP, all records are changes)
|
|
1601
|
-
BPP_ENABLED=true
|
|
1602
|
-
|
|
1603
|
-
# Optional Feature Toggles (NEW)
|
|
1604
|
-
ENABLE_FILE_TRACKING=true # Track processed files to prevent duplicates (default: true)
|
|
1605
|
-
DETAILED_LOGGING=false # Enable verbose logging for debugging (default: false)
|
|
1606
|
-
```
|
|
1607
|
-
|
|
1608
|
-
---
|
|
1609
|
-
|
|
1610
|
-
## Batch API Payload Example
|
|
1611
|
-
|
|
1612
|
-
What gets sent to Fluent Commerce Batch API:
|
|
1613
|
-
|
|
1614
|
-
```json
|
|
1615
|
-
{
|
|
1616
|
-
"action": "UPSERT",
|
|
1617
|
-
"entityType": "INVENTORY",
|
|
1618
|
-
"source": "S3_JSON",
|
|
1619
|
-
"event": "InventoryChanged",
|
|
1620
|
-
"entities": [
|
|
1621
|
-
{
|
|
1622
|
-
"retailerId": 1,
|
|
1623
|
-
"locationRef": "LOC001",
|
|
1624
|
-
"skuRef": "SKU-12345",
|
|
1625
|
-
"qty": 100,
|
|
1626
|
-
"type": "LAST_ON_HAND",
|
|
1627
|
-
"status": "ACTIVE",
|
|
1628
|
-
"expectedOn": "2025-01-25T00:00:00.000Z",
|
|
1629
|
-
"attributes": {
|
|
1630
|
-
"expiryDate": "2026-12-31T00:00:00.000Z",
|
|
1631
|
-
"batchNumber": "BATCH-A001",
|
|
1632
|
-
"condition": "NEW",
|
|
1633
|
-
"storageZone": "ZONE-A"
|
|
1634
|
-
}
|
|
1635
|
-
},
|
|
1636
|
-
{
|
|
1637
|
-
"retailerId": 1,
|
|
1638
|
-
"locationRef": "LOC001",
|
|
1639
|
-
"skuRef": "SKU-67890",
|
|
1640
|
-
"qty": 50,
|
|
1641
|
-
"type": "LAST_ON_HAND",
|
|
1642
|
-
"status": "ACTIVE",
|
|
1643
|
-
"expectedOn": "2025-01-25T00:00:00.000Z",
|
|
1644
|
-
"attributes": {
|
|
1645
|
-
"expiryDate": "2026-06-30T00:00:00.000Z",
|
|
1646
|
-
"batchNumber": "BATCH-A002",
|
|
1647
|
-
"condition": "NEW",
|
|
1648
|
-
"storageZone": "ZONE-B"
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
]
|
|
1652
|
-
}
|
|
1653
|
-
```
|
|
1654
|
-
|
|
1655
|
-
> **⚠️ CRITICAL:** Each entity MUST include `retailerId` - it's required in the entity payload, not just in createJob()
|
|
1656
|
-
|
|
1657
|
-
---
|
|
1658
|
-
|
|
1659
|
-
## Versori Deployment
|
|
1660
|
-
|
|
1661
|
-
Use the Versori CLI for deploy and operations:
|
|
1662
|
-
|
|
1663
|
-
```bash
|
|
1664
|
-
# Install dependencies
|
|
1665
|
-
npm install
|
|
1666
|
-
|
|
1667
|
-
# Deploy to Versori
|
|
1668
|
-
versori deploy
|
|
1669
|
-
|
|
1670
|
-
# View logs
|
|
1671
|
-
versori logs s3-json-inventory-batch-sync
|
|
1672
|
-
|
|
1673
|
-
# Trigger manual run (if defined)
|
|
1674
|
-
versori run ingest-now
|
|
1675
|
-
```
|
|
1676
|
-
|
|
1677
|
-
---
|
|
1678
|
-
|
|
1679
|
-
## Testing
|
|
1680
|
-
|
|
1681
|
-
### Test Scheduled Batch
|
|
1682
|
-
|
|
1683
|
-
Upload a test JSON file to S3 incoming prefix and wait for the scheduled run.
|
|
1684
|
-
|
|
1685
|
-
**Check logs:**
|
|
1686
|
-
|
|
1687
|
-
```
|
|
1688
|
-
[STEP 1/8] Initializing job tracking
|
|
1689
|
-
[STEP 2/8] Initializing Fluent Commerce client and S3
|
|
1690
|
-
[STEP 3/8] Discovering files on S3
|
|
1691
|
-
[FILE 1/1] Processing file: inventory_20250124.json
|
|
1692
|
-
[STEP 4/8] Downloading and parsing: inventory_20250124.json
|
|
1693
|
-
[STEP 5/8] Transforming 5000 inventory records from inventory_20250124.json
|
|
1694
|
-
[STEP 6/8] Creating batch job and sending 5 batches to Fluent Commerce
|
|
1695
|
-
[STEP 7/8] Archiving file: inventory_20250124.json
|
|
1696
|
-
[STEP 8/8] Completing job and calculating totals
|
|
1697
|
-
```
|
|
1698
|
-
|
|
1699
|
-
### Test Ad hoc Batch
|
|
1700
|
-
|
|
1701
|
-
```bash
|
|
1702
|
-
# Process all pending files
|
|
1703
|
-
curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
|
|
1704
|
-
-H "Content-Type: application/json" \
|
|
1705
|
-
-d '{}'
|
|
1706
|
-
|
|
1707
|
-
# Process specific pattern
|
|
1708
|
-
curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
|
|
1709
|
-
-H "Content-Type: application/json" \
|
|
1710
|
-
-d '{
|
|
1711
|
-
"filePattern": "urgent_*.json"
|
|
1712
|
-
}'
|
|
1713
|
-
|
|
1714
|
-
# Force reprocess
|
|
1715
|
-
curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
|
|
1716
|
-
-H "Content-Type: application/json" \
|
|
1717
|
-
-d '{
|
|
1718
|
-
"forceReprocess": true,
|
|
1719
|
-
"filePattern": "inventory_20250124.json"
|
|
1720
|
-
}'
|
|
1721
|
-
```
|
|
1722
|
-
|
|
1723
|
-
### Test Job Status Query
|
|
1724
|
-
|
|
1725
|
-
```bash
|
|
1726
|
-
curl -X POST https://api.versori.com/webhooks/inventory-batch-job-status \
|
|
1727
|
-
-H "Content-Type: application/json" \
|
|
1728
|
-
-d '{
|
|
1729
|
-
"jobId": "ADHOC_INV_20251024_183045_abc123"
|
|
1730
|
-
}'
|
|
1731
|
-
```
|
|
1732
|
-
|
|
1733
|
-
### Verify Batch Job in Fluent
|
|
1734
|
-
|
|
1735
|
-
After processing, check the Batch job status in Fluent Commerce:
|
|
1736
|
-
|
|
1737
|
-
```bash
|
|
1738
|
-
# Upload test file to S3
|
|
1739
|
-
aws s3 cp inventory-test.json s3://my-bucket/inventory/
|
|
1740
|
-
|
|
1741
|
-
# Query job status via GraphQL
|
|
1742
|
-
curl -X POST https://your-fluent-instance.com/graphql \
|
|
1743
|
-
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
1744
|
-
-H "Content-Type: application/json" \
|
|
1745
|
-
-d '{
|
|
1746
|
-
"query": "query { job(id: \"job-123456\") { id status recordCount processedCount } }"
|
|
1747
|
-
}'
|
|
1748
|
-
```
|
|
1749
|
-
|
|
1750
|
-
---
|
|
1751
|
-
|
|
1752
|
-
## Monitoring
|
|
1753
|
-
|
|
1754
|
-
### Success Response
|
|
1755
|
-
|
|
1756
|
-
```json
|
|
1757
|
-
{
|
|
1758
|
-
"success": true,
|
|
1759
|
-
"filesProcessed": 1,
|
|
1760
|
-
"filesSkipped": 0,
|
|
1761
|
-
"filesFailed": 0,
|
|
1762
|
-
"results": [
|
|
1763
|
-
{
|
|
1764
|
-
"file": "inventory_2025-01-22.json",
|
|
1765
|
-
"success": true,
|
|
1766
|
-
"recordCount": 5000,
|
|
1767
|
-
"batchCount": 5,
|
|
1768
|
-
"jobId": "job-123456",
|
|
1769
|
-
"duration": 12345
|
|
1770
|
-
}
|
|
1771
|
-
],
|
|
1772
|
-
"duration": 13456
|
|
1773
|
-
}
|
|
1774
|
-
```
|
|
1775
|
-
|
|
1776
|
-
### Error Response
|
|
1777
|
-
|
|
1778
|
-
```json
|
|
1779
|
-
{
|
|
1780
|
-
"success": false,
|
|
1781
|
-
"filesProcessed": 0,
|
|
1782
|
-
"filesFailed": 1,
|
|
1783
|
-
"results": [
|
|
1784
|
-
{
|
|
1785
|
-
"file": "inventory_2025-01-22.json",
|
|
1786
|
-
"success": false,
|
|
1787
|
-
"error": "No valid records after mapping"
|
|
1788
|
-
}
|
|
1789
|
-
],
|
|
1790
|
-
"duration": 876
|
|
1791
|
-
}
|
|
1792
|
-
```
|
|
1793
|
-
|
|
1794
|
-
---
|
|
1795
|
-
|
|
1796
|
-
## Common Pitfalls and Solutions
|
|
1797
|
-
|
|
1798
|
-
### 1. JSON Format Detection
|
|
1799
|
-
|
|
1800
|
-
❌ **Wrong:**
|
|
1801
|
-
|
|
1802
|
-
```typescript
|
|
1803
|
-
// Assuming format without checking
|
|
1804
|
-
const records = parsed.inventory;
|
|
1805
|
-
```
|
|
1806
|
-
|
|
1807
|
-
✅ **Correct:**
|
|
1808
|
-
|
|
1809
|
-
```typescript
|
|
1810
|
-
// Handle both standard JSON and JSON Lines
|
|
1811
|
-
let records: any[] = [];
|
|
1812
|
-
if (format === 'jsonl') {
|
|
1813
|
-
records = Array.isArray(parsed) ? parsed : [parsed];
|
|
1814
|
-
} else {
|
|
1815
|
-
if (Array.isArray(parsed)) records = parsed;
|
|
1816
|
-
else if (parsed?.inventory) records = parsed.inventory;
|
|
1817
|
-
else records = [parsed];
|
|
1818
|
-
}
|
|
1819
|
-
```
|
|
1820
|
-
|
|
1821
|
-
**Why:** JSON files can have different structures; always normalize.
|
|
1822
|
-
|
|
1823
|
-
### 2. JSON Field Mapping
|
|
1824
|
-
|
|
1825
|
-
❌ **Wrong:**
|
|
1826
|
-
|
|
1827
|
-
```json
|
|
1828
|
-
{
|
|
1829
|
-
"fields": {
|
|
1830
|
-
"locationRef": { "source": "@locationRef" }
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
```
|
|
1834
|
-
|
|
1835
|
-
✅ **Correct:**
|
|
1836
|
-
|
|
1837
|
-
```json
|
|
1838
|
-
{
|
|
1839
|
-
"fields": {
|
|
1840
|
-
"locationRef": { "source": "locationRef" }
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
```
|
|
1844
|
-
|
|
1845
|
-
**Why:** JSON uses direct field access (no `@` prefix like XML attributes).
|
|
1846
|
-
|
|
1847
|
-
### 3. Mapping Data Structure
|
|
1848
|
-
|
|
1849
|
-
❌ **Wrong:**
|
|
1850
|
-
|
|
1851
|
-
```typescript
|
|
1852
|
-
// Wrapping JSON record unnecessarily
|
|
1853
|
-
const mappingResult = await mapper.map({ inventory: rec });
|
|
1854
|
-
```
|
|
1855
|
-
|
|
1856
|
-
✅ **Correct:**
|
|
1857
|
-
|
|
1858
|
-
```typescript
|
|
1859
|
-
// Map JSON record directly
|
|
1860
|
-
const mappingResult = await mapper.map(rec);
|
|
1861
|
-
```
|
|
1862
|
-
|
|
1863
|
-
**Why:** Unlike XML, JSON records don't need wrapper objects for mapping.
|
|
1864
|
-
|
|
1865
|
-
### 4. BPP Configuration
|
|
1866
|
-
|
|
1867
|
-
❌ **Wrong:**
|
|
1868
|
-
|
|
1869
|
-
```typescript
|
|
1870
|
-
// Using BPP for delta feeds (wastes processing time)
|
|
1871
|
-
const job = await client.createJob({
|
|
1872
|
-
name: 'Hourly Deltas',
|
|
1873
|
-
retailerId: 'my-retailer',
|
|
1874
|
-
// BPP enabled by default - unnecessary for delta feeds
|
|
1875
|
-
});
|
|
1876
|
-
```
|
|
1877
|
-
|
|
1878
|
-
✅ **Correct:**
|
|
1879
|
-
|
|
1880
|
-
```typescript
|
|
1881
|
-
// Skip BPP for delta feeds (all records are changes)
|
|
1882
|
-
const job = await client.createJob({
|
|
1883
|
-
name: 'Hourly Deltas',
|
|
1884
|
-
retailerId: 'my-retailer',
|
|
1885
|
-
meta: {
|
|
1886
|
-
preprocessing: 'skip', // Skip BPP - all records are changes
|
|
1887
|
-
},
|
|
1888
|
-
});
|
|
1889
|
-
```
|
|
1890
|
-
|
|
1891
|
-
**Why:** Delta feeds are pre-filtered by source system, BPP overhead is wasted.
|
|
1892
|
-
|
|
1893
|
-
### 5. Large JSON Files
|
|
1894
|
-
|
|
1895
|
-
❌ **Wrong:**
|
|
1896
|
-
|
|
1897
|
-
```typescript
|
|
1898
|
-
// Loading entire 500MB JSON into memory
|
|
1899
|
-
const parsed = await parser.parse(largeJsonContent);
|
|
1900
|
-
```
|
|
1901
|
-
|
|
1902
|
-
✅ **Correct:**
|
|
1903
|
-
|
|
1904
|
-
```typescript
|
|
1905
|
-
// Use JSON Lines format for streaming
|
|
1906
|
-
const format = 'jsonl';
|
|
1907
|
-
const parsed = await parser.parse(largeJsonContent, { format });
|
|
1908
|
-
```
|
|
1909
|
-
|
|
1910
|
-
**Why:** JSON Lines processes one record at a time, preventing memory issues.
|
|
1911
|
-
|
|
1912
|
-
---
|
|
1913
|
-
|
|
1914
|
-
## Key Takeaways
|
|
1915
|
-
|
|
1916
|
-
- 🎯 **Use Batch API for inventory** - Not Event API (Event API is for products/orders/etc.)
|
|
1917
|
-
- 🎯 **Processing mode selection** - Per-file (default) for safety, batch for speed, chunked for scale
|
|
1918
|
-
- 🎯 **TRUE modular architecture** - Separate service files with clear responsibilities (see Modular Structure section)
|
|
1919
|
-
- 🎯 **BPP by default** - Enabled for full snapshots, skip for delta feeds
|
|
1920
|
-
- 🎯 **Format detection** - Auto-detects standard JSON vs JSON Lines
|
|
1921
|
-
- 🎯 **Direct field access** - Use simple paths like `"locationRef"` (no special notation)
|
|
1922
|
-
- 🎯 **Map records directly** - No need to wrap in object (unlike XML)
|
|
1923
|
-
- 🎯 **Chunk records** - Default 1000 per batch, tune based on record size
|
|
1924
|
-
- 🎯 **EntityType: INVENTORY** - Correct entity type for inventory records
|
|
1925
|
-
- 🎯 **State management** - VersoriFileTracker + JobTracker prevent duplicates
|
|
1926
|
-
- 🎯 **Enhanced retry logic** - S3 operations with exponential backoff
|
|
1927
|
-
- 🎯 **Always dispose** - Release S3 resources with `dispose()` in finally block
|
|
1928
|
-
- 🎯 **Error handling** - Move failed files to error folder, don't fail entire workflow
|
|
1929
|
-
- 🎯 **Native logging** - Use `log` from context on Versori platform
|
|
1930
|
-
- 🎯 **Streaming for large files** - Use JSON Lines format for files >100MB
|
|
1931
|
-
- 🎯 **Emoji logging** - Use 🚀 ✅ ❌ ⚠️ 🔍 📄 for visual log scanning
|
|
1932
|
-
- 🎯 **Execution boundaries** - Use ═══ separators for workflow start/end
|
|
1933
|
-
- 🎯 **validateConnection** - Enable connection validation on client creation
|
|
1934
|
-
- 🎯 **Optional toggles** - Support enableFileTracking and detailedLogging flags
|
|
1935
|
-
- 🎯 **Duration tracking** - Track execution time at file and workflow levels
|
|
1936
|
-
- 🎯 **Error recommendations** - Include actionable suggestions in error logs
|
|
1937
|
-
|
|
1938
|
-
---
|
|
1939
|
-
|
|
1940
|
-
## Related Documentation
|
|
1941
|
-
|
|
1942
|
-
- [Batch API vs Event API Decision Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
|
|
1943
|
-
- [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
|
|
1944
|
-
- [S3 Data Source](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md#s3-data-source)
|
|
1945
|
-
- [JSON Parser](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md#json-parser-service)
|
|
1946
|
-
- [State Management](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md)
|
|
1947
|
-
- [Job Tracker](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md)
|
|
1948
|
-
- [BPP Documentation](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md#batch-pre-processing-bpp-change-detection)
|
|
1949
|
-
|
|
1950
|
-
---
|
|
1951
|
-
|
|
1952
|
-
[→ Back to Versori Scheduled Workflows](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) | [Versori Platform Guide →](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-ingest-s3-json-inventory-batch
|
|
3
|
+
canonical_filename: template-ingestion-s3-json-inventory-batch.md
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
sdk_version: ^0.1.39
|
|
6
|
+
runtime: versori
|
|
7
|
+
direction: ingestion
|
|
8
|
+
source: s3-json
|
|
9
|
+
destination: fluent-batch-api
|
|
10
|
+
entity: inventory
|
|
11
|
+
format: json
|
|
12
|
+
logging: versori
|
|
13
|
+
status: stable
|
|
14
|
+
features:
|
|
15
|
+
- batch-api-integration
|
|
16
|
+
- memory-management
|
|
17
|
+
- enhanced-logging
|
|
18
|
+
- attribute-transformation
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Template: Ingestion - S3 JSON Inventory to Batch API
|
|
22
|
+
|
|
23
|
+
**FC Connect SDK Use Case Guide**
|
|
24
|
+
|
|
25
|
+
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
26
|
+
> **Version**: @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
27
|
+
|
|
28
|
+
**🆕 Production Code Enhancements (Applied):**
|
|
29
|
+
1. ✅ Batch API with retry logic and BPP change detection
|
|
30
|
+
2. ✅ Memory management (clearing large arrays after batch processing)
|
|
31
|
+
3. ✅ Enhanced logging with emoji progress tracking (📦 batch creation, 📤 batch sending, ✅ completion)
|
|
32
|
+
4. ✅ Attribute transformation with nested field support
|
|
33
|
+
|
|
34
|
+
**Template Version:** 2.0.0
|
|
35
|
+
**Last Updated:** 2025-01-24
|
|
36
|
+
|
|
37
|
+
**Context**: Versori scheduled workflow that reads inventory JSON files from S3, transforms data with UniversalMapper, and sends bulk inventory updates to Fluent Commerce Batch API with BPP change detection
|
|
38
|
+
|
|
39
|
+
**Complexity**: Medium
|
|
40
|
+
|
|
41
|
+
**Runtime**: Versori Platform
|
|
42
|
+
|
|
43
|
+
**Estimated Lines**: ~520 lines
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## STEP 1: Understand This Template
|
|
48
|
+
|
|
49
|
+
**What This Template Does:**
|
|
50
|
+
|
|
51
|
+
- Scheduled Versori workflow for bulk inventory ingestion from S3 JSON files
|
|
52
|
+
- Reads JSON files from S3 with retry logic and streaming support
|
|
53
|
+
- Parses JSON with format detection (standard JSON vs JSON Lines)
|
|
54
|
+
- Transforms data using UniversalMapper with direct field access (no special notation)
|
|
55
|
+
- Sends bulk updates to Fluent Commerce Batch API with chunking
|
|
56
|
+
- Uses BPP (Batch Pre-Processing) for change detection
|
|
57
|
+
- Tracks processing state with VersoriFileTracker to prevent duplicates
|
|
58
|
+
- Archives processed files and handles errors gracefully
|
|
59
|
+
|
|
60
|
+
**Key SDK Components:**
|
|
61
|
+
|
|
62
|
+
- `createClient()` - Universal client factory (auto-detects Versori context)
|
|
63
|
+
- `S3DataSource` - S3 operations with streaming (NEW: enhanced retry logic)
|
|
64
|
+
- `JSONParserService` - JSON parsing with format auto-detection (JSON vs JSON Lines)
|
|
65
|
+
- `UniversalMapper` - Field transformation with SDK resolvers
|
|
66
|
+
- `VersoriFileTracker` - State management (prevent duplicate processing)
|
|
67
|
+
- `JobTracker` - Job lifecycle tracking
|
|
68
|
+
- Native Versori `log` - Use `log` from context
|
|
69
|
+
|
|
70
|
+
**Entity Type:**
|
|
71
|
+
|
|
72
|
+
- **InventoryQuantity** - Fluent entity for inventory positions and quantities
|
|
73
|
+
- **EntityType: 'INVENTORY'** - Used in Batch API `sendBatch()` call
|
|
74
|
+
- **Batch API Method** - Uses `createJob()` and `sendBatch()` (not Event API)
|
|
75
|
+
|
|
76
|
+
**Critical Patterns:**
|
|
77
|
+
|
|
78
|
+
- **JSON Format Support**: Standard JSON objects and JSON Lines (one record per line)
|
|
79
|
+
- **Direct Field Access**: Use simple paths like `"locationRef"` (no `@` prefix needed for JSON)
|
|
80
|
+
- **Safe S3 Paths**: Use absolute paths in S3DataSource config
|
|
81
|
+
- **Always Dispose**: Call `s3.dispose()` in `finally` block to release connections
|
|
82
|
+
- **BPP Configuration**: Enabled by default for full snapshots, skip for delta feeds
|
|
83
|
+
- **Progress Logging**: Enhanced logging with context (sample SKUs, locations)
|
|
84
|
+
- **Detailed Logging Toggle**: Optional detailed payload logging for debugging (default: disabled)
|
|
85
|
+
- **File Tracking Toggle**: Optional file tracking to prevent duplicates (default: enabled)
|
|
86
|
+
- **Retry Logic**: Enhanced S3 operations with exponential backoff
|
|
87
|
+
|
|
88
|
+
**When to Use This Template:**
|
|
89
|
+
|
|
90
|
+
- ✅ Daily/hourly full inventory snapshots from S3 JSON files
|
|
91
|
+
- ✅ Bulk inventory updates with BPP change detection
|
|
92
|
+
- ✅ JSON or JSON Lines format data
|
|
93
|
+
- ✅ Need state management to prevent duplicate processing
|
|
94
|
+
- ✅ Files should be archived after processing
|
|
95
|
+
|
|
96
|
+
**When NOT to Use:**
|
|
97
|
+
|
|
98
|
+
- ❌ Single inventory updates (use GraphQL mutation instead)
|
|
99
|
+
- ❌ Products, Locations, Customers (use Event API templates)
|
|
100
|
+
- ❌ Real-time inventory events (use Event API)
|
|
101
|
+
- ❌ XML/CSV/Parquet formats (use appropriate format template)
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## STEP 2: AI Prompt
|
|
106
|
+
|
|
107
|
+
**Copy this prompt to generate the complete implementation:**
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Create a Versori scheduled workflow for S3 JSON inventory ingestion to Fluent Commerce Batch API.
|
|
111
|
+
|
|
112
|
+
REQUIREMENTS:
|
|
113
|
+
1. Runtime: Versori Platform (scheduled workflow)
|
|
114
|
+
2. Source: S3 JSON files from s3://bucket/inventory/
|
|
115
|
+
3. Destination: Fluent Commerce Batch API (InventoryQuantity entity)
|
|
116
|
+
4. Format: JSON (standard JSON objects and JSON Lines support)
|
|
117
|
+
5. Entity: InventoryQuantity (EntityType: 'INVENTORY')
|
|
118
|
+
|
|
119
|
+
KEY FEATURES:
|
|
120
|
+
- S3 JSON file discovery with VersoriFileTracker for state management
|
|
121
|
+
- JSON parsing with format auto-detection (JSON vs JSON Lines)
|
|
122
|
+
- Direct field mapping using UniversalMapper (no special notation needed)
|
|
123
|
+
- Batch API with chunking (1000 records per batch) and BPP change detection
|
|
124
|
+
- Safe S3 paths with absolute path requirements
|
|
125
|
+
- S3 dispose() in finally block
|
|
126
|
+
- Job lifecycle tracking with JobTracker
|
|
127
|
+
- File archival on S3 after successful processing
|
|
128
|
+
- Error handling with exponential backoff retry
|
|
129
|
+
|
|
130
|
+
CRITICAL REQUIREMENTS:
|
|
131
|
+
1. JSON Parser: JSONParserService with format auto-detection
|
|
132
|
+
2. Array Extraction: Handle both standard JSON arrays and JSON Lines
|
|
133
|
+
3. Mapping Config: Use direct paths like "locationRef" (no @ prefix)
|
|
134
|
+
4. Entity Type: 'INVENTORY' (for InventoryQuantity)
|
|
135
|
+
5. BPP: Enabled by default (full snapshots), skip for delta feeds
|
|
136
|
+
6. S3 Config: Include proper AWS credentials configuration
|
|
137
|
+
7. S3 Dispose: Always call s3.dispose() in finally block
|
|
138
|
+
8. Native Logging: Use log from context
|
|
139
|
+
9. Retry Logic: Enhanced S3 operations with exponential backoff
|
|
140
|
+
|
|
141
|
+
SDK METHODS TO USE:
|
|
142
|
+
- createClient(ctx) - Pass entire Versori context, auto-detects platform
|
|
143
|
+
- client.setRetailerId(retailerId) - REQUIRED after createClient for Batch API operations
|
|
144
|
+
- new S3DataSource(config, log) - Config includes AWS credentials
|
|
145
|
+
- await s3.listFiles({ prefix, maxKeys })
|
|
146
|
+
- await s3.downloadFile(key, { encoding: 'utf8' })
|
|
147
|
+
- await s3.moveFile(sourceKey, destKey)
|
|
148
|
+
- await s3.dispose() - MUST call in finally block
|
|
149
|
+
- new JSONParserService()
|
|
150
|
+
- await parser.parse(jsonContent, { format: 'json' | 'jsonl' })
|
|
151
|
+
- new UniversalMapper(mappingConfig)
|
|
152
|
+
- await mapper.map(records)
|
|
153
|
+
- new VersoriFileTracker(ctx.openKv(':project:'), 'prefix')
|
|
154
|
+
- await fileTracker.wasFileProcessed(fileName)
|
|
155
|
+
- await fileTracker.markFileProcessed(fileName, metadata)
|
|
156
|
+
- new JobTracker(ctx.openKv(':project:'), log)
|
|
157
|
+
- await tracker.createJob(jobId, { triggeredBy: 'schedule', stage: 'initialization' })
|
|
158
|
+
- await tracker.updateJob(jobId, { status: 'processing' })
|
|
159
|
+
- await tracker.markCompleted(jobId, details)
|
|
160
|
+
- await tracker.markFailed(jobId, error)
|
|
161
|
+
- await client.createJob({ name, meta: { preprocessing: 'skip' } })
|
|
162
|
+
- await client.sendBatch(jobId, { action: 'UPSERT', entityType: 'INVENTORY', entities })
|
|
163
|
+
- Fire-and-forget batch submission (no polling)
|
|
164
|
+
|
|
165
|
+
FORBIDDEN PATTERNS:
|
|
166
|
+
- ❌ LoggingService (removed - use native log on Versori)
|
|
167
|
+
- ❌ Don't use @ prefix for JSON fields (that's for XML attributes only)
|
|
168
|
+
- ❌ Don't forget to wrap records in proper structure for mapping
|
|
169
|
+
- ❌ Don't use Event API (use Batch API)
|
|
170
|
+
- ❌ Don't forget to call s3.dispose() in finally block
|
|
171
|
+
- ❌ Don't use relative S3 paths (use absolute paths)
|
|
172
|
+
|
|
173
|
+
MAPPING CONFIGURATION FILE: config/inventory.batch.json
|
|
174
|
+
Structure:
|
|
175
|
+
{
|
|
176
|
+
"name": "inventory.batch.json",
|
|
177
|
+
"version": "1.0.0",
|
|
178
|
+
"description": "JSON inventory to Fluent Commerce Batch API mapping",
|
|
179
|
+
"fields": {
|
|
180
|
+
"locationRef": { "source": "locationRef", "required": true, "resolver": "sdk.trim" },
|
|
181
|
+
"skuRef": { "source": "skuRef", "required": true, "resolver": "sdk.trim" },
|
|
182
|
+
"qty": { "source": "qty", "required": true, "resolver": "sdk.parseInt" },
|
|
183
|
+
"type": { "source": "type", "required": true, "resolver": "sdk.uppercase" },
|
|
184
|
+
"status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
|
|
185
|
+
"expectedOn": { "source": "expectedOn", "required": false, "resolver": "sdk.formatDate" },
|
|
186
|
+
"attributes.expiryDate": { "source": "attributes.expiryDate", "required": false, "resolver": "sdk.formatDate" },
|
|
187
|
+
"attributes.batchNumber": { "source": "attributes.batchNumber", "required": false },
|
|
188
|
+
"attributes.condition": { "source": "attributes.condition", "required": false, "defaultValue": "NEW", "resolver": "sdk.uppercase" },
|
|
189
|
+
"attributes.storageZone": { "source": "attributes.storageZone", "required": false }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
GENERATE:
|
|
194
|
+
1. package.json with dependencies
|
|
195
|
+
2. index.ts (workflow entry point with scheduled trigger)
|
|
196
|
+
3. src/workflows/scheduled/daily-inventory-sync.ts (scheduled workflow)
|
|
197
|
+
4. src/workflows/webhook/adhoc-inventory-sync.ts (webhook workflow)
|
|
198
|
+
5. src/workflows/webhook/job-status-check.ts (status webhook)
|
|
199
|
+
6. src/services/inventory-sync.service.ts (shared orchestration logic)
|
|
200
|
+
7. src/types/inventory.types.ts (TypeScript type definitions)
|
|
201
|
+
8. config/inventory.batch.json (mapping configuration - external JSON file)
|
|
202
|
+
9. .env.example (environment variables)
|
|
203
|
+
|
|
204
|
+
NOTE: Use external JSON files for mapping configuration (not TypeScript .config files)
|
|
205
|
+
|
|
206
|
+
Ensure all code is production-ready with proper error handling, S3 dispose() in finally block. Polling is intentionally omitted (fire-and-forget).
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## What You'll Build
|
|
212
|
+
|
|
213
|
+
### Project Structure
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
s3-json-inventory-batch-sync/
|
|
217
|
+
├── package.json
|
|
218
|
+
├── index.ts # Workflow entry point
|
|
219
|
+
└── src/
|
|
220
|
+
├── workflows/
|
|
221
|
+
│ ├── scheduled/
|
|
222
|
+
│ │ └── daily-inventory-sync.ts # Scheduled: Daily inventory sync
|
|
223
|
+
│ │
|
|
224
|
+
│ └── webhook/
|
|
225
|
+
│ ├── adhoc-inventory-sync.ts # Webhook: Manual trigger
|
|
226
|
+
│ └── job-status-check.ts # Webhook: Status query
|
|
227
|
+
│
|
|
228
|
+
├── services/
|
|
229
|
+
│ └── inventory-sync.service.ts # Shared orchestration logic (reusable)
|
|
230
|
+
│
|
|
231
|
+
├── config/
|
|
232
|
+
│ └── inventory.batch.json # Mapping configuration (external JSON)
|
|
233
|
+
│
|
|
234
|
+
└── types/
|
|
235
|
+
└── inventory.types.ts # TypeScript interfaces
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Features
|
|
239
|
+
|
|
240
|
+
- ✅ Scheduled Versori workflow (daily/hourly inventory sync)
|
|
241
|
+
- ✅ S3 file download with enhanced retry logic
|
|
242
|
+
- ✅ JSON parsing with format auto-detection (standard JSON vs JSON Lines)
|
|
243
|
+
- ✅ Direct field mapping (no special notation needed for JSON)
|
|
244
|
+
- ✅ Field mapping with UniversalMapper and external JSON config
|
|
245
|
+
- ✅ Batch API job creation and chunked processing (1000 records/batch)
|
|
246
|
+
- ✅ BPP (Batch Pre-Processing) change detection
|
|
247
|
+
- ✅ Fire-and-forget batch submission with audit logging
|
|
248
|
+
- ✅ State management (VersoriFileTracker + JobTracker prevent duplicates)
|
|
249
|
+
- ✅ File archival on S3 (success → processed/, failure → errors/)
|
|
250
|
+
- ✅ Comprehensive error handling with exponential backoff retry
|
|
251
|
+
- ✅ Modular architecture (reusable services, easy to test)
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Versori Workflows Structure
|
|
256
|
+
|
|
257
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
258
|
+
|
|
259
|
+
**Trigger Types:**
|
|
260
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
261
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
262
|
+
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
263
|
+
|
|
264
|
+
**Execution Steps (chained to triggers):**
|
|
265
|
+
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
266
|
+
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
267
|
+
|
|
268
|
+
### Recommended Project Structure
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
s3-json-inventory-batch-sync/
|
|
272
|
+
├── index.ts # Entry point - exports all workflows
|
|
273
|
+
└── src/
|
|
274
|
+
├── workflows/
|
|
275
|
+
│ ├── scheduled/
|
|
276
|
+
│ │ └── daily-inventory-sync.ts # Scheduled: Daily inventory sync
|
|
277
|
+
│ │
|
|
278
|
+
│ └── webhook/
|
|
279
|
+
│ ├── adhoc-inventory-sync.ts # Webhook: Manual trigger
|
|
280
|
+
│ └── job-status-check.ts # Webhook: Status query
|
|
281
|
+
│
|
|
282
|
+
├── services/
|
|
283
|
+
│ └── inventory-sync.service.ts # Shared orchestration logic (reusable)
|
|
284
|
+
│
|
|
285
|
+
└── types/
|
|
286
|
+
└── inventory.types.ts # Shared type definitions
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Benefits:**
|
|
290
|
+
- ✅ Clear trigger separation (`scheduled/` vs `webhook/`)
|
|
291
|
+
- ✅ Descriptive file names (easy to browse and understand)
|
|
292
|
+
- ✅ Scalable (add new workflows without cluttering)
|
|
293
|
+
- ✅ Reusable code in `services/` (DRY principle)
|
|
294
|
+
- ✅ Easy to modify individual workflows without affecting others
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Workflow Files
|
|
299
|
+
|
|
300
|
+
### 1. Scheduled Workflows (`src/workflows/scheduled/`)
|
|
301
|
+
|
|
302
|
+
All time-based triggers that run automatically on cron schedules.
|
|
303
|
+
|
|
304
|
+
#### `src/workflows/scheduled/daily-inventory-sync.ts`
|
|
305
|
+
|
|
306
|
+
**Purpose**: Automatic daily inventory sync
|
|
307
|
+
**Trigger**: Cron schedule (`0 2 * * *`)
|
|
308
|
+
**Exposed as Endpoint**: ❌ NO - Runs automatically
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { schedule, http } from '@versori/run';
|
|
312
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
313
|
+
import { runIngestion } from '../../services/inventory-sync.service';
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Scheduled Workflow: Daily Inventory Sync
|
|
317
|
+
*
|
|
318
|
+
* Runs automatically daily at 2 AM UTC
|
|
319
|
+
* NOT exposed as HTTP endpoint - Versori executes on schedule
|
|
320
|
+
*
|
|
321
|
+
* Uses shared service: inventory-sync.service.ts
|
|
322
|
+
*/
|
|
323
|
+
export const dailyInventorySync = schedule(
|
|
324
|
+
'inventory-batch-scheduled',
|
|
325
|
+
'0 2 * * *' // Daily at 2 AM UTC
|
|
326
|
+
).then(
|
|
327
|
+
http('run-inventory-batch', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
328
|
+
const { log, openKv } = ctx;
|
|
329
|
+
const executionStartTime = Date.now();
|
|
330
|
+
const jobId = `inventory-batch-${Date.now()}`;
|
|
331
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
332
|
+
|
|
333
|
+
log.info('═══════════════════════════════════════════════════════════════');
|
|
334
|
+
log.info('🚀 [WORKFLOW] Starting scheduled inventory sync', { jobId });
|
|
335
|
+
log.info('═══════════════════════════════════════════════════════════════');
|
|
336
|
+
|
|
337
|
+
await tracker.createJob(jobId, {
|
|
338
|
+
triggeredBy: 'schedule',
|
|
339
|
+
stage: 'initialization',
|
|
340
|
+
startTime: executionStartTime,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await tracker.updateJob(jobId, { status: 'processing' });
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
// Reuse shared orchestration logic
|
|
347
|
+
const result = await runIngestion(ctx, jobId, tracker);
|
|
348
|
+
|
|
349
|
+
if (result.success) {
|
|
350
|
+
const duration = Date.now() - executionStartTime;
|
|
351
|
+
await tracker.markCompleted(jobId, { ...result, duration });
|
|
352
|
+
log.info('✅ [WORKFLOW] Inventory sync completed successfully', {
|
|
353
|
+
jobId,
|
|
354
|
+
filesProcessed: result.filesProcessed,
|
|
355
|
+
duration: `${duration}ms (${(duration / 1000).toFixed(2)}s)`,
|
|
356
|
+
});
|
|
357
|
+
} else {
|
|
358
|
+
await tracker.markFailed(jobId, result.error || 'Unknown error');
|
|
359
|
+
log.error('❌ [WORKFLOW] Inventory sync failed', {
|
|
360
|
+
jobId,
|
|
361
|
+
error: result.error,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
log.info('═══════════════════════════════════════════════════════════════');
|
|
366
|
+
log.info('🏁 [WORKFLOW] Execution complete', {
|
|
367
|
+
jobId,
|
|
368
|
+
success: result.success,
|
|
369
|
+
duration: `${Date.now() - executionStartTime}ms`,
|
|
370
|
+
});
|
|
371
|
+
log.info('═══════════════════════════════════════════════════════════════');
|
|
372
|
+
|
|
373
|
+
return { success: true, jobId, ...result };
|
|
374
|
+
} catch (e: any) {
|
|
375
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
376
|
+
await tracker.markFailed(jobId, errorMessage);
|
|
377
|
+
log.error('❌ [WORKFLOW] Inventory sync failed with exception', {
|
|
378
|
+
jobId,
|
|
379
|
+
error: errorMessage,
|
|
380
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
381
|
+
recommendation: 'Check SFTP credentials, network connectivity, and file permissions',
|
|
382
|
+
});
|
|
383
|
+
return { success: false, jobId, error: errorMessage };
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### 2. Webhook Workflows (`src/workflows/webhook/`)
|
|
392
|
+
|
|
393
|
+
All HTTP-based triggers that create webhook endpoints.
|
|
394
|
+
|
|
395
|
+
#### `src/workflows/webhook/adhoc-inventory-sync.ts`
|
|
396
|
+
|
|
397
|
+
**Purpose**: Manual inventory sync trigger (on-demand)
|
|
398
|
+
**Trigger**: HTTP POST
|
|
399
|
+
**Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-adhoc`
|
|
400
|
+
**Use Cases**: Testing, priority processing, ad-hoc runs
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import { webhook, http } from '@versori/run';
|
|
404
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
405
|
+
import { runIngestion } from '../../services/inventory-sync.service';
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Webhook: Manual Inventory Sync Trigger
|
|
409
|
+
*
|
|
410
|
+
* Endpoint: POST https://{workspace}.versori.run/inventory-batch-adhoc
|
|
411
|
+
* Request body (optional): { filePattern: "urgent_*.json", maxFiles: 5 }
|
|
412
|
+
*
|
|
413
|
+
* Pattern: Sync mode + fire-and-forget
|
|
414
|
+
* - Returns jobId immediately
|
|
415
|
+
* - Background processing continues without blocking response
|
|
416
|
+
* - ✅ Works because Versori keeps execution context alive for unawaited promises
|
|
417
|
+
*
|
|
418
|
+
* Uses shared service: inventory-sync.service.ts
|
|
419
|
+
*/
|
|
420
|
+
export const adhocInventorySync = webhook('inventory-batch-adhoc', {
|
|
421
|
+
response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
|
|
422
|
+
connection: 'inventory-batch-adhoc', // Versori validates API key
|
|
423
|
+
}).then(
|
|
424
|
+
http('run-inventory-batch-adhoc', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
425
|
+
const { log, openKv, data } = ctx;
|
|
426
|
+
const executionStartTime = Date.now();
|
|
427
|
+
const jobId = `inventory-batch-adhoc-${Date.now()}`;
|
|
428
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
429
|
+
|
|
430
|
+
const filePattern = data?.filePattern as string || '*.json';
|
|
431
|
+
const maxFiles = data?.maxFiles as number;
|
|
432
|
+
|
|
433
|
+
log.info('🚀 [WEBHOOK] Adhoc inventory sync triggered', {
|
|
434
|
+
jobId,
|
|
435
|
+
filePattern,
|
|
436
|
+
maxFiles,
|
|
437
|
+
requestData: data,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Create job entry FIRST (awaited to ensure job exists in KV)
|
|
441
|
+
await tracker.createJob(jobId, {
|
|
442
|
+
triggeredBy: 'manual',
|
|
443
|
+
stage: 'initialization',
|
|
444
|
+
status: 'queued',
|
|
445
|
+
startTime: executionStartTime,
|
|
446
|
+
options: { filePattern, maxFiles },
|
|
447
|
+
createdAt: new Date().toISOString(),
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// ✅ Fire-and-forget: Start background processing WITHOUT await
|
|
451
|
+
// The promise continues execution after we return the response
|
|
452
|
+
runIngestion(ctx, jobId, tracker)
|
|
453
|
+
.then((result) => {
|
|
454
|
+
const duration = Date.now() - executionStartTime;
|
|
455
|
+
if (result.success) {
|
|
456
|
+
log.info('✅ [BACKGROUND] Inventory sync completed successfully', {
|
|
457
|
+
jobId,
|
|
458
|
+
filesProcessed: result.filesProcessed,
|
|
459
|
+
filesFailed: result.filesFailed,
|
|
460
|
+
recordsProcessed: result.recordsProcessed,
|
|
461
|
+
duration: `${duration}ms (${(duration / 1000).toFixed(2)}s)`,
|
|
462
|
+
});
|
|
463
|
+
return tracker.markCompleted(jobId, { ...result, duration });
|
|
464
|
+
} else {
|
|
465
|
+
log.error('❌ [BACKGROUND] Inventory sync failed', {
|
|
466
|
+
jobId,
|
|
467
|
+
error: result.error,
|
|
468
|
+
});
|
|
469
|
+
return tracker.markFailed(jobId, result.error || 'Unknown error');
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
.catch((error: unknown) => {
|
|
473
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
474
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
475
|
+
|
|
476
|
+
log.error('❌ [BACKGROUND] Inventory sync failed with exception', {
|
|
477
|
+
jobId,
|
|
478
|
+
error: errorMessage,
|
|
479
|
+
stack: errorStack,
|
|
480
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
481
|
+
recommendation: 'Check S3 credentials, bucket permissions, and network connectivity',
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
return tracker.markFailed(jobId, errorMessage);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Return immediately with jobId (response sent with this return value)
|
|
488
|
+
return {
|
|
489
|
+
success: true,
|
|
490
|
+
jobId,
|
|
491
|
+
message: 'Inventory sync started in background',
|
|
492
|
+
statusEndpoint: `https://{workspace}.versori.run/inventory-batch-job-status`,
|
|
493
|
+
note: 'Poll the status endpoint with jobId to check progress',
|
|
494
|
+
};
|
|
495
|
+
})
|
|
496
|
+
);
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
#### `src/workflows/webhook/job-status-check.ts`
|
|
502
|
+
|
|
503
|
+
**Purpose**: Query job status and progress
|
|
504
|
+
**Trigger**: HTTP POST/GET
|
|
505
|
+
**Endpoint**: `POST https://{workspace}.versori.run/inventory-batch-job-status`
|
|
506
|
+
**Request Body**: `{ "jobId": "inventory-batch-1234567890" }`
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
import { webhook, fn } from '@versori/run';
|
|
510
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Webhook: Job Status Check
|
|
514
|
+
*
|
|
515
|
+
* Endpoint: POST https://{workspace}.versori.run/inventory-batch-job-status
|
|
516
|
+
* Request body: { "jobId": "inventory-batch-1234567890" }
|
|
517
|
+
*
|
|
518
|
+
* Pattern: webhook().then(fn()) - no external API needed, only KV storage
|
|
519
|
+
* Lightweight: Only queries KV store, no Fluent API calls
|
|
520
|
+
*/
|
|
521
|
+
export const jobStatusCheck = webhook('inventory-batch-job-status', {
|
|
522
|
+
response: { mode: 'sync' },
|
|
523
|
+
connection: 'inventory-batch-job-status',
|
|
524
|
+
}).then(
|
|
525
|
+
fn('status', async ctx => {
|
|
526
|
+
const { data, log, openKv } = ctx;
|
|
527
|
+
const jobId = data?.jobId as string;
|
|
528
|
+
|
|
529
|
+
if (!jobId) {
|
|
530
|
+
return { success: false, error: 'jobId required' };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
534
|
+
const status = await tracker.getJob(jobId);
|
|
535
|
+
|
|
536
|
+
return status
|
|
537
|
+
? { success: true, jobId, ...status }
|
|
538
|
+
: { success: false, error: 'Job not found', jobId };
|
|
539
|
+
})
|
|
540
|
+
);
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
### 3. Entry Point (`index.ts`)
|
|
546
|
+
|
|
547
|
+
**Purpose**: Register all workflows with Versori platform
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
/**
|
|
551
|
+
* Entry Point - Registers all workflows with Versori platform
|
|
552
|
+
*
|
|
553
|
+
* Versori automatically discovers and registers exported workflows
|
|
554
|
+
*
|
|
555
|
+
* File Structure:
|
|
556
|
+
* - src/workflows/scheduled/ → Time-based triggers (cron)
|
|
557
|
+
* - src/workflows/webhook/ → HTTP-based triggers (webhooks)
|
|
558
|
+
*/
|
|
559
|
+
|
|
560
|
+
// Scheduled workflows
|
|
561
|
+
export { dailyInventorySync } from './src/workflows/scheduled/daily-inventory-sync';
|
|
562
|
+
|
|
563
|
+
// Webhook workflows
|
|
564
|
+
export { adhocInventorySync } from './src/workflows/webhook/adhoc-inventory-sync';
|
|
565
|
+
export { jobStatusCheck } from './src/workflows/webhook/job-status-check';
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**What Gets Exposed:**
|
|
569
|
+
- ✅ `adhocInventorySync` → `https://{workspace}.versori.run/inventory-batch-adhoc`
|
|
570
|
+
- ✅ `jobStatusCheck` → `https://{workspace}.versori.run/inventory-batch-job-status`
|
|
571
|
+
- ❌ `dailyInventorySync` → NOT exposed (runs automatically on cron)
|
|
572
|
+
|
|
573
|
+
## When to Use Batch API vs Event API
|
|
574
|
+
|
|
575
|
+
### ✅ Use Batch API (`createJob`/`sendBatch`) For:
|
|
576
|
+
|
|
577
|
+
| Entity Type | Use Case | Why Batch API |
|
|
578
|
+
| --------------------- | ---------------------------------- | ----------------------------------------------- |
|
|
579
|
+
| **Inventory** | Bulk inventory updates, daily sync | Optimized for high-volume, BPP change detection |
|
|
580
|
+
| **InventoryQuantity** | Inventory positions and quantities | Native Batch API entity type |
|
|
581
|
+
|
|
582
|
+
### ❌ Use Event API Instead For:
|
|
583
|
+
|
|
584
|
+
| Entity Type | Use Case | Why Event API / GraphQL |
|
|
585
|
+
| ------------------- | ------------------------------------- | --------------------------------------------- |
|
|
586
|
+
| **Products** | Product catalog sync, variant updates | Triggers workflows, validates business rules |
|
|
587
|
+
| **Locations** | Store/warehouse setup | Requires workflow orchestration |
|
|
588
|
+
| **Customers** | Customer registration/profile updates | Prefer GraphQL mutations (no Rubix support) |
|
|
589
|
+
| **Orders** | Updates/events (not creation) | Events fine for updates; creation via GraphQL |
|
|
590
|
+
| **Custom Entities** | Any entity needing workflow triggers | Full Rubix workflow support |
|
|
591
|
+
|
|
592
|
+
### 🔍 Use GraphQL Mutations For:
|
|
593
|
+
|
|
594
|
+
| Scenario | Why GraphQL |
|
|
595
|
+
| --------------------- | -------------------------------------- |
|
|
596
|
+
| **Single operations** | Create one record, update one record |
|
|
597
|
+
| **Complex queries** | Fetch data with relationships |
|
|
598
|
+
| **Testing/debugging** | Direct API control, immediate feedback |
|
|
599
|
+
|
|
600
|
+
Note:
|
|
601
|
+
|
|
602
|
+
- Orders: Use `createOrder` GraphQL mutation for order creation (this triggers the Order CREATED event in Rubix). Order updates/events are fine via Event API.
|
|
603
|
+
- Customers: Use GraphQL mutations (no Rubix workflow support for customers).
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## Understanding BPP (Batch Pre-Processing)
|
|
608
|
+
|
|
609
|
+
**BPP** is Fluent's change detection system that filters out unchanged records before workflow processing.
|
|
610
|
+
|
|
611
|
+
Note on defaults and control:
|
|
612
|
+
|
|
613
|
+
- BPP is a Fluent platform feature. The default on/off behavior is controlled at your Fluent account level, not by the SDK.
|
|
614
|
+
- If you omit `meta.preprocessing` in `createJob()`, the account-level default applies.
|
|
615
|
+
- To force behavior per job, set `meta.preprocessing` explicitly (`'skip'` to disable for delta feeds).
|
|
616
|
+
|
|
617
|
+
### When to Use BPP (Default - Enabled)
|
|
618
|
+
|
|
619
|
+
**✅ Full Inventory Snapshots:**
|
|
620
|
+
|
|
621
|
+
- Daily complete inventory dumps
|
|
622
|
+
- Entire warehouse stock files
|
|
623
|
+
- Records may be identical to previous run
|
|
624
|
+
|
|
625
|
+
**Example:**
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
// BPP enabled by default - filters unchanged records
|
|
629
|
+
const job = await client.createJob({
|
|
630
|
+
name: 'Daily Full Inventory',
|
|
631
|
+
retailerId: 'my-retailer',
|
|
632
|
+
// BPP automatically enabled - no meta needed
|
|
633
|
+
});
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**What BPP does:**
|
|
637
|
+
|
|
638
|
+
- Compares incoming records with existing records in Fluent
|
|
639
|
+
- Filters out records with no changes
|
|
640
|
+
- Only passes changed/new records to workflows
|
|
641
|
+
- Significantly reduces workflow processing load
|
|
642
|
+
|
|
643
|
+
### When to Skip BPP
|
|
644
|
+
|
|
645
|
+
**✅ Delta Feeds (Pre-Filtered Data):**
|
|
646
|
+
|
|
647
|
+
- Hourly change files (only updates since last run)
|
|
648
|
+
- Event-driven inventory changes
|
|
649
|
+
- Pre-filtered data from source system
|
|
650
|
+
- All records are guaranteed to be changes
|
|
651
|
+
|
|
652
|
+
**Example:**
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
// Skip BPP for delta feeds - all records are changes
|
|
656
|
+
const job = await client.createJob({
|
|
657
|
+
name: 'Hourly Delta Inventory',
|
|
658
|
+
retailerId: 'my-retailer',
|
|
659
|
+
meta: {
|
|
660
|
+
preprocessing: 'skip', // All records already filtered
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Performance Impact:**
|
|
666
|
+
|
|
667
|
+
- **Full snapshot + BPP**: 10,000 records → 500 changes → Fast
|
|
668
|
+
- **Full snapshot, no BPP**: 10,000 records → 10,000 processed → Slow
|
|
669
|
+
- **Delta feed + BPP**: 500 records → 500 changes → Fast (but BPP overhead wasted)
|
|
670
|
+
- **Delta feed, no BPP**: 500 records → 500 processed → Fastest
|
|
671
|
+
|
|
672
|
+
### Decision Guide
|
|
673
|
+
|
|
674
|
+
| Source Data Type | BPP Setting | Reason |
|
|
675
|
+
| ---------------------- | ----------------------- | ----------------------------------------------- |
|
|
676
|
+
| **Daily full dump** | Enabled (default) | Most records unchanged, BPP filters efficiently |
|
|
677
|
+
| **Hourly delta feed** | `preprocessing: 'skip'` | All records are changes, BPP overhead wasted |
|
|
678
|
+
| **Initial load** | Enabled (default) | No previous data, but good practice |
|
|
679
|
+
| **Manual corrections** | Enabled (default) | Unknown change ratio, let BPP filter |
|
|
680
|
+
| **Real-time events** | `preprocessing: 'skip'` | Each record is a change event |
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## Processing Modes
|
|
685
|
+
|
|
686
|
+
**⚠️ IMPORTANT:** Choose ONE processing mode per connector. These are alternative patterns, not features to use together.
|
|
687
|
+
|
|
688
|
+
The SDK supports three processing modes for handling multiple files. **Select the mode that best fits your use case** and configure your connector accordingly:
|
|
689
|
+
|
|
690
|
+
### Mode 1: Per-File Processing (Recommended Default) ✅ IMPLEMENTED
|
|
691
|
+
|
|
692
|
+
**When to use:**
|
|
693
|
+
|
|
694
|
+
- Multiple large files that shouldn't be in memory together
|
|
695
|
+
- Need file-level consistency (file 3 fails → files 1-2 already archived)
|
|
696
|
+
- Memory constraints
|
|
697
|
+
- Fault isolation (one file failure doesn't affect others)
|
|
698
|
+
|
|
699
|
+
**Flow:**
|
|
700
|
+
|
|
701
|
+
```
|
|
702
|
+
FOR EACH FILE:
|
|
703
|
+
1. Download file
|
|
704
|
+
2. Parse JSON
|
|
705
|
+
3. Map records
|
|
706
|
+
4. Create Batch API job
|
|
707
|
+
5. Send batches
|
|
708
|
+
6. Write log
|
|
709
|
+
7. Archive file
|
|
710
|
+
8. Mark file processed
|
|
711
|
+
|
|
712
|
+
IF FILE FAILS:
|
|
713
|
+
- Move to /errors/
|
|
714
|
+
- Continue to next file
|
|
715
|
+
- Other files unaffected
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
**Configuration:**
|
|
719
|
+
|
|
720
|
+
```json
|
|
721
|
+
{
|
|
722
|
+
"processingMode": "per-file",
|
|
723
|
+
"maxFilesPerRun": 10
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
**Benefits:**
|
|
728
|
+
|
|
729
|
+
- ✅ File-level atomicity (each file processed independently)
|
|
730
|
+
- ✅ Low memory footprint (1 file at a time)
|
|
731
|
+
- ✅ Clear error isolation (failed file doesn't block others)
|
|
732
|
+
- ✅ Incremental progress (files archived as completed)
|
|
733
|
+
|
|
734
|
+
**Example implementation (see code section below)**
|
|
735
|
+
|
|
736
|
+
### Mode 2: Batch Processing (All Files at Once) ⚠️ OPTIONAL - Choose if needed
|
|
737
|
+
|
|
738
|
+
**When to use:**
|
|
739
|
+
|
|
740
|
+
- Small files (all fit in memory comfortably)
|
|
741
|
+
- Need one Batch API job for all files
|
|
742
|
+
- Atomic processing (all files or none)
|
|
743
|
+
|
|
744
|
+
**Flow:**
|
|
745
|
+
|
|
746
|
+
```
|
|
747
|
+
1. Download ALL files
|
|
748
|
+
2. Parse ALL files
|
|
749
|
+
3. Map ALL records
|
|
750
|
+
4. Create ONE Batch API job
|
|
751
|
+
5. Send ALL batches
|
|
752
|
+
6. Write logs
|
|
753
|
+
7. Archive ALL files
|
|
754
|
+
8. Mark ALL processed
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**Configuration:**
|
|
758
|
+
|
|
759
|
+
```json
|
|
760
|
+
{
|
|
761
|
+
"processingMode": "batch",
|
|
762
|
+
"maxFilesPerRun": 10
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
**Benefits:**
|
|
767
|
+
|
|
768
|
+
- ✅ Single job for all files (simplifies tracking)
|
|
769
|
+
- ✅ Faster for many small files (parallel parsing possible)
|
|
770
|
+
|
|
771
|
+
**Drawbacks:**
|
|
772
|
+
|
|
773
|
+
- ❌ High memory usage (all files in memory)
|
|
774
|
+
- ❌ All-or-nothing (one failure affects all)
|
|
775
|
+
- ❌ No incremental progress
|
|
776
|
+
|
|
777
|
+
### Mode 3: Chunked Per-File Processing (Balanced) ⚠️ OPTIONAL - Choose if needed
|
|
778
|
+
|
|
779
|
+
**When to use:**
|
|
780
|
+
|
|
781
|
+
- Many files (e.g., 100+ files)
|
|
782
|
+
- Process N files at a time
|
|
783
|
+
- Balance between memory and parallelism
|
|
784
|
+
|
|
785
|
+
**Flow:**
|
|
786
|
+
|
|
787
|
+
```
|
|
788
|
+
CHUNK files into groups of N (e.g., 5 files per chunk):
|
|
789
|
+
|
|
790
|
+
FOR EACH CHUNK:
|
|
791
|
+
FOR EACH FILE in chunk:
|
|
792
|
+
1. Download file
|
|
793
|
+
2. Parse JSON
|
|
794
|
+
3. Map records
|
|
795
|
+
4. Create Batch API job
|
|
796
|
+
5. Send batches
|
|
797
|
+
6. Archive file
|
|
798
|
+
|
|
799
|
+
Wait for chunk to complete before next chunk
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
**Configuration:**
|
|
803
|
+
|
|
804
|
+
```json
|
|
805
|
+
{
|
|
806
|
+
"processingMode": "chunked",
|
|
807
|
+
"fileChunkSize": 5,
|
|
808
|
+
"maxFilesPerRun": 100
|
|
809
|
+
}
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**Benefits:**
|
|
813
|
+
|
|
814
|
+
- ✅ Bounded memory usage (N files at a time)
|
|
815
|
+
- ✅ Better throughput than per-file (parallel processing within chunk)
|
|
816
|
+
- ✅ Incremental progress (per-chunk)
|
|
817
|
+
|
|
818
|
+
**Use cases:**
|
|
819
|
+
|
|
820
|
+
- High-volume file ingestion (100+ files per run)
|
|
821
|
+
- Rate limiting (control API load)
|
|
822
|
+
- Resource-constrained environments
|
|
823
|
+
|
|
824
|
+
### Comparison Matrix
|
|
825
|
+
|
|
826
|
+
| Aspect | Per-File | Batch | Chunked |
|
|
827
|
+
| ------------------- | ---------------------------------- | ----------------------------- | --------------------------------------- |
|
|
828
|
+
| **Memory Usage** | Low (1 file at a time) | High (all files) | Medium (N files) |
|
|
829
|
+
| **Consistency** | Per-file atomic | All-or-nothing | Per-chunk atomic |
|
|
830
|
+
| **Fault Isolation** | ✅ Best (1 file fails → others OK) | ❌ Worst (1 fails → all fail) | ✅ Good (chunk fails → other chunks OK) |
|
|
831
|
+
| **Performance** | Slower (sequential) | Fastest (parallel parsing) | Balanced |
|
|
832
|
+
| **Batch API Jobs** | N jobs (1 per file) | 1 job (all files) | N jobs (1 per file) |
|
|
833
|
+
| **Use Case** | Large files, strict consistency | Small files, fast processing | Many files, balanced approach |
|
|
834
|
+
| **Recommended For** | Production (safest) | Testing, small datasets | High-volume production |
|
|
835
|
+
|
|
836
|
+
> **📋 This Template Implementation:**
|
|
837
|
+
>
|
|
838
|
+
> **✅ This template implements ALL THREE modes** and selects based on `PROCESSING_MODE` variable.
|
|
839
|
+
>
|
|
840
|
+
> **Choose ONE mode per connector:**
|
|
841
|
+
> - **Default:** `PROCESSING_MODE=per-file` (recommended for production)
|
|
842
|
+
> - **Alternative:** `PROCESSING_MODE=batch` (for small files, atomic processing)
|
|
843
|
+
> - **Alternative:** `PROCESSING_MODE=chunked` (for high-volume scenarios)
|
|
844
|
+
>
|
|
845
|
+
> **Important:** Do NOT use multiple modes in the same connector. Each connector should use ONE consistent pattern.
|
|
846
|
+
|
|
847
|
+
### Decision Guide
|
|
848
|
+
|
|
849
|
+
| Source Data | File Count | File Size | Recommended Mode | Reason |
|
|
850
|
+
| -------------- | ---------- | ----------- | ---------------- | --------------------------------------- |
|
|
851
|
+
| Daily snapshot | 1-10 | >10MB each | **Per-File** | Memory efficient, fault isolation |
|
|
852
|
+
| Hourly deltas | 10-50 | <1MB each | **Batch** | Fast processing, small memory footprint |
|
|
853
|
+
| Real-time feed | 100+ | <100KB each | **Chunked** | Balanced throughput + memory |
|
|
854
|
+
| Initial load | 1 | >100MB | **Per-File** | Memory safety |
|
|
855
|
+
| Testing | 1-5 | Any | **Batch** | Simplicity |
|
|
856
|
+
|
|
857
|
+
---
|
|
858
|
+
|
|
859
|
+
## JSON File Format
|
|
860
|
+
|
|
861
|
+
### Sample: inventory.json (Standard JSON)
|
|
862
|
+
|
|
863
|
+
```json
|
|
864
|
+
{
|
|
865
|
+
"inventory": [
|
|
866
|
+
{
|
|
867
|
+
"locationRef": "LOC001",
|
|
868
|
+
"skuRef": "SKU-12345",
|
|
869
|
+
"qty": 100,
|
|
870
|
+
"type": "LAST_ON_HAND",
|
|
871
|
+
"status": "ACTIVE",
|
|
872
|
+
"expectedOn": "2025-01-25",
|
|
873
|
+
"attributes": {
|
|
874
|
+
"expiryDate": "2026-12-31",
|
|
875
|
+
"batchNumber": "BATCH-A001",
|
|
876
|
+
"condition": "NEW",
|
|
877
|
+
"storageZone": "ZONE-A"
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
"locationRef": "LOC001",
|
|
882
|
+
"skuRef": "SKU-67890",
|
|
883
|
+
"qty": 50,
|
|
884
|
+
"type": "LAST_ON_HAND",
|
|
885
|
+
"status": "ACTIVE",
|
|
886
|
+
"expectedOn": "2025-01-25",
|
|
887
|
+
"attributes": {
|
|
888
|
+
"expiryDate": "2026-06-30",
|
|
889
|
+
"batchNumber": "BATCH-A002",
|
|
890
|
+
"condition": "NEW",
|
|
891
|
+
"storageZone": "ZONE-B"
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
]
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
### Sample: inventory.jsonl (JSON Lines)
|
|
899
|
+
|
|
900
|
+
```jsonl
|
|
901
|
+
{"locationRef":"LOC001","skuRef":"SKU-12345","qty":100,"type":"LAST_ON_HAND","status":"ACTIVE","expectedOn":"2025-01-25","attributes":{"expiryDate":"2026-12-31","batchNumber":"BATCH-A001","condition":"NEW","storageZone":"ZONE-A"}}
|
|
902
|
+
{"locationRef":"LOC001","skuRef":"SKU-67890","qty":50,"type":"LAST_ON_HAND","status":"ACTIVE","expectedOn":"2025-01-25","attributes":{"expiryDate":"2026-06-30","batchNumber":"BATCH-A002","condition":"NEW","storageZone":"ZONE-B"}}
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
**JSON Structure:**
|
|
906
|
+
|
|
907
|
+
- Standard JSON: Root object with nested arrays (`{ "inventory": [...] }`)
|
|
908
|
+
- JSON Lines: One JSON object per line (streaming-friendly for large files)
|
|
909
|
+
- JSONParserService auto-detects format based on content
|
|
910
|
+
- Both formats support nested objects with dot notation
|
|
911
|
+
|
|
912
|
+
**Field Descriptions:**
|
|
913
|
+
|
|
914
|
+
- `locationRef`: Fluent location reference
|
|
915
|
+
- `skuRef`: Fluent SKU/product reference
|
|
916
|
+
- `qty`: Inventory quantity (integer)
|
|
917
|
+
- `type`: Inventory type (LAST_ON_HAND for full snapshots, DELTA for incremental changes)
|
|
918
|
+
- `status`: Record status (ACTIVE, INACTIVE)
|
|
919
|
+
- `expectedOn`: Expected date (ISO 8601 or parseable format)
|
|
920
|
+
- `attributes.expiryDate`: Product expiry date (optional)
|
|
921
|
+
- `attributes.batchNumber`: Manufacturing batch/lot number (optional)
|
|
922
|
+
- `attributes.condition`: Product condition (NEW, USED, DAMAGED)
|
|
923
|
+
- `attributes.storageZone`: Warehouse zone identifier (optional)
|
|
924
|
+
|
|
925
|
+
### JSON Field Mapping
|
|
926
|
+
|
|
927
|
+
**IMPORTANT**: JSON uses direct field access (no special prefix needed):
|
|
928
|
+
|
|
929
|
+
```json
|
|
930
|
+
{
|
|
931
|
+
"fields": {
|
|
932
|
+
"locationRef": { "source": "locationRef", "required": true },
|
|
933
|
+
"skuRef": { "source": "skuRef", "required": true }
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
**Why**: Unlike XML attributes (which need `@` prefix), JSON fields are accessed directly by name.
|
|
939
|
+
|
|
940
|
+
### Array Handling (Standard JSON vs JSON Lines)
|
|
941
|
+
|
|
942
|
+
**JSONParserService behavior:**
|
|
943
|
+
|
|
944
|
+
- **Standard JSON**: Returns object or array based on structure
|
|
945
|
+
- **JSON Lines**: Returns array of objects (one per line)
|
|
946
|
+
|
|
947
|
+
**Solution**: Always normalize to array after parsing:
|
|
948
|
+
|
|
949
|
+
```typescript
|
|
950
|
+
const parsed = await parser.parse(jsonContent, { format: 'json' | 'jsonl' });
|
|
951
|
+
|
|
952
|
+
// Extract inventory array - handle different JSON structures
|
|
953
|
+
let records: any[] = [];
|
|
954
|
+
if (format === 'jsonl') {
|
|
955
|
+
records = Array.isArray(parsed) ? parsed : [parsed];
|
|
956
|
+
} else {
|
|
957
|
+
if (Array.isArray(parsed)) {
|
|
958
|
+
records = parsed; // Root level array
|
|
959
|
+
} else if (parsed?.inventory && Array.isArray(parsed.inventory)) {
|
|
960
|
+
records = parsed.inventory; // { "inventory": [...] }
|
|
961
|
+
} else if (parsed?.records && Array.isArray(parsed.records)) {
|
|
962
|
+
records = parsed.records; // { "records": [...] }
|
|
963
|
+
} else {
|
|
964
|
+
records = [parsed]; // Single object
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
---
|
|
970
|
+
|
|
971
|
+
## Service Implementation
|
|
972
|
+
|
|
973
|
+
### File: `src/services/inventory-sync.service.ts`
|
|
974
|
+
|
|
975
|
+
**Complete implementation showing all SDK patterns:**
|
|
976
|
+
|
|
977
|
+
```typescript
|
|
978
|
+
import { Buffer } from 'node:buffer'; // ✅ Required for Versori/Deno runtime
|
|
979
|
+
import {
|
|
980
|
+
createClient,
|
|
981
|
+
S3DataSource,
|
|
982
|
+
JSONParserService,
|
|
983
|
+
UniversalMapper,
|
|
984
|
+
VersoriFileTracker,
|
|
985
|
+
JobTracker,
|
|
986
|
+
extractFileName,
|
|
987
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
988
|
+
import type { FileMetadata } from '@fluentcommerce/fc-connect-sdk';
|
|
989
|
+
import inventoryMapping from '../config/inventory.batch.json' with { type: 'json' }; // ✅ External JSON import
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Shared inventory ingestion orchestration logic
|
|
993
|
+
* Used by both scheduled and webhook workflows
|
|
994
|
+
*
|
|
995
|
+
* @param ctx - Versori HTTP context
|
|
996
|
+
* @param jobId - Unique job identifier
|
|
997
|
+
* @param tracker - JobTracker instance
|
|
998
|
+
*/
|
|
999
|
+
export async function runIngestion(
|
|
1000
|
+
ctx: any,
|
|
1001
|
+
jobId: string,
|
|
1002
|
+
tracker: JobTracker
|
|
1003
|
+
) {
|
|
1004
|
+
const { log, activation, openKv } = ctx;
|
|
1005
|
+
const startTime = Date.now();
|
|
1006
|
+
let s3: S3DataSource | null = null;
|
|
1007
|
+
|
|
1008
|
+
// Optional feature toggles
|
|
1009
|
+
const enableFileTracking = activation.getVariable('enableFileTracking') !== 'false';
|
|
1010
|
+
const detailedLogging = activation.getVariable('detailedLogging') === 'true';
|
|
1011
|
+
|
|
1012
|
+
try {
|
|
1013
|
+
// ========================================
|
|
1014
|
+
// CLIENT INITIALIZATION (with retailerId and validation)
|
|
1015
|
+
// ========================================
|
|
1016
|
+
log.info('🔧 [InventorySync] Initializing Fluent Commerce client...');
|
|
1017
|
+
|
|
1018
|
+
const client = await createClient({
|
|
1019
|
+
...ctx,
|
|
1020
|
+
validateConnection: true, // ✅ Validate connection on creation
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
// ✅ CRITICAL: Set retailerId for Batch API
|
|
1024
|
+
const retailerId = activation.getVariable('fluentRetailerId');
|
|
1025
|
+
if (!retailerId) {
|
|
1026
|
+
log.error('❌ [InventorySync] Missing required fluentRetailerId activation variable');
|
|
1027
|
+
throw new Error('fluentRetailerId activation variable is required for Batch API');
|
|
1028
|
+
}
|
|
1029
|
+
client.setRetailerId(retailerId);
|
|
1030
|
+
|
|
1031
|
+
log.info('✅ [InventorySync] Client initialized and validated', { retailerId });
|
|
1032
|
+
|
|
1033
|
+
// ========================================
|
|
1034
|
+
// S3 DATA SOURCE INITIALIZATION
|
|
1035
|
+
// ========================================
|
|
1036
|
+
log.info('🔧 [InventorySync] Initializing S3 data source...');
|
|
1037
|
+
|
|
1038
|
+
const s3Config = {
|
|
1039
|
+
bucket: activation.getVariable('s3BucketName'),
|
|
1040
|
+
region: activation.getVariable('awsRegion') || 'us-east-1',
|
|
1041
|
+
accessKeyId: activation.getVariable('awsAccessKeyId'),
|
|
1042
|
+
secretAccessKey: activation.getVariable('awsSecretAccessKey'),
|
|
1043
|
+
prefix: activation.getVariable('s3Prefix') || 'inventory/',
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
s3 = new S3DataSource(
|
|
1047
|
+
{
|
|
1048
|
+
type: 'S3_JSON',
|
|
1049
|
+
connectionId: 's3-inventory-sync',
|
|
1050
|
+
name: 'inventory-sync',
|
|
1051
|
+
s3Config,
|
|
1052
|
+
},
|
|
1053
|
+
log
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
log.info('✅ [InventorySync] S3 data source initialized', {
|
|
1057
|
+
bucket: s3Config.bucket,
|
|
1058
|
+
region: s3Config.region,
|
|
1059
|
+
prefix: s3Config.prefix,
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// ========================================
|
|
1063
|
+
// SERVICE INITIALIZATION
|
|
1064
|
+
// ========================================
|
|
1065
|
+
const parser = new JSONParserService();
|
|
1066
|
+
const mapper = new UniversalMapper(inventoryMapping);
|
|
1067
|
+
const kv = openKv(':project:');
|
|
1068
|
+
const fileTracker = enableFileTracking
|
|
1069
|
+
? new VersoriFileTracker(kv, 's3-json-inventory-sync')
|
|
1070
|
+
: null;
|
|
1071
|
+
const bppEnabled = activation.getVariable('bppEnabled') !== 'false';
|
|
1072
|
+
|
|
1073
|
+
if (detailedLogging) {
|
|
1074
|
+
log.info('🔍 [InventorySync] Configuration loaded', {
|
|
1075
|
+
bppEnabled,
|
|
1076
|
+
enableFileTracking,
|
|
1077
|
+
detailedLogging,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// ========================================
|
|
1082
|
+
// FILE DISCOVERY
|
|
1083
|
+
// ========================================
|
|
1084
|
+
log.info('🔍 [InventorySync] Discovering files on S3...');
|
|
1085
|
+
|
|
1086
|
+
const files = await s3.listFiles({ prefix: s3Config.prefix });
|
|
1087
|
+
log.info('📄 [InventorySync] File discovery complete', { count: files.length });
|
|
1088
|
+
|
|
1089
|
+
if (files.length === 0) {
|
|
1090
|
+
log.info('⚠️ [InventorySync] No files found to process');
|
|
1091
|
+
return { success: true, message: 'No files to process', filesProcessed: 0 };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// ========================================
|
|
1095
|
+
// FILE PROCESSING
|
|
1096
|
+
// ========================================
|
|
1097
|
+
const results = [];
|
|
1098
|
+
let fileIndex = 0;
|
|
1099
|
+
|
|
1100
|
+
for (const file of files) {
|
|
1101
|
+
fileIndex++;
|
|
1102
|
+
const fileStartTime = Date.now();
|
|
1103
|
+
const fileName = extractFileName(file.path);
|
|
1104
|
+
|
|
1105
|
+
log.info(`📄 [FILE ${fileIndex}/${files.length}] Processing: ${fileName}`);
|
|
1106
|
+
|
|
1107
|
+
try {
|
|
1108
|
+
// Check if already processed
|
|
1109
|
+
if (fileTracker && (await fileTracker.wasFileProcessed(file.name))) {
|
|
1110
|
+
log.info(`⏭️ [FILE ${fileIndex}/${files.length}] Skipping already processed: ${fileName}`);
|
|
1111
|
+
results.push({ fileName, success: true, skipped: true });
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Download and parse
|
|
1116
|
+
if (detailedLogging) {
|
|
1117
|
+
log.info(`🔍 [FILE ${fileIndex}/${files.length}] Downloading: ${fileName}`);
|
|
1118
|
+
}
|
|
1119
|
+
const jsonContent = (await s3.downloadFile(file.path, { encoding: 'utf8' })) as string;
|
|
1120
|
+
const parsed = await parser.parse(jsonContent);
|
|
1121
|
+
|
|
1122
|
+
// Extract inventory array
|
|
1123
|
+
const records = Array.isArray(parsed) ? parsed :
|
|
1124
|
+
parsed?.inventory ? parsed.inventory :
|
|
1125
|
+
[parsed];
|
|
1126
|
+
|
|
1127
|
+
if (records.length === 0) {
|
|
1128
|
+
log.warn(`⚠️ [FILE ${fileIndex}/${files.length}] No records found in: ${fileName}`);
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
log.info(`🔄 [FILE ${fileIndex}/${files.length}] Transforming ${records.length} records`);
|
|
1133
|
+
|
|
1134
|
+
// Transform records
|
|
1135
|
+
const mappedRecords = [];
|
|
1136
|
+
const aggregatedSkippedFields: string[] = [];
|
|
1137
|
+
for (const rec of records) {
|
|
1138
|
+
const sourceDataWithContext = { ...rec, $context: { retailerId } };
|
|
1139
|
+
const mappingResult = await mapper.map(sourceDataWithContext);
|
|
1140
|
+
if (mappingResult.success) {
|
|
1141
|
+
mappedRecords.push(mappingResult.data);
|
|
1142
|
+
}
|
|
1143
|
+
// Aggregate skipped fields
|
|
1144
|
+
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
1145
|
+
for (const fieldName of mappingResult.skippedFields) {
|
|
1146
|
+
if (!aggregatedSkippedFields.includes(fieldName)) {
|
|
1147
|
+
aggregatedSkippedFields.push(fieldName);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (aggregatedSkippedFields.length > 0) {
|
|
1154
|
+
log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
1155
|
+
file: fileName,
|
|
1156
|
+
skippedFields: aggregatedSkippedFields,
|
|
1157
|
+
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (mappedRecords.length === 0) {
|
|
1162
|
+
log.warn(`⚠️ [FILE ${fileIndex}/${files.length}] No valid records after mapping: ${fileName}`, {
|
|
1163
|
+
recommendation: 'Check mapping configuration and required fields',
|
|
1164
|
+
});
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Create job and send batches
|
|
1169
|
+
log.info(`📦 [FILE ${fileIndex}/${files.length}] Creating Batch API job for ${mappedRecords.length} records`);
|
|
1170
|
+
|
|
1171
|
+
const job = await client.createJob({
|
|
1172
|
+
name: `s3-json-${fileName}-${Date.now()}`,
|
|
1173
|
+
meta: bppEnabled ? undefined : { preprocessing: 'skip' },
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
await tracker.updateJob(jobId, {
|
|
1177
|
+
status: 'processing',
|
|
1178
|
+
details: { fileName, recordCount: mappedRecords.length },
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
// ? Enhanced: Extract context for progress logging
|
|
1182
|
+
const uniqueLocations = [...new Set(mappedRecords.map((r: any) => r.locationRef))];
|
|
1183
|
+
const sampleSKUs = mappedRecords.slice(0, 5).map((r: any) => r.skuRef);
|
|
1184
|
+
|
|
1185
|
+
// ? Enhanced: Start logging with context
|
|
1186
|
+
log.info(`📤 [BatchProcessor] Sending batch for file "${fileName}"`, {
|
|
1187
|
+
totalRecords: mappedRecords.length,
|
|
1188
|
+
locations: uniqueLocations.join(', '),
|
|
1189
|
+
sampleSKUs: sampleSKUs.join(', '),
|
|
1190
|
+
jobId: job.id
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
// Send batches (fire-and-forget)
|
|
1194
|
+
await client.sendBatch(job.id, {
|
|
1195
|
+
action: 'UPSERT',
|
|
1196
|
+
entityType: 'INVENTORY',
|
|
1197
|
+
entities: mappedRecords,
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// ? Enhanced: Completion logging
|
|
1201
|
+
log.info(`✅ [BatchProcessor] Batch sent successfully for file "${fileName}"`, {
|
|
1202
|
+
totalRecords: mappedRecords.length,
|
|
1203
|
+
jobId: job.id,
|
|
1204
|
+
duration: Date.now() - fileStartTime
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// ? Enhanced: Clear large arrays after processing to free memory
|
|
1208
|
+
mappedRecords.length = 0;
|
|
1209
|
+
|
|
1210
|
+
// Mark as processed
|
|
1211
|
+
if (fileTracker) {
|
|
1212
|
+
await fileTracker.markFileProcessed(file.name, {
|
|
1213
|
+
recordCount: mappedRecords.length,
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const fileDuration = Date.now() - fileStartTime;
|
|
1218
|
+
results.push({
|
|
1219
|
+
fileName,
|
|
1220
|
+
success: true,
|
|
1221
|
+
recordCount: mappedRecords.length,
|
|
1222
|
+
duration: fileDuration,
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
log.info(`✅ [FILE ${fileIndex}/${files.length}] Successfully processed: ${fileName}`, {
|
|
1226
|
+
records: mappedRecords.length,
|
|
1227
|
+
jobId: job.id,
|
|
1228
|
+
duration: `${fileDuration}ms (${(fileDuration / 1000).toFixed(2)}s)`,
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
} catch (error: any) {
|
|
1232
|
+
const fileDuration = Date.now() - fileStartTime;
|
|
1233
|
+
log.error(`❌ [FILE ${fileIndex}/${files.length}] Processing failed: ${fileName}`, {
|
|
1234
|
+
error: error?.message,
|
|
1235
|
+
stack: error?.stack,
|
|
1236
|
+
duration: `${fileDuration}ms`,
|
|
1237
|
+
recommendation: 'Check S3 permissions, file format, and mapping configuration',
|
|
1238
|
+
});
|
|
1239
|
+
results.push({ fileName, success: false, error: error?.message, duration: fileDuration });
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const totalDuration = Date.now() - startTime;
|
|
1244
|
+
const successCount = results.filter((r) => r.success && !r.skipped).length;
|
|
1245
|
+
|
|
1246
|
+
log.info('✅ [InventorySync] Batch processing complete', {
|
|
1247
|
+
total: files.length,
|
|
1248
|
+
processed: successCount,
|
|
1249
|
+
skipped: results.filter((r) => r.skipped).length,
|
|
1250
|
+
failed: results.filter((r) => !r.success).length,
|
|
1251
|
+
duration: `${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)`,
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
return {
|
|
1255
|
+
success: true,
|
|
1256
|
+
results,
|
|
1257
|
+
filesProcessed: successCount,
|
|
1258
|
+
duration: totalDuration,
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
} catch (error: any) {
|
|
1262
|
+
const totalDuration = Date.now() - startTime;
|
|
1263
|
+
log.error('❌ [InventorySync] Fatal error during ingestion', {
|
|
1264
|
+
message: error?.message,
|
|
1265
|
+
stack: error?.stack,
|
|
1266
|
+
duration: `${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)`,
|
|
1267
|
+
recommendation: 'Check activation variables, S3 credentials, and Fluent API connectivity',
|
|
1268
|
+
});
|
|
1269
|
+
throw error; // Re-throw for workflow error handling
|
|
1270
|
+
} finally {
|
|
1271
|
+
// ✅ CRITICAL: Always dispose S3 connection
|
|
1272
|
+
if (s3) {
|
|
1273
|
+
try {
|
|
1274
|
+
await s3.dispose();
|
|
1275
|
+
log.info('✅ [InventorySync] S3 connection disposed successfully');
|
|
1276
|
+
} catch (disposeError: any) {
|
|
1277
|
+
log.error('⚠️ [InventorySync] Failed to dispose S3 connection', {
|
|
1278
|
+
error: disposeError?.message,
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
**Key Patterns Demonstrated:**
|
|
1287
|
+
- ✅ Buffer import for Versori/Deno compatibility
|
|
1288
|
+
- ✅ External JSON mapping import with `{ type: 'json' }`
|
|
1289
|
+
- ✅ `validateConnection: true` for client initialization
|
|
1290
|
+
- ✅ `setRetailerId()` call after client creation (REQUIRED for Batch API)
|
|
1291
|
+
- ✅ Emoji-based logging (🚀 ✅ ❌ ⚠️ 🔍 📄) for visual log scanning
|
|
1292
|
+
- ✅ Execution boundaries with ═══ separators
|
|
1293
|
+
- ✅ `extractFileName()` usage for clean file names
|
|
1294
|
+
- ✅ Optional feature toggles (enableFileTracking, detailedLogging)
|
|
1295
|
+
- ✅ Duration tracking at file and workflow levels (ms and seconds)
|
|
1296
|
+
- ✅ Recommendations in error logs for actionable debugging
|
|
1297
|
+
- ✅ S3 `dispose()` in finally block with error handling
|
|
1298
|
+
- ✅ VersoriFileTracker for state management (optional via toggle)
|
|
1299
|
+
- ✅ JobTracker for job lifecycle tracking
|
|
1300
|
+
|
|
1301
|
+
---
|
|
1302
|
+
|
|
1303
|
+
## Code Flow Explanation
|
|
1304
|
+
|
|
1305
|
+
### Initialization Phase
|
|
1306
|
+
|
|
1307
|
+
**Logger Setup:**
|
|
1308
|
+
|
|
1309
|
+
```typescript
|
|
1310
|
+
// ✅ CORRECT: Use native Versori log from context
|
|
1311
|
+
const { log } = ctx;
|
|
1312
|
+
log.info('Starting workflow');
|
|
1313
|
+
|
|
1314
|
+
// ❌ WRONG: LoggingService (removed - use native log on Versori)
|
|
1315
|
+
// import { LoggingService } from '@fluentcommerce/fc-connect-sdk';
|
|
1316
|
+
// const logging = new LoggingService();
|
|
1317
|
+
// const log = logging.createLogger({ logLevel: 'info' });
|
|
1318
|
+
```
|
|
1319
|
+
|
|
1320
|
+
- Versori provides `log` in the context - use it directly
|
|
1321
|
+
|
|
1322
|
+
**Client Creation:**
|
|
1323
|
+
|
|
1324
|
+
```typescript
|
|
1325
|
+
// Pass entire Versori context object
|
|
1326
|
+
const client = await createClient(ctx);
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
- `createClient(ctx)` accepts entire Versori context (fetch, connections, log, activation)
|
|
1330
|
+
- Auto-detects Versori platform from context
|
|
1331
|
+
- Returns `FluentClient` configured with OAuth2 from connections.fluent_commerce
|
|
1332
|
+
- OAuth2 authentication handled automatically
|
|
1333
|
+
- This is the **CORRECT** pattern for Versori workflows
|
|
1334
|
+
|
|
1335
|
+
**S3 Data Source:**
|
|
1336
|
+
|
|
1337
|
+
```typescript
|
|
1338
|
+
const s3 = new S3DataSource(
|
|
1339
|
+
{
|
|
1340
|
+
type: 'S3_JSON', // Type indicates JSON files (metadata only)
|
|
1341
|
+
connectionId: 's3-inventory-sync', // Unique identifier (required)
|
|
1342
|
+
name: 'inventory-sync', // Human-readable name (required)
|
|
1343
|
+
s3Config: {
|
|
1344
|
+
bucket: 'my-inventory-bucket',
|
|
1345
|
+
region: 'us-east-1',
|
|
1346
|
+
accessKeyId: 'AKIAXXXX',
|
|
1347
|
+
secretAccessKey: 'xxxxxxxx',
|
|
1348
|
+
},
|
|
1349
|
+
},
|
|
1350
|
+
log
|
|
1351
|
+
);
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
- **Constructor params:** `(config: S3DataSourceConfig, log)` - Versori native log, no explicit typing needed
|
|
1355
|
+
- `connectionId` and `name` are required in config
|
|
1356
|
+
- `type` must be `'S3_JSON'` for JSON files
|
|
1357
|
+
- Enhanced retry logic with exponential backoff
|
|
1358
|
+
|
|
1359
|
+
### File Discovery Phase
|
|
1360
|
+
|
|
1361
|
+
**List Files:**
|
|
1362
|
+
|
|
1363
|
+
```typescript
|
|
1364
|
+
const files = await s3.listFiles({
|
|
1365
|
+
prefix: config.s3.prefix, // Override config (optional)
|
|
1366
|
+
maxKeys: 1000, // Maximum files to retrieve
|
|
1367
|
+
});
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
- Returns `FileMetadata[]` with: `name`, `lastModified`, `size`, `path`, `source`
|
|
1371
|
+
- Files sorted by modification time (newest first)
|
|
1372
|
+
- Directories excluded automatically
|
|
1373
|
+
|
|
1374
|
+
**State Check:**
|
|
1375
|
+
|
|
1376
|
+
```typescript
|
|
1377
|
+
const alreadyProcessed = await fileTracker.wasFileProcessed(file.name);
|
|
1378
|
+
if (alreadyProcessed) {
|
|
1379
|
+
continue; // Skip already processed files
|
|
1380
|
+
}
|
|
1381
|
+
```
|
|
1382
|
+
|
|
1383
|
+
- Uses Versori KV store (`openKv()`) for distributed state
|
|
1384
|
+
- Prevents duplicate processing across workflow runs
|
|
1385
|
+
- State persists between executions
|
|
1386
|
+
|
|
1387
|
+
### File Processing Phase
|
|
1388
|
+
|
|
1389
|
+
**Download File:**
|
|
1390
|
+
|
|
1391
|
+
```typescript
|
|
1392
|
+
const jsonContent = (await s3.downloadFile(file.path, {
|
|
1393
|
+
encoding: 'utf8',
|
|
1394
|
+
})) as string;
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
- **Important:** Cast to `string` when `encoding` is specified
|
|
1398
|
+
- Without encoding, returns `Buffer`
|
|
1399
|
+
- Supports streaming for large files
|
|
1400
|
+
|
|
1401
|
+
**Parse JSON:**
|
|
1402
|
+
|
|
1403
|
+
```typescript
|
|
1404
|
+
const jsonFormat = activation.getVariable('jsonFormat') || 'json';
|
|
1405
|
+
const parsed = await parser.parse(jsonContent, { format: jsonFormat });
|
|
1406
|
+
|
|
1407
|
+
// Extract inventory array - handle different JSON structures
|
|
1408
|
+
let records: any[] = [];
|
|
1409
|
+
if (jsonFormat === 'jsonl') {
|
|
1410
|
+
records = Array.isArray(parsed) ? parsed : [parsed];
|
|
1411
|
+
} else {
|
|
1412
|
+
if (Array.isArray(parsed)) {
|
|
1413
|
+
records = parsed; // Root level array
|
|
1414
|
+
} else if (parsed?.inventory && Array.isArray(parsed.inventory)) {
|
|
1415
|
+
records = parsed.inventory; // { "inventory": [...] }
|
|
1416
|
+
} else {
|
|
1417
|
+
records = [parsed]; // Single object
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
- Parses JSON into JavaScript object or array
|
|
1423
|
+
- **Format detection**: Auto-detects standard JSON vs JSON Lines
|
|
1424
|
+
- Always normalize to array for consistent processing
|
|
1425
|
+
- Throws `ParsingError` on invalid JSON
|
|
1426
|
+
|
|
1427
|
+
**JSON Field Handling:**
|
|
1428
|
+
|
|
1429
|
+
- Direct field access by name (no special prefix needed)
|
|
1430
|
+
- Example: `{ "locationRef": "LOC001" }` → `{ locationRef: 'LOC001' }`
|
|
1431
|
+
- Use simple paths like `"locationRef"` in mapping config
|
|
1432
|
+
|
|
1433
|
+
**Transform Data:**
|
|
1434
|
+
|
|
1435
|
+
```typescript
|
|
1436
|
+
const mapper = new UniversalMapper(inventoryMapping);
|
|
1437
|
+
|
|
1438
|
+
// Map each record directly (no wrapper needed for JSON)
|
|
1439
|
+
const mappingResult = await mapper.map(rec);
|
|
1440
|
+
|
|
1441
|
+
if (!mappingResult.success) {
|
|
1442
|
+
// Mapping validation failed for required fields
|
|
1443
|
+
log.warn('Mapping failed:', mappingResult.errors);
|
|
1444
|
+
continue; // Skip invalid records
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
mappedRecords.push(mappingResult.data);
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
- Direct mapping (no need to wrap in object like XML)
|
|
1451
|
+
- `MappingResult`: `{ success: boolean, data: any, errors?: string[] }`
|
|
1452
|
+
- `success: false` only if required fields fail
|
|
1453
|
+
- Optional field errors reported but don't fail mapping
|
|
1454
|
+
|
|
1455
|
+
### Batch API Phase
|
|
1456
|
+
|
|
1457
|
+
**Create Job:**
|
|
1458
|
+
|
|
1459
|
+
```typescript
|
|
1460
|
+
// Full snapshot (BPP enabled by default)
|
|
1461
|
+
const job = await client.createJob({
|
|
1462
|
+
name: 'Daily Full Inventory',
|
|
1463
|
+
retailerId: 'my-retailer',
|
|
1464
|
+
// BPP automatically enabled - filters unchanged records
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
// Delta feed (skip BPP)
|
|
1468
|
+
const job = await client.createJob({
|
|
1469
|
+
name: 'Hourly Deltas',
|
|
1470
|
+
retailerId: 'my-retailer',
|
|
1471
|
+
meta: {
|
|
1472
|
+
preprocessing: 'skip', // All records are changes
|
|
1473
|
+
},
|
|
1474
|
+
});
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
- Returns job object with `id` and metadata
|
|
1478
|
+
- `retailerId` required for tenant isolation
|
|
1479
|
+
- BPP enabled by default, skip with `meta: { preprocessing: 'skip' }`
|
|
1480
|
+
|
|
1481
|
+
**Send Batches:**
|
|
1482
|
+
|
|
1483
|
+
```typescript
|
|
1484
|
+
const batch = await client.sendBatch(jobId, {
|
|
1485
|
+
action: 'UPSERT', // Most common: create or update
|
|
1486
|
+
entityType: 'INVENTORY', // Entity type for inventory
|
|
1487
|
+
source: 'S3_JSON', // Data source identifier (optional)
|
|
1488
|
+
event: 'InventoryChanged', // Valid Rubix event (optional, defaults to InventoryChanged)
|
|
1489
|
+
entities: chunk, // Array of inventory records
|
|
1490
|
+
});
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
- `action`: `'UPSERT'` (currently the only action supported by Fluent Batch API)
|
|
1494
|
+
- `entityType`: `'INVENTORY'` for inventory records
|
|
1495
|
+
- `entities`: Array of transformed inventory objects
|
|
1496
|
+
- Returns batch object with `id` for tracking
|
|
1497
|
+
|
|
1498
|
+
**Fire-and-Forget Pattern:**
|
|
1499
|
+
|
|
1500
|
+
```typescript
|
|
1501
|
+
const batch = await client.sendBatch(jobId, { ... });
|
|
1502
|
+
log.info('Batch sent (fire-and-forget)', { batchId: batch.id });
|
|
1503
|
+
|
|
1504
|
+
// Record batch details for audit trail
|
|
1505
|
+
result.batches.push({
|
|
1506
|
+
batchId: batch.id,
|
|
1507
|
+
recordCount: chunk.length,
|
|
1508
|
+
timestamp: new Date().toISOString(),
|
|
1509
|
+
status: 'SENT',
|
|
1510
|
+
});
|
|
1511
|
+
```
|
|
1512
|
+
|
|
1513
|
+
- No polling - batches sent and tracked immediately
|
|
1514
|
+
- Batch details recorded for audit logging
|
|
1515
|
+
- Log files written to S3 for tracking (optional, configurable)
|
|
1516
|
+
|
|
1517
|
+
### Cleanup Phase
|
|
1518
|
+
|
|
1519
|
+
**Archive File:**
|
|
1520
|
+
|
|
1521
|
+
```typescript
|
|
1522
|
+
await s3.moveFile(
|
|
1523
|
+
file.path,
|
|
1524
|
+
`${config.s3.archivePrefix}${file.name}`
|
|
1525
|
+
);
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
- Moves file on S3 (atomic operation)
|
|
1529
|
+
- Creates destination prefix if needed
|
|
1530
|
+
- Original file removed after successful move
|
|
1531
|
+
|
|
1532
|
+
**Mark Processed:**
|
|
1533
|
+
|
|
1534
|
+
```typescript
|
|
1535
|
+
await fileTracker.markFileProcessed(file.name, {
|
|
1536
|
+
recordCount: batchResults.totalSent,
|
|
1537
|
+
batchCount: batchResults.batchCount,
|
|
1538
|
+
jobId: job.id,
|
|
1539
|
+
});
|
|
1540
|
+
```
|
|
1541
|
+
|
|
1542
|
+
- Stores metadata in Versori KV store
|
|
1543
|
+
- Metadata optional but useful for audit trails
|
|
1544
|
+
- Prevents reprocessing in future runs
|
|
1545
|
+
|
|
1546
|
+
**Dispose Resources:**
|
|
1547
|
+
|
|
1548
|
+
```typescript
|
|
1549
|
+
await s3.dispose();
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
- Releases S3 connection pool
|
|
1553
|
+
- Should be called at end of workflow in `finally` block
|
|
1554
|
+
- Ensures proper cleanup
|
|
1555
|
+
|
|
1556
|
+
---
|
|
1557
|
+
|
|
1558
|
+
## Versori Activation Variables
|
|
1559
|
+
|
|
1560
|
+
Configure in Versori platform settings:
|
|
1561
|
+
|
|
1562
|
+
```bash
|
|
1563
|
+
# S3 Configuration
|
|
1564
|
+
S3_BUCKET_NAME=my-inventory-bucket
|
|
1565
|
+
AWS_REGION=us-east-1
|
|
1566
|
+
AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXX
|
|
1567
|
+
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
1568
|
+
|
|
1569
|
+
# S3 Paths
|
|
1570
|
+
S3_PREFIX=inventory/
|
|
1571
|
+
ARCHIVE_PREFIX=processed/
|
|
1572
|
+
ERROR_PREFIX=errors/
|
|
1573
|
+
LOG_PREFIX=logs/
|
|
1574
|
+
|
|
1575
|
+
# File Processing
|
|
1576
|
+
FILE_PATTERN=.json
|
|
1577
|
+
JSON_FORMAT=json # or 'jsonl' for JSON Lines
|
|
1578
|
+
MAX_FILES=10
|
|
1579
|
+
|
|
1580
|
+
# Processing Mode Configuration
|
|
1581
|
+
# ⚠️ IMPORTANT: Choose ONE mode per connector (these are alternatives, not used together)
|
|
1582
|
+
# Options: "per-file" (default, recommended), "batch", "chunked"
|
|
1583
|
+
PROCESSING_MODE=per-file
|
|
1584
|
+
# For chunked mode only: number of files to process per chunk (default: 5)
|
|
1585
|
+
FILE_CHUNK_SIZE=5
|
|
1586
|
+
|
|
1587
|
+
# Batch Log Configuration
|
|
1588
|
+
LOG_ENABLED=true # Enable/disable batch log writing (default: true)
|
|
1589
|
+
LOG_FORMAT=json # Log format: json or text (default: json)
|
|
1590
|
+
|
|
1591
|
+
# Fluent Configuration (via Versori connection)
|
|
1592
|
+
# Connection: fluent_commerce (OAuth2)
|
|
1593
|
+
FLUENT_RETAILER_ID=my-retailer
|
|
1594
|
+
|
|
1595
|
+
# Batch Processing
|
|
1596
|
+
BATCH_SIZE=1000
|
|
1597
|
+
|
|
1598
|
+
# BPP Configuration
|
|
1599
|
+
# true = Full snapshot (BPP filters unchanged records - DEFAULT)
|
|
1600
|
+
# false = Delta feed (skip BPP, all records are changes)
|
|
1601
|
+
BPP_ENABLED=true
|
|
1602
|
+
|
|
1603
|
+
# Optional Feature Toggles (NEW)
|
|
1604
|
+
ENABLE_FILE_TRACKING=true # Track processed files to prevent duplicates (default: true)
|
|
1605
|
+
DETAILED_LOGGING=false # Enable verbose logging for debugging (default: false)
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
---
|
|
1609
|
+
|
|
1610
|
+
## Batch API Payload Example
|
|
1611
|
+
|
|
1612
|
+
What gets sent to Fluent Commerce Batch API:
|
|
1613
|
+
|
|
1614
|
+
```json
|
|
1615
|
+
{
|
|
1616
|
+
"action": "UPSERT",
|
|
1617
|
+
"entityType": "INVENTORY",
|
|
1618
|
+
"source": "S3_JSON",
|
|
1619
|
+
"event": "InventoryChanged",
|
|
1620
|
+
"entities": [
|
|
1621
|
+
{
|
|
1622
|
+
"retailerId": 1,
|
|
1623
|
+
"locationRef": "LOC001",
|
|
1624
|
+
"skuRef": "SKU-12345",
|
|
1625
|
+
"qty": 100,
|
|
1626
|
+
"type": "LAST_ON_HAND",
|
|
1627
|
+
"status": "ACTIVE",
|
|
1628
|
+
"expectedOn": "2025-01-25T00:00:00.000Z",
|
|
1629
|
+
"attributes": {
|
|
1630
|
+
"expiryDate": "2026-12-31T00:00:00.000Z",
|
|
1631
|
+
"batchNumber": "BATCH-A001",
|
|
1632
|
+
"condition": "NEW",
|
|
1633
|
+
"storageZone": "ZONE-A"
|
|
1634
|
+
}
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
"retailerId": 1,
|
|
1638
|
+
"locationRef": "LOC001",
|
|
1639
|
+
"skuRef": "SKU-67890",
|
|
1640
|
+
"qty": 50,
|
|
1641
|
+
"type": "LAST_ON_HAND",
|
|
1642
|
+
"status": "ACTIVE",
|
|
1643
|
+
"expectedOn": "2025-01-25T00:00:00.000Z",
|
|
1644
|
+
"attributes": {
|
|
1645
|
+
"expiryDate": "2026-06-30T00:00:00.000Z",
|
|
1646
|
+
"batchNumber": "BATCH-A002",
|
|
1647
|
+
"condition": "NEW",
|
|
1648
|
+
"storageZone": "ZONE-B"
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
]
|
|
1652
|
+
}
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
> **⚠️ CRITICAL:** Each entity MUST include `retailerId` - it's required in the entity payload, not just in createJob()
|
|
1656
|
+
|
|
1657
|
+
---
|
|
1658
|
+
|
|
1659
|
+
## Versori Deployment
|
|
1660
|
+
|
|
1661
|
+
Use the Versori CLI for deploy and operations:
|
|
1662
|
+
|
|
1663
|
+
```bash
|
|
1664
|
+
# Install dependencies
|
|
1665
|
+
npm install
|
|
1666
|
+
|
|
1667
|
+
# Deploy to Versori
|
|
1668
|
+
versori deploy
|
|
1669
|
+
|
|
1670
|
+
# View logs
|
|
1671
|
+
versori logs s3-json-inventory-batch-sync
|
|
1672
|
+
|
|
1673
|
+
# Trigger manual run (if defined)
|
|
1674
|
+
versori run ingest-now
|
|
1675
|
+
```
|
|
1676
|
+
|
|
1677
|
+
---
|
|
1678
|
+
|
|
1679
|
+
## Testing
|
|
1680
|
+
|
|
1681
|
+
### Test Scheduled Batch
|
|
1682
|
+
|
|
1683
|
+
Upload a test JSON file to S3 incoming prefix and wait for the scheduled run.
|
|
1684
|
+
|
|
1685
|
+
**Check logs:**
|
|
1686
|
+
|
|
1687
|
+
```
|
|
1688
|
+
[STEP 1/8] Initializing job tracking
|
|
1689
|
+
[STEP 2/8] Initializing Fluent Commerce client and S3
|
|
1690
|
+
[STEP 3/8] Discovering files on S3
|
|
1691
|
+
[FILE 1/1] Processing file: inventory_20250124.json
|
|
1692
|
+
[STEP 4/8] Downloading and parsing: inventory_20250124.json
|
|
1693
|
+
[STEP 5/8] Transforming 5000 inventory records from inventory_20250124.json
|
|
1694
|
+
[STEP 6/8] Creating batch job and sending 5 batches to Fluent Commerce
|
|
1695
|
+
[STEP 7/8] Archiving file: inventory_20250124.json
|
|
1696
|
+
[STEP 8/8] Completing job and calculating totals
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
### Test Ad hoc Batch
|
|
1700
|
+
|
|
1701
|
+
```bash
|
|
1702
|
+
# Process all pending files
|
|
1703
|
+
curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
|
|
1704
|
+
-H "Content-Type: application/json" \
|
|
1705
|
+
-d '{}'
|
|
1706
|
+
|
|
1707
|
+
# Process specific pattern
|
|
1708
|
+
curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
|
|
1709
|
+
-H "Content-Type: application/json" \
|
|
1710
|
+
-d '{
|
|
1711
|
+
"filePattern": "urgent_*.json"
|
|
1712
|
+
}'
|
|
1713
|
+
|
|
1714
|
+
# Force reprocess
|
|
1715
|
+
curl -X POST https://api.versori.com/webhooks/inventory-batch-adhoc \
|
|
1716
|
+
-H "Content-Type: application/json" \
|
|
1717
|
+
-d '{
|
|
1718
|
+
"forceReprocess": true,
|
|
1719
|
+
"filePattern": "inventory_20250124.json"
|
|
1720
|
+
}'
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
### Test Job Status Query
|
|
1724
|
+
|
|
1725
|
+
```bash
|
|
1726
|
+
curl -X POST https://api.versori.com/webhooks/inventory-batch-job-status \
|
|
1727
|
+
-H "Content-Type: application/json" \
|
|
1728
|
+
-d '{
|
|
1729
|
+
"jobId": "ADHOC_INV_20251024_183045_abc123"
|
|
1730
|
+
}'
|
|
1731
|
+
```
|
|
1732
|
+
|
|
1733
|
+
### Verify Batch Job in Fluent
|
|
1734
|
+
|
|
1735
|
+
After processing, check the Batch job status in Fluent Commerce:
|
|
1736
|
+
|
|
1737
|
+
```bash
|
|
1738
|
+
# Upload test file to S3
|
|
1739
|
+
aws s3 cp inventory-test.json s3://my-bucket/inventory/
|
|
1740
|
+
|
|
1741
|
+
# Query job status via GraphQL
|
|
1742
|
+
curl -X POST https://your-fluent-instance.com/graphql \
|
|
1743
|
+
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
1744
|
+
-H "Content-Type: application/json" \
|
|
1745
|
+
-d '{
|
|
1746
|
+
"query": "query { job(id: \"job-123456\") { id status recordCount processedCount } }"
|
|
1747
|
+
}'
|
|
1748
|
+
```
|
|
1749
|
+
|
|
1750
|
+
---
|
|
1751
|
+
|
|
1752
|
+
## Monitoring
|
|
1753
|
+
|
|
1754
|
+
### Success Response
|
|
1755
|
+
|
|
1756
|
+
```json
|
|
1757
|
+
{
|
|
1758
|
+
"success": true,
|
|
1759
|
+
"filesProcessed": 1,
|
|
1760
|
+
"filesSkipped": 0,
|
|
1761
|
+
"filesFailed": 0,
|
|
1762
|
+
"results": [
|
|
1763
|
+
{
|
|
1764
|
+
"file": "inventory_2025-01-22.json",
|
|
1765
|
+
"success": true,
|
|
1766
|
+
"recordCount": 5000,
|
|
1767
|
+
"batchCount": 5,
|
|
1768
|
+
"jobId": "job-123456",
|
|
1769
|
+
"duration": 12345
|
|
1770
|
+
}
|
|
1771
|
+
],
|
|
1772
|
+
"duration": 13456
|
|
1773
|
+
}
|
|
1774
|
+
```
|
|
1775
|
+
|
|
1776
|
+
### Error Response
|
|
1777
|
+
|
|
1778
|
+
```json
|
|
1779
|
+
{
|
|
1780
|
+
"success": false,
|
|
1781
|
+
"filesProcessed": 0,
|
|
1782
|
+
"filesFailed": 1,
|
|
1783
|
+
"results": [
|
|
1784
|
+
{
|
|
1785
|
+
"file": "inventory_2025-01-22.json",
|
|
1786
|
+
"success": false,
|
|
1787
|
+
"error": "No valid records after mapping"
|
|
1788
|
+
}
|
|
1789
|
+
],
|
|
1790
|
+
"duration": 876
|
|
1791
|
+
}
|
|
1792
|
+
```
|
|
1793
|
+
|
|
1794
|
+
---
|
|
1795
|
+
|
|
1796
|
+
## Common Pitfalls and Solutions
|
|
1797
|
+
|
|
1798
|
+
### 1. JSON Format Detection
|
|
1799
|
+
|
|
1800
|
+
❌ **Wrong:**
|
|
1801
|
+
|
|
1802
|
+
```typescript
|
|
1803
|
+
// Assuming format without checking
|
|
1804
|
+
const records = parsed.inventory;
|
|
1805
|
+
```
|
|
1806
|
+
|
|
1807
|
+
✅ **Correct:**
|
|
1808
|
+
|
|
1809
|
+
```typescript
|
|
1810
|
+
// Handle both standard JSON and JSON Lines
|
|
1811
|
+
let records: any[] = [];
|
|
1812
|
+
if (format === 'jsonl') {
|
|
1813
|
+
records = Array.isArray(parsed) ? parsed : [parsed];
|
|
1814
|
+
} else {
|
|
1815
|
+
if (Array.isArray(parsed)) records = parsed;
|
|
1816
|
+
else if (parsed?.inventory) records = parsed.inventory;
|
|
1817
|
+
else records = [parsed];
|
|
1818
|
+
}
|
|
1819
|
+
```
|
|
1820
|
+
|
|
1821
|
+
**Why:** JSON files can have different structures; always normalize.
|
|
1822
|
+
|
|
1823
|
+
### 2. JSON Field Mapping
|
|
1824
|
+
|
|
1825
|
+
❌ **Wrong:**
|
|
1826
|
+
|
|
1827
|
+
```json
|
|
1828
|
+
{
|
|
1829
|
+
"fields": {
|
|
1830
|
+
"locationRef": { "source": "@locationRef" }
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
✅ **Correct:**
|
|
1836
|
+
|
|
1837
|
+
```json
|
|
1838
|
+
{
|
|
1839
|
+
"fields": {
|
|
1840
|
+
"locationRef": { "source": "locationRef" }
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
```
|
|
1844
|
+
|
|
1845
|
+
**Why:** JSON uses direct field access (no `@` prefix like XML attributes).
|
|
1846
|
+
|
|
1847
|
+
### 3. Mapping Data Structure
|
|
1848
|
+
|
|
1849
|
+
❌ **Wrong:**
|
|
1850
|
+
|
|
1851
|
+
```typescript
|
|
1852
|
+
// Wrapping JSON record unnecessarily
|
|
1853
|
+
const mappingResult = await mapper.map({ inventory: rec });
|
|
1854
|
+
```
|
|
1855
|
+
|
|
1856
|
+
✅ **Correct:**
|
|
1857
|
+
|
|
1858
|
+
```typescript
|
|
1859
|
+
// Map JSON record directly
|
|
1860
|
+
const mappingResult = await mapper.map(rec);
|
|
1861
|
+
```
|
|
1862
|
+
|
|
1863
|
+
**Why:** Unlike XML, JSON records don't need wrapper objects for mapping.
|
|
1864
|
+
|
|
1865
|
+
### 4. BPP Configuration
|
|
1866
|
+
|
|
1867
|
+
❌ **Wrong:**
|
|
1868
|
+
|
|
1869
|
+
```typescript
|
|
1870
|
+
// Using BPP for delta feeds (wastes processing time)
|
|
1871
|
+
const job = await client.createJob({
|
|
1872
|
+
name: 'Hourly Deltas',
|
|
1873
|
+
retailerId: 'my-retailer',
|
|
1874
|
+
// BPP enabled by default - unnecessary for delta feeds
|
|
1875
|
+
});
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
✅ **Correct:**
|
|
1879
|
+
|
|
1880
|
+
```typescript
|
|
1881
|
+
// Skip BPP for delta feeds (all records are changes)
|
|
1882
|
+
const job = await client.createJob({
|
|
1883
|
+
name: 'Hourly Deltas',
|
|
1884
|
+
retailerId: 'my-retailer',
|
|
1885
|
+
meta: {
|
|
1886
|
+
preprocessing: 'skip', // Skip BPP - all records are changes
|
|
1887
|
+
},
|
|
1888
|
+
});
|
|
1889
|
+
```
|
|
1890
|
+
|
|
1891
|
+
**Why:** Delta feeds are pre-filtered by source system, BPP overhead is wasted.
|
|
1892
|
+
|
|
1893
|
+
### 5. Large JSON Files
|
|
1894
|
+
|
|
1895
|
+
❌ **Wrong:**
|
|
1896
|
+
|
|
1897
|
+
```typescript
|
|
1898
|
+
// Loading entire 500MB JSON into memory
|
|
1899
|
+
const parsed = await parser.parse(largeJsonContent);
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
✅ **Correct:**
|
|
1903
|
+
|
|
1904
|
+
```typescript
|
|
1905
|
+
// Use JSON Lines format for streaming
|
|
1906
|
+
const format = 'jsonl';
|
|
1907
|
+
const parsed = await parser.parse(largeJsonContent, { format });
|
|
1908
|
+
```
|
|
1909
|
+
|
|
1910
|
+
**Why:** JSON Lines processes one record at a time, preventing memory issues.
|
|
1911
|
+
|
|
1912
|
+
---
|
|
1913
|
+
|
|
1914
|
+
## Key Takeaways
|
|
1915
|
+
|
|
1916
|
+
- 🎯 **Use Batch API for inventory** - Not Event API (Event API is for products/orders/etc.)
|
|
1917
|
+
- 🎯 **Processing mode selection** - Per-file (default) for safety, batch for speed, chunked for scale
|
|
1918
|
+
- 🎯 **TRUE modular architecture** - Separate service files with clear responsibilities (see Modular Structure section)
|
|
1919
|
+
- 🎯 **BPP by default** - Enabled for full snapshots, skip for delta feeds
|
|
1920
|
+
- 🎯 **Format detection** - Auto-detects standard JSON vs JSON Lines
|
|
1921
|
+
- 🎯 **Direct field access** - Use simple paths like `"locationRef"` (no special notation)
|
|
1922
|
+
- 🎯 **Map records directly** - No need to wrap in object (unlike XML)
|
|
1923
|
+
- 🎯 **Chunk records** - Default 1000 per batch, tune based on record size
|
|
1924
|
+
- 🎯 **EntityType: INVENTORY** - Correct entity type for inventory records
|
|
1925
|
+
- 🎯 **State management** - VersoriFileTracker + JobTracker prevent duplicates
|
|
1926
|
+
- 🎯 **Enhanced retry logic** - S3 operations with exponential backoff
|
|
1927
|
+
- 🎯 **Always dispose** - Release S3 resources with `dispose()` in finally block
|
|
1928
|
+
- 🎯 **Error handling** - Move failed files to error folder, don't fail entire workflow
|
|
1929
|
+
- 🎯 **Native logging** - Use `log` from context on Versori platform
|
|
1930
|
+
- 🎯 **Streaming for large files** - Use JSON Lines format for files >100MB
|
|
1931
|
+
- 🎯 **Emoji logging** - Use 🚀 ✅ ❌ ⚠️ 🔍 📄 for visual log scanning
|
|
1932
|
+
- 🎯 **Execution boundaries** - Use ═══ separators for workflow start/end
|
|
1933
|
+
- 🎯 **validateConnection** - Enable connection validation on client creation
|
|
1934
|
+
- 🎯 **Optional toggles** - Support enableFileTracking and detailedLogging flags
|
|
1935
|
+
- 🎯 **Duration tracking** - Track execution time at file and workflow levels
|
|
1936
|
+
- 🎯 **Error recommendations** - Include actionable suggestions in error logs
|
|
1937
|
+
|
|
1938
|
+
---
|
|
1939
|
+
|
|
1940
|
+
## Related Documentation
|
|
1941
|
+
|
|
1942
|
+
- [Batch API vs Event API Decision Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
|
|
1943
|
+
- [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
|
|
1944
|
+
- [S3 Data Source](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md#s3-data-source)
|
|
1945
|
+
- [JSON Parser](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md#json-parser-service)
|
|
1946
|
+
- [State Management](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md)
|
|
1947
|
+
- [Job Tracker](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md)
|
|
1948
|
+
- [BPP Documentation](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md#batch-pre-processing-bpp-change-detection)
|
|
1949
|
+
|
|
1950
|
+
---
|
|
1951
|
+
|
|
1952
|
+
[→ Back to Versori Scheduled Workflows](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) | [Versori Platform Guide →](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md)
|