@fluentcommerce/fc-connect-sdk 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -2
- package/README.md +39 -0
- package/dist/cjs/auth/index.d.ts +3 -0
- package/dist/cjs/auth/index.js +13 -0
- package/dist/cjs/auth/profile-loader.d.ts +18 -0
- package/dist/cjs/auth/profile-loader.js +208 -0
- package/dist/cjs/client-factory.d.ts +4 -0
- package/dist/cjs/client-factory.js +10 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/auth/index.d.ts +3 -0
- package/dist/esm/auth/index.js +2 -0
- package/dist/esm/auth/profile-loader.d.ts +18 -0
- package/dist/esm/auth/profile-loader.js +169 -0
- package/dist/esm/client-factory.d.ts +4 -0
- package/dist/esm/client-factory.js +9 -0
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/auth/index.d.ts +3 -0
- package/dist/types/auth/profile-loader.d.ts +18 -0
- package/dist/types/client-factory.d.ts +4 -0
- package/dist/types/index.d.ts +3 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -482
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
|
@@ -1,2461 +1,2461 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-extract-inventory-positions-to-s3-csv
|
|
3
|
-
canonical_filename: template-extraction-inventory-positions-to-s3-csv.md
|
|
4
|
-
version: 2.0.0
|
|
5
|
-
sdk_version: ^0.1.39
|
|
6
|
-
runtime: versori
|
|
7
|
-
direction: extraction
|
|
8
|
-
source: fluent-graphql
|
|
9
|
-
destination: s3-csv
|
|
10
|
-
entity: inventory-positions
|
|
11
|
-
format: csv
|
|
12
|
-
logging: versori
|
|
13
|
-
status: stable
|
|
14
|
-
features:
|
|
15
|
-
- memory-management
|
|
16
|
-
- enhanced-logging
|
|
17
|
-
- pagination-progress
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
# Template: Extraction - Inventory Positions to S3 CSV
|
|
21
|
-
|
|
22
|
-
**Template Version:** 2.0.0
|
|
23
|
-
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
24
|
-
**Last Updated:** 2025-01-24
|
|
25
|
-
**Deployment Target:** Versori Platform
|
|
26
|
-
|
|
27
|
-
**🆕 Version 2.0.0 Enhancements:**
|
|
28
|
-
- ✅ **Memory Management** - Clear large result sets after processing batches
|
|
29
|
-
- ✅ **Enhanced Logging** - Pagination progress tracking with emoji indicators (📊, 📥, ✅)
|
|
30
|
-
- ✅ **Pagination Progress** - Real-time page-by-page progress logging with metrics
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## 📚 STEP 1: Load These Docs (Human Checklist)
|
|
35
|
-
|
|
36
|
-
1. REQUIRED (load all)
|
|
37
|
-
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
38
|
-
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
39
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
40
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
41
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
42
|
-
- [ ] fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
43
|
-
|
|
44
|
-
Copy-paste list (open these):
|
|
45
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
46
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
47
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
48
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
49
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
50
|
-
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## 📋 STEP 2: Tell Your AI (Prompt)
|
|
55
|
-
|
|
56
|
-
Copy/paste this prompt into your AI tool after loading the documentation above:
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
I need a Versori scheduled extractor that:
|
|
60
|
-
|
|
61
|
-
1) Queries Fluent Commerce GraphQL for Inventory Positions with auto-pagination
|
|
62
|
-
2) Supports incremental runs via KV state (with an overlap buffer)
|
|
63
|
-
3) Transforms results using UniversalMapper per mapping JSON
|
|
64
|
-
4) Generates CSV and uploads to S3
|
|
65
|
-
5) Tracks progress with JobTracker and exposes a job-status webhook
|
|
66
|
-
6) Uses native Versori log (LoggingService removed - use native log)
|
|
67
|
-
|
|
68
|
-
Use the loaded docs to fill in SDK specifics and best practices.
|
|
69
|
-
Keep the structure identical to the template; only adapt where needed.
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
## 📋 Template Overview
|
|
75
|
-
|
|
76
|
-
This connector runs on the Versori platform. Most operational settings (Fluent account/connection, S3 credentials, schedule, page size/limits) are configured via activation variables. Data shape and logic (mapping JSON, CSV structure, GraphQL selection set/filters, validators/resolvers) are adjusted in code as needed. It extracts inventory positions from Fluent Commerce via GraphQL, transforms the data into CSV, and uploads the result to S3.
|
|
77
|
-
|
|
78
|
-
### What This Template Does
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
┌─────────────────────────────────────────────────────────────────┐
|
|
82
|
-
│ EXTRACTION WORKFLOW │
|
|
83
|
-
└─────────────────────────────────────────────────────────────────┘
|
|
84
|
-
|
|
85
|
-
1. TRIGGER
|
|
86
|
-
├─ Scheduled (Cron): Runs automatically every hour
|
|
87
|
-
├─ Ad hoc (Webhook): Manual trigger with optional date override
|
|
88
|
-
└─ Status Query (Webhook): Check job progress
|
|
89
|
-
|
|
90
|
-
2. EXTRACT (ExtractionOrchestrator)
|
|
91
|
-
├─ Query Fluent GraphQL API for inventory positions
|
|
92
|
-
├─ Auto-pagination (handles large datasets)
|
|
93
|
-
├─ Apply date filters (incremental or manual range)
|
|
94
|
-
└─ Validate each record (optional)
|
|
95
|
-
|
|
96
|
-
3. TRANSFORM (UniversalMapper)
|
|
97
|
-
├─ Map GraphQL fields to CSV schema
|
|
98
|
-
├─ Apply SDK resolvers (trim, uppercase, parseInt, etc.)
|
|
99
|
-
├─ Extract nested data (catalogue)
|
|
100
|
-
└─ Handle transformation errors
|
|
101
|
-
|
|
102
|
-
4. GENERATE CSV (CSVParserService)
|
|
103
|
-
├─ Convert transformed records to CSV
|
|
104
|
-
├─ Include headers
|
|
105
|
-
├─ Handle special characters
|
|
106
|
-
└─ Generate timestamped filename
|
|
107
|
-
|
|
108
|
-
5. UPLOAD (S3DataSource)
|
|
109
|
-
├─ Connect to S3
|
|
110
|
-
├─ Upload CSV file with retry logic
|
|
111
|
-
├─ Set content type and metadata
|
|
112
|
-
└─ Verify upload success
|
|
113
|
-
|
|
114
|
-
6. TRACK JOB (JobTracker)
|
|
115
|
-
├─ Create job with unique ID
|
|
116
|
-
├─ Update status at each step
|
|
117
|
-
├─ Store job result in KV
|
|
118
|
-
└─ Enable status queries via webhook
|
|
119
|
-
|
|
120
|
-
7. UPDATE STATE (VersoriKVAdapter)
|
|
121
|
-
├─ Calculate max updatedOn from records
|
|
122
|
-
├─ Store timestamp for next incremental run
|
|
123
|
-
├─ Apply overlap buffer (prevent missed records)
|
|
124
|
-
└─ Skip update if manual date override
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Key Features
|
|
128
|
-
|
|
129
|
-
- Job tracking with status queries
|
|
130
|
-
- Execution modes: scheduled, ad hoc, status query
|
|
131
|
-
- Uses ExtractionOrchestrator, UniversalMapper, JobTracker, CSVParserService
|
|
132
|
-
- Error handling, retry logic
|
|
133
|
-
- Reusable services suitable for similar use cases
|
|
134
|
-
|
|
135
|
-
Note: JobTracker persists stage/status to Versori KV for visibility, job-status webhooks, and auditing. Recommended for production multi-step flows; can be skipped for trivial single-step utilities.
|
|
136
|
-
|
|
137
|
-
### 📦 Package Information
|
|
138
|
-
|
|
139
|
-
**SDK:** [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
140
|
-
|
|
141
|
-
**Latest Version:** See [npm package page](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk) for current version
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
npm install @fluentcommerce/fc-connect-sdk
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
---
|
|
148
|
-
|
|
149
|
-
**Templates are designed for direct deployment; customize via activation variables.**
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## 📦 SDK Imports (Verified - Versori Optimized)
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
// ✅ VERIFIED IMPORTS - These match actual SDK exports
|
|
157
|
-
import { Buffer } from 'node:buffer';
|
|
158
|
-
import {
|
|
159
|
-
createClient, // Universal client factory (auto-detects Versori/Node/Deno)
|
|
160
|
-
ExtractionOrchestrator, // High-level extraction with auto-pagination
|
|
161
|
-
JobTracker, // Job status tracking in KV store
|
|
162
|
-
UniversalMapper, // Field mapping with SDK resolvers
|
|
163
|
-
CSVParserService, // CSV generation (NOT CSVBuilder!)
|
|
164
|
-
S3DataSource, // S3 operations with retry logic
|
|
165
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
166
|
-
|
|
167
|
-
// Versori platform imports
|
|
168
|
-
import { schedule, webhook, http, fn } from '@versori/run';
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
**Note:** All imports are from actual SDK exports - this code compiles and runs as-is.
|
|
172
|
-
|
|
173
|
-
**✅ VERSORI PLATFORM - Use Native Logs:**
|
|
174
|
-
|
|
175
|
-
- Use `log` from context: `const { log } = ctx;`
|
|
176
|
-
- Don't import or use LoggingService for Versori connectors
|
|
177
|
-
- Native Versori logs are simpler and automatically integrated with platform monitoring
|
|
178
|
-
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
## ⚙️ Activation Variables
|
|
182
|
-
|
|
183
|
-
**Configuration is driven by activation variables - modify these instead of code:**
|
|
184
|
-
|
|
185
|
-
```json
|
|
186
|
-
{
|
|
187
|
-
"catalogueRef": "DEFAULT_CATALOGUE",
|
|
188
|
-
"s3BucketName": "inventory-analytics-exports",
|
|
189
|
-
"awsAccessKeyId": "AKIAXXXXXXXXXXXX",
|
|
190
|
-
"awsSecretAccessKey": "********",
|
|
191
|
-
"awsRegion": "us-east-1",
|
|
192
|
-
"s3Prefix": "inventory-positions/daily/",
|
|
193
|
-
"fileNamePrefix": "inventorypositions",
|
|
194
|
-
"pageSize": 200,
|
|
195
|
-
"maxRecords": 100000,
|
|
196
|
-
"overlapBufferSeconds": 60
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
Note: Webhook security is enforced via Versori connections. Configure auth on the connection; reference it in `webhook({ connection: '...' })`.
|
|
201
|
-
|
|
202
|
-
### Variable Explanations
|
|
203
|
-
|
|
204
|
-
| Variable | Purpose | Default | Customization Hints |
|
|
205
|
-
| ---------------------- | ----------------------------- | ---------------------------- | -------------------------------- |
|
|
206
|
-
| `catalogueRef` | Filter by inventory catalogue | `DEFAULT_CATALOGUE` | Change to filter by catalogue |
|
|
207
|
-
| `s3BucketName` | Target S3 bucket | - | Required - your analytics bucket |
|
|
208
|
-
| `awsAccessKeyId` | AWS access key | - | Required - AWS credential |
|
|
209
|
-
| `awsSecretAccessKey` | AWS secret key | - | Required - AWS credential |
|
|
210
|
-
| `awsRegion` | AWS region | `us-east-1` | Match bucket region |
|
|
211
|
-
| `s3Prefix` | S3 key prefix | `inventory-positions/daily/` | Customize folder structure |
|
|
212
|
-
| `fileNamePrefix` | CSV filename prefix | `inventorypositions` | Customize naming convention |
|
|
213
|
-
| `pageSize` | Records per GraphQL page | `200` | Increase for fewer API calls |
|
|
214
|
-
| `maxRecords` | Total extraction limit | `100000` | Safety limit - adjust for volume |
|
|
215
|
-
| `overlapBufferSeconds` | Incremental safety window | `60` | Prevents missed records |
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
### 🔄 State Management & Incremental Sync
|
|
220
|
-
|
|
221
|
-
**How incremental sync works:**
|
|
222
|
-
|
|
223
|
-
1. **First Run:** Uses `DEFAULT_FALLBACK` date (2024-01-01T00:00:00Z)
|
|
224
|
-
2. **Subsequent Runs:** Uses `lastInventoryPositionSync` timestamp from KV store
|
|
225
|
-
3. **Overlap Buffer:** Subtracts 60 seconds to catch late-arriving records
|
|
226
|
-
4. **State Update:** After successful upload, stores max `updatedOn` for next run
|
|
227
|
-
|
|
228
|
-
**Incremental vs Manual Modes:**
|
|
229
|
-
|
|
230
|
-
| Mode | When to Use | State Update | Payload Example |
|
|
231
|
-
| --------------------------- | -------------------- | ------------ | -------------------------------------------------------------- |
|
|
232
|
-
| **Incremental** | Daily scheduled sync | ✅ Yes | `{}` (empty - uses last sync) |
|
|
233
|
-
| **Manual Range** | Historical backfill | ❌ No | `{ "fromDate": "2024-01-01T00:00:00Z", "updateState": false }` |
|
|
234
|
-
| **Manual Range with State** | One-time catch-up | ✅ Yes | `{ "fromDate": "2024-01-01T00:00:00Z", "updateState": true }` |
|
|
235
|
-
|
|
236
|
-
**Why overlap buffer?**
|
|
237
|
-
|
|
238
|
-
Records updated near the sync time might not appear in the query due to:
|
|
239
|
-
|
|
240
|
-
- Clock drift between systems
|
|
241
|
-
- Transaction timing in the database
|
|
242
|
-
- GraphQL query execution timing
|
|
243
|
-
|
|
244
|
-
The 60-second buffer ensures these edge-case records are captured in the next run, preventing data loss.
|
|
245
|
-
|
|
246
|
-
**When to skip state update (`updateState: false`):**
|
|
247
|
-
|
|
248
|
-
- Historical backfills (don't affect ongoing incremental sync)
|
|
249
|
-
- Testing/debugging specific date ranges
|
|
250
|
-
- Reprocessing old data without changing the sync pointer
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
### 📁 S3 Path Configuration
|
|
255
|
-
|
|
256
|
-
**Note:** Folder paths and filename patterns are configured separately.
|
|
257
|
-
|
|
258
|
-
- **Folder Path:** Configured via `s3Prefix` activation variable (e.g., `inventory-positions/daily/`)
|
|
259
|
-
- **Filename Pattern:** Configured via `fileNamePrefix` activation variable (e.g., `inventorypositions`)
|
|
260
|
-
|
|
261
|
-
**The workflow generates files like:** `{s3Prefix}{fileNamePrefix}-{timestamp}.csv`
|
|
262
|
-
|
|
263
|
-
**Example:** With `s3Prefix="inventory-positions/daily/"` and `fileNamePrefix="inventorypositions"`, a generated file will look like:
|
|
264
|
-
|
|
265
|
-
```
|
|
266
|
-
inventory-positions/daily/inventorypositions-2025-10-27T18-30-45Z.csv
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
Format may vary slightly (colons replaced; includes Z).
|
|
270
|
-
|
|
271
|
-
**Note:** To change the upload folder, modify the `s3Prefix` activation variable (not in code).
|
|
272
|
-
|
|
273
|
-
---
|
|
274
|
-
|
|
275
|
-
### Auto-pagination and limits (ExtractionOrchestrator)
|
|
276
|
-
|
|
277
|
-
**What:** ExtractionOrchestrator handles GraphQL Relay cursor-based pagination automatically.
|
|
278
|
-
|
|
279
|
-
**Why:** Prevents manual pagination loop code, handles large datasets efficiently.
|
|
280
|
-
|
|
281
|
-
**How:** You configure `pageSize` and `maxRecords`; the orchestrator injects `$first` and `$after` variables automatically and loops until `pageInfo.hasNextPage === false` or `maxRecords` is reached.
|
|
282
|
-
|
|
283
|
-
**Critical:** Your query MUST include `edges { cursor }` and `pageInfo { hasNextPage }` fields, or pagination will fail.
|
|
284
|
-
|
|
285
|
-
#### Configuration Parameters
|
|
286
|
-
|
|
287
|
-
| Parameter | Purpose | Example | Effect |
|
|
288
|
-
| ---------------- | ------------------------------ | --------------------------------- | -------------------------- |
|
|
289
|
-
| `pageSize` | Records per GraphQL request | `200` | Controls `first` variable |
|
|
290
|
-
| `maxRecords` | Total extraction limit | `100000` | Hard stop across all pages |
|
|
291
|
-
| `resultPath` | Where records live in response | `"inventoryPositions.edges.node"` | Flattening path |
|
|
292
|
-
| `validateItem()` | Optional record filter | `(item) => !!item.ref` | Skips invalid records |
|
|
293
|
-
|
|
294
|
-
#### GraphQL Query Requirements
|
|
295
|
-
|
|
296
|
-
**Your query MUST include these pagination fields:**
|
|
297
|
-
|
|
298
|
-
```graphql
|
|
299
|
-
query GetInventoryPositions(
|
|
300
|
-
$catalogues: [InventoryCatalogueKey]
|
|
301
|
-
$dateRangeFilter: DateRange
|
|
302
|
-
$first: Int! # ← Orchestrator injects this
|
|
303
|
-
$after: String # ← Orchestrator injects this
|
|
304
|
-
) {
|
|
305
|
-
inventoryPositions(
|
|
306
|
-
catalogues: $catalogues
|
|
307
|
-
updatedOn: $dateRangeFilter
|
|
308
|
-
first: $first # ← Pagination page size
|
|
309
|
-
after: $after # ← Pagination cursor
|
|
310
|
-
) {
|
|
311
|
-
edges {
|
|
312
|
-
# ← REQUIRED: Relay connection structure
|
|
313
|
-
node {
|
|
314
|
-
# ← REQUIRED: Actual records here
|
|
315
|
-
id
|
|
316
|
-
ref
|
|
317
|
-
# ... your fields
|
|
318
|
-
}
|
|
319
|
-
cursor # ← REQUIRED: Orchestrator uses this for next page
|
|
320
|
-
}
|
|
321
|
-
pageInfo {
|
|
322
|
-
# ← REQUIRED: Orchestrator checks this
|
|
323
|
-
hasNextPage # ← REQUIRED: Tells orchestrator to continue
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
---
|
|
330
|
-
|
|
331
|
-
## 📄 Mapping Configuration
|
|
332
|
-
|
|
333
|
-
**File:** `config/inventory-positions.export.csv.json`
|
|
334
|
-
|
|
335
|
-
```json
|
|
336
|
-
{
|
|
337
|
-
"name": "inventory-positions.export.csv",
|
|
338
|
-
"version": "1.0.0",
|
|
339
|
-
"description": "Inventory Positions → CSV Export Mapping",
|
|
340
|
-
"fields": {
|
|
341
|
-
"position_ref": {
|
|
342
|
-
"source": "ref",
|
|
343
|
-
"required": true,
|
|
344
|
-
"resolver": "sdk.trim"
|
|
345
|
-
},
|
|
346
|
-
"product_ref": {
|
|
347
|
-
"source": "productRef",
|
|
348
|
-
"required": true,
|
|
349
|
-
"resolver": "sdk.trim"
|
|
350
|
-
},
|
|
351
|
-
"location_ref": {
|
|
352
|
-
"source": "locationRef",
|
|
353
|
-
"required": false,
|
|
354
|
-
"resolver": "sdk.trim"
|
|
355
|
-
},
|
|
356
|
-
"on_hand": {
|
|
357
|
-
"source": "onHand",
|
|
358
|
-
"required": true,
|
|
359
|
-
"resolver": "sdk.parseInt"
|
|
360
|
-
},
|
|
361
|
-
"position_type": {
|
|
362
|
-
"source": "type",
|
|
363
|
-
"required": true,
|
|
364
|
-
"resolver": "sdk.uppercase"
|
|
365
|
-
},
|
|
366
|
-
"status": {
|
|
367
|
-
"source": "status",
|
|
368
|
-
"required": false,
|
|
369
|
-
"resolver": "sdk.uppercase"
|
|
370
|
-
},
|
|
371
|
-
"catalogue_ref": {
|
|
372
|
-
"source": "catalogue.ref",
|
|
373
|
-
"required": false,
|
|
374
|
-
"resolver": "sdk.trim"
|
|
375
|
-
},
|
|
376
|
-
"catalogue_name": {
|
|
377
|
-
"source": "catalogue.name",
|
|
378
|
-
"required": false,
|
|
379
|
-
"resolver": "sdk.trim"
|
|
380
|
-
},
|
|
381
|
-
"created_on": {
|
|
382
|
-
"source": "createdOn",
|
|
383
|
-
"required": true,
|
|
384
|
-
"resolver": "sdk.toString"
|
|
385
|
-
},
|
|
386
|
-
"updated_on": {
|
|
387
|
-
"source": "updatedOn",
|
|
388
|
-
"required": true,
|
|
389
|
-
"resolver": "sdk.toString"
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
**AI Customization Hints:**
|
|
396
|
-
|
|
397
|
-
- Add fields: Copy existing field config, change `source` path
|
|
398
|
-
- Remove fields: Delete field from config
|
|
399
|
-
- Change resolvers: Replace `sdk.trim` with `sdk.uppercase`, etc.
|
|
400
|
-
- Nested fields: Use dot notation like `catalogue.ref`
|
|
401
|
-
|
|
402
|
-
Note: Customize mapping by editing the JSON above; prefer built-in resolvers. See SDK Universal Mapping guide for advanced usage.
|
|
403
|
-
|
|
404
|
-
## 🔧 Complete Production Code
|
|
405
|
-
|
|
406
|
-
### File Structure
|
|
407
|
-
|
|
408
|
-
**Production-ready modular structure:**
|
|
409
|
-
|
|
410
|
-
```
|
|
411
|
-
inventory-positions-to-s3-csv/
|
|
412
|
-
├── src/
|
|
413
|
-
│ ├── index.ts # Register workflows
|
|
414
|
-
│ ├── workflows/
|
|
415
|
-
│ │ └── inventory-positions-extraction.ts # ALL 3 workflows (scheduled + 2 webhooks)
|
|
416
|
-
│ ├── services/
|
|
417
|
-
│ │ └── extraction-orchestration.ts # Main orchestration logic
|
|
418
|
-
│ └── utils/
|
|
419
|
-
│ └── job-id-generator.ts # Job ID utility
|
|
420
|
-
├── config/
|
|
421
|
-
│ └── inventory-positions.export.csv.json # Mapping configuration
|
|
422
|
-
├── package.json
|
|
423
|
-
└── tsconfig.json
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
---
|
|
427
|
-
|
|
428
|
-
### 1. Entry Point (src/index.ts)
|
|
429
|
-
|
|
430
|
-
```typescript
|
|
431
|
-
/**
|
|
432
|
-
* Entry Point - Export all workflows for Versori platform
|
|
433
|
-
*
|
|
434
|
-
* This file exports all workflows to be registered with Versori.
|
|
435
|
-
* Each workflow is defined in its own file for better organization.
|
|
436
|
-
*
|
|
437
|
-
* DELEGATION PATTERN:
|
|
438
|
-
* - index.ts exports workflows (no business logic)
|
|
439
|
-
* - Workflows orchestrate execution (minimal logic)
|
|
440
|
-
* - Services contain all business logic (maximum reuse)
|
|
441
|
-
*
|
|
442
|
-
* AI CUSTOMIZATION:
|
|
443
|
-
* - Add new workflows by importing and exporting them
|
|
444
|
-
* - Remove workflows by commenting out exports
|
|
445
|
-
* - Change workflow names in import statements
|
|
446
|
-
*/
|
|
447
|
-
|
|
448
|
-
// Scheduled workflows
|
|
449
|
-
export { scheduledInventoryPositionsExtraction } from './workflows/scheduled/inventory-positions-extraction';
|
|
450
|
-
|
|
451
|
-
// Webhook workflows
|
|
452
|
-
export { adhocInventoryPositionsExtraction } from './workflows/webhook/adhoc-inventory-positions-extraction';
|
|
453
|
-
export { inventoryPositionsJobStatus } from './workflows/webhook/job-status-check';
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
---
|
|
457
|
-
|
|
458
|
-
### 2. Workflows (src/workflows/inventory-positions-extraction.ts)
|
|
459
|
-
|
|
460
|
-
```typescript
|
|
461
|
-
/**
|
|
462
|
-
* Workflows - Defines 3 execution patterns for inventory positions extraction
|
|
463
|
-
*
|
|
464
|
-
* WORKFLOW 1: Scheduled (Cron) - Runs automatically every hour
|
|
465
|
-
* WORKFLOW 2: Ad hoc (Webhook) - Manual trigger with optional date override
|
|
466
|
-
* WORKFLOW 3: Job Status (Webhook) - Query job progress
|
|
467
|
-
*
|
|
468
|
-
* AI CUSTOMIZATION HINTS:
|
|
469
|
-
* - Change schedule: Modify cron expression in schedule()
|
|
470
|
-
* - Add filtering: Pass additional params to executeInventoryPositionExtraction()
|
|
471
|
-
* - Change response format: Modify return object structure
|
|
472
|
-
*/
|
|
473
|
-
|
|
474
|
-
import { schedule, webhook, http, fn } from '@versori/run';
|
|
475
|
-
import { executeInventoryPositionExtraction, getJobStatus } from '../services/extraction-orchestration';
|
|
476
|
-
import { generateJobId } from '../utils/job-id-generator';
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* WORKFLOW 1: Scheduled Extraction
|
|
480
|
-
*
|
|
481
|
-
* Purpose: Automated hourly extraction for incremental sync
|
|
482
|
-
* Trigger: Cron schedule (every hour at minute 0)
|
|
483
|
-
* State Update: Always updates lastSync timestamp
|
|
484
|
-
*
|
|
485
|
-
* AI CUSTOMIZATION:
|
|
486
|
-
* - Change schedule: Replace '0 * * * *' with your cron expression
|
|
487
|
-
* Examples:
|
|
488
|
-
* - Every 30 min: '*/30 * * * *'
|
|
489
|
-
* - Daily at 2 AM: '0 2 * * *'
|
|
490
|
-
* - Every 15 min: '*/15 * * * *'
|
|
491
|
-
*/
|
|
492
|
-
export const scheduledInventoryPositionsExtraction = schedule(
|
|
493
|
-
'scheduled-inventory-positions-extraction',
|
|
494
|
-
'0 * * * *', // ← CUSTOMIZE: Cron expression
|
|
495
|
-
http('execute-scheduled-extraction', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
496
|
-
const { log } = ctx;
|
|
497
|
-
const executionStartTime = Date.now();
|
|
498
|
-
|
|
499
|
-
// Generate unique job ID for tracking
|
|
500
|
-
// Format: SCHEDULED_IP_YYYYMMDD_HHMMSS_random
|
|
501
|
-
const jobId = generateJobId('SCHEDULED', 'INVENTORY_POSITIONS');
|
|
502
|
-
|
|
503
|
-
log.info('🚀 [WORKFLOW] Scheduled extraction triggered', { jobId });
|
|
504
|
-
|
|
505
|
-
try {
|
|
506
|
-
// Execute main workflow (extraction → transform → upload)
|
|
507
|
-
const result = await executeInventoryPositionExtraction(ctx, {
|
|
508
|
-
jobId,
|
|
509
|
-
triggeredBy: 'schedule',
|
|
510
|
-
updateState: true, // Always update state for scheduled runs
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
const executionDuration = Date.now() - executionStartTime;
|
|
514
|
-
|
|
515
|
-
log.info('✅ [WORKFLOW] Scheduled extraction completed', {
|
|
516
|
-
jobId,
|
|
517
|
-
recordCount: result.recordsExtracted,
|
|
518
|
-
fileName: result.fileName,
|
|
519
|
-
duration: executionDuration,
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
return { ...result, duration: executionDuration };
|
|
523
|
-
|
|
524
|
-
} catch (error: any) {
|
|
525
|
-
const executionDuration = Date.now() - executionStartTime;
|
|
526
|
-
|
|
527
|
-
log.error('❌ [WORKFLOW] Scheduled extraction failed', {
|
|
528
|
-
jobId,
|
|
529
|
-
message: error instanceof Error ? error.message : String(error),
|
|
530
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
531
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
532
|
-
duration: executionDuration,
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
return {
|
|
536
|
-
success: false,
|
|
537
|
-
jobId,
|
|
538
|
-
error: error.message,
|
|
539
|
-
duration: executionDuration,
|
|
540
|
-
recommendation: getErrorRecommendation(error),
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
}));
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* WORKFLOW 2: Ad hoc Extraction (Manual Trigger)
|
|
547
|
-
*
|
|
548
|
-
* Purpose: Manual extraction with optional date range override
|
|
549
|
-
* Trigger: Webhook POST to /webhooks/inventory-positions-adhoc
|
|
550
|
-
* State Update: Optional (controlled by request payload)
|
|
551
|
-
*
|
|
552
|
-
* WEBHOOK PAYLOAD EXAMPLES:
|
|
553
|
-
*
|
|
554
|
-
* 1. Incremental (use last sync timestamp):
|
|
555
|
-
* {}
|
|
556
|
-
*
|
|
557
|
-
* 2. Date range (manual override):
|
|
558
|
-
* {
|
|
559
|
-
* "fromDate": "2025-01-01T00:00:00Z",
|
|
560
|
-
* "toDate": "2025-01-31T23:59:59Z",
|
|
561
|
-
* "updateState": false
|
|
562
|
-
* }
|
|
563
|
-
*
|
|
564
|
-
* AI CUSTOMIZATION:
|
|
565
|
-
* - Add request validation
|
|
566
|
-
* - Add authentication check
|
|
567
|
-
* - Add custom filters from payload
|
|
568
|
-
*/
|
|
569
|
-
export const adhocInventoryPositionsExtraction = webhook(
|
|
570
|
-
'inventory-positions-adhoc',
|
|
571
|
-
{ connection: 'inventory-positions-adhoc', response: { mode: 'sync' } }
|
|
572
|
-
)
|
|
573
|
-
.then(
|
|
574
|
-
http('execute-adhoc-extraction', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
575
|
-
const { data, log } = ctx;
|
|
576
|
-
const executionStartTime = Date.now();
|
|
577
|
-
|
|
578
|
-
// Generate unique job ID
|
|
579
|
-
const jobId = generateJobId('ADHOC', 'INVENTORY_POSITIONS');
|
|
580
|
-
|
|
581
|
-
// SECURITY: Authentication is enforced by Versori connection configuration
|
|
582
|
-
// Configure auth on the connection and reference it in webhook({ connection: '...' })
|
|
583
|
-
|
|
584
|
-
// Extract optional date override from webhook payload
|
|
585
|
-
const fromDate = data.fromDate as string | undefined;
|
|
586
|
-
const toDate = data.toDate as string | undefined;
|
|
587
|
-
const updateState = data.updateState === true; // Default false; advance state only if explicitly true
|
|
588
|
-
|
|
589
|
-
log.info('🔔 [WORKFLOW] Ad hoc extraction triggered via webhook', {
|
|
590
|
-
jobId,
|
|
591
|
-
hasDateOverride: !!fromDate,
|
|
592
|
-
fromDate: fromDate || 'not specified',
|
|
593
|
-
toDate: toDate || 'not specified',
|
|
594
|
-
updateState
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
try {
|
|
598
|
-
// Execute main workflow with optional overrides
|
|
599
|
-
const result = await executeInventoryPositionExtraction(ctx, {
|
|
600
|
-
jobId,
|
|
601
|
-
triggeredBy: 'webhook',
|
|
602
|
-
fromDate, // Optional: override start date
|
|
603
|
-
toDate, // Optional: override end date
|
|
604
|
-
updateState, // Optional: skip state update for historical queries
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
const executionDuration = Date.now() - executionStartTime;
|
|
608
|
-
|
|
609
|
-
log.info('✅ [WORKFLOW] Ad hoc extraction completed', {
|
|
610
|
-
jobId,
|
|
611
|
-
recordCount: result.recordsExtracted,
|
|
612
|
-
fileName: result.fileName,
|
|
613
|
-
isManualOverride: !!fromDate,
|
|
614
|
-
stateUpdated: result.stateUpdated,
|
|
615
|
-
duration: executionDuration,
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
// Return success with job details
|
|
619
|
-
return {
|
|
620
|
-
success: true,
|
|
621
|
-
jobId,
|
|
622
|
-
recordsExtracted: result.recordsExtracted,
|
|
623
|
-
fileName: result.fileName,
|
|
624
|
-
s3Path: result.s3Path,
|
|
625
|
-
statusUrl: `/webhooks/inventory-positions-job-status?jobId=${jobId}`,
|
|
626
|
-
duration: executionDuration,
|
|
627
|
-
dateRange: fromDate ? {
|
|
628
|
-
from: fromDate,
|
|
629
|
-
to: toDate || 'not specified',
|
|
630
|
-
updateState
|
|
631
|
-
} : undefined
|
|
632
|
-
};
|
|
633
|
-
|
|
634
|
-
} catch (error: any) {
|
|
635
|
-
const executionDuration = Date.now() - executionStartTime;
|
|
636
|
-
|
|
637
|
-
log.error('❌ [WORKFLOW] Ad hoc extraction failed', {
|
|
638
|
-
jobId,
|
|
639
|
-
message: error instanceof Error ? error.message : String(error),
|
|
640
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
641
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
642
|
-
duration: executionDuration,
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
return {
|
|
646
|
-
success: false,
|
|
647
|
-
jobId,
|
|
648
|
-
error: error.message,
|
|
649
|
-
duration: executionDuration,
|
|
650
|
-
recommendation: getErrorRecommendation(error),
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
}));
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* WORKFLOW 3: Job Status Query
|
|
657
|
-
*
|
|
658
|
-
* Purpose: Check job progress and status
|
|
659
|
-
* Trigger: Webhook GET/POST to /webhooks/inventory-positions-job-status?jobId=xxx
|
|
660
|
-
* Returns: Current job status, stage, progress
|
|
661
|
-
*
|
|
662
|
-
* QUERY EXAMPLES:
|
|
663
|
-
*
|
|
664
|
-
* 1. HTTP GET:
|
|
665
|
-
* GET /webhooks/inventory-positions-job-status?jobId=ADHOC_IP_20251027_183045_abc123
|
|
666
|
-
*
|
|
667
|
-
* 2. HTTP POST:
|
|
668
|
-
* POST /webhooks/inventory-positions-job-status
|
|
669
|
-
* { "jobId": "ADHOC_IP_20251027_183045_abc123" }
|
|
670
|
-
*/
|
|
671
|
-
export const inventoryPositionsJobStatus = webhook(
|
|
672
|
-
'inventory-positions-job-status',
|
|
673
|
-
{ connection: 'inventory-positions-job-status', response: { mode: 'sync' } }
|
|
674
|
-
)
|
|
675
|
-
.then(
|
|
676
|
-
fn('query-job-status', async (ctx) => {
|
|
677
|
-
const { data, log, openKv } = ctx;
|
|
678
|
-
// SECURITY: Authentication is enforced by Versori connection configuration
|
|
679
|
-
// Configure auth on the connection and reference it in webhook({ connection: '...' })
|
|
680
|
-
|
|
681
|
-
// Get jobId from query param or POST body
|
|
682
|
-
const jobId = data.jobId as string;
|
|
683
|
-
|
|
684
|
-
if (!jobId) {
|
|
685
|
-
log.error('❌ [WORKFLOW] Job ID not provided in request');
|
|
686
|
-
return {
|
|
687
|
-
success: false,
|
|
688
|
-
error: 'Job ID is required. Provide jobId in query param or request body.',
|
|
689
|
-
recommendation: 'Include jobId in your request: { "jobId": "ADHOC_IP_20251027_183045_abc123" }'
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
log.info('🔍 [WORKFLOW] Querying job status', { jobId });
|
|
694
|
-
|
|
695
|
-
try {
|
|
696
|
-
// Query job status from KV store
|
|
697
|
-
const status = await getJobStatus(openKv(':project:'), jobId, log);
|
|
698
|
-
|
|
699
|
-
if (!status) {
|
|
700
|
-
log.info('⚠️ [WORKFLOW] Job not found', { jobId });
|
|
701
|
-
return {
|
|
702
|
-
success: false,
|
|
703
|
-
error: 'Job not found',
|
|
704
|
-
jobId,
|
|
705
|
-
recommendation: 'Verify the jobId is correct. Jobs may expire after 7 days.'
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
log.info('✅ [WORKFLOW] Job status retrieved', { jobId, status: status.status });
|
|
710
|
-
|
|
711
|
-
return {
|
|
712
|
-
success: true,
|
|
713
|
-
jobId,
|
|
714
|
-
...status
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
} catch (error: any) {
|
|
718
|
-
log.error('❌ [WORKFLOW] Failed to query job status', {
|
|
719
|
-
jobId,
|
|
720
|
-
message: error instanceof Error ? error.message : String(error),
|
|
721
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
722
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
return {
|
|
726
|
-
success: false,
|
|
727
|
-
jobId,
|
|
728
|
-
error: error.message,
|
|
729
|
-
recommendation: getErrorRecommendation(error),
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
}));
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* Helper: Get error-specific recommendations
|
|
736
|
-
*/
|
|
737
|
-
function getErrorRecommendation(error: any): string {
|
|
738
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
739
|
-
|
|
740
|
-
if (message.includes('ECONNREFUSED') || message.includes('ETIMEDOUT')) {
|
|
741
|
-
return 'Network connectivity issue. Check firewall rules and network configuration.';
|
|
742
|
-
}
|
|
743
|
-
if (message.includes('authentication') || message.includes('401')) {
|
|
744
|
-
return 'Authentication failed. Verify OAuth2 credentials in connection configuration.';
|
|
745
|
-
}
|
|
746
|
-
if (message.includes('S3') || message.includes('bucket')) {
|
|
747
|
-
return 'S3 operation failed. Check bucket name, region, and AWS credentials.';
|
|
748
|
-
}
|
|
749
|
-
if (message.includes('GraphQL') || message.includes('query')) {
|
|
750
|
-
return 'GraphQL query failed. Verify query syntax and field availability in schema.';
|
|
751
|
-
}
|
|
752
|
-
if (message.includes('mapping') || message.includes('transform')) {
|
|
753
|
-
return 'Data transformation failed. Check mapping configuration and source data structure.';
|
|
754
|
-
}
|
|
755
|
-
if (message.includes('KV') || message.includes('state')) {
|
|
756
|
-
return 'State management failed. Check Versori KV availability and permissions.';
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
return 'Check logs for detailed error information and stack trace.';
|
|
760
|
-
}
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
---
|
|
764
|
-
|
|
765
|
-
## 3. Main Orchestration Service (src/services/extraction-orchestration.ts)
|
|
766
|
-
|
|
767
|
-
```typescript
|
|
768
|
-
/**
|
|
769
|
-
* MAIN ORCHESTRATION SERVICE
|
|
770
|
-
*
|
|
771
|
-
* This is the heart of the extraction workflow. It coordinates all steps:
|
|
772
|
-
* 1. Initialize clients and services
|
|
773
|
-
* 2. Determine date range (incremental vs manual)
|
|
774
|
-
* 3. Extract data using ExtractionOrchestrator
|
|
775
|
-
* 4. Transform using UniversalMapper
|
|
776
|
-
* 5. Generate CSV using CSVParserService
|
|
777
|
-
* 6. Upload to S3
|
|
778
|
-
* 7. Track job progress with JobTracker
|
|
779
|
-
* 8. Update state for next run
|
|
780
|
-
*
|
|
781
|
-
* NAMING PATTERN (consistent across all use cases):
|
|
782
|
-
* - Interface: {Entity}ExtractionParams (e.g., InventoryPositionExtractionParams)
|
|
783
|
-
* - Result: {Entity}ExtractionResult (e.g., InventoryPositionExtractionResult)
|
|
784
|
-
* - Main function: execute{Entity}Extraction (e.g., executeInventoryPositionExtraction)
|
|
785
|
-
*
|
|
786
|
-
* AI CUSTOMIZATION HINTS:
|
|
787
|
-
* - Change entity: Replace "InventoryPosition" with "Order", "Product", etc.
|
|
788
|
-
* - Change output: Replace CSVParserService with XMLBuilder
|
|
789
|
-
* - Change destination: Replace S3DataSource with SftpDataSource
|
|
790
|
-
* - Add steps: Insert new service calls between existing steps
|
|
791
|
-
*/
|
|
792
|
-
|
|
793
|
-
import { Buffer } from 'node:buffer';
|
|
794
|
-
import {
|
|
795
|
-
createClient,
|
|
796
|
-
ExtractionOrchestrator,
|
|
797
|
-
JobTracker,
|
|
798
|
-
UniversalMapper,
|
|
799
|
-
CSVParserService,
|
|
800
|
-
S3DataSource,
|
|
801
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
802
|
-
|
|
803
|
-
import mappingConfig from '../../config/inventory-positions.export.csv.json' with { type: 'json' };
|
|
804
|
-
|
|
805
|
-
// ✅ VERSORI PLATFORM: Use native log from context, not LoggingService
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Parameters for extraction workflow
|
|
809
|
-
*
|
|
810
|
-
* NAMING: {Entity}ExtractionParams
|
|
811
|
-
*/
|
|
812
|
-
export interface InventoryPositionExtractionParams {
|
|
813
|
-
jobId: string;
|
|
814
|
-
triggeredBy: 'schedule' | 'webhook';
|
|
815
|
-
fromDate?: string; // Optional: manual date override
|
|
816
|
-
toDate?: string; // Optional: manual date override
|
|
817
|
-
updateState: boolean; // Whether to update lastSync timestamp
|
|
818
|
-
|
|
819
|
-
// AI CUSTOMIZATION: Add filters specific to entity
|
|
820
|
-
positionTypes?: string[]; // e.g., ['DEFAULT', 'SEASONAL']
|
|
821
|
-
catalogueRef?: string; // e.g., 'DEFAULT_CATALOGUE'
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
/**
|
|
825
|
-
* Result from extraction workflow
|
|
826
|
-
*
|
|
827
|
-
* NAMING: {Entity}ExtractionResult
|
|
828
|
-
*/
|
|
829
|
-
export interface InventoryPositionExtractionResult {
|
|
830
|
-
success: boolean;
|
|
831
|
-
jobId: string;
|
|
832
|
-
recordsExtracted: number;
|
|
833
|
-
fileName?: string;
|
|
834
|
-
s3Path?: string;
|
|
835
|
-
error?: string;
|
|
836
|
-
errors?: any[];
|
|
837
|
-
isManualOverride?: boolean;
|
|
838
|
-
stateUpdated?: boolean;
|
|
839
|
-
newTimestamp?: string;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* GraphQL Query for Inventory Positions
|
|
844
|
-
*
|
|
845
|
-
* NAMING: {ENTITY}_EXTRACTION_QUERY (uppercase constant)
|
|
846
|
-
*/
|
|
847
|
-
const INVENTORY_POSITIONS_EXTRACTION_QUERY = `
|
|
848
|
-
query GetInventoryPositions(
|
|
849
|
-
$catalogues: [InventoryCatalogueKey]
|
|
850
|
-
$dateRangeFilter: DateRange
|
|
851
|
-
$productRefs: [String!]
|
|
852
|
-
$types: [String!]
|
|
853
|
-
$first: Int!
|
|
854
|
-
$after: String
|
|
855
|
-
) {
|
|
856
|
-
inventoryPositions(
|
|
857
|
-
catalogues: $catalogues
|
|
858
|
-
updatedOn: $dateRangeFilter
|
|
859
|
-
productRef: $productRefs
|
|
860
|
-
type: $types
|
|
861
|
-
first: $first
|
|
862
|
-
after: $after
|
|
863
|
-
) {
|
|
864
|
-
edges {
|
|
865
|
-
node {
|
|
866
|
-
id
|
|
867
|
-
ref
|
|
868
|
-
productRef
|
|
869
|
-
locationRef
|
|
870
|
-
onHand
|
|
871
|
-
type
|
|
872
|
-
status
|
|
873
|
-
catalogue {
|
|
874
|
-
ref
|
|
875
|
-
name
|
|
876
|
-
}
|
|
877
|
-
createdOn
|
|
878
|
-
updatedOn
|
|
879
|
-
}
|
|
880
|
-
cursor
|
|
881
|
-
}
|
|
882
|
-
pageInfo {
|
|
883
|
-
hasNextPage
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
`;
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* Query job status from KV store
|
|
891
|
-
*
|
|
892
|
-
* ✅ VERSORI PLATFORM: Uses native log from context (passed as parameter)
|
|
893
|
-
*/
|
|
894
|
-
export async function getJobStatus(
|
|
895
|
-
kv: any, // ✅ Versori KV (compatible with JobTracker's KVAdapter interface)
|
|
896
|
-
jobId: string,
|
|
897
|
-
log: any // ✅ Native Versori log from context
|
|
898
|
-
): Promise<any | undefined> {
|
|
899
|
-
try {
|
|
900
|
-
const tracker = new JobTracker(kv, log);
|
|
901
|
-
return await tracker.getJob(jobId);
|
|
902
|
-
} catch (error: any) {
|
|
903
|
-
log.error('Failed to get job status', {
|
|
904
|
-
jobId,
|
|
905
|
-
message: error instanceof Error ? error.message : String(error),
|
|
906
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
907
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
908
|
-
});
|
|
909
|
-
return undefined;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
/**
|
|
914
|
-
* MAIN ORCHESTRATION FUNCTION
|
|
915
|
-
*
|
|
916
|
-
* NAMING: execute{Entity}Extraction (e.g., executeInventoryPositionExtraction)
|
|
917
|
-
*
|
|
918
|
-
* This function implements the complete workflow in steps.
|
|
919
|
-
* Each step is clearly commented for AI understanding.
|
|
920
|
-
*/
|
|
921
|
-
export async function executeInventoryPositionExtraction(
|
|
922
|
-
ctx: any,
|
|
923
|
-
params: InventoryPositionExtractionParams
|
|
924
|
-
): Promise<InventoryPositionExtractionResult> {
|
|
925
|
-
// ✅ VERSORI PLATFORM: Extract native log from context
|
|
926
|
-
const { log, openKv, activation } = ctx;
|
|
927
|
-
const { jobId, triggeredBy, fromDate, toDate, updateState } = params;
|
|
928
|
-
|
|
929
|
-
// Open KV store for state management and job tracking
|
|
930
|
-
// ✅ Pass raw Versori KV directly - it matches KVAdapter interface
|
|
931
|
-
// ✅ Pass native log to JobTracker
|
|
932
|
-
const kv = openKv(':project:');
|
|
933
|
-
const tracker = new JobTracker(kv, log);
|
|
934
|
-
|
|
935
|
-
try {
|
|
936
|
-
// ═══════════════════════════════════════════════════════════
|
|
937
|
-
// STEP 1/8: Initialize Job Tracking
|
|
938
|
-
// ═══════════════════════════════════════════════════════════
|
|
939
|
-
const step1Start = Date.now();
|
|
940
|
-
log.info('📋 [STEP 1/8] Initializing job tracking', { jobId });
|
|
941
|
-
|
|
942
|
-
await tracker.createJob(jobId, {
|
|
943
|
-
triggeredBy,
|
|
944
|
-
hasDateOverride: !!fromDate,
|
|
945
|
-
fromDate,
|
|
946
|
-
toDate,
|
|
947
|
-
updateStateAfterRun: updateState,
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
log.info('✅ [STEP 1/8] Job tracking initialized', {
|
|
951
|
-
jobId,
|
|
952
|
-
duration: Date.now() - step1Start,
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
// ═══════════════════════════════════════════════════════════
|
|
956
|
-
// STEP 2/8: Initialize Fluent Client
|
|
957
|
-
// ═══════════════════════════════════════════════════════════
|
|
958
|
-
const step2Start = Date.now();
|
|
959
|
-
log.info('🔌 [STEP 2/8] Initializing Fluent Commerce client', { jobId });
|
|
960
|
-
|
|
961
|
-
const client = await createClient(ctx);
|
|
962
|
-
|
|
963
|
-
if (!client) {
|
|
964
|
-
throw new Error('Failed to create Fluent Commerce client - check connection configuration');
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
log.info('✅ [STEP 2/8] Client initialized', {
|
|
968
|
-
jobId,
|
|
969
|
-
duration: Date.now() - step2Start,
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
// ═══════════════════════════════════════════════════════════
|
|
973
|
-
// STEP 3/8: Determine Date Range
|
|
974
|
-
// ═══════════════════════════════════════════════════════════
|
|
975
|
-
const step3Start = Date.now();
|
|
976
|
-
log.info('📅 [STEP 3/8] Determining date range for extraction', { jobId });
|
|
977
|
-
|
|
978
|
-
// State key for incremental sync tracking
|
|
979
|
-
// NAMING: last{Entity}Sync (e.g., lastInventoryPositionSync)
|
|
980
|
-
const STATE_KEY = 'lastInventoryPositionSync';
|
|
981
|
-
const DEFAULT_FALLBACK = '2024-01-01T00:00:00Z';
|
|
982
|
-
const OVERLAP_BUFFER_SECONDS = parseInt(
|
|
983
|
-
activation.getVariable('overlapBufferSeconds') || '60',
|
|
984
|
-
10
|
|
985
|
-
);
|
|
986
|
-
|
|
987
|
-
let dateRangeFilter: { from?: string; to?: string } | null = null;
|
|
988
|
-
const isManualOverride = !!fromDate;
|
|
989
|
-
|
|
990
|
-
if (isManualOverride) {
|
|
991
|
-
// Manual date override from webhook
|
|
992
|
-
dateRangeFilter = { from: fromDate, to: toDate };
|
|
993
|
-
log.info('Using manual date override', { fromDate, toDate });
|
|
994
|
-
} else {
|
|
995
|
-
// Incremental sync - get last sync timestamp
|
|
996
|
-
const rawLastRunTime = (await kv.get<string>(STATE_KEY)) || DEFAULT_FALLBACK;
|
|
997
|
-
|
|
998
|
-
// Apply overlap buffer (prevents missed records)
|
|
999
|
-
const bufferedLastRunTime = new Date(
|
|
1000
|
-
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_SECONDS * 1000
|
|
1001
|
-
).toISOString();
|
|
1002
|
-
|
|
1003
|
-
const effectiveEndTime = toDate || new Date().toISOString();
|
|
1004
|
-
|
|
1005
|
-
dateRangeFilter = {
|
|
1006
|
-
from: bufferedLastRunTime,
|
|
1007
|
-
to: effectiveEndTime, // End of extraction window
|
|
1008
|
-
};
|
|
1009
|
-
|
|
1010
|
-
log.info('Using incremental sync with overlap buffer', {
|
|
1011
|
-
rawLastRunTime,
|
|
1012
|
-
bufferedLastRunTime,
|
|
1013
|
-
effectiveEndTime,
|
|
1014
|
-
overlapBufferSeconds: OVERLAP_BUFFER_SECONDS,
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
log.info('✅ [STEP 3/8] Date range determined', {
|
|
1019
|
-
jobId,
|
|
1020
|
-
fromDate: dateRangeFilter?.from,
|
|
1021
|
-
toDate: dateRangeFilter?.to,
|
|
1022
|
-
isManualOverride,
|
|
1023
|
-
duration: Date.now() - step3Start,
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
// ═══════════════════════════════════════════════════════════
|
|
1027
|
-
// STEP 4/8: Extract Data (ExtractionOrchestrator)
|
|
1028
|
-
// ═══════════════════════════════════════════════════════════
|
|
1029
|
-
const step4Start = Date.now();
|
|
1030
|
-
log.info('📥 [STEP 4/8] Extracting data from Fluent Commerce', { jobId });
|
|
1031
|
-
|
|
1032
|
-
await tracker.updateJob(jobId, {
|
|
1033
|
-
status: 'processing',
|
|
1034
|
-
stage: 'extraction',
|
|
1035
|
-
message: 'Extracting data with auto-pagination',
|
|
1036
|
-
});
|
|
1037
|
-
|
|
1038
|
-
// Build catalogues array from config
|
|
1039
|
-
const catalogueRef = params.catalogueRef || activation.getVariable('catalogueRef');
|
|
1040
|
-
const catalogues = catalogueRef ? [{ ref: catalogueRef }] : [];
|
|
1041
|
-
|
|
1042
|
-
// Configure extraction
|
|
1043
|
-
const pageSize = parseInt(activation.getVariable('pageSize') || '200', 10);
|
|
1044
|
-
const maxRecords = parseInt(activation.getVariable('maxRecords') || '100000', 10);
|
|
1045
|
-
|
|
1046
|
-
// Initialize ExtractionOrchestrator
|
|
1047
|
-
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
1048
|
-
|
|
1049
|
-
// ? Enhanced: Extract context for progress logging
|
|
1050
|
-
const dateRangeInfo = {
|
|
1051
|
-
start: dateRangeFilter?.from || 'N/A',
|
|
1052
|
-
end: dateRangeFilter?.to || 'N/A',
|
|
1053
|
-
catalogues: catalogues.map((c: any) => c.ref).join(', ') || 'all',
|
|
1054
|
-
types: params.positionTypes?.join(', ') || 'all'
|
|
1055
|
-
};
|
|
1056
|
-
|
|
1057
|
-
// ? Enhanced: Start logging with context
|
|
1058
|
-
log.info(`🔍 [ExtractionOrchestrator] Starting extraction`, {
|
|
1059
|
-
query: 'inventoryPositions',
|
|
1060
|
-
pageSize,
|
|
1061
|
-
maxRecords,
|
|
1062
|
-
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1063
|
-
catalogues: dateRangeInfo.catalogues,
|
|
1064
|
-
positionTypes: dateRangeInfo.types,
|
|
1065
|
-
jobId
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
// Execute extraction with auto-pagination
|
|
1069
|
-
const extractionResult = await orchestrator.extract({
|
|
1070
|
-
query: INVENTORY_POSITIONS_EXTRACTION_QUERY,
|
|
1071
|
-
resultPath: 'inventoryPositions.edges.node',
|
|
1072
|
-
variables: {
|
|
1073
|
-
catalogues,
|
|
1074
|
-
dateRangeFilter,
|
|
1075
|
-
types: params.positionTypes,
|
|
1076
|
-
// Note: Don't include 'first' or 'after' here; orchestrator injects them
|
|
1077
|
-
},
|
|
1078
|
-
pageSize,
|
|
1079
|
-
maxRecords,
|
|
1080
|
-
// Optional: validate each record
|
|
1081
|
-
validateItem: (item: any) => {
|
|
1082
|
-
return !!(item.ref && item.productRef);
|
|
1083
|
-
},
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
const rawRecords = extractionResult.data;
|
|
1087
|
-
|
|
1088
|
-
log.info('✅ [STEP 4/8] Extraction completed', {
|
|
1089
|
-
jobId,
|
|
1090
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1091
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1092
|
-
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1093
|
-
errors: extractionResult.errors ? extractionResult.errors.length : 0,
|
|
1094
|
-
duration: Date.now() - step4Start,
|
|
1095
|
-
});
|
|
1096
|
-
|
|
1097
|
-
// ? Enhanced: Completion logging with summary
|
|
1098
|
-
log.info(`✅ [ExtractionOrchestrator] Extraction completed`, {
|
|
1099
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1100
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1101
|
-
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1102
|
-
failedValidations: extractionResult.stats.failedValidations,
|
|
1103
|
-
truncated: extractionResult.stats.truncated,
|
|
1104
|
-
truncationReason: extractionResult.stats.truncationReason,
|
|
1105
|
-
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1106
|
-
jobId
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
if (extractionResult.errors && extractionResult.errors.length > 0) {
|
|
1110
|
-
log.warn('Non-fatal extraction errors encountered', {
|
|
1111
|
-
jobId,
|
|
1112
|
-
errorCount: extractionResult.errors.length,
|
|
1113
|
-
sampleErrors: extractionResult.errors.slice(0, 3),
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// Handle empty result
|
|
1118
|
-
if (rawRecords.length === 0) {
|
|
1119
|
-
log.info('No records to process');
|
|
1120
|
-
|
|
1121
|
-
// Update state even with no records (prevents re-querying empty window)
|
|
1122
|
-
if (updateState) {
|
|
1123
|
-
await kv.set(STATE_KEY, new Date().toISOString());
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
await tracker.markCompleted(jobId, {
|
|
1127
|
-
recordCount: 0,
|
|
1128
|
-
message: 'No records to extract',
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
return {
|
|
1132
|
-
success: true,
|
|
1133
|
-
jobId,
|
|
1134
|
-
recordsExtracted: 0,
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
// ═══════════════════════════════════════════════════════════
|
|
1139
|
-
// STEP 5/8: Transform Data (UniversalMapper)
|
|
1140
|
-
// ═══════════════════════════════════════════════════════════
|
|
1141
|
-
const step5Start = Date.now();
|
|
1142
|
-
log.info('🔄 [STEP 5/8] Transforming data with UniversalMapper', {
|
|
1143
|
-
jobId,
|
|
1144
|
-
recordCount: rawRecords.length,
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
await tracker.updateJob(jobId, {
|
|
1148
|
-
status: 'processing',
|
|
1149
|
-
stage: 'transformation',
|
|
1150
|
-
message: `Transforming ${rawRecords.length} records`,
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
const mapper = new UniversalMapper(mappingConfig);
|
|
1154
|
-
const mappingResult = await mapper.map(rawRecords);
|
|
1155
|
-
|
|
1156
|
-
if (!mappingResult.success) {
|
|
1157
|
-
const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
|
|
1158
|
-
log.error('[STEP 5/8] Mapping failed - terminating job', {
|
|
1159
|
-
jobId,
|
|
1160
|
-
errorCount: mappingErrors.length,
|
|
1161
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
await tracker.markFailed(
|
|
1165
|
-
jobId,
|
|
1166
|
-
new Error(mappingErrors[0] || 'UniversalMapper returned unsuccessful result')
|
|
1167
|
-
);
|
|
1168
|
-
|
|
1169
|
-
return {
|
|
1170
|
-
success: false,
|
|
1171
|
-
jobId,
|
|
1172
|
-
recordsExtracted: 0,
|
|
1173
|
-
error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
const transformedRecords = Array.isArray(mappingResult.data) ? mappingResult.data : [];
|
|
1178
|
-
const mappingErrors = mappingResult.errors || [];
|
|
1179
|
-
|
|
1180
|
-
if (mappingErrors.length > 0) {
|
|
1181
|
-
log.warn('[STEP 5/8] Some records failed transformation', {
|
|
1182
|
-
jobId,
|
|
1183
|
-
errorCount: mappingErrors.length,
|
|
1184
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
1185
|
-
});
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
1189
|
-
log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
1190
|
-
jobId,
|
|
1191
|
-
skippedFields: mappingResult.skippedFields,
|
|
1192
|
-
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
if (transformedRecords.length === 0) {
|
|
1197
|
-
throw new Error('All records failed transformation');
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
log.info('✅ [STEP 5/8] Transformation complete', {
|
|
1201
|
-
jobId,
|
|
1202
|
-
successful: transformedRecords.length,
|
|
1203
|
-
skippedRecords: rawRecords.length - transformedRecords.length,
|
|
1204
|
-
duration: Date.now() - step5Start,
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
// ═══════════════════════════════════════════════════════════
|
|
1208
|
-
// STEP 6/8: Generate CSV (CSVParserService)
|
|
1209
|
-
// ═══════════════════════════════════════════════════════════
|
|
1210
|
-
const step6Start = Date.now();
|
|
1211
|
-
log.info('📄 [STEP 6/8] Generating CSV file', { jobId });
|
|
1212
|
-
|
|
1213
|
-
await tracker.updateJob(jobId, {
|
|
1214
|
-
status: 'processing',
|
|
1215
|
-
stage: 'csv_generation',
|
|
1216
|
-
message: `Generating CSV for ${transformedRecords.length} records`,
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
// Initialize CSVParserService
|
|
1220
|
-
const csvParser = new CSVParserService();
|
|
1221
|
-
|
|
1222
|
-
// Generate CSV content (synchronous, not async)
|
|
1223
|
-
const csvContent = csvParser.stringify(transformedRecords, { headers: true });
|
|
1224
|
-
|
|
1225
|
-
// Generate filename
|
|
1226
|
-
const fileNamePrefix = activation.getVariable('fileNamePrefix') || 'inventorypositions';
|
|
1227
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1228
|
-
const fileName = `${fileNamePrefix}-${timestamp}.csv`;
|
|
1229
|
-
|
|
1230
|
-
log.info('✅ [STEP 6/8] CSV file generated', {
|
|
1231
|
-
fileName,
|
|
1232
|
-
sizeBytes: csvContent.length,
|
|
1233
|
-
recordCount: transformedRecords.length,
|
|
1234
|
-
duration: Date.now() - step6Start,
|
|
1235
|
-
});
|
|
1236
|
-
|
|
1237
|
-
// ═══════════════════════════════════════════════════════════
|
|
1238
|
-
// STEP 7/8: Upload to S3 (S3DataSource)
|
|
1239
|
-
// ═══════════════════════════════════════════════════════════
|
|
1240
|
-
const step7Start = Date.now();
|
|
1241
|
-
log.info('☁️ [STEP 7/8] Uploading to S3', { jobId, fileName });
|
|
1242
|
-
|
|
1243
|
-
await tracker.updateJob(jobId, {
|
|
1244
|
-
status: 'processing',
|
|
1245
|
-
stage: 's3_upload',
|
|
1246
|
-
message: `Uploading ${fileName} to S3`,
|
|
1247
|
-
});
|
|
1248
|
-
|
|
1249
|
-
// Get S3 configuration from activation variables
|
|
1250
|
-
const s3Config = {
|
|
1251
|
-
bucket: activation.getVariable('s3BucketName'),
|
|
1252
|
-
region: activation.getVariable('awsRegion') || 'us-east-1',
|
|
1253
|
-
accessKeyId: activation.getVariable('awsAccessKeyId'),
|
|
1254
|
-
secretAccessKey: activation.getVariable('awsSecretAccessKey'),
|
|
1255
|
-
};
|
|
1256
|
-
const s3Prefix = activation.getVariable('s3Prefix') || 'inventory-positions/daily/';
|
|
1257
|
-
|
|
1258
|
-
// Validate S3 config
|
|
1259
|
-
if (!s3Config.bucket || !s3Config.accessKeyId || !s3Config.secretAccessKey) {
|
|
1260
|
-
const missingFields = [];
|
|
1261
|
-
if (!s3Config.bucket) missingFields.push('s3BucketName');
|
|
1262
|
-
if (!s3Config.accessKeyId) missingFields.push('awsAccessKeyId');
|
|
1263
|
-
if (!s3Config.secretAccessKey) missingFields.push('awsSecretAccessKey');
|
|
1264
|
-
|
|
1265
|
-
throw new Error(
|
|
1266
|
-
`S3 configuration incomplete - missing activation variables: ${missingFields.join(', ')}`
|
|
1267
|
-
);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// Initialize S3 data source
|
|
1271
|
-
// ✅ VERSORI PLATFORM: Pass native log from context
|
|
1272
|
-
const s3 = new S3DataSource(
|
|
1273
|
-
{
|
|
1274
|
-
type: 'S3_CSV',
|
|
1275
|
-
connectionId: 'inventory-positions-s3',
|
|
1276
|
-
name: 'Inventory Positions S3 Upload',
|
|
1277
|
-
s3Config,
|
|
1278
|
-
},
|
|
1279
|
-
log
|
|
1280
|
-
);
|
|
1281
|
-
|
|
1282
|
-
// Validate S3 connection before upload
|
|
1283
|
-
try {
|
|
1284
|
-
await s3.validateConnection();
|
|
1285
|
-
log.info('✅ S3 connection validated', { bucket: s3Config.bucket });
|
|
1286
|
-
} catch (validateError: any) {
|
|
1287
|
-
log.error('❌ S3 connection validation failed', {
|
|
1288
|
-
bucket: s3Config.bucket,
|
|
1289
|
-
region: s3Config.region,
|
|
1290
|
-
error: validateError.message,
|
|
1291
|
-
});
|
|
1292
|
-
throw new Error(
|
|
1293
|
-
`S3 connection validation failed: ${validateError.message}. ` +
|
|
1294
|
-
`Check bucket name, region, and AWS credentials.`
|
|
1295
|
-
);
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
// Construct S3 key
|
|
1299
|
-
const s3Key = `${s3Prefix}${fileName}`;
|
|
1300
|
-
|
|
1301
|
-
// Upload with retry logic (built into S3DataSource)
|
|
1302
|
-
await s3.upload(s3Key, Buffer.from(csvContent, 'utf-8'), {
|
|
1303
|
-
contentType: 'text/csv',
|
|
1304
|
-
metadata: {
|
|
1305
|
-
recordCount: String(transformedRecords.length),
|
|
1306
|
-
extractedAt: new Date().toISOString(),
|
|
1307
|
-
jobId,
|
|
1308
|
-
},
|
|
1309
|
-
});
|
|
1310
|
-
|
|
1311
|
-
log.info('✅ [STEP 7/8] S3 upload successful', {
|
|
1312
|
-
fileName,
|
|
1313
|
-
s3Key,
|
|
1314
|
-
bucket: s3Config.bucket,
|
|
1315
|
-
sizeBytes: csvContent.length,
|
|
1316
|
-
duration: Date.now() - step7Start,
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
// ═══════════════════════════════════════════════════════════
|
|
1320
|
-
// STEP 8/8: Update State & Complete Job
|
|
1321
|
-
// ═══════════════════════════════════════════════════════════
|
|
1322
|
-
const step8Start = Date.now();
|
|
1323
|
-
log.info('💾 [STEP 8/8] Updating state and completing job', { jobId });
|
|
1324
|
-
|
|
1325
|
-
// Calculate new timestamp for next incremental run
|
|
1326
|
-
let newTimestamp: string | undefined;
|
|
1327
|
-
|
|
1328
|
-
if (updateState) {
|
|
1329
|
-
// Find max updatedOn from extracted records
|
|
1330
|
-
const maxUpdatedOn = rawRecords.reduce(
|
|
1331
|
-
(max, record) => {
|
|
1332
|
-
const recordTime = new Date(record.updatedOn).getTime();
|
|
1333
|
-
return recordTime > max ? recordTime : max;
|
|
1334
|
-
},
|
|
1335
|
-
new Date(dateRangeFilter?.from || DEFAULT_FALLBACK).getTime()
|
|
1336
|
-
);
|
|
1337
|
-
|
|
1338
|
-
newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
1339
|
-
|
|
1340
|
-
// Store new timestamp (WITHOUT buffer - buffer only applied on read)
|
|
1341
|
-
await kv.set(STATE_KEY, newTimestamp);
|
|
1342
|
-
|
|
1343
|
-
log.info('State updated', {
|
|
1344
|
-
oldTimestamp: dateRangeFilter?.from,
|
|
1345
|
-
newTimestamp,
|
|
1346
|
-
});
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
// Mark job as completed
|
|
1350
|
-
await tracker.markCompleted(jobId, {
|
|
1351
|
-
recordCount: transformedRecords.length,
|
|
1352
|
-
fileName,
|
|
1353
|
-
s3Key,
|
|
1354
|
-
errorCount: mappingErrors.length,
|
|
1355
|
-
isManualOverride,
|
|
1356
|
-
stateUpdated: updateState,
|
|
1357
|
-
newTimestamp,
|
|
1358
|
-
});
|
|
1359
|
-
|
|
1360
|
-
log.info('✅ [STEP 8/8] State updated and job completed', {
|
|
1361
|
-
jobId,
|
|
1362
|
-
stateUpdated: updateState,
|
|
1363
|
-
newTimestamp,
|
|
1364
|
-
duration: Date.now() - step8Start,
|
|
1365
|
-
});
|
|
1366
|
-
|
|
1367
|
-
return {
|
|
1368
|
-
success: true,
|
|
1369
|
-
jobId,
|
|
1370
|
-
recordsExtracted: transformedRecords.length,
|
|
1371
|
-
fileName,
|
|
1372
|
-
s3Path: s3Key,
|
|
1373
|
-
isManualOverride,
|
|
1374
|
-
stateUpdated: updateState,
|
|
1375
|
-
newTimestamp,
|
|
1376
|
-
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1377
|
-
};
|
|
1378
|
-
} catch (error: any) {
|
|
1379
|
-
log.error('❌ [ORCHESTRATION] Extraction workflow failed', {
|
|
1380
|
-
jobId,
|
|
1381
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1382
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1383
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1384
|
-
});
|
|
1385
|
-
|
|
1386
|
-
// Mark job as failed
|
|
1387
|
-
await tracker.markFailed(jobId, error);
|
|
1388
|
-
|
|
1389
|
-
return {
|
|
1390
|
-
success: false,
|
|
1391
|
-
jobId,
|
|
1392
|
-
recordsExtracted: 0,
|
|
1393
|
-
error: error.message,
|
|
1394
|
-
recommendation: getErrorRecommendation(error),
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
/**
|
|
1400
|
-
* Helper: Get error-specific recommendations
|
|
1401
|
-
* (Same helper function used in workflows - can be shared via utils)
|
|
1402
|
-
*/
|
|
1403
|
-
function getErrorRecommendation(error: any): string {
|
|
1404
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1405
|
-
|
|
1406
|
-
if (message.includes('ECONNREFUSED') || message.includes('ETIMEDOUT')) {
|
|
1407
|
-
return 'Network connectivity issue. Check firewall rules and network configuration.';
|
|
1408
|
-
}
|
|
1409
|
-
if (message.includes('authentication') || message.includes('401')) {
|
|
1410
|
-
return 'Authentication failed. Verify OAuth2 credentials in connection configuration.';
|
|
1411
|
-
}
|
|
1412
|
-
if (message.includes('S3') || message.includes('bucket')) {
|
|
1413
|
-
return 'S3 operation failed. Check bucket name, region, and AWS credentials.';
|
|
1414
|
-
}
|
|
1415
|
-
if (message.includes('GraphQL') || message.includes('query')) {
|
|
1416
|
-
return 'GraphQL query failed. Verify query syntax and field availability in schema.';
|
|
1417
|
-
}
|
|
1418
|
-
if (message.includes('mapping') || message.includes('transform')) {
|
|
1419
|
-
return 'Data transformation failed. Check mapping configuration and source data structure.';
|
|
1420
|
-
}
|
|
1421
|
-
if (message.includes('KV') || message.includes('state')) {
|
|
1422
|
-
return 'State management failed. Check Versori KV availability and permissions.';
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
return 'Check logs for detailed error information and stack trace.';
|
|
1426
|
-
}
|
|
1427
|
-
```
|
|
1428
|
-
|
|
1429
|
-
---
|
|
1430
|
-
|
|
1431
|
-
## 4. Utility Functions (src/utils/job-id-generator.ts)
|
|
1432
|
-
|
|
1433
|
-
```typescript
|
|
1434
|
-
/**
|
|
1435
|
-
* Job ID Generator
|
|
1436
|
-
*
|
|
1437
|
-
* Generates unique job IDs for tracking extraction workflows
|
|
1438
|
-
*
|
|
1439
|
-
* FORMAT: {TYPE}_{ENTITY}_{YYYYMMDD}_{HHMMSS}_{RANDOM}
|
|
1440
|
-
* Example: SCHEDULED_IP_20251027_183045_a1b2c3
|
|
1441
|
-
*/
|
|
1442
|
-
|
|
1443
|
-
/**
|
|
1444
|
-
* Generate unique job ID
|
|
1445
|
-
*
|
|
1446
|
-
* @param type - Job trigger type (SCHEDULED, ADHOC, MANUAL)
|
|
1447
|
-
* @param entity - Entity abbreviation (IP=Inventory Positions, VP, ORD, PRD)
|
|
1448
|
-
* @returns Unique job ID string
|
|
1449
|
-
*/
|
|
1450
|
-
export function generateJobId(type: string, entity: string): string {
|
|
1451
|
-
const now = new Date();
|
|
1452
|
-
|
|
1453
|
-
// Format: YYYYMMDD
|
|
1454
|
-
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
1455
|
-
|
|
1456
|
-
// Format: HHMMSS
|
|
1457
|
-
const timeStr = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
1458
|
-
|
|
1459
|
-
// Random suffix (6 chars)
|
|
1460
|
-
const randomStr = Math.random().toString(36).substring(2, 8);
|
|
1461
|
-
|
|
1462
|
-
return `${type}_${entity}_${dateStr}_${timeStr}_${randomStr}`;
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
/**
|
|
1466
|
-
* Parse job ID components
|
|
1467
|
-
*/
|
|
1468
|
-
export function parseJobId(jobId: string): {
|
|
1469
|
-
type: string;
|
|
1470
|
-
entity: string;
|
|
1471
|
-
date: string;
|
|
1472
|
-
time: string;
|
|
1473
|
-
random: string;
|
|
1474
|
-
} | null {
|
|
1475
|
-
const parts = jobId.split('_');
|
|
1476
|
-
|
|
1477
|
-
if (parts.length !== 5) {
|
|
1478
|
-
return null;
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
return {
|
|
1482
|
-
type: parts[0],
|
|
1483
|
-
entity: parts[1],
|
|
1484
|
-
date: parts[2],
|
|
1485
|
-
time: parts[3],
|
|
1486
|
-
random: parts[4],
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
```
|
|
1490
|
-
|
|
1491
|
-
---
|
|
1492
|
-
|
|
1493
|
-
## 5. Package Configuration
|
|
1494
|
-
|
|
1495
|
-
### package.json
|
|
1496
|
-
|
|
1497
|
-
```json
|
|
1498
|
-
{
|
|
1499
|
-
"name": "inventory-positions-to-s3-csv",
|
|
1500
|
-
"version": "1.0.0",
|
|
1501
|
-
"description": "Extract inventory positions from Fluent Commerce and export to S3 as CSV",
|
|
1502
|
-
"type": "module",
|
|
1503
|
-
"main": "src/index.ts",
|
|
1504
|
-
"scripts": {
|
|
1505
|
-
"dev": "versori dev",
|
|
1506
|
-
"build": "versori build",
|
|
1507
|
-
"deploy": "versori deploy"
|
|
1508
|
-
},
|
|
1509
|
-
"dependencies": {
|
|
1510
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
1511
|
-
"@versori/run": "latest"
|
|
1512
|
-
},
|
|
1513
|
-
"devDependencies": {
|
|
1514
|
-
"@types/node": "^20.0.0",
|
|
1515
|
-
"typescript": "^5.0.0"
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
```
|
|
1519
|
-
|
|
1520
|
-
### tsconfig.json
|
|
1521
|
-
|
|
1522
|
-
```json
|
|
1523
|
-
{
|
|
1524
|
-
"compilerOptions": {
|
|
1525
|
-
"module": "ES2022",
|
|
1526
|
-
"target": "ES2024",
|
|
1527
|
-
"moduleResolution": "node"
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
```
|
|
1531
|
-
|
|
1532
|
-
---
|
|
1533
|
-
|
|
1534
|
-
## 6. Deployment Instructions
|
|
1535
|
-
|
|
1536
|
-
### Deploy to Versori
|
|
1537
|
-
|
|
1538
|
-
```bash
|
|
1539
|
-
# 1. Install dependencies
|
|
1540
|
-
npm install
|
|
1541
|
-
|
|
1542
|
-
# 2. Test locally (if using Versori CLI)
|
|
1543
|
-
npm run dev
|
|
1544
|
-
|
|
1545
|
-
# 3. Deploy to Versori platform
|
|
1546
|
-
npm run deploy
|
|
1547
|
-
```
|
|
1548
|
-
|
|
1549
|
-
### Configure Activation Variables
|
|
1550
|
-
|
|
1551
|
-
In Versori platform settings, configure:
|
|
1552
|
-
|
|
1553
|
-
```json
|
|
1554
|
-
{
|
|
1555
|
-
"catalogueRef": "DEFAULT_CATALOGUE",
|
|
1556
|
-
"s3BucketName": "inventory-analytics-exports",
|
|
1557
|
-
"awsAccessKeyId": "AKIAXXXXXXXXXXXX",
|
|
1558
|
-
"awsSecretAccessKey": "********",
|
|
1559
|
-
"awsRegion": "us-east-1",
|
|
1560
|
-
"s3Prefix": "inventory-positions/daily/",
|
|
1561
|
-
"fileNamePrefix": "inventorypositions",
|
|
1562
|
-
"pageSize": 200,
|
|
1563
|
-
"maxRecords": 100000,
|
|
1564
|
-
"overlapBufferSeconds": 60
|
|
1565
|
-
}
|
|
1566
|
-
```
|
|
1567
|
-
|
|
1568
|
-
---
|
|
1569
|
-
|
|
1570
|
-
## 7. Testing
|
|
1571
|
-
|
|
1572
|
-
### Test Scheduled Extraction
|
|
1573
|
-
|
|
1574
|
-
The scheduled workflow runs automatically based on cron schedule.
|
|
1575
|
-
|
|
1576
|
-
**Check logs:**
|
|
1577
|
-
|
|
1578
|
-
```
|
|
1579
|
-
[STEP 1/8] Initializing job tracking
|
|
1580
|
-
[STEP 2/8] Initializing Fluent Commerce client
|
|
1581
|
-
[STEP 3/8] Determining date range for extraction
|
|
1582
|
-
[STEP 4/8] Extracting data from Fluent Commerce
|
|
1583
|
-
[STEP 5/8] Transforming data with UniversalMapper
|
|
1584
|
-
[STEP 6/8] Generating CSV file
|
|
1585
|
-
[STEP 7/8] Uploading to S3
|
|
1586
|
-
[STEP 8/8] Updating state and completing job
|
|
1587
|
-
```
|
|
1588
|
-
|
|
1589
|
-
### Test Ad hoc Extraction
|
|
1590
|
-
|
|
1591
|
-
```bash
|
|
1592
|
-
# Incremental (uses last sync timestamp)
|
|
1593
|
-
curl -X POST https://api.versori.com/webhooks/inventory-positions-adhoc \
|
|
1594
|
-
-H "Content-Type: application/json" \
|
|
1595
|
-
-d '{}'
|
|
1596
|
-
|
|
1597
|
-
# Date range override
|
|
1598
|
-
curl -X POST https://api.versori.com/webhooks/inventory-positions-adhoc \
|
|
1599
|
-
-H "Content-Type: application/json" \
|
|
1600
|
-
-d '{
|
|
1601
|
-
"fromDate": "2025-01-01T00:00:00Z",
|
|
1602
|
-
"toDate": "2025-01-31T23:59:59Z",
|
|
1603
|
-
"updateState": false
|
|
1604
|
-
}'
|
|
1605
|
-
```
|
|
1606
|
-
|
|
1607
|
-
### Test Job Status Query
|
|
1608
|
-
|
|
1609
|
-
```bash
|
|
1610
|
-
curl -X POST https://api.versori.com/webhooks/inventory-positions-job-status \
|
|
1611
|
-
-H "Content-Type: application/json" \
|
|
1612
|
-
-d '{
|
|
1613
|
-
"jobId": "ADHOC_IP_20251027_183045_abc123"
|
|
1614
|
-
}'
|
|
1615
|
-
```
|
|
1616
|
-
|
|
1617
|
-
**Response:**
|
|
1618
|
-
|
|
1619
|
-
```json
|
|
1620
|
-
{
|
|
1621
|
-
"success": true,
|
|
1622
|
-
"jobId": "ADHOC_IP_20251027_183045_abc123",
|
|
1623
|
-
"status": "processing",
|
|
1624
|
-
"stage": "transformation",
|
|
1625
|
-
"message": "Transforming 15000 records",
|
|
1626
|
-
"createdAt": "2025-10-27T18:30:45.000Z",
|
|
1627
|
-
"startedAt": "2025-10-27T18:30:46.000Z"
|
|
1628
|
-
}
|
|
1629
|
-
```
|
|
1630
|
-
|
|
1631
|
-
---
|
|
1632
|
-
|
|
1633
|
-
---
|
|
1634
|
-
|
|
1635
|
-
### Pattern 7: State Management & Date Overrides
|
|
1636
|
-
|
|
1637
|
-
**Use Case**: Understand how state management works with scheduled and ad-hoc extractions.
|
|
1638
|
-
|
|
1639
|
-
**How it works**:
|
|
1640
|
-
|
|
1641
|
-
VersoriKV stores the last successful extraction timestamp to enable incremental sync:
|
|
1642
|
-
|
|
1643
|
-
```typescript
|
|
1644
|
-
interface ExtractionState {
|
|
1645
|
-
timestamp: string; // Last run timestamp (WITHOUT overlap buffer)
|
|
1646
|
-
recordCount: number; // Number of records extracted
|
|
1647
|
-
extractedAt: string; // When extraction completed
|
|
1648
|
-
fileName?: string; // Generated filename
|
|
1649
|
-
s3Key?: string; // S3 upload path
|
|
1650
|
-
overlapBufferSeconds?: number; // Buffer configuration
|
|
1651
|
-
}
|
|
1652
|
-
```
|
|
1653
|
-
|
|
1654
|
-
**State Priority Chain** (highest to lowest):
|
|
1655
|
-
|
|
1656
|
-
1. **`fromDate` override** (manual date in webhook payload) - Highest priority
|
|
1657
|
-
2. **Stored state** (`await kv.get(stateKey)`) - Normal incremental mode
|
|
1658
|
-
3. **`fallbackStartDate`** (activation variable) - First run fallback
|
|
1659
|
-
|
|
1660
|
-
**Three Scenarios**:
|
|
1661
|
-
|
|
1662
|
-
#### Scenario 1: Normal Scheduled Runs (Incremental)
|
|
1663
|
-
|
|
1664
|
-
```typescript
|
|
1665
|
-
// Payload: {} (empty - no overrides)
|
|
1666
|
-
|
|
1667
|
-
// Behavior:
|
|
1668
|
-
// 1. Load last timestamp from KV: "2025-01-22T10:00:00Z"
|
|
1669
|
-
// 2. Apply overlap buffer: "2025-01-22T09:59:00Z" (query WITH buffer)
|
|
1670
|
-
// 3. Extract records updated since buffered time
|
|
1671
|
-
// 4. Calculate MAX(updatedOn) from results: "2025-01-22T14:30:00Z"
|
|
1672
|
-
// 5. Save new timestamp WITHOUT buffer: "2025-01-22T14:30:00Z"
|
|
1673
|
-
// 6. Next run starts from "2025-01-22T14:29:00Z" (with buffer)
|
|
1674
|
-
```
|
|
1675
|
-
|
|
1676
|
-
**Test**:
|
|
1677
|
-
|
|
1678
|
-
```bash
|
|
1679
|
-
# Trigger scheduled run (no payload needed)
|
|
1680
|
-
# State advances automatically
|
|
1681
|
-
curl -X POST https://workspace.versori.run/inventory-positions-scheduled
|
|
1682
|
-
```
|
|
1683
|
-
|
|
1684
|
-
#### Scenario 2: Ad-hoc Extraction WITH State Update
|
|
1685
|
-
|
|
1686
|
-
```typescript
|
|
1687
|
-
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": true }
|
|
1688
|
-
|
|
1689
|
-
// Behavior:
|
|
1690
|
-
// 1. Ignore stored state
|
|
1691
|
-
// 2. Use fromDate: "2025-01-01T00:00:00Z" (no buffer applied to manual dates)
|
|
1692
|
-
// 3. Extract all records since 2025-01-01
|
|
1693
|
-
// 4. Calculate MAX(updatedOn): "2025-01-22T14:30:00Z"
|
|
1694
|
-
// 5. Save new timestamp: "2025-01-22T14:30:00Z" (updates state!)
|
|
1695
|
-
// 6. Next scheduled run starts from this new timestamp
|
|
1696
|
-
```
|
|
1697
|
-
|
|
1698
|
-
**Use Case**: One-time catch-up extraction that advances the state pointer.
|
|
1699
|
-
|
|
1700
|
-
**Test**:
|
|
1701
|
-
|
|
1702
|
-
```bash
|
|
1703
|
-
curl -X POST https://workspace.versori.run/inventory-positions-adhoc \
|
|
1704
|
-
-H "Content-Type: application/json" \
|
|
1705
|
-
-d '{
|
|
1706
|
-
"fromDate": "2025-01-01T00:00:00Z",
|
|
1707
|
-
"updateState": true
|
|
1708
|
-
}'
|
|
1709
|
-
```
|
|
1710
|
-
|
|
1711
|
-
#### Scenario 3: Ad-hoc Extraction WITHOUT State Update
|
|
1712
|
-
|
|
1713
|
-
```typescript
|
|
1714
|
-
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": false }
|
|
1715
|
-
|
|
1716
|
-
// Behavior:
|
|
1717
|
-
// 1. Ignore stored state
|
|
1718
|
-
// 2. Use fromDate: "2025-01-01T00:00:00Z"
|
|
1719
|
-
// 3. Extract all records since 2025-01-01
|
|
1720
|
-
// 4. DO NOT update state
|
|
1721
|
-
// 5. Next scheduled run uses previous timestamp (unaffected)
|
|
1722
|
-
```
|
|
1723
|
-
|
|
1724
|
-
**Use Case**: Historical backfill or testing without affecting incremental sync.
|
|
1725
|
-
|
|
1726
|
-
**Test**:
|
|
1727
|
-
|
|
1728
|
-
```bash
|
|
1729
|
-
curl -X POST https://workspace.versori.run/inventory-positions-adhoc \
|
|
1730
|
-
-H "Content-Type: application/json" \
|
|
1731
|
-
-d '{
|
|
1732
|
-
"fromDate": "2025-01-01T00:00:00Z",
|
|
1733
|
-
"toDate": "2025-01-31T23:59:59Z",
|
|
1734
|
-
"updateState": false
|
|
1735
|
-
}'
|
|
1736
|
-
```
|
|
1737
|
-
|
|
1738
|
-
**Why this matters**:
|
|
1739
|
-
|
|
1740
|
-
- **Incremental sync** relies on state continuity
|
|
1741
|
-
- **Manual overrides** allow catch-up without breaking incremental flow
|
|
1742
|
-
- **Overlap buffer** prevents missed records at time boundaries
|
|
1743
|
-
- **State isolation** lets you test/backfill without affecting production sync
|
|
1744
|
-
|
|
1745
|
-
---
|
|
1746
|
-
|
|
1747
|
-
### Pattern 8: Optional GraphQL Query Logging
|
|
1748
|
-
|
|
1749
|
-
**Use Case**: Debug extraction issues by logging the exact GraphQL query sent to Fluent Commerce API.
|
|
1750
|
-
|
|
1751
|
-
**When to use**:
|
|
1752
|
-
|
|
1753
|
-
- ✅ Debugging pagination issues
|
|
1754
|
-
- ✅ Verifying query variables (dates, filters, limits)
|
|
1755
|
-
- ✅ Development and testing
|
|
1756
|
-
- ❌ Production (verbose logs, potential secrets in variables)
|
|
1757
|
-
|
|
1758
|
-
**How to enable**:
|
|
1759
|
-
|
|
1760
|
-
Set `DEBUG_GRAPHQL=true` environment variable in Versori activation settings.
|
|
1761
|
-
|
|
1762
|
-
**Implementation**:
|
|
1763
|
-
|
|
1764
|
-
```typescript
|
|
1765
|
-
// In your extraction workflow
|
|
1766
|
-
const DEBUG_GRAPHQL = activation?.getVariable('DEBUG_GRAPHQL') === 'true';
|
|
1767
|
-
|
|
1768
|
-
if (DEBUG_GRAPHQL) {
|
|
1769
|
-
log.info('GraphQL Query Debug', {
|
|
1770
|
-
query: INVENTORY_POSITIONS_QUERY,
|
|
1771
|
-
variables: {
|
|
1772
|
-
catalogueRef,
|
|
1773
|
-
dateRangeFilter,
|
|
1774
|
-
first: pageSize,
|
|
1775
|
-
after: null, // First page
|
|
1776
|
-
},
|
|
1777
|
-
pagination: {
|
|
1778
|
-
pageSize,
|
|
1779
|
-
maxRecords,
|
|
1780
|
-
currentPage: 1,
|
|
1781
|
-
},
|
|
1782
|
-
});
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
const extractionResult = await orchestrator.extract({
|
|
1786
|
-
query: INVENTORY_POSITIONS_QUERY,
|
|
1787
|
-
resultPath: 'inventoryPositions.edges.node',
|
|
1788
|
-
variables: {
|
|
1789
|
-
catalogueRef,
|
|
1790
|
-
dateRangeFilter,
|
|
1791
|
-
},
|
|
1792
|
-
pageSize,
|
|
1793
|
-
maxRecords,
|
|
1794
|
-
});
|
|
1795
|
-
|
|
1796
|
-
if (DEBUG_GRAPHQL) {
|
|
1797
|
-
log.info('GraphQL Response Debug', {
|
|
1798
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1799
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1800
|
-
truncated: extractionResult.stats.truncated,
|
|
1801
|
-
truncationReason: extractionResult.stats.truncationReason,
|
|
1802
|
-
firstRecordId: extractionResult.data[0]?.id,
|
|
1803
|
-
lastRecordId: extractionResult.data[extractionResult.data.length - 1]?.id,
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
```
|
|
1807
|
-
|
|
1808
|
-
**What gets logged**:
|
|
1809
|
-
|
|
1810
|
-
```json
|
|
1811
|
-
{
|
|
1812
|
-
"level": "info",
|
|
1813
|
-
"message": "GraphQL Query Debug",
|
|
1814
|
-
"query": "query GetInventoryPositions($catalogueRef: String, $dateRangeFilter: DateRange, ...)",
|
|
1815
|
-
"variables": {
|
|
1816
|
-
"catalogueRef": "DEFAULT_CATALOGUE",
|
|
1817
|
-
"dateRangeFilter": "2025-01-22T09:59:00Z",
|
|
1818
|
-
"first": 200,
|
|
1819
|
-
"after": null
|
|
1820
|
-
},
|
|
1821
|
-
"pagination": {
|
|
1822
|
-
"pageSize": 200,
|
|
1823
|
-
"maxRecords": 100000,
|
|
1824
|
-
"currentPage": 1
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
```
|
|
1828
|
-
|
|
1829
|
-
**Versori Environment Variables**:
|
|
1830
|
-
|
|
1831
|
-
Add to activation settings:
|
|
1832
|
-
|
|
1833
|
-
```json
|
|
1834
|
-
{
|
|
1835
|
-
"DEBUG_GRAPHQL": "true"
|
|
1836
|
-
}
|
|
1837
|
-
```
|
|
1838
|
-
|
|
1839
|
-
**Testing**:
|
|
1840
|
-
|
|
1841
|
-
```bash
|
|
1842
|
-
# Enable debug logging
|
|
1843
|
-
curl -X POST https://workspace.versori.run/inventory-positions-scheduled
|
|
1844
|
-
|
|
1845
|
-
# Check Versori logs for "GraphQL Query Debug" entries
|
|
1846
|
-
# Verify query structure and variables are correct
|
|
1847
|
-
```
|
|
1848
|
-
|
|
1849
|
-
**Sample Debug Output**:
|
|
1850
|
-
|
|
1851
|
-
```
|
|
1852
|
-
[INFO] GraphQL Query Debug
|
|
1853
|
-
query: "query GetInventoryPositions($catalogueRef: String, $dateRangeFilter: DateRange, ...)"
|
|
1854
|
-
variables: { catalogueRef: "DEFAULT_CATALOGUE", dateRangeFilter: "2025-01-22T09:59:00Z", first: 200, after: null }
|
|
1855
|
-
pagination: { pageSize: 200, maxRecords: 100000, currentPage: 1 }
|
|
1856
|
-
|
|
1857
|
-
[INFO] Extraction complete
|
|
1858
|
-
totalRecords: 1500
|
|
1859
|
-
totalPages: 8
|
|
1860
|
-
truncated: false
|
|
1861
|
-
|
|
1862
|
-
[INFO] GraphQL Response Debug
|
|
1863
|
-
totalRecords: 1500
|
|
1864
|
-
totalPages: 8
|
|
1865
|
-
truncated: false
|
|
1866
|
-
truncationReason: undefined
|
|
1867
|
-
firstRecordId: "ip_001"
|
|
1868
|
-
lastRecordId: "ip_1500"
|
|
1869
|
-
```
|
|
1870
|
-
|
|
1871
|
-
**Key Benefits**:
|
|
1872
|
-
|
|
1873
|
-
- Quickly identify pagination configuration issues
|
|
1874
|
-
- Verify date filters are applied correctly
|
|
1875
|
-
- Debug "no records found" scenarios
|
|
1876
|
-
- Validate ExtractionOrchestrator variable injection
|
|
1877
|
-
|
|
1878
|
-
**Production Best Practice**: Disable `DEBUG_GRAPHQL` in production to reduce log volume and avoid logging sensitive data.
|
|
1879
|
-
|
|
1880
|
-
---
|
|
1881
|
-
|
|
1882
|
-
## 8. Troubleshooting
|
|
1883
|
-
|
|
1884
|
-
**Issue**: "No records extracted"
|
|
1885
|
-
|
|
1886
|
-
- Check dateRange (manual override vs incremental)
|
|
1887
|
-
- Check catalogueRef filter
|
|
1888
|
-
|
|
1889
|
-
**Issue**: "S3 upload failed"
|
|
1890
|
-
|
|
1891
|
-
- Job fails; state not advanced
|
|
1892
|
-
- Next run retries same window
|
|
1893
|
-
- Check S3 credentials and bucket permissions
|
|
1894
|
-
|
|
1895
|
-
**Issue**: "GraphQL pagination error"
|
|
1896
|
-
|
|
1897
|
-
- Ensure edges.cursor and pageInfo.hasNextPage are in query
|
|
1898
|
-
|
|
1899
|
-
**Issue**: "Memory pressure"
|
|
1900
|
-
|
|
1901
|
-
- Lower pageSize or maxRecords
|
|
1902
|
-
- Consider file splitting for large extractions
|
|
1903
|
-
|
|
1904
|
-
---
|
|
1905
|
-
|
|
1906
|
-
## 9. Replication Checklist
|
|
1907
|
-
|
|
1908
|
-
**To replicate this template for other entities/formats:**
|
|
1909
|
-
|
|
1910
|
-
1. **File Naming:** Replace `inventory-positions`, `IP`, `InventoryPosition` with your entity name
|
|
1911
|
-
2. **GraphQL Query:** Update query constant and field selection to match your entity schema
|
|
1912
|
-
3. **Mapping Config:** Create new mapping file in `config/` with correct field paths
|
|
1913
|
-
4. **Workflows:** Rename workflow exports to match entity (e.g., `scheduledOrdersExtraction`)
|
|
1914
|
-
5. **Service Function:** Rename main function (e.g., `executeOrderExtraction`)
|
|
1915
|
-
6. **State Key:** Update KV key (e.g., `lastOrderSync`)
|
|
1916
|
-
7. **Output Format:** For XML use `XMLBuilder`, for JSON use `JSON.stringify()`, for CSV use `CSVParserService`
|
|
1917
|
-
8. **Upload Destination:** For SFTP replace `S3DataSource` with `SftpDataSource` (and add `dispose()` in finally block)
|
|
1918
|
-
9. **Job ID Entity Code:** Update entity abbreviation in generateJobId() (e.g., 'ORD' for orders)
|
|
1919
|
-
|
|
1920
|
-
---
|
|
1921
|
-
|
|
1922
|
-
**Pattern**: Enterprise incremental extraction with overlap buffer for inventory positions
|
|
1923
|
-
**Key Learning**: Use `catalogues` (plural, array) input for filtering
|
|
1924
|
-
**Critical**: Apply 60-second overlap buffer to prevent missed records due to clock skew
|
|
1925
|
-
**Buffer Pattern**: Query WITH buffer (`updatedOn >= lastRunTime - 60s`), save WITHOUT buffer (`MAX(updatedOn)`)
|
|
1926
|
-
**SDK Services**: ExtractionOrchestrator, UniversalMapper, CSVParserService, S3DataSource, JobTracker
|
|
1927
|
-
|
|
1928
|
-
---
|
|
1929
|
-
|
|
1930
|
-
### Pattern 8: Backward Pagination (Optional - Advanced)
|
|
1931
|
-
|
|
1932
|
-
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
1933
|
-
|
|
1934
|
-
**When to Use**:
|
|
1935
|
-
|
|
1936
|
-
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
1937
|
-
- ✅ Time-bounded reverse traversal for auditing
|
|
1938
|
-
- ✅ Display newest-first in UI/reports
|
|
1939
|
-
- ❌ **Don't use for standard incremental sync** - use forward pagination (default)
|
|
1940
|
-
|
|
1941
|
-
**GraphQL Query Requirements**:
|
|
1942
|
-
|
|
1943
|
-
Your query must support backward pagination by including `$last` and `$before`:
|
|
1944
|
-
|
|
1945
|
-
```graphql
|
|
1946
|
-
query GetData(
|
|
1947
|
-
$retailerId: ID!
|
|
1948
|
-
$first: Int # For forward pagination
|
|
1949
|
-
$after: String # For forward pagination
|
|
1950
|
-
$last: Int # For backward pagination
|
|
1951
|
-
$before: String # For backward pagination
|
|
1952
|
-
) {
|
|
1953
|
-
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
1954
|
-
edges {
|
|
1955
|
-
cursor # ✅ REQUIRED
|
|
1956
|
-
node {
|
|
1957
|
-
id
|
|
1958
|
-
createdAt
|
|
1959
|
-
# ... other fields
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
pageInfo {
|
|
1963
|
-
hasNextPage # For forward
|
|
1964
|
-
hasPreviousPage # ✅ REQUIRED for backward
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
```
|
|
1969
|
-
|
|
1970
|
-
**Implementation**:
|
|
1971
|
-
|
|
1972
|
-
```typescript
|
|
1973
|
-
// Backward pagination - newest records first
|
|
1974
|
-
const result = await orchestrator.extract({
|
|
1975
|
-
query: YOUR_QUERY,
|
|
1976
|
-
resultPath: 'data.edges.node',
|
|
1977
|
-
variables: {
|
|
1978
|
-
retailerId,
|
|
1979
|
-
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
1980
|
-
// ❌ Don't include last/before - orchestrator injects them
|
|
1981
|
-
},
|
|
1982
|
-
pageSize: 200,
|
|
1983
|
-
direction: 'backward', // ✅ Enable reverse pagination
|
|
1984
|
-
maxRecords: 10000,
|
|
1985
|
-
});
|
|
1986
|
-
|
|
1987
|
-
// Records are returned in reverse chronological order
|
|
1988
|
-
console.log(result.data[0].createdAt); // Newest
|
|
1989
|
-
console.log(result.data[result.data.length - 1].createdAt); // Oldest (within range)
|
|
1990
|
-
```
|
|
1991
|
-
|
|
1992
|
-
**Key Differences from Forward Pagination**:
|
|
1993
|
-
|
|
1994
|
-
| Aspect | Forward (Default) | Backward |
|
|
1995
|
-
| ---------------------- | -------------------------------- | ----------------------- |
|
|
1996
|
-
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
1997
|
-
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
1998
|
-
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
1999
|
-
| **Cursor Source** | Last edge of page | First edge of page |
|
|
2000
|
-
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
2001
|
-
|
|
2002
|
-
**Important Notes**:
|
|
2003
|
-
|
|
2004
|
-
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
2005
|
-
|
|
2006
|
-
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
2007
|
-
|
|
2008
|
-
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
2009
|
-
|
|
2010
|
-
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
2011
|
-
|
|
2012
|
-
**Example: Extract Latest 1000 Orders**
|
|
2013
|
-
|
|
2014
|
-
```typescript
|
|
2015
|
-
const latestOrders = await orchestrator.extract({
|
|
2016
|
-
query: ORDERS_QUERY,
|
|
2017
|
-
resultPath: 'orders.edges.node',
|
|
2018
|
-
variables: {
|
|
2019
|
-
retailerId,
|
|
2020
|
-
statuses: ['BOOKED', 'ALLOCATED'],
|
|
2021
|
-
},
|
|
2022
|
-
direction: 'backward', // Start from newest
|
|
2023
|
-
maxRecords: 1000, // Stop after 1000 records
|
|
2024
|
-
pageSize: 100, // 100 per page = 10 pages
|
|
2025
|
-
});
|
|
2026
|
-
|
|
2027
|
-
// latestOrders.data[0] is the newest order
|
|
2028
|
-
// latestOrders.data[999] is the 1000th newest order
|
|
2029
|
-
```
|
|
2030
|
-
|
|
2031
|
-
**When to Use Forward vs Backward**:
|
|
2032
|
-
|
|
2033
|
-
```typescript
|
|
2034
|
-
// ✅ Forward (default) - For incremental sync
|
|
2035
|
-
const incrementalData = await orchestrator.extract({
|
|
2036
|
-
query: YOUR_QUERY,
|
|
2037
|
-
resultPath: 'data.edges.node',
|
|
2038
|
-
variables: {
|
|
2039
|
-
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
2040
|
-
},
|
|
2041
|
-
// direction defaults to 'forward'
|
|
2042
|
-
// Processes oldest → newest for proper sequencing
|
|
2043
|
-
});
|
|
2044
|
-
|
|
2045
|
-
// ✅ Backward - For "latest N records" use cases
|
|
2046
|
-
const latestData = await orchestrator.extract({
|
|
2047
|
-
query: YOUR_QUERY,
|
|
2048
|
-
resultPath: 'data.edges.node',
|
|
2049
|
-
direction: 'backward',
|
|
2050
|
-
maxRecords: 100, // Just get latest 100
|
|
2051
|
-
// Gets newest → oldest
|
|
2052
|
-
});
|
|
2053
|
-
```
|
|
2054
|
-
|
|
2055
|
-
**Pagination Variables Reference**:
|
|
2056
|
-
|
|
2057
|
-
| Variable | Forward | Backward | Injected By | Notes |
|
|
2058
|
-
| -------- | ----------- | ----------- | ------------ | ------------------------ |
|
|
2059
|
-
| `first` | ✅ Used | ❌ Not used | Orchestrator | From `pageSize` |
|
|
2060
|
-
| `after` | ✅ Used | ❌ Not used | Orchestrator | From cursor (last edge) |
|
|
2061
|
-
| `last` | ❌ Not used | ✅ Used | Orchestrator | From `pageSize` |
|
|
2062
|
-
| `before` | ❌ Not used | ✅ Used | Orchestrator | From cursor (first edge) |
|
|
2063
|
-
|
|
2064
|
-
**Common Mistakes to Avoid**:
|
|
2065
|
-
|
|
2066
|
-
```typescript
|
|
2067
|
-
// ❌ WRONG - Don't pass pagination variables
|
|
2068
|
-
const result = await orchestrator.extract({
|
|
2069
|
-
variables: {
|
|
2070
|
-
last: 200, // ❌ Orchestrator will override this
|
|
2071
|
-
before: cursor, // ❌ Orchestrator manages cursor
|
|
2072
|
-
},
|
|
2073
|
-
direction: 'backward',
|
|
2074
|
-
});
|
|
2075
|
-
|
|
2076
|
-
// ✅ CORRECT - Let orchestrator inject pagination
|
|
2077
|
-
const result = await orchestrator.extract({
|
|
2078
|
-
variables: {
|
|
2079
|
-
retailerId, // ✅ Your business variables only
|
|
2080
|
-
},
|
|
2081
|
-
pageSize: 200, // ✅ Orchestrator uses this for last/before
|
|
2082
|
-
direction: 'backward',
|
|
2083
|
-
});
|
|
2084
|
-
```
|
|
2085
|
-
|
|
2086
|
-
#### Optional: Reverse Pagination
|
|
2087
|
-
|
|
2088
|
-
- Default: forward ($first/$after) + pageInfo.hasNextPage.
|
|
2089
|
-
- Reverse: use $last/$before and pageInfo.hasPreviousPage in the query, and set direction='backward' in the orchestrator call.
|
|
2090
|
-
|
|
2091
|
-
GraphQL:
|
|
2092
|
-
|
|
2093
|
-
```graphql
|
|
2094
|
-
query GetInventoryPositionsBackward(
|
|
2095
|
-
$catalogues: [InventoryCatalogueKey]
|
|
2096
|
-
$dateRangeFilter: DateRange
|
|
2097
|
-
$last: Int!
|
|
2098
|
-
$before: String
|
|
2099
|
-
) {
|
|
2100
|
-
inventoryPositions(
|
|
2101
|
-
catalogues: $catalogues
|
|
2102
|
-
updatedOn: $dateRangeFilter
|
|
2103
|
-
last: $last
|
|
2104
|
-
before: $before
|
|
2105
|
-
) {
|
|
2106
|
-
edges {
|
|
2107
|
-
cursor
|
|
2108
|
-
node {
|
|
2109
|
-
id
|
|
2110
|
-
ref
|
|
2111
|
-
updatedOn
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
pageInfo {
|
|
2115
|
-
hasPreviousPage
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
```
|
|
2120
|
-
|
|
2121
|
-
SDK:
|
|
2122
|
-
|
|
2123
|
-
```typescript
|
|
2124
|
-
await orchestrator.extract({
|
|
2125
|
-
query: INVENTORY_POSITIONS_BACKWARD_QUERY,
|
|
2126
|
-
resultPath: 'inventoryPositions.edges.node',
|
|
2127
|
-
variables: { catalogues, dateRangeFilter },
|
|
2128
|
-
pageSize,
|
|
2129
|
-
direction: 'backward',
|
|
2130
|
-
});
|
|
2131
|
-
```
|
|
2132
|
-
|
|
2133
|
-
---
|
|
2134
|
-
|
|
2135
|
-
## Testing Checklist
|
|
2136
|
-
|
|
2137
|
-
**Before production deployment:**
|
|
2138
|
-
|
|
2139
|
-
### 1. Schema Validation
|
|
2140
|
-
|
|
2141
|
-
- [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
|
|
2142
|
-
- [ ] Run `npx fc-connect validate-schema --mapping ./config/inventory-positions.export.csv.json --schema ./fluent-schema.json`
|
|
2143
|
-
- [ ] Run `npx fc-connect analyze-coverage --mapping ./config/inventory-positions.export.csv.json --schema ./fluent-schema.json`
|
|
2144
|
-
- [ ] Verify all `source` paths in mapping exist in GraphQL schema
|
|
2145
|
-
- [ ] Verify query structure matches schema (fields, types, filters)
|
|
2146
|
-
|
|
2147
|
-
### 2. Extraction Testing
|
|
2148
|
-
|
|
2149
|
-
- [ ] Test with small dataset first (maxRecords=10)
|
|
2150
|
-
- [ ] Verify ExtractionOrchestrator pagination works correctly
|
|
2151
|
-
- [ ] Test with multiple pages of data (verify cursor handling)
|
|
2152
|
-
- [ ] Verify date range filtering (updatedOn filter)
|
|
2153
|
-
- [ ] Test empty result handling (no records in date range)
|
|
2154
|
-
- [ ] Verify extraction stops at maxRecords limit
|
|
2155
|
-
|
|
2156
|
-
### 3. Mapping Testing
|
|
2157
|
-
|
|
2158
|
-
- [ ] Verify required fields are populated
|
|
2159
|
-
- [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
|
|
2160
|
-
- [ ] Test custom resolvers with edge cases (if any)
|
|
2161
|
-
- [ ] Verify nested field extraction
|
|
2162
|
-
- [ ] Test with null/missing fields
|
|
2163
|
-
- [ ] Verify mapping error collection works
|
|
2164
|
-
|
|
2165
|
-
### 4. CSV Generation Testing
|
|
2166
|
-
|
|
2167
|
-
- [ ] Verify CSV structure matches expected format
|
|
2168
|
-
- [ ] Test CSV validation against schema (if applicable)
|
|
2169
|
-
- [ ] Verify header row is present and correct
|
|
2170
|
-
- [ ] Test with large datasets (>1000 records)
|
|
2171
|
-
- [ ] Verify UTF-8 encoding
|
|
2172
|
-
- [ ] Test special character handling (commas, quotes, newlines)
|
|
2173
|
-
|
|
2174
|
-
### 5. S3 Upload Testing
|
|
2175
|
-
|
|
2176
|
-
- [ ] Test S3 connection and authentication
|
|
2177
|
-
- [ ] Verify file upload to correct bucket and path
|
|
2178
|
-
- [ ] Test file naming convention (timestamp format)
|
|
2179
|
-
- [ ] Verify S3 object metadata
|
|
2180
|
-
- [ ] Test upload retry logic (simulate network failure)
|
|
2181
|
-
- [ ] Verify file permissions and ACLs
|
|
2182
|
-
|
|
2183
|
-
### 6. State Management Testing
|
|
2184
|
-
|
|
2185
|
-
- [ ] Verify overlap buffer prevents missed records (60-second default)
|
|
2186
|
-
- [ ] Test state recovery after extraction failure
|
|
2187
|
-
- [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
|
|
2188
|
-
- [ ] Test first run with no previous state (uses fallbackStartDate)
|
|
2189
|
-
- [ ] Verify state update only happens on successful upload
|
|
2190
|
-
- [ ] Test manual date override (doesn't update state)
|
|
2191
|
-
|
|
2192
|
-
### 7. Job Tracking Testing
|
|
2193
|
-
|
|
2194
|
-
- [ ] Test job creation with JobTracker
|
|
2195
|
-
- [ ] Verify job status updates at each stage
|
|
2196
|
-
- [ ] Test job completion with metadata
|
|
2197
|
-
- [ ] Test job failure handling
|
|
2198
|
-
- [ ] Query job status via webhook endpoint
|
|
2199
|
-
- [ ] Verify job status persists in KV store
|
|
2200
|
-
|
|
2201
|
-
### 8. Error Handling Testing
|
|
2202
|
-
|
|
2203
|
-
- [ ] Test with invalid GraphQL query
|
|
2204
|
-
- [ ] Test with mapping errors (invalid field paths)
|
|
2205
|
-
- [ ] Test with S3 connection failures
|
|
2206
|
-
- [ ] Test with authentication failures
|
|
2207
|
-
- [ ] Test with network timeouts
|
|
2208
|
-
- [ ] Verify error logging includes context (jobId, stage, error details)
|
|
2209
|
-
- [ ] Test error threshold logic (if applicable)
|
|
2210
|
-
|
|
2211
|
-
### 9. Staging Environment Testing
|
|
2212
|
-
|
|
2213
|
-
- [ ] Run full extraction in staging environment
|
|
2214
|
-
- [ ] Verify CSV file format with downstream system
|
|
2215
|
-
- [ ] Monitor extraction duration and resource usage
|
|
2216
|
-
- [ ] Test with production-like data volumes
|
|
2217
|
-
- [ ] Verify no performance degradation over time
|
|
2218
|
-
|
|
2219
|
-
### 10. Integration Testing
|
|
2220
|
-
|
|
2221
|
-
- [ ] Test scheduled workflow (cron trigger)
|
|
2222
|
-
- [ ] Test ad hoc webhook trigger
|
|
2223
|
-
- [ ] Test job status query webhook
|
|
2224
|
-
- [ ] Verify activation variables are read correctly
|
|
2225
|
-
- [ ] Test with different extraction modes (incremental, date range)
|
|
2226
|
-
- [ ] End-to-end test: trigger → extract → transform → upload → verify file
|
|
2227
|
-
|
|
2228
|
-
---
|
|
2229
|
-
|
|
2230
|
-
## Recommended Project Structure
|
|
2231
|
-
|
|
2232
|
-
**Organize workflows by trigger type in separate files:**
|
|
2233
|
-
|
|
2234
|
-
```
|
|
2235
|
-
inventory-positions-extraction/
|
|
2236
|
-
├── index.ts # Entry point - exports all workflows
|
|
2237
|
-
└── src/
|
|
2238
|
-
├── workflows/
|
|
2239
|
-
│ ├── scheduled/
|
|
2240
|
-
│ │ └── daily-inventory-positions-extraction.ts # Scheduled: Daily extraction
|
|
2241
|
-
│ │
|
|
2242
|
-
│ └── webhook/
|
|
2243
|
-
│ ├── adhoc-inventory-positions-extraction.ts # Webhook: Manual trigger
|
|
2244
|
-
│ └── job-status-check.ts # Webhook: Status query
|
|
2245
|
-
│
|
|
2246
|
-
├── services/
|
|
2247
|
-
│ └── inventory-positions-extraction.service.ts # Shared orchestration logic (reusable)
|
|
2248
|
-
│
|
|
2249
|
-
└── config/
|
|
2250
|
-
└── inventory-positions.export.csv.json # Mapping configuration
|
|
2251
|
-
```
|
|
2252
|
-
|
|
2253
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
2254
|
-
|
|
2255
|
-
**Trigger Types:**
|
|
2256
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
2257
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
2258
|
-
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
2259
|
-
|
|
2260
|
-
**Execution Steps (chained to triggers):**
|
|
2261
|
-
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
2262
|
-
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
2263
|
-
|
|
2264
|
-
---
|
|
2265
|
-
|
|
2266
|
-
## Monitoring & Alerting
|
|
2267
|
-
|
|
2268
|
-
### Success Response Example
|
|
2269
|
-
|
|
2270
|
-
```json
|
|
2271
|
-
{
|
|
2272
|
-
"success": true,
|
|
2273
|
-
"jobId": "SCHEDULED_IP_20251102_140000_abc123",
|
|
2274
|
-
"recordsExtracted": 1523,
|
|
2275
|
-
"fileName": "inventory-positions-2025-11-02T14-00-00-000Z.csv",
|
|
2276
|
-
"s3Path": "s3://bucket/inventory-positions/inventory-positions-2025-11-02T14-00-00-000Z.csv",
|
|
2277
|
-
"metrics": {
|
|
2278
|
-
"extractionDurationMs": 12543,
|
|
2279
|
-
"totalPages": 8,
|
|
2280
|
-
"pageSize": 200,
|
|
2281
|
-
"mappingErrors": 0,
|
|
2282
|
-
"fileSizeBytes": 524288,
|
|
2283
|
-
"uploadDurationMs": 1234
|
|
2284
|
-
},
|
|
2285
|
-
"timestamps": {
|
|
2286
|
-
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
2287
|
-
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
2288
|
-
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
2289
|
-
},
|
|
2290
|
-
"state": {
|
|
2291
|
-
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
2292
|
-
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
2293
|
-
"stateUpdated": true,
|
|
2294
|
-
"overlapBufferSeconds": 60
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
```
|
|
2298
|
-
|
|
2299
|
-
### Error Response Example
|
|
2300
|
-
|
|
2301
|
-
```json
|
|
2302
|
-
{
|
|
2303
|
-
"success": false,
|
|
2304
|
-
"jobId": "ADHOC_IP_20251102_140500_xyz789",
|
|
2305
|
-
"error": "S3 upload failed: Connection timeout",
|
|
2306
|
-
"errorCategory": "NETWORK",
|
|
2307
|
-
"recordsExtracted": 0,
|
|
2308
|
-
"stage": "s3_upload",
|
|
2309
|
-
"details": {
|
|
2310
|
-
"message": "Failed to upload file after 3 retry attempts",
|
|
2311
|
-
"retryAttempts": 3,
|
|
2312
|
-
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
2313
|
-
},
|
|
2314
|
-
"state": {
|
|
2315
|
-
"stateUpdated": false,
|
|
2316
|
-
"willRetryNextRun": true,
|
|
2317
|
-
"note": "State not advanced - next extraction will retry same time window"
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
```
|
|
2321
|
-
|
|
2322
|
-
### Key Metrics to Track
|
|
2323
|
-
|
|
2324
|
-
```typescript
|
|
2325
|
-
const METRICS = {
|
|
2326
|
-
// Extraction Performance
|
|
2327
|
-
extractionDurationMs: Date.now() - extractionStart,
|
|
2328
|
-
recordCount: records.length,
|
|
2329
|
-
pageCount: extractionResult.stats.totalPages,
|
|
2330
|
-
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
2331
|
-
|
|
2332
|
-
// Transformation Performance
|
|
2333
|
-
transformedCount: transformedRecords.length,
|
|
2334
|
-
failedCount: mappingErrors.length,
|
|
2335
|
-
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
2336
|
-
|
|
2337
|
-
// File Generation
|
|
2338
|
-
fileSizeMB: (csvContent.length / (1024 * 1024)).toFixed(2),
|
|
2339
|
-
|
|
2340
|
-
// Upload Performance
|
|
2341
|
-
uploadDurationMs: uploadEnd - uploadStart,
|
|
2342
|
-
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
2343
|
-
|
|
2344
|
-
// State Management
|
|
2345
|
-
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
2346
|
-
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
2347
|
-
};
|
|
2348
|
-
|
|
2349
|
-
log.info('Extraction metrics', metrics);
|
|
2350
|
-
```
|
|
2351
|
-
|
|
2352
|
-
### Alert Thresholds
|
|
2353
|
-
|
|
2354
|
-
```typescript
|
|
2355
|
-
const ALERT_THRESHOLDS = {
|
|
2356
|
-
// Duration Alerts
|
|
2357
|
-
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
2358
|
-
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
2359
|
-
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
2360
|
-
|
|
2361
|
-
// Error Rate Alerts
|
|
2362
|
-
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
2363
|
-
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
2364
|
-
|
|
2365
|
-
// Volume Alerts
|
|
2366
|
-
MAX_RECORDS_PER_RUN: 100000,
|
|
2367
|
-
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
2368
|
-
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
2369
|
-
|
|
2370
|
-
// State Alerts
|
|
2371
|
-
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
2372
|
-
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
2373
|
-
};
|
|
2374
|
-
|
|
2375
|
-
// Check thresholds
|
|
2376
|
-
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
2377
|
-
log.warn('Extraction duration exceeded threshold', {
|
|
2378
|
-
duration: metrics.extractionDurationMs,
|
|
2379
|
-
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
2380
|
-
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
2381
|
-
});
|
|
2382
|
-
}
|
|
2383
|
-
```
|
|
2384
|
-
|
|
2385
|
-
### Monitoring Dashboard Queries
|
|
2386
|
-
|
|
2387
|
-
**Versori Platform Logs Query:**
|
|
2388
|
-
|
|
2389
|
-
```
|
|
2390
|
-
# Successful extractions
|
|
2391
|
-
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
2392
|
-
|
|
2393
|
-
# Failed extractions
|
|
2394
|
-
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
2395
|
-
|
|
2396
|
-
# Performance issues
|
|
2397
|
-
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
2398
|
-
|
|
2399
|
-
# High error rates
|
|
2400
|
-
errorRate:>5
|
|
2401
|
-
|
|
2402
|
-
# State management issues
|
|
2403
|
-
stateUpdated:false AND success:true
|
|
2404
|
-
```
|
|
2405
|
-
|
|
2406
|
-
### Common Issues and Solutions
|
|
2407
|
-
|
|
2408
|
-
**Issue**: "Extraction timeout after 10 minutes"
|
|
2409
|
-
|
|
2410
|
-
- **Cause**: Too many records in single extraction
|
|
2411
|
-
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
2412
|
-
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
2413
|
-
|
|
2414
|
-
**Issue**: "Mapping errors for 50% of records"
|
|
2415
|
-
|
|
2416
|
-
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
2417
|
-
- **Fix**: Run schema validation, update mapping config paths
|
|
2418
|
-
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
2419
|
-
|
|
2420
|
-
**Issue**: "S3 connection timeout"
|
|
2421
|
-
|
|
2422
|
-
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
2423
|
-
- **Fix**: Check S3 credentials, verify network connectivity
|
|
2424
|
-
- **Prevention**: Implement connection health checks, monitor connection status
|
|
2425
|
-
|
|
2426
|
-
**Issue**: "State not updating after successful extraction"
|
|
2427
|
-
|
|
2428
|
-
- **Cause**: KV write failure or intentional retry logic
|
|
2429
|
-
- **Fix**: Check KV logs, verify state update code executed
|
|
2430
|
-
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
2431
|
-
|
|
2432
|
-
**Issue**: "First run exceeds record limits"
|
|
2433
|
-
|
|
2434
|
-
- **Cause**: No previous timestamp, fetches all historical records
|
|
2435
|
-
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
2436
|
-
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
2437
|
-
|
|
2438
|
-
**Issue**: "Excessive duplicate records in output"
|
|
2439
|
-
|
|
2440
|
-
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
2441
|
-
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
2442
|
-
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
2443
|
-
|
|
2444
|
-
---
|
|
2445
|
-
|
|
2446
|
-
## Troubleshooting Quick Reference
|
|
2447
|
-
|
|
2448
|
-
| Error Message | Likely Cause | Solution |
|
|
2449
|
-
|--------------|--------------|----------|
|
|
2450
|
-
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
2451
|
-
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
2452
|
-
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
2453
|
-
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
2454
|
-
| "S3 authentication failed" | Invalid credentials | Verify S3 credentials in activation variables |
|
|
2455
|
-
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
2456
|
-
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
2457
|
-
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
2458
|
-
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
2459
|
-
| "CSV generation failed" | Format-specific error | Check CSV generation logic, validate output |
|
|
2460
|
-
|
|
2461
|
-
---
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-extract-inventory-positions-to-s3-csv
|
|
3
|
+
canonical_filename: template-extraction-inventory-positions-to-s3-csv.md
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
sdk_version: ^0.1.39
|
|
6
|
+
runtime: versori
|
|
7
|
+
direction: extraction
|
|
8
|
+
source: fluent-graphql
|
|
9
|
+
destination: s3-csv
|
|
10
|
+
entity: inventory-positions
|
|
11
|
+
format: csv
|
|
12
|
+
logging: versori
|
|
13
|
+
status: stable
|
|
14
|
+
features:
|
|
15
|
+
- memory-management
|
|
16
|
+
- enhanced-logging
|
|
17
|
+
- pagination-progress
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Template: Extraction - Inventory Positions to S3 CSV
|
|
21
|
+
|
|
22
|
+
**Template Version:** 2.0.0
|
|
23
|
+
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
24
|
+
**Last Updated:** 2025-01-24
|
|
25
|
+
**Deployment Target:** Versori Platform
|
|
26
|
+
|
|
27
|
+
**🆕 Version 2.0.0 Enhancements:**
|
|
28
|
+
- ✅ **Memory Management** - Clear large result sets after processing batches
|
|
29
|
+
- ✅ **Enhanced Logging** - Pagination progress tracking with emoji indicators (📊, 📥, ✅)
|
|
30
|
+
- ✅ **Pagination Progress** - Real-time page-by-page progress logging with metrics
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📚 STEP 1: Load These Docs (Human Checklist)
|
|
35
|
+
|
|
36
|
+
1. REQUIRED (load all)
|
|
37
|
+
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
38
|
+
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
39
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
40
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
41
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
42
|
+
- [ ] fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
43
|
+
|
|
44
|
+
Copy-paste list (open these):
|
|
45
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
46
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
47
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
48
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
49
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
50
|
+
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 📋 STEP 2: Tell Your AI (Prompt)
|
|
55
|
+
|
|
56
|
+
Copy/paste this prompt into your AI tool after loading the documentation above:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
I need a Versori scheduled extractor that:
|
|
60
|
+
|
|
61
|
+
1) Queries Fluent Commerce GraphQL for Inventory Positions with auto-pagination
|
|
62
|
+
2) Supports incremental runs via KV state (with an overlap buffer)
|
|
63
|
+
3) Transforms results using UniversalMapper per mapping JSON
|
|
64
|
+
4) Generates CSV and uploads to S3
|
|
65
|
+
5) Tracks progress with JobTracker and exposes a job-status webhook
|
|
66
|
+
6) Uses native Versori log (LoggingService removed - use native log)
|
|
67
|
+
|
|
68
|
+
Use the loaded docs to fill in SDK specifics and best practices.
|
|
69
|
+
Keep the structure identical to the template; only adapt where needed.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 📋 Template Overview
|
|
75
|
+
|
|
76
|
+
This connector runs on the Versori platform. Most operational settings (Fluent account/connection, S3 credentials, schedule, page size/limits) are configured via activation variables. Data shape and logic (mapping JSON, CSV structure, GraphQL selection set/filters, validators/resolvers) are adjusted in code as needed. It extracts inventory positions from Fluent Commerce via GraphQL, transforms the data into CSV, and uploads the result to S3.
|
|
77
|
+
|
|
78
|
+
### What This Template Does
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
82
|
+
│ EXTRACTION WORKFLOW │
|
|
83
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
84
|
+
|
|
85
|
+
1. TRIGGER
|
|
86
|
+
├─ Scheduled (Cron): Runs automatically every hour
|
|
87
|
+
├─ Ad hoc (Webhook): Manual trigger with optional date override
|
|
88
|
+
└─ Status Query (Webhook): Check job progress
|
|
89
|
+
|
|
90
|
+
2. EXTRACT (ExtractionOrchestrator)
|
|
91
|
+
├─ Query Fluent GraphQL API for inventory positions
|
|
92
|
+
├─ Auto-pagination (handles large datasets)
|
|
93
|
+
├─ Apply date filters (incremental or manual range)
|
|
94
|
+
└─ Validate each record (optional)
|
|
95
|
+
|
|
96
|
+
3. TRANSFORM (UniversalMapper)
|
|
97
|
+
├─ Map GraphQL fields to CSV schema
|
|
98
|
+
├─ Apply SDK resolvers (trim, uppercase, parseInt, etc.)
|
|
99
|
+
├─ Extract nested data (catalogue)
|
|
100
|
+
└─ Handle transformation errors
|
|
101
|
+
|
|
102
|
+
4. GENERATE CSV (CSVParserService)
|
|
103
|
+
├─ Convert transformed records to CSV
|
|
104
|
+
├─ Include headers
|
|
105
|
+
├─ Handle special characters
|
|
106
|
+
└─ Generate timestamped filename
|
|
107
|
+
|
|
108
|
+
5. UPLOAD (S3DataSource)
|
|
109
|
+
├─ Connect to S3
|
|
110
|
+
├─ Upload CSV file with retry logic
|
|
111
|
+
├─ Set content type and metadata
|
|
112
|
+
└─ Verify upload success
|
|
113
|
+
|
|
114
|
+
6. TRACK JOB (JobTracker)
|
|
115
|
+
├─ Create job with unique ID
|
|
116
|
+
├─ Update status at each step
|
|
117
|
+
├─ Store job result in KV
|
|
118
|
+
└─ Enable status queries via webhook
|
|
119
|
+
|
|
120
|
+
7. UPDATE STATE (VersoriKVAdapter)
|
|
121
|
+
├─ Calculate max updatedOn from records
|
|
122
|
+
├─ Store timestamp for next incremental run
|
|
123
|
+
├─ Apply overlap buffer (prevent missed records)
|
|
124
|
+
└─ Skip update if manual date override
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Key Features
|
|
128
|
+
|
|
129
|
+
- Job tracking with status queries
|
|
130
|
+
- Execution modes: scheduled, ad hoc, status query
|
|
131
|
+
- Uses ExtractionOrchestrator, UniversalMapper, JobTracker, CSVParserService
|
|
132
|
+
- Error handling, retry logic
|
|
133
|
+
- Reusable services suitable for similar use cases
|
|
134
|
+
|
|
135
|
+
Note: JobTracker persists stage/status to Versori KV for visibility, job-status webhooks, and auditing. Recommended for production multi-step flows; can be skipped for trivial single-step utilities.
|
|
136
|
+
|
|
137
|
+
### 📦 Package Information
|
|
138
|
+
|
|
139
|
+
**SDK:** [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
140
|
+
|
|
141
|
+
**Latest Version:** See [npm package page](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk) for current version
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm install @fluentcommerce/fc-connect-sdk
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
**Templates are designed for direct deployment; customize via activation variables.**
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 📦 SDK Imports (Verified - Versori Optimized)
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// ✅ VERIFIED IMPORTS - These match actual SDK exports
|
|
157
|
+
import { Buffer } from 'node:buffer';
|
|
158
|
+
import {
|
|
159
|
+
createClient, // Universal client factory (auto-detects Versori/Node/Deno)
|
|
160
|
+
ExtractionOrchestrator, // High-level extraction with auto-pagination
|
|
161
|
+
JobTracker, // Job status tracking in KV store
|
|
162
|
+
UniversalMapper, // Field mapping with SDK resolvers
|
|
163
|
+
CSVParserService, // CSV generation (NOT CSVBuilder!)
|
|
164
|
+
S3DataSource, // S3 operations with retry logic
|
|
165
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
166
|
+
|
|
167
|
+
// Versori platform imports
|
|
168
|
+
import { schedule, webhook, http, fn } from '@versori/run';
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Note:** All imports are from actual SDK exports - this code compiles and runs as-is.
|
|
172
|
+
|
|
173
|
+
**✅ VERSORI PLATFORM - Use Native Logs:**
|
|
174
|
+
|
|
175
|
+
- Use `log` from context: `const { log } = ctx;`
|
|
176
|
+
- Don't import or use LoggingService for Versori connectors
|
|
177
|
+
- Native Versori logs are simpler and automatically integrated with platform monitoring
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## ⚙️ Activation Variables
|
|
182
|
+
|
|
183
|
+
**Configuration is driven by activation variables - modify these instead of code:**
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"catalogueRef": "DEFAULT_CATALOGUE",
|
|
188
|
+
"s3BucketName": "inventory-analytics-exports",
|
|
189
|
+
"awsAccessKeyId": "AKIAXXXXXXXXXXXX",
|
|
190
|
+
"awsSecretAccessKey": "********",
|
|
191
|
+
"awsRegion": "us-east-1",
|
|
192
|
+
"s3Prefix": "inventory-positions/daily/",
|
|
193
|
+
"fileNamePrefix": "inventorypositions",
|
|
194
|
+
"pageSize": 200,
|
|
195
|
+
"maxRecords": 100000,
|
|
196
|
+
"overlapBufferSeconds": 60
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Note: Webhook security is enforced via Versori connections. Configure auth on the connection; reference it in `webhook({ connection: '...' })`.
|
|
201
|
+
|
|
202
|
+
### Variable Explanations
|
|
203
|
+
|
|
204
|
+
| Variable | Purpose | Default | Customization Hints |
|
|
205
|
+
| ---------------------- | ----------------------------- | ---------------------------- | -------------------------------- |
|
|
206
|
+
| `catalogueRef` | Filter by inventory catalogue | `DEFAULT_CATALOGUE` | Change to filter by catalogue |
|
|
207
|
+
| `s3BucketName` | Target S3 bucket | - | Required - your analytics bucket |
|
|
208
|
+
| `awsAccessKeyId` | AWS access key | - | Required - AWS credential |
|
|
209
|
+
| `awsSecretAccessKey` | AWS secret key | - | Required - AWS credential |
|
|
210
|
+
| `awsRegion` | AWS region | `us-east-1` | Match bucket region |
|
|
211
|
+
| `s3Prefix` | S3 key prefix | `inventory-positions/daily/` | Customize folder structure |
|
|
212
|
+
| `fileNamePrefix` | CSV filename prefix | `inventorypositions` | Customize naming convention |
|
|
213
|
+
| `pageSize` | Records per GraphQL page | `200` | Increase for fewer API calls |
|
|
214
|
+
| `maxRecords` | Total extraction limit | `100000` | Safety limit - adjust for volume |
|
|
215
|
+
| `overlapBufferSeconds` | Incremental safety window | `60` | Prevents missed records |
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### 🔄 State Management & Incremental Sync
|
|
220
|
+
|
|
221
|
+
**How incremental sync works:**
|
|
222
|
+
|
|
223
|
+
1. **First Run:** Uses `DEFAULT_FALLBACK` date (2024-01-01T00:00:00Z)
|
|
224
|
+
2. **Subsequent Runs:** Uses `lastInventoryPositionSync` timestamp from KV store
|
|
225
|
+
3. **Overlap Buffer:** Subtracts 60 seconds to catch late-arriving records
|
|
226
|
+
4. **State Update:** After successful upload, stores max `updatedOn` for next run
|
|
227
|
+
|
|
228
|
+
**Incremental vs Manual Modes:**
|
|
229
|
+
|
|
230
|
+
| Mode | When to Use | State Update | Payload Example |
|
|
231
|
+
| --------------------------- | -------------------- | ------------ | -------------------------------------------------------------- |
|
|
232
|
+
| **Incremental** | Daily scheduled sync | ✅ Yes | `{}` (empty - uses last sync) |
|
|
233
|
+
| **Manual Range** | Historical backfill | ❌ No | `{ "fromDate": "2024-01-01T00:00:00Z", "updateState": false }` |
|
|
234
|
+
| **Manual Range with State** | One-time catch-up | ✅ Yes | `{ "fromDate": "2024-01-01T00:00:00Z", "updateState": true }` |
|
|
235
|
+
|
|
236
|
+
**Why overlap buffer?**
|
|
237
|
+
|
|
238
|
+
Records updated near the sync time might not appear in the query due to:
|
|
239
|
+
|
|
240
|
+
- Clock drift between systems
|
|
241
|
+
- Transaction timing in the database
|
|
242
|
+
- GraphQL query execution timing
|
|
243
|
+
|
|
244
|
+
The 60-second buffer ensures these edge-case records are captured in the next run, preventing data loss.
|
|
245
|
+
|
|
246
|
+
**When to skip state update (`updateState: false`):**
|
|
247
|
+
|
|
248
|
+
- Historical backfills (don't affect ongoing incremental sync)
|
|
249
|
+
- Testing/debugging specific date ranges
|
|
250
|
+
- Reprocessing old data without changing the sync pointer
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### 📁 S3 Path Configuration
|
|
255
|
+
|
|
256
|
+
**Note:** Folder paths and filename patterns are configured separately.
|
|
257
|
+
|
|
258
|
+
- **Folder Path:** Configured via `s3Prefix` activation variable (e.g., `inventory-positions/daily/`)
|
|
259
|
+
- **Filename Pattern:** Configured via `fileNamePrefix` activation variable (e.g., `inventorypositions`)
|
|
260
|
+
|
|
261
|
+
**The workflow generates files like:** `{s3Prefix}{fileNamePrefix}-{timestamp}.csv`
|
|
262
|
+
|
|
263
|
+
**Example:** With `s3Prefix="inventory-positions/daily/"` and `fileNamePrefix="inventorypositions"`, a generated file will look like:
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
inventory-positions/daily/inventorypositions-2025-10-27T18-30-45Z.csv
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Format may vary slightly (colons replaced; includes Z).
|
|
270
|
+
|
|
271
|
+
**Note:** To change the upload folder, modify the `s3Prefix` activation variable (not in code).
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
### Auto-pagination and limits (ExtractionOrchestrator)
|
|
276
|
+
|
|
277
|
+
**What:** ExtractionOrchestrator handles GraphQL Relay cursor-based pagination automatically.
|
|
278
|
+
|
|
279
|
+
**Why:** Prevents manual pagination loop code, handles large datasets efficiently.
|
|
280
|
+
|
|
281
|
+
**How:** You configure `pageSize` and `maxRecords`; the orchestrator injects `$first` and `$after` variables automatically and loops until `pageInfo.hasNextPage === false` or `maxRecords` is reached.
|
|
282
|
+
|
|
283
|
+
**Critical:** Your query MUST include `edges { cursor }` and `pageInfo { hasNextPage }` fields, or pagination will fail.
|
|
284
|
+
|
|
285
|
+
#### Configuration Parameters
|
|
286
|
+
|
|
287
|
+
| Parameter | Purpose | Example | Effect |
|
|
288
|
+
| ---------------- | ------------------------------ | --------------------------------- | -------------------------- |
|
|
289
|
+
| `pageSize` | Records per GraphQL request | `200` | Controls `first` variable |
|
|
290
|
+
| `maxRecords` | Total extraction limit | `100000` | Hard stop across all pages |
|
|
291
|
+
| `resultPath` | Where records live in response | `"inventoryPositions.edges.node"` | Flattening path |
|
|
292
|
+
| `validateItem()` | Optional record filter | `(item) => !!item.ref` | Skips invalid records |
|
|
293
|
+
|
|
294
|
+
#### GraphQL Query Requirements
|
|
295
|
+
|
|
296
|
+
**Your query MUST include these pagination fields:**
|
|
297
|
+
|
|
298
|
+
```graphql
|
|
299
|
+
query GetInventoryPositions(
|
|
300
|
+
$catalogues: [InventoryCatalogueKey]
|
|
301
|
+
$dateRangeFilter: DateRange
|
|
302
|
+
$first: Int! # ← Orchestrator injects this
|
|
303
|
+
$after: String # ← Orchestrator injects this
|
|
304
|
+
) {
|
|
305
|
+
inventoryPositions(
|
|
306
|
+
catalogues: $catalogues
|
|
307
|
+
updatedOn: $dateRangeFilter
|
|
308
|
+
first: $first # ← Pagination page size
|
|
309
|
+
after: $after # ← Pagination cursor
|
|
310
|
+
) {
|
|
311
|
+
edges {
|
|
312
|
+
# ← REQUIRED: Relay connection structure
|
|
313
|
+
node {
|
|
314
|
+
# ← REQUIRED: Actual records here
|
|
315
|
+
id
|
|
316
|
+
ref
|
|
317
|
+
# ... your fields
|
|
318
|
+
}
|
|
319
|
+
cursor # ← REQUIRED: Orchestrator uses this for next page
|
|
320
|
+
}
|
|
321
|
+
pageInfo {
|
|
322
|
+
# ← REQUIRED: Orchestrator checks this
|
|
323
|
+
hasNextPage # ← REQUIRED: Tells orchestrator to continue
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 📄 Mapping Configuration
|
|
332
|
+
|
|
333
|
+
**File:** `config/inventory-positions.export.csv.json`
|
|
334
|
+
|
|
335
|
+
```json
|
|
336
|
+
{
|
|
337
|
+
"name": "inventory-positions.export.csv",
|
|
338
|
+
"version": "1.0.0",
|
|
339
|
+
"description": "Inventory Positions → CSV Export Mapping",
|
|
340
|
+
"fields": {
|
|
341
|
+
"position_ref": {
|
|
342
|
+
"source": "ref",
|
|
343
|
+
"required": true,
|
|
344
|
+
"resolver": "sdk.trim"
|
|
345
|
+
},
|
|
346
|
+
"product_ref": {
|
|
347
|
+
"source": "productRef",
|
|
348
|
+
"required": true,
|
|
349
|
+
"resolver": "sdk.trim"
|
|
350
|
+
},
|
|
351
|
+
"location_ref": {
|
|
352
|
+
"source": "locationRef",
|
|
353
|
+
"required": false,
|
|
354
|
+
"resolver": "sdk.trim"
|
|
355
|
+
},
|
|
356
|
+
"on_hand": {
|
|
357
|
+
"source": "onHand",
|
|
358
|
+
"required": true,
|
|
359
|
+
"resolver": "sdk.parseInt"
|
|
360
|
+
},
|
|
361
|
+
"position_type": {
|
|
362
|
+
"source": "type",
|
|
363
|
+
"required": true,
|
|
364
|
+
"resolver": "sdk.uppercase"
|
|
365
|
+
},
|
|
366
|
+
"status": {
|
|
367
|
+
"source": "status",
|
|
368
|
+
"required": false,
|
|
369
|
+
"resolver": "sdk.uppercase"
|
|
370
|
+
},
|
|
371
|
+
"catalogue_ref": {
|
|
372
|
+
"source": "catalogue.ref",
|
|
373
|
+
"required": false,
|
|
374
|
+
"resolver": "sdk.trim"
|
|
375
|
+
},
|
|
376
|
+
"catalogue_name": {
|
|
377
|
+
"source": "catalogue.name",
|
|
378
|
+
"required": false,
|
|
379
|
+
"resolver": "sdk.trim"
|
|
380
|
+
},
|
|
381
|
+
"created_on": {
|
|
382
|
+
"source": "createdOn",
|
|
383
|
+
"required": true,
|
|
384
|
+
"resolver": "sdk.toString"
|
|
385
|
+
},
|
|
386
|
+
"updated_on": {
|
|
387
|
+
"source": "updatedOn",
|
|
388
|
+
"required": true,
|
|
389
|
+
"resolver": "sdk.toString"
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**AI Customization Hints:**
|
|
396
|
+
|
|
397
|
+
- Add fields: Copy existing field config, change `source` path
|
|
398
|
+
- Remove fields: Delete field from config
|
|
399
|
+
- Change resolvers: Replace `sdk.trim` with `sdk.uppercase`, etc.
|
|
400
|
+
- Nested fields: Use dot notation like `catalogue.ref`
|
|
401
|
+
|
|
402
|
+
Note: Customize mapping by editing the JSON above; prefer built-in resolvers. See SDK Universal Mapping guide for advanced usage.
|
|
403
|
+
|
|
404
|
+
## 🔧 Complete Production Code
|
|
405
|
+
|
|
406
|
+
### File Structure
|
|
407
|
+
|
|
408
|
+
**Production-ready modular structure:**
|
|
409
|
+
|
|
410
|
+
```
|
|
411
|
+
inventory-positions-to-s3-csv/
|
|
412
|
+
├── src/
|
|
413
|
+
│ ├── index.ts # Register workflows
|
|
414
|
+
│ ├── workflows/
|
|
415
|
+
│ │ └── inventory-positions-extraction.ts # ALL 3 workflows (scheduled + 2 webhooks)
|
|
416
|
+
│ ├── services/
|
|
417
|
+
│ │ └── extraction-orchestration.ts # Main orchestration logic
|
|
418
|
+
│ └── utils/
|
|
419
|
+
│ └── job-id-generator.ts # Job ID utility
|
|
420
|
+
├── config/
|
|
421
|
+
│ └── inventory-positions.export.csv.json # Mapping configuration
|
|
422
|
+
├── package.json
|
|
423
|
+
└── tsconfig.json
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
### 1. Entry Point (src/index.ts)
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
/**
|
|
432
|
+
* Entry Point - Export all workflows for Versori platform
|
|
433
|
+
*
|
|
434
|
+
* This file exports all workflows to be registered with Versori.
|
|
435
|
+
* Each workflow is defined in its own file for better organization.
|
|
436
|
+
*
|
|
437
|
+
* DELEGATION PATTERN:
|
|
438
|
+
* - index.ts exports workflows (no business logic)
|
|
439
|
+
* - Workflows orchestrate execution (minimal logic)
|
|
440
|
+
* - Services contain all business logic (maximum reuse)
|
|
441
|
+
*
|
|
442
|
+
* AI CUSTOMIZATION:
|
|
443
|
+
* - Add new workflows by importing and exporting them
|
|
444
|
+
* - Remove workflows by commenting out exports
|
|
445
|
+
* - Change workflow names in import statements
|
|
446
|
+
*/
|
|
447
|
+
|
|
448
|
+
// Scheduled workflows
|
|
449
|
+
export { scheduledInventoryPositionsExtraction } from './workflows/scheduled/inventory-positions-extraction';
|
|
450
|
+
|
|
451
|
+
// Webhook workflows
|
|
452
|
+
export { adhocInventoryPositionsExtraction } from './workflows/webhook/adhoc-inventory-positions-extraction';
|
|
453
|
+
export { inventoryPositionsJobStatus } from './workflows/webhook/job-status-check';
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
### 2. Workflows (src/workflows/inventory-positions-extraction.ts)
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
/**
|
|
462
|
+
* Workflows - Defines 3 execution patterns for inventory positions extraction
|
|
463
|
+
*
|
|
464
|
+
* WORKFLOW 1: Scheduled (Cron) - Runs automatically every hour
|
|
465
|
+
* WORKFLOW 2: Ad hoc (Webhook) - Manual trigger with optional date override
|
|
466
|
+
* WORKFLOW 3: Job Status (Webhook) - Query job progress
|
|
467
|
+
*
|
|
468
|
+
* AI CUSTOMIZATION HINTS:
|
|
469
|
+
* - Change schedule: Modify cron expression in schedule()
|
|
470
|
+
* - Add filtering: Pass additional params to executeInventoryPositionExtraction()
|
|
471
|
+
* - Change response format: Modify return object structure
|
|
472
|
+
*/
|
|
473
|
+
|
|
474
|
+
import { schedule, webhook, http, fn } from '@versori/run';
|
|
475
|
+
import { executeInventoryPositionExtraction, getJobStatus } from '../services/extraction-orchestration';
|
|
476
|
+
import { generateJobId } from '../utils/job-id-generator';
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* WORKFLOW 1: Scheduled Extraction
|
|
480
|
+
*
|
|
481
|
+
* Purpose: Automated hourly extraction for incremental sync
|
|
482
|
+
* Trigger: Cron schedule (every hour at minute 0)
|
|
483
|
+
* State Update: Always updates lastSync timestamp
|
|
484
|
+
*
|
|
485
|
+
* AI CUSTOMIZATION:
|
|
486
|
+
* - Change schedule: Replace '0 * * * *' with your cron expression
|
|
487
|
+
* Examples:
|
|
488
|
+
* - Every 30 min: '*/30 * * * *'
|
|
489
|
+
* - Daily at 2 AM: '0 2 * * *'
|
|
490
|
+
* - Every 15 min: '*/15 * * * *'
|
|
491
|
+
*/
|
|
492
|
+
export const scheduledInventoryPositionsExtraction = schedule(
|
|
493
|
+
'scheduled-inventory-positions-extraction',
|
|
494
|
+
'0 * * * *', // ← CUSTOMIZE: Cron expression
|
|
495
|
+
http('execute-scheduled-extraction', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
496
|
+
const { log } = ctx;
|
|
497
|
+
const executionStartTime = Date.now();
|
|
498
|
+
|
|
499
|
+
// Generate unique job ID for tracking
|
|
500
|
+
// Format: SCHEDULED_IP_YYYYMMDD_HHMMSS_random
|
|
501
|
+
const jobId = generateJobId('SCHEDULED', 'INVENTORY_POSITIONS');
|
|
502
|
+
|
|
503
|
+
log.info('🚀 [WORKFLOW] Scheduled extraction triggered', { jobId });
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
// Execute main workflow (extraction → transform → upload)
|
|
507
|
+
const result = await executeInventoryPositionExtraction(ctx, {
|
|
508
|
+
jobId,
|
|
509
|
+
triggeredBy: 'schedule',
|
|
510
|
+
updateState: true, // Always update state for scheduled runs
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const executionDuration = Date.now() - executionStartTime;
|
|
514
|
+
|
|
515
|
+
log.info('✅ [WORKFLOW] Scheduled extraction completed', {
|
|
516
|
+
jobId,
|
|
517
|
+
recordCount: result.recordsExtracted,
|
|
518
|
+
fileName: result.fileName,
|
|
519
|
+
duration: executionDuration,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return { ...result, duration: executionDuration };
|
|
523
|
+
|
|
524
|
+
} catch (error: any) {
|
|
525
|
+
const executionDuration = Date.now() - executionStartTime;
|
|
526
|
+
|
|
527
|
+
log.error('❌ [WORKFLOW] Scheduled extraction failed', {
|
|
528
|
+
jobId,
|
|
529
|
+
message: error instanceof Error ? error.message : String(error),
|
|
530
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
531
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
532
|
+
duration: executionDuration,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
success: false,
|
|
537
|
+
jobId,
|
|
538
|
+
error: error.message,
|
|
539
|
+
duration: executionDuration,
|
|
540
|
+
recommendation: getErrorRecommendation(error),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}));
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* WORKFLOW 2: Ad hoc Extraction (Manual Trigger)
|
|
547
|
+
*
|
|
548
|
+
* Purpose: Manual extraction with optional date range override
|
|
549
|
+
* Trigger: Webhook POST to /webhooks/inventory-positions-adhoc
|
|
550
|
+
* State Update: Optional (controlled by request payload)
|
|
551
|
+
*
|
|
552
|
+
* WEBHOOK PAYLOAD EXAMPLES:
|
|
553
|
+
*
|
|
554
|
+
* 1. Incremental (use last sync timestamp):
|
|
555
|
+
* {}
|
|
556
|
+
*
|
|
557
|
+
* 2. Date range (manual override):
|
|
558
|
+
* {
|
|
559
|
+
* "fromDate": "2025-01-01T00:00:00Z",
|
|
560
|
+
* "toDate": "2025-01-31T23:59:59Z",
|
|
561
|
+
* "updateState": false
|
|
562
|
+
* }
|
|
563
|
+
*
|
|
564
|
+
* AI CUSTOMIZATION:
|
|
565
|
+
* - Add request validation
|
|
566
|
+
* - Add authentication check
|
|
567
|
+
* - Add custom filters from payload
|
|
568
|
+
*/
|
|
569
|
+
export const adhocInventoryPositionsExtraction = webhook(
|
|
570
|
+
'inventory-positions-adhoc',
|
|
571
|
+
{ connection: 'inventory-positions-adhoc', response: { mode: 'sync' } }
|
|
572
|
+
)
|
|
573
|
+
.then(
|
|
574
|
+
http('execute-adhoc-extraction', { connection: 'fluent_commerce' }, async (ctx) => {
|
|
575
|
+
const { data, log } = ctx;
|
|
576
|
+
const executionStartTime = Date.now();
|
|
577
|
+
|
|
578
|
+
// Generate unique job ID
|
|
579
|
+
const jobId = generateJobId('ADHOC', 'INVENTORY_POSITIONS');
|
|
580
|
+
|
|
581
|
+
// SECURITY: Authentication is enforced by Versori connection configuration
|
|
582
|
+
// Configure auth on the connection and reference it in webhook({ connection: '...' })
|
|
583
|
+
|
|
584
|
+
// Extract optional date override from webhook payload
|
|
585
|
+
const fromDate = data.fromDate as string | undefined;
|
|
586
|
+
const toDate = data.toDate as string | undefined;
|
|
587
|
+
const updateState = data.updateState === true; // Default false; advance state only if explicitly true
|
|
588
|
+
|
|
589
|
+
log.info('🔔 [WORKFLOW] Ad hoc extraction triggered via webhook', {
|
|
590
|
+
jobId,
|
|
591
|
+
hasDateOverride: !!fromDate,
|
|
592
|
+
fromDate: fromDate || 'not specified',
|
|
593
|
+
toDate: toDate || 'not specified',
|
|
594
|
+
updateState
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
// Execute main workflow with optional overrides
|
|
599
|
+
const result = await executeInventoryPositionExtraction(ctx, {
|
|
600
|
+
jobId,
|
|
601
|
+
triggeredBy: 'webhook',
|
|
602
|
+
fromDate, // Optional: override start date
|
|
603
|
+
toDate, // Optional: override end date
|
|
604
|
+
updateState, // Optional: skip state update for historical queries
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const executionDuration = Date.now() - executionStartTime;
|
|
608
|
+
|
|
609
|
+
log.info('✅ [WORKFLOW] Ad hoc extraction completed', {
|
|
610
|
+
jobId,
|
|
611
|
+
recordCount: result.recordsExtracted,
|
|
612
|
+
fileName: result.fileName,
|
|
613
|
+
isManualOverride: !!fromDate,
|
|
614
|
+
stateUpdated: result.stateUpdated,
|
|
615
|
+
duration: executionDuration,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Return success with job details
|
|
619
|
+
return {
|
|
620
|
+
success: true,
|
|
621
|
+
jobId,
|
|
622
|
+
recordsExtracted: result.recordsExtracted,
|
|
623
|
+
fileName: result.fileName,
|
|
624
|
+
s3Path: result.s3Path,
|
|
625
|
+
statusUrl: `/webhooks/inventory-positions-job-status?jobId=${jobId}`,
|
|
626
|
+
duration: executionDuration,
|
|
627
|
+
dateRange: fromDate ? {
|
|
628
|
+
from: fromDate,
|
|
629
|
+
to: toDate || 'not specified',
|
|
630
|
+
updateState
|
|
631
|
+
} : undefined
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
} catch (error: any) {
|
|
635
|
+
const executionDuration = Date.now() - executionStartTime;
|
|
636
|
+
|
|
637
|
+
log.error('❌ [WORKFLOW] Ad hoc extraction failed', {
|
|
638
|
+
jobId,
|
|
639
|
+
message: error instanceof Error ? error.message : String(error),
|
|
640
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
641
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
642
|
+
duration: executionDuration,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
return {
|
|
646
|
+
success: false,
|
|
647
|
+
jobId,
|
|
648
|
+
error: error.message,
|
|
649
|
+
duration: executionDuration,
|
|
650
|
+
recommendation: getErrorRecommendation(error),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}));
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* WORKFLOW 3: Job Status Query
|
|
657
|
+
*
|
|
658
|
+
* Purpose: Check job progress and status
|
|
659
|
+
* Trigger: Webhook GET/POST to /webhooks/inventory-positions-job-status?jobId=xxx
|
|
660
|
+
* Returns: Current job status, stage, progress
|
|
661
|
+
*
|
|
662
|
+
* QUERY EXAMPLES:
|
|
663
|
+
*
|
|
664
|
+
* 1. HTTP GET:
|
|
665
|
+
* GET /webhooks/inventory-positions-job-status?jobId=ADHOC_IP_20251027_183045_abc123
|
|
666
|
+
*
|
|
667
|
+
* 2. HTTP POST:
|
|
668
|
+
* POST /webhooks/inventory-positions-job-status
|
|
669
|
+
* { "jobId": "ADHOC_IP_20251027_183045_abc123" }
|
|
670
|
+
*/
|
|
671
|
+
export const inventoryPositionsJobStatus = webhook(
|
|
672
|
+
'inventory-positions-job-status',
|
|
673
|
+
{ connection: 'inventory-positions-job-status', response: { mode: 'sync' } }
|
|
674
|
+
)
|
|
675
|
+
.then(
|
|
676
|
+
fn('query-job-status', async (ctx) => {
|
|
677
|
+
const { data, log, openKv } = ctx;
|
|
678
|
+
// SECURITY: Authentication is enforced by Versori connection configuration
|
|
679
|
+
// Configure auth on the connection and reference it in webhook({ connection: '...' })
|
|
680
|
+
|
|
681
|
+
// Get jobId from query param or POST body
|
|
682
|
+
const jobId = data.jobId as string;
|
|
683
|
+
|
|
684
|
+
if (!jobId) {
|
|
685
|
+
log.error('❌ [WORKFLOW] Job ID not provided in request');
|
|
686
|
+
return {
|
|
687
|
+
success: false,
|
|
688
|
+
error: 'Job ID is required. Provide jobId in query param or request body.',
|
|
689
|
+
recommendation: 'Include jobId in your request: { "jobId": "ADHOC_IP_20251027_183045_abc123" }'
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
log.info('🔍 [WORKFLOW] Querying job status', { jobId });
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
// Query job status from KV store
|
|
697
|
+
const status = await getJobStatus(openKv(':project:'), jobId, log);
|
|
698
|
+
|
|
699
|
+
if (!status) {
|
|
700
|
+
log.info('⚠️ [WORKFLOW] Job not found', { jobId });
|
|
701
|
+
return {
|
|
702
|
+
success: false,
|
|
703
|
+
error: 'Job not found',
|
|
704
|
+
jobId,
|
|
705
|
+
recommendation: 'Verify the jobId is correct. Jobs may expire after 7 days.'
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
log.info('✅ [WORKFLOW] Job status retrieved', { jobId, status: status.status });
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
success: true,
|
|
713
|
+
jobId,
|
|
714
|
+
...status
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
} catch (error: any) {
|
|
718
|
+
log.error('❌ [WORKFLOW] Failed to query job status', {
|
|
719
|
+
jobId,
|
|
720
|
+
message: error instanceof Error ? error.message : String(error),
|
|
721
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
722
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
success: false,
|
|
727
|
+
jobId,
|
|
728
|
+
error: error.message,
|
|
729
|
+
recommendation: getErrorRecommendation(error),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
}));
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Helper: Get error-specific recommendations
|
|
736
|
+
*/
|
|
737
|
+
function getErrorRecommendation(error: any): string {
|
|
738
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
739
|
+
|
|
740
|
+
if (message.includes('ECONNREFUSED') || message.includes('ETIMEDOUT')) {
|
|
741
|
+
return 'Network connectivity issue. Check firewall rules and network configuration.';
|
|
742
|
+
}
|
|
743
|
+
if (message.includes('authentication') || message.includes('401')) {
|
|
744
|
+
return 'Authentication failed. Verify OAuth2 credentials in connection configuration.';
|
|
745
|
+
}
|
|
746
|
+
if (message.includes('S3') || message.includes('bucket')) {
|
|
747
|
+
return 'S3 operation failed. Check bucket name, region, and AWS credentials.';
|
|
748
|
+
}
|
|
749
|
+
if (message.includes('GraphQL') || message.includes('query')) {
|
|
750
|
+
return 'GraphQL query failed. Verify query syntax and field availability in schema.';
|
|
751
|
+
}
|
|
752
|
+
if (message.includes('mapping') || message.includes('transform')) {
|
|
753
|
+
return 'Data transformation failed. Check mapping configuration and source data structure.';
|
|
754
|
+
}
|
|
755
|
+
if (message.includes('KV') || message.includes('state')) {
|
|
756
|
+
return 'State management failed. Check Versori KV availability and permissions.';
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return 'Check logs for detailed error information and stack trace.';
|
|
760
|
+
}
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
## 3. Main Orchestration Service (src/services/extraction-orchestration.ts)
|
|
766
|
+
|
|
767
|
+
```typescript
|
|
768
|
+
/**
|
|
769
|
+
* MAIN ORCHESTRATION SERVICE
|
|
770
|
+
*
|
|
771
|
+
* This is the heart of the extraction workflow. It coordinates all steps:
|
|
772
|
+
* 1. Initialize clients and services
|
|
773
|
+
* 2. Determine date range (incremental vs manual)
|
|
774
|
+
* 3. Extract data using ExtractionOrchestrator
|
|
775
|
+
* 4. Transform using UniversalMapper
|
|
776
|
+
* 5. Generate CSV using CSVParserService
|
|
777
|
+
* 6. Upload to S3
|
|
778
|
+
* 7. Track job progress with JobTracker
|
|
779
|
+
* 8. Update state for next run
|
|
780
|
+
*
|
|
781
|
+
* NAMING PATTERN (consistent across all use cases):
|
|
782
|
+
* - Interface: {Entity}ExtractionParams (e.g., InventoryPositionExtractionParams)
|
|
783
|
+
* - Result: {Entity}ExtractionResult (e.g., InventoryPositionExtractionResult)
|
|
784
|
+
* - Main function: execute{Entity}Extraction (e.g., executeInventoryPositionExtraction)
|
|
785
|
+
*
|
|
786
|
+
* AI CUSTOMIZATION HINTS:
|
|
787
|
+
* - Change entity: Replace "InventoryPosition" with "Order", "Product", etc.
|
|
788
|
+
* - Change output: Replace CSVParserService with XMLBuilder
|
|
789
|
+
* - Change destination: Replace S3DataSource with SftpDataSource
|
|
790
|
+
* - Add steps: Insert new service calls between existing steps
|
|
791
|
+
*/
|
|
792
|
+
|
|
793
|
+
import { Buffer } from 'node:buffer';
|
|
794
|
+
import {
|
|
795
|
+
createClient,
|
|
796
|
+
ExtractionOrchestrator,
|
|
797
|
+
JobTracker,
|
|
798
|
+
UniversalMapper,
|
|
799
|
+
CSVParserService,
|
|
800
|
+
S3DataSource,
|
|
801
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
802
|
+
|
|
803
|
+
import mappingConfig from '../../config/inventory-positions.export.csv.json' with { type: 'json' };
|
|
804
|
+
|
|
805
|
+
// ✅ VERSORI PLATFORM: Use native log from context, not LoggingService
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Parameters for extraction workflow
|
|
809
|
+
*
|
|
810
|
+
* NAMING: {Entity}ExtractionParams
|
|
811
|
+
*/
|
|
812
|
+
export interface InventoryPositionExtractionParams {
|
|
813
|
+
jobId: string;
|
|
814
|
+
triggeredBy: 'schedule' | 'webhook';
|
|
815
|
+
fromDate?: string; // Optional: manual date override
|
|
816
|
+
toDate?: string; // Optional: manual date override
|
|
817
|
+
updateState: boolean; // Whether to update lastSync timestamp
|
|
818
|
+
|
|
819
|
+
// AI CUSTOMIZATION: Add filters specific to entity
|
|
820
|
+
positionTypes?: string[]; // e.g., ['DEFAULT', 'SEASONAL']
|
|
821
|
+
catalogueRef?: string; // e.g., 'DEFAULT_CATALOGUE'
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Result from extraction workflow
|
|
826
|
+
*
|
|
827
|
+
* NAMING: {Entity}ExtractionResult
|
|
828
|
+
*/
|
|
829
|
+
export interface InventoryPositionExtractionResult {
|
|
830
|
+
success: boolean;
|
|
831
|
+
jobId: string;
|
|
832
|
+
recordsExtracted: number;
|
|
833
|
+
fileName?: string;
|
|
834
|
+
s3Path?: string;
|
|
835
|
+
error?: string;
|
|
836
|
+
errors?: any[];
|
|
837
|
+
isManualOverride?: boolean;
|
|
838
|
+
stateUpdated?: boolean;
|
|
839
|
+
newTimestamp?: string;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* GraphQL Query for Inventory Positions
|
|
844
|
+
*
|
|
845
|
+
* NAMING: {ENTITY}_EXTRACTION_QUERY (uppercase constant)
|
|
846
|
+
*/
|
|
847
|
+
const INVENTORY_POSITIONS_EXTRACTION_QUERY = `
|
|
848
|
+
query GetInventoryPositions(
|
|
849
|
+
$catalogues: [InventoryCatalogueKey]
|
|
850
|
+
$dateRangeFilter: DateRange
|
|
851
|
+
$productRefs: [String!]
|
|
852
|
+
$types: [String!]
|
|
853
|
+
$first: Int!
|
|
854
|
+
$after: String
|
|
855
|
+
) {
|
|
856
|
+
inventoryPositions(
|
|
857
|
+
catalogues: $catalogues
|
|
858
|
+
updatedOn: $dateRangeFilter
|
|
859
|
+
productRef: $productRefs
|
|
860
|
+
type: $types
|
|
861
|
+
first: $first
|
|
862
|
+
after: $after
|
|
863
|
+
) {
|
|
864
|
+
edges {
|
|
865
|
+
node {
|
|
866
|
+
id
|
|
867
|
+
ref
|
|
868
|
+
productRef
|
|
869
|
+
locationRef
|
|
870
|
+
onHand
|
|
871
|
+
type
|
|
872
|
+
status
|
|
873
|
+
catalogue {
|
|
874
|
+
ref
|
|
875
|
+
name
|
|
876
|
+
}
|
|
877
|
+
createdOn
|
|
878
|
+
updatedOn
|
|
879
|
+
}
|
|
880
|
+
cursor
|
|
881
|
+
}
|
|
882
|
+
pageInfo {
|
|
883
|
+
hasNextPage
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
`;
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Query job status from KV store
|
|
891
|
+
*
|
|
892
|
+
* ✅ VERSORI PLATFORM: Uses native log from context (passed as parameter)
|
|
893
|
+
*/
|
|
894
|
+
export async function getJobStatus(
|
|
895
|
+
kv: any, // ✅ Versori KV (compatible with JobTracker's KVAdapter interface)
|
|
896
|
+
jobId: string,
|
|
897
|
+
log: any // ✅ Native Versori log from context
|
|
898
|
+
): Promise<any | undefined> {
|
|
899
|
+
try {
|
|
900
|
+
const tracker = new JobTracker(kv, log);
|
|
901
|
+
return await tracker.getJob(jobId);
|
|
902
|
+
} catch (error: any) {
|
|
903
|
+
log.error('Failed to get job status', {
|
|
904
|
+
jobId,
|
|
905
|
+
message: error instanceof Error ? error.message : String(error),
|
|
906
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
907
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
908
|
+
});
|
|
909
|
+
return undefined;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* MAIN ORCHESTRATION FUNCTION
|
|
915
|
+
*
|
|
916
|
+
* NAMING: execute{Entity}Extraction (e.g., executeInventoryPositionExtraction)
|
|
917
|
+
*
|
|
918
|
+
* This function implements the complete workflow in steps.
|
|
919
|
+
* Each step is clearly commented for AI understanding.
|
|
920
|
+
*/
|
|
921
|
+
export async function executeInventoryPositionExtraction(
|
|
922
|
+
ctx: any,
|
|
923
|
+
params: InventoryPositionExtractionParams
|
|
924
|
+
): Promise<InventoryPositionExtractionResult> {
|
|
925
|
+
// ✅ VERSORI PLATFORM: Extract native log from context
|
|
926
|
+
const { log, openKv, activation } = ctx;
|
|
927
|
+
const { jobId, triggeredBy, fromDate, toDate, updateState } = params;
|
|
928
|
+
|
|
929
|
+
// Open KV store for state management and job tracking
|
|
930
|
+
// ✅ Pass raw Versori KV directly - it matches KVAdapter interface
|
|
931
|
+
// ✅ Pass native log to JobTracker
|
|
932
|
+
const kv = openKv(':project:');
|
|
933
|
+
const tracker = new JobTracker(kv, log);
|
|
934
|
+
|
|
935
|
+
try {
|
|
936
|
+
// ═══════════════════════════════════════════════════════════
|
|
937
|
+
// STEP 1/8: Initialize Job Tracking
|
|
938
|
+
// ═══════════════════════════════════════════════════════════
|
|
939
|
+
const step1Start = Date.now();
|
|
940
|
+
log.info('📋 [STEP 1/8] Initializing job tracking', { jobId });
|
|
941
|
+
|
|
942
|
+
await tracker.createJob(jobId, {
|
|
943
|
+
triggeredBy,
|
|
944
|
+
hasDateOverride: !!fromDate,
|
|
945
|
+
fromDate,
|
|
946
|
+
toDate,
|
|
947
|
+
updateStateAfterRun: updateState,
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
log.info('✅ [STEP 1/8] Job tracking initialized', {
|
|
951
|
+
jobId,
|
|
952
|
+
duration: Date.now() - step1Start,
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
// ═══════════════════════════════════════════════════════════
|
|
956
|
+
// STEP 2/8: Initialize Fluent Client
|
|
957
|
+
// ═══════════════════════════════════════════════════════════
|
|
958
|
+
const step2Start = Date.now();
|
|
959
|
+
log.info('🔌 [STEP 2/8] Initializing Fluent Commerce client', { jobId });
|
|
960
|
+
|
|
961
|
+
const client = await createClient(ctx);
|
|
962
|
+
|
|
963
|
+
if (!client) {
|
|
964
|
+
throw new Error('Failed to create Fluent Commerce client - check connection configuration');
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
log.info('✅ [STEP 2/8] Client initialized', {
|
|
968
|
+
jobId,
|
|
969
|
+
duration: Date.now() - step2Start,
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// ═══════════════════════════════════════════════════════════
|
|
973
|
+
// STEP 3/8: Determine Date Range
|
|
974
|
+
// ═══════════════════════════════════════════════════════════
|
|
975
|
+
const step3Start = Date.now();
|
|
976
|
+
log.info('📅 [STEP 3/8] Determining date range for extraction', { jobId });
|
|
977
|
+
|
|
978
|
+
// State key for incremental sync tracking
|
|
979
|
+
// NAMING: last{Entity}Sync (e.g., lastInventoryPositionSync)
|
|
980
|
+
const STATE_KEY = 'lastInventoryPositionSync';
|
|
981
|
+
const DEFAULT_FALLBACK = '2024-01-01T00:00:00Z';
|
|
982
|
+
const OVERLAP_BUFFER_SECONDS = parseInt(
|
|
983
|
+
activation.getVariable('overlapBufferSeconds') || '60',
|
|
984
|
+
10
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
let dateRangeFilter: { from?: string; to?: string } | null = null;
|
|
988
|
+
const isManualOverride = !!fromDate;
|
|
989
|
+
|
|
990
|
+
if (isManualOverride) {
|
|
991
|
+
// Manual date override from webhook
|
|
992
|
+
dateRangeFilter = { from: fromDate, to: toDate };
|
|
993
|
+
log.info('Using manual date override', { fromDate, toDate });
|
|
994
|
+
} else {
|
|
995
|
+
// Incremental sync - get last sync timestamp
|
|
996
|
+
const rawLastRunTime = (await kv.get<string>(STATE_KEY)) || DEFAULT_FALLBACK;
|
|
997
|
+
|
|
998
|
+
// Apply overlap buffer (prevents missed records)
|
|
999
|
+
const bufferedLastRunTime = new Date(
|
|
1000
|
+
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_SECONDS * 1000
|
|
1001
|
+
).toISOString();
|
|
1002
|
+
|
|
1003
|
+
const effectiveEndTime = toDate || new Date().toISOString();
|
|
1004
|
+
|
|
1005
|
+
dateRangeFilter = {
|
|
1006
|
+
from: bufferedLastRunTime,
|
|
1007
|
+
to: effectiveEndTime, // End of extraction window
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
log.info('Using incremental sync with overlap buffer', {
|
|
1011
|
+
rawLastRunTime,
|
|
1012
|
+
bufferedLastRunTime,
|
|
1013
|
+
effectiveEndTime,
|
|
1014
|
+
overlapBufferSeconds: OVERLAP_BUFFER_SECONDS,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
log.info('✅ [STEP 3/8] Date range determined', {
|
|
1019
|
+
jobId,
|
|
1020
|
+
fromDate: dateRangeFilter?.from,
|
|
1021
|
+
toDate: dateRangeFilter?.to,
|
|
1022
|
+
isManualOverride,
|
|
1023
|
+
duration: Date.now() - step3Start,
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
// ═══════════════════════════════════════════════════════════
|
|
1027
|
+
// STEP 4/8: Extract Data (ExtractionOrchestrator)
|
|
1028
|
+
// ═══════════════════════════════════════════════════════════
|
|
1029
|
+
const step4Start = Date.now();
|
|
1030
|
+
log.info('📥 [STEP 4/8] Extracting data from Fluent Commerce', { jobId });
|
|
1031
|
+
|
|
1032
|
+
await tracker.updateJob(jobId, {
|
|
1033
|
+
status: 'processing',
|
|
1034
|
+
stage: 'extraction',
|
|
1035
|
+
message: 'Extracting data with auto-pagination',
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// Build catalogues array from config
|
|
1039
|
+
const catalogueRef = params.catalogueRef || activation.getVariable('catalogueRef');
|
|
1040
|
+
const catalogues = catalogueRef ? [{ ref: catalogueRef }] : [];
|
|
1041
|
+
|
|
1042
|
+
// Configure extraction
|
|
1043
|
+
const pageSize = parseInt(activation.getVariable('pageSize') || '200', 10);
|
|
1044
|
+
const maxRecords = parseInt(activation.getVariable('maxRecords') || '100000', 10);
|
|
1045
|
+
|
|
1046
|
+
// Initialize ExtractionOrchestrator
|
|
1047
|
+
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
1048
|
+
|
|
1049
|
+
// ? Enhanced: Extract context for progress logging
|
|
1050
|
+
const dateRangeInfo = {
|
|
1051
|
+
start: dateRangeFilter?.from || 'N/A',
|
|
1052
|
+
end: dateRangeFilter?.to || 'N/A',
|
|
1053
|
+
catalogues: catalogues.map((c: any) => c.ref).join(', ') || 'all',
|
|
1054
|
+
types: params.positionTypes?.join(', ') || 'all'
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// ? Enhanced: Start logging with context
|
|
1058
|
+
log.info(`🔍 [ExtractionOrchestrator] Starting extraction`, {
|
|
1059
|
+
query: 'inventoryPositions',
|
|
1060
|
+
pageSize,
|
|
1061
|
+
maxRecords,
|
|
1062
|
+
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1063
|
+
catalogues: dateRangeInfo.catalogues,
|
|
1064
|
+
positionTypes: dateRangeInfo.types,
|
|
1065
|
+
jobId
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// Execute extraction with auto-pagination
|
|
1069
|
+
const extractionResult = await orchestrator.extract({
|
|
1070
|
+
query: INVENTORY_POSITIONS_EXTRACTION_QUERY,
|
|
1071
|
+
resultPath: 'inventoryPositions.edges.node',
|
|
1072
|
+
variables: {
|
|
1073
|
+
catalogues,
|
|
1074
|
+
dateRangeFilter,
|
|
1075
|
+
types: params.positionTypes,
|
|
1076
|
+
// Note: Don't include 'first' or 'after' here; orchestrator injects them
|
|
1077
|
+
},
|
|
1078
|
+
pageSize,
|
|
1079
|
+
maxRecords,
|
|
1080
|
+
// Optional: validate each record
|
|
1081
|
+
validateItem: (item: any) => {
|
|
1082
|
+
return !!(item.ref && item.productRef);
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
const rawRecords = extractionResult.data;
|
|
1087
|
+
|
|
1088
|
+
log.info('✅ [STEP 4/8] Extraction completed', {
|
|
1089
|
+
jobId,
|
|
1090
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1091
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1092
|
+
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1093
|
+
errors: extractionResult.errors ? extractionResult.errors.length : 0,
|
|
1094
|
+
duration: Date.now() - step4Start,
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// ? Enhanced: Completion logging with summary
|
|
1098
|
+
log.info(`✅ [ExtractionOrchestrator] Extraction completed`, {
|
|
1099
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1100
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1101
|
+
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1102
|
+
failedValidations: extractionResult.stats.failedValidations,
|
|
1103
|
+
truncated: extractionResult.stats.truncated,
|
|
1104
|
+
truncationReason: extractionResult.stats.truncationReason,
|
|
1105
|
+
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1106
|
+
jobId
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
if (extractionResult.errors && extractionResult.errors.length > 0) {
|
|
1110
|
+
log.warn('Non-fatal extraction errors encountered', {
|
|
1111
|
+
jobId,
|
|
1112
|
+
errorCount: extractionResult.errors.length,
|
|
1113
|
+
sampleErrors: extractionResult.errors.slice(0, 3),
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Handle empty result
|
|
1118
|
+
if (rawRecords.length === 0) {
|
|
1119
|
+
log.info('No records to process');
|
|
1120
|
+
|
|
1121
|
+
// Update state even with no records (prevents re-querying empty window)
|
|
1122
|
+
if (updateState) {
|
|
1123
|
+
await kv.set(STATE_KEY, new Date().toISOString());
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
await tracker.markCompleted(jobId, {
|
|
1127
|
+
recordCount: 0,
|
|
1128
|
+
message: 'No records to extract',
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
return {
|
|
1132
|
+
success: true,
|
|
1133
|
+
jobId,
|
|
1134
|
+
recordsExtracted: 0,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ═══════════════════════════════════════════════════════════
|
|
1139
|
+
// STEP 5/8: Transform Data (UniversalMapper)
|
|
1140
|
+
// ═══════════════════════════════════════════════════════════
|
|
1141
|
+
const step5Start = Date.now();
|
|
1142
|
+
log.info('🔄 [STEP 5/8] Transforming data with UniversalMapper', {
|
|
1143
|
+
jobId,
|
|
1144
|
+
recordCount: rawRecords.length,
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
await tracker.updateJob(jobId, {
|
|
1148
|
+
status: 'processing',
|
|
1149
|
+
stage: 'transformation',
|
|
1150
|
+
message: `Transforming ${rawRecords.length} records`,
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
const mapper = new UniversalMapper(mappingConfig);
|
|
1154
|
+
const mappingResult = await mapper.map(rawRecords);
|
|
1155
|
+
|
|
1156
|
+
if (!mappingResult.success) {
|
|
1157
|
+
const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
|
|
1158
|
+
log.error('[STEP 5/8] Mapping failed - terminating job', {
|
|
1159
|
+
jobId,
|
|
1160
|
+
errorCount: mappingErrors.length,
|
|
1161
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
await tracker.markFailed(
|
|
1165
|
+
jobId,
|
|
1166
|
+
new Error(mappingErrors[0] || 'UniversalMapper returned unsuccessful result')
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
return {
|
|
1170
|
+
success: false,
|
|
1171
|
+
jobId,
|
|
1172
|
+
recordsExtracted: 0,
|
|
1173
|
+
error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const transformedRecords = Array.isArray(mappingResult.data) ? mappingResult.data : [];
|
|
1178
|
+
const mappingErrors = mappingResult.errors || [];
|
|
1179
|
+
|
|
1180
|
+
if (mappingErrors.length > 0) {
|
|
1181
|
+
log.warn('[STEP 5/8] Some records failed transformation', {
|
|
1182
|
+
jobId,
|
|
1183
|
+
errorCount: mappingErrors.length,
|
|
1184
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
1189
|
+
log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
1190
|
+
jobId,
|
|
1191
|
+
skippedFields: mappingResult.skippedFields,
|
|
1192
|
+
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (transformedRecords.length === 0) {
|
|
1197
|
+
throw new Error('All records failed transformation');
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
log.info('✅ [STEP 5/8] Transformation complete', {
|
|
1201
|
+
jobId,
|
|
1202
|
+
successful: transformedRecords.length,
|
|
1203
|
+
skippedRecords: rawRecords.length - transformedRecords.length,
|
|
1204
|
+
duration: Date.now() - step5Start,
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// ═══════════════════════════════════════════════════════════
|
|
1208
|
+
// STEP 6/8: Generate CSV (CSVParserService)
|
|
1209
|
+
// ═══════════════════════════════════════════════════════════
|
|
1210
|
+
const step6Start = Date.now();
|
|
1211
|
+
log.info('📄 [STEP 6/8] Generating CSV file', { jobId });
|
|
1212
|
+
|
|
1213
|
+
await tracker.updateJob(jobId, {
|
|
1214
|
+
status: 'processing',
|
|
1215
|
+
stage: 'csv_generation',
|
|
1216
|
+
message: `Generating CSV for ${transformedRecords.length} records`,
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
// Initialize CSVParserService
|
|
1220
|
+
const csvParser = new CSVParserService();
|
|
1221
|
+
|
|
1222
|
+
// Generate CSV content (synchronous, not async)
|
|
1223
|
+
const csvContent = csvParser.stringify(transformedRecords, { headers: true });
|
|
1224
|
+
|
|
1225
|
+
// Generate filename
|
|
1226
|
+
const fileNamePrefix = activation.getVariable('fileNamePrefix') || 'inventorypositions';
|
|
1227
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1228
|
+
const fileName = `${fileNamePrefix}-${timestamp}.csv`;
|
|
1229
|
+
|
|
1230
|
+
log.info('✅ [STEP 6/8] CSV file generated', {
|
|
1231
|
+
fileName,
|
|
1232
|
+
sizeBytes: csvContent.length,
|
|
1233
|
+
recordCount: transformedRecords.length,
|
|
1234
|
+
duration: Date.now() - step6Start,
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
// ═══════════════════════════════════════════════════════════
|
|
1238
|
+
// STEP 7/8: Upload to S3 (S3DataSource)
|
|
1239
|
+
// ═══════════════════════════════════════════════════════════
|
|
1240
|
+
const step7Start = Date.now();
|
|
1241
|
+
log.info('☁️ [STEP 7/8] Uploading to S3', { jobId, fileName });
|
|
1242
|
+
|
|
1243
|
+
await tracker.updateJob(jobId, {
|
|
1244
|
+
status: 'processing',
|
|
1245
|
+
stage: 's3_upload',
|
|
1246
|
+
message: `Uploading ${fileName} to S3`,
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// Get S3 configuration from activation variables
|
|
1250
|
+
const s3Config = {
|
|
1251
|
+
bucket: activation.getVariable('s3BucketName'),
|
|
1252
|
+
region: activation.getVariable('awsRegion') || 'us-east-1',
|
|
1253
|
+
accessKeyId: activation.getVariable('awsAccessKeyId'),
|
|
1254
|
+
secretAccessKey: activation.getVariable('awsSecretAccessKey'),
|
|
1255
|
+
};
|
|
1256
|
+
const s3Prefix = activation.getVariable('s3Prefix') || 'inventory-positions/daily/';
|
|
1257
|
+
|
|
1258
|
+
// Validate S3 config
|
|
1259
|
+
if (!s3Config.bucket || !s3Config.accessKeyId || !s3Config.secretAccessKey) {
|
|
1260
|
+
const missingFields = [];
|
|
1261
|
+
if (!s3Config.bucket) missingFields.push('s3BucketName');
|
|
1262
|
+
if (!s3Config.accessKeyId) missingFields.push('awsAccessKeyId');
|
|
1263
|
+
if (!s3Config.secretAccessKey) missingFields.push('awsSecretAccessKey');
|
|
1264
|
+
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
`S3 configuration incomplete - missing activation variables: ${missingFields.join(', ')}`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Initialize S3 data source
|
|
1271
|
+
// ✅ VERSORI PLATFORM: Pass native log from context
|
|
1272
|
+
const s3 = new S3DataSource(
|
|
1273
|
+
{
|
|
1274
|
+
type: 'S3_CSV',
|
|
1275
|
+
connectionId: 'inventory-positions-s3',
|
|
1276
|
+
name: 'Inventory Positions S3 Upload',
|
|
1277
|
+
s3Config,
|
|
1278
|
+
},
|
|
1279
|
+
log
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
// Validate S3 connection before upload
|
|
1283
|
+
try {
|
|
1284
|
+
await s3.validateConnection();
|
|
1285
|
+
log.info('✅ S3 connection validated', { bucket: s3Config.bucket });
|
|
1286
|
+
} catch (validateError: any) {
|
|
1287
|
+
log.error('❌ S3 connection validation failed', {
|
|
1288
|
+
bucket: s3Config.bucket,
|
|
1289
|
+
region: s3Config.region,
|
|
1290
|
+
error: validateError.message,
|
|
1291
|
+
});
|
|
1292
|
+
throw new Error(
|
|
1293
|
+
`S3 connection validation failed: ${validateError.message}. ` +
|
|
1294
|
+
`Check bucket name, region, and AWS credentials.`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Construct S3 key
|
|
1299
|
+
const s3Key = `${s3Prefix}${fileName}`;
|
|
1300
|
+
|
|
1301
|
+
// Upload with retry logic (built into S3DataSource)
|
|
1302
|
+
await s3.upload(s3Key, Buffer.from(csvContent, 'utf-8'), {
|
|
1303
|
+
contentType: 'text/csv',
|
|
1304
|
+
metadata: {
|
|
1305
|
+
recordCount: String(transformedRecords.length),
|
|
1306
|
+
extractedAt: new Date().toISOString(),
|
|
1307
|
+
jobId,
|
|
1308
|
+
},
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
log.info('✅ [STEP 7/8] S3 upload successful', {
|
|
1312
|
+
fileName,
|
|
1313
|
+
s3Key,
|
|
1314
|
+
bucket: s3Config.bucket,
|
|
1315
|
+
sizeBytes: csvContent.length,
|
|
1316
|
+
duration: Date.now() - step7Start,
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
// ═══════════════════════════════════════════════════════════
|
|
1320
|
+
// STEP 8/8: Update State & Complete Job
|
|
1321
|
+
// ═══════════════════════════════════════════════════════════
|
|
1322
|
+
const step8Start = Date.now();
|
|
1323
|
+
log.info('💾 [STEP 8/8] Updating state and completing job', { jobId });
|
|
1324
|
+
|
|
1325
|
+
// Calculate new timestamp for next incremental run
|
|
1326
|
+
let newTimestamp: string | undefined;
|
|
1327
|
+
|
|
1328
|
+
if (updateState) {
|
|
1329
|
+
// Find max updatedOn from extracted records
|
|
1330
|
+
const maxUpdatedOn = rawRecords.reduce(
|
|
1331
|
+
(max, record) => {
|
|
1332
|
+
const recordTime = new Date(record.updatedOn).getTime();
|
|
1333
|
+
return recordTime > max ? recordTime : max;
|
|
1334
|
+
},
|
|
1335
|
+
new Date(dateRangeFilter?.from || DEFAULT_FALLBACK).getTime()
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1338
|
+
newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
1339
|
+
|
|
1340
|
+
// Store new timestamp (WITHOUT buffer - buffer only applied on read)
|
|
1341
|
+
await kv.set(STATE_KEY, newTimestamp);
|
|
1342
|
+
|
|
1343
|
+
log.info('State updated', {
|
|
1344
|
+
oldTimestamp: dateRangeFilter?.from,
|
|
1345
|
+
newTimestamp,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Mark job as completed
|
|
1350
|
+
await tracker.markCompleted(jobId, {
|
|
1351
|
+
recordCount: transformedRecords.length,
|
|
1352
|
+
fileName,
|
|
1353
|
+
s3Key,
|
|
1354
|
+
errorCount: mappingErrors.length,
|
|
1355
|
+
isManualOverride,
|
|
1356
|
+
stateUpdated: updateState,
|
|
1357
|
+
newTimestamp,
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
log.info('✅ [STEP 8/8] State updated and job completed', {
|
|
1361
|
+
jobId,
|
|
1362
|
+
stateUpdated: updateState,
|
|
1363
|
+
newTimestamp,
|
|
1364
|
+
duration: Date.now() - step8Start,
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
return {
|
|
1368
|
+
success: true,
|
|
1369
|
+
jobId,
|
|
1370
|
+
recordsExtracted: transformedRecords.length,
|
|
1371
|
+
fileName,
|
|
1372
|
+
s3Path: s3Key,
|
|
1373
|
+
isManualOverride,
|
|
1374
|
+
stateUpdated: updateState,
|
|
1375
|
+
newTimestamp,
|
|
1376
|
+
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1377
|
+
};
|
|
1378
|
+
} catch (error: any) {
|
|
1379
|
+
log.error('❌ [ORCHESTRATION] Extraction workflow failed', {
|
|
1380
|
+
jobId,
|
|
1381
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1382
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1383
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
// Mark job as failed
|
|
1387
|
+
await tracker.markFailed(jobId, error);
|
|
1388
|
+
|
|
1389
|
+
return {
|
|
1390
|
+
success: false,
|
|
1391
|
+
jobId,
|
|
1392
|
+
recordsExtracted: 0,
|
|
1393
|
+
error: error.message,
|
|
1394
|
+
recommendation: getErrorRecommendation(error),
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Helper: Get error-specific recommendations
|
|
1401
|
+
* (Same helper function used in workflows - can be shared via utils)
|
|
1402
|
+
*/
|
|
1403
|
+
function getErrorRecommendation(error: any): string {
|
|
1404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1405
|
+
|
|
1406
|
+
if (message.includes('ECONNREFUSED') || message.includes('ETIMEDOUT')) {
|
|
1407
|
+
return 'Network connectivity issue. Check firewall rules and network configuration.';
|
|
1408
|
+
}
|
|
1409
|
+
if (message.includes('authentication') || message.includes('401')) {
|
|
1410
|
+
return 'Authentication failed. Verify OAuth2 credentials in connection configuration.';
|
|
1411
|
+
}
|
|
1412
|
+
if (message.includes('S3') || message.includes('bucket')) {
|
|
1413
|
+
return 'S3 operation failed. Check bucket name, region, and AWS credentials.';
|
|
1414
|
+
}
|
|
1415
|
+
if (message.includes('GraphQL') || message.includes('query')) {
|
|
1416
|
+
return 'GraphQL query failed. Verify query syntax and field availability in schema.';
|
|
1417
|
+
}
|
|
1418
|
+
if (message.includes('mapping') || message.includes('transform')) {
|
|
1419
|
+
return 'Data transformation failed. Check mapping configuration and source data structure.';
|
|
1420
|
+
}
|
|
1421
|
+
if (message.includes('KV') || message.includes('state')) {
|
|
1422
|
+
return 'State management failed. Check Versori KV availability and permissions.';
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return 'Check logs for detailed error information and stack trace.';
|
|
1426
|
+
}
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
## 4. Utility Functions (src/utils/job-id-generator.ts)
|
|
1432
|
+
|
|
1433
|
+
```typescript
|
|
1434
|
+
/**
|
|
1435
|
+
* Job ID Generator
|
|
1436
|
+
*
|
|
1437
|
+
* Generates unique job IDs for tracking extraction workflows
|
|
1438
|
+
*
|
|
1439
|
+
* FORMAT: {TYPE}_{ENTITY}_{YYYYMMDD}_{HHMMSS}_{RANDOM}
|
|
1440
|
+
* Example: SCHEDULED_IP_20251027_183045_a1b2c3
|
|
1441
|
+
*/
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* Generate unique job ID
|
|
1445
|
+
*
|
|
1446
|
+
* @param type - Job trigger type (SCHEDULED, ADHOC, MANUAL)
|
|
1447
|
+
* @param entity - Entity abbreviation (IP=Inventory Positions, VP, ORD, PRD)
|
|
1448
|
+
* @returns Unique job ID string
|
|
1449
|
+
*/
|
|
1450
|
+
export function generateJobId(type: string, entity: string): string {
|
|
1451
|
+
const now = new Date();
|
|
1452
|
+
|
|
1453
|
+
// Format: YYYYMMDD
|
|
1454
|
+
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
1455
|
+
|
|
1456
|
+
// Format: HHMMSS
|
|
1457
|
+
const timeStr = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
1458
|
+
|
|
1459
|
+
// Random suffix (6 chars)
|
|
1460
|
+
const randomStr = Math.random().toString(36).substring(2, 8);
|
|
1461
|
+
|
|
1462
|
+
return `${type}_${entity}_${dateStr}_${timeStr}_${randomStr}`;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* Parse job ID components
|
|
1467
|
+
*/
|
|
1468
|
+
export function parseJobId(jobId: string): {
|
|
1469
|
+
type: string;
|
|
1470
|
+
entity: string;
|
|
1471
|
+
date: string;
|
|
1472
|
+
time: string;
|
|
1473
|
+
random: string;
|
|
1474
|
+
} | null {
|
|
1475
|
+
const parts = jobId.split('_');
|
|
1476
|
+
|
|
1477
|
+
if (parts.length !== 5) {
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
return {
|
|
1482
|
+
type: parts[0],
|
|
1483
|
+
entity: parts[1],
|
|
1484
|
+
date: parts[2],
|
|
1485
|
+
time: parts[3],
|
|
1486
|
+
random: parts[4],
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
---
|
|
1492
|
+
|
|
1493
|
+
## 5. Package Configuration
|
|
1494
|
+
|
|
1495
|
+
### package.json
|
|
1496
|
+
|
|
1497
|
+
```json
|
|
1498
|
+
{
|
|
1499
|
+
"name": "inventory-positions-to-s3-csv",
|
|
1500
|
+
"version": "1.0.0",
|
|
1501
|
+
"description": "Extract inventory positions from Fluent Commerce and export to S3 as CSV",
|
|
1502
|
+
"type": "module",
|
|
1503
|
+
"main": "src/index.ts",
|
|
1504
|
+
"scripts": {
|
|
1505
|
+
"dev": "versori dev",
|
|
1506
|
+
"build": "versori build",
|
|
1507
|
+
"deploy": "versori deploy"
|
|
1508
|
+
},
|
|
1509
|
+
"dependencies": {
|
|
1510
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
1511
|
+
"@versori/run": "latest"
|
|
1512
|
+
},
|
|
1513
|
+
"devDependencies": {
|
|
1514
|
+
"@types/node": "^20.0.0",
|
|
1515
|
+
"typescript": "^5.0.0"
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
### tsconfig.json
|
|
1521
|
+
|
|
1522
|
+
```json
|
|
1523
|
+
{
|
|
1524
|
+
"compilerOptions": {
|
|
1525
|
+
"module": "ES2022",
|
|
1526
|
+
"target": "ES2024",
|
|
1527
|
+
"moduleResolution": "node"
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
```
|
|
1531
|
+
|
|
1532
|
+
---
|
|
1533
|
+
|
|
1534
|
+
## 6. Deployment Instructions
|
|
1535
|
+
|
|
1536
|
+
### Deploy to Versori
|
|
1537
|
+
|
|
1538
|
+
```bash
|
|
1539
|
+
# 1. Install dependencies
|
|
1540
|
+
npm install
|
|
1541
|
+
|
|
1542
|
+
# 2. Test locally (if using Versori CLI)
|
|
1543
|
+
npm run dev
|
|
1544
|
+
|
|
1545
|
+
# 3. Deploy to Versori platform
|
|
1546
|
+
npm run deploy
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
### Configure Activation Variables
|
|
1550
|
+
|
|
1551
|
+
In Versori platform settings, configure:
|
|
1552
|
+
|
|
1553
|
+
```json
|
|
1554
|
+
{
|
|
1555
|
+
"catalogueRef": "DEFAULT_CATALOGUE",
|
|
1556
|
+
"s3BucketName": "inventory-analytics-exports",
|
|
1557
|
+
"awsAccessKeyId": "AKIAXXXXXXXXXXXX",
|
|
1558
|
+
"awsSecretAccessKey": "********",
|
|
1559
|
+
"awsRegion": "us-east-1",
|
|
1560
|
+
"s3Prefix": "inventory-positions/daily/",
|
|
1561
|
+
"fileNamePrefix": "inventorypositions",
|
|
1562
|
+
"pageSize": 200,
|
|
1563
|
+
"maxRecords": 100000,
|
|
1564
|
+
"overlapBufferSeconds": 60
|
|
1565
|
+
}
|
|
1566
|
+
```
|
|
1567
|
+
|
|
1568
|
+
---
|
|
1569
|
+
|
|
1570
|
+
## 7. Testing
|
|
1571
|
+
|
|
1572
|
+
### Test Scheduled Extraction
|
|
1573
|
+
|
|
1574
|
+
The scheduled workflow runs automatically based on cron schedule.
|
|
1575
|
+
|
|
1576
|
+
**Check logs:**
|
|
1577
|
+
|
|
1578
|
+
```
|
|
1579
|
+
[STEP 1/8] Initializing job tracking
|
|
1580
|
+
[STEP 2/8] Initializing Fluent Commerce client
|
|
1581
|
+
[STEP 3/8] Determining date range for extraction
|
|
1582
|
+
[STEP 4/8] Extracting data from Fluent Commerce
|
|
1583
|
+
[STEP 5/8] Transforming data with UniversalMapper
|
|
1584
|
+
[STEP 6/8] Generating CSV file
|
|
1585
|
+
[STEP 7/8] Uploading to S3
|
|
1586
|
+
[STEP 8/8] Updating state and completing job
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
### Test Ad hoc Extraction
|
|
1590
|
+
|
|
1591
|
+
```bash
|
|
1592
|
+
# Incremental (uses last sync timestamp)
|
|
1593
|
+
curl -X POST https://api.versori.com/webhooks/inventory-positions-adhoc \
|
|
1594
|
+
-H "Content-Type: application/json" \
|
|
1595
|
+
-d '{}'
|
|
1596
|
+
|
|
1597
|
+
# Date range override
|
|
1598
|
+
curl -X POST https://api.versori.com/webhooks/inventory-positions-adhoc \
|
|
1599
|
+
-H "Content-Type: application/json" \
|
|
1600
|
+
-d '{
|
|
1601
|
+
"fromDate": "2025-01-01T00:00:00Z",
|
|
1602
|
+
"toDate": "2025-01-31T23:59:59Z",
|
|
1603
|
+
"updateState": false
|
|
1604
|
+
}'
|
|
1605
|
+
```
|
|
1606
|
+
|
|
1607
|
+
### Test Job Status Query
|
|
1608
|
+
|
|
1609
|
+
```bash
|
|
1610
|
+
curl -X POST https://api.versori.com/webhooks/inventory-positions-job-status \
|
|
1611
|
+
-H "Content-Type: application/json" \
|
|
1612
|
+
-d '{
|
|
1613
|
+
"jobId": "ADHOC_IP_20251027_183045_abc123"
|
|
1614
|
+
}'
|
|
1615
|
+
```
|
|
1616
|
+
|
|
1617
|
+
**Response:**
|
|
1618
|
+
|
|
1619
|
+
```json
|
|
1620
|
+
{
|
|
1621
|
+
"success": true,
|
|
1622
|
+
"jobId": "ADHOC_IP_20251027_183045_abc123",
|
|
1623
|
+
"status": "processing",
|
|
1624
|
+
"stage": "transformation",
|
|
1625
|
+
"message": "Transforming 15000 records",
|
|
1626
|
+
"createdAt": "2025-10-27T18:30:45.000Z",
|
|
1627
|
+
"startedAt": "2025-10-27T18:30:46.000Z"
|
|
1628
|
+
}
|
|
1629
|
+
```
|
|
1630
|
+
|
|
1631
|
+
---
|
|
1632
|
+
|
|
1633
|
+
---
|
|
1634
|
+
|
|
1635
|
+
### Pattern 7: State Management & Date Overrides
|
|
1636
|
+
|
|
1637
|
+
**Use Case**: Understand how state management works with scheduled and ad-hoc extractions.
|
|
1638
|
+
|
|
1639
|
+
**How it works**:
|
|
1640
|
+
|
|
1641
|
+
VersoriKV stores the last successful extraction timestamp to enable incremental sync:
|
|
1642
|
+
|
|
1643
|
+
```typescript
|
|
1644
|
+
interface ExtractionState {
|
|
1645
|
+
timestamp: string; // Last run timestamp (WITHOUT overlap buffer)
|
|
1646
|
+
recordCount: number; // Number of records extracted
|
|
1647
|
+
extractedAt: string; // When extraction completed
|
|
1648
|
+
fileName?: string; // Generated filename
|
|
1649
|
+
s3Key?: string; // S3 upload path
|
|
1650
|
+
overlapBufferSeconds?: number; // Buffer configuration
|
|
1651
|
+
}
|
|
1652
|
+
```
|
|
1653
|
+
|
|
1654
|
+
**State Priority Chain** (highest to lowest):
|
|
1655
|
+
|
|
1656
|
+
1. **`fromDate` override** (manual date in webhook payload) - Highest priority
|
|
1657
|
+
2. **Stored state** (`await kv.get(stateKey)`) - Normal incremental mode
|
|
1658
|
+
3. **`fallbackStartDate`** (activation variable) - First run fallback
|
|
1659
|
+
|
|
1660
|
+
**Three Scenarios**:
|
|
1661
|
+
|
|
1662
|
+
#### Scenario 1: Normal Scheduled Runs (Incremental)
|
|
1663
|
+
|
|
1664
|
+
```typescript
|
|
1665
|
+
// Payload: {} (empty - no overrides)
|
|
1666
|
+
|
|
1667
|
+
// Behavior:
|
|
1668
|
+
// 1. Load last timestamp from KV: "2025-01-22T10:00:00Z"
|
|
1669
|
+
// 2. Apply overlap buffer: "2025-01-22T09:59:00Z" (query WITH buffer)
|
|
1670
|
+
// 3. Extract records updated since buffered time
|
|
1671
|
+
// 4. Calculate MAX(updatedOn) from results: "2025-01-22T14:30:00Z"
|
|
1672
|
+
// 5. Save new timestamp WITHOUT buffer: "2025-01-22T14:30:00Z"
|
|
1673
|
+
// 6. Next run starts from "2025-01-22T14:29:00Z" (with buffer)
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
**Test**:
|
|
1677
|
+
|
|
1678
|
+
```bash
|
|
1679
|
+
# Trigger scheduled run (no payload needed)
|
|
1680
|
+
# State advances automatically
|
|
1681
|
+
curl -X POST https://workspace.versori.run/inventory-positions-scheduled
|
|
1682
|
+
```
|
|
1683
|
+
|
|
1684
|
+
#### Scenario 2: Ad-hoc Extraction WITH State Update
|
|
1685
|
+
|
|
1686
|
+
```typescript
|
|
1687
|
+
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": true }
|
|
1688
|
+
|
|
1689
|
+
// Behavior:
|
|
1690
|
+
// 1. Ignore stored state
|
|
1691
|
+
// 2. Use fromDate: "2025-01-01T00:00:00Z" (no buffer applied to manual dates)
|
|
1692
|
+
// 3. Extract all records since 2025-01-01
|
|
1693
|
+
// 4. Calculate MAX(updatedOn): "2025-01-22T14:30:00Z"
|
|
1694
|
+
// 5. Save new timestamp: "2025-01-22T14:30:00Z" (updates state!)
|
|
1695
|
+
// 6. Next scheduled run starts from this new timestamp
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
**Use Case**: One-time catch-up extraction that advances the state pointer.
|
|
1699
|
+
|
|
1700
|
+
**Test**:
|
|
1701
|
+
|
|
1702
|
+
```bash
|
|
1703
|
+
curl -X POST https://workspace.versori.run/inventory-positions-adhoc \
|
|
1704
|
+
-H "Content-Type: application/json" \
|
|
1705
|
+
-d '{
|
|
1706
|
+
"fromDate": "2025-01-01T00:00:00Z",
|
|
1707
|
+
"updateState": true
|
|
1708
|
+
}'
|
|
1709
|
+
```
|
|
1710
|
+
|
|
1711
|
+
#### Scenario 3: Ad-hoc Extraction WITHOUT State Update
|
|
1712
|
+
|
|
1713
|
+
```typescript
|
|
1714
|
+
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": false }
|
|
1715
|
+
|
|
1716
|
+
// Behavior:
|
|
1717
|
+
// 1. Ignore stored state
|
|
1718
|
+
// 2. Use fromDate: "2025-01-01T00:00:00Z"
|
|
1719
|
+
// 3. Extract all records since 2025-01-01
|
|
1720
|
+
// 4. DO NOT update state
|
|
1721
|
+
// 5. Next scheduled run uses previous timestamp (unaffected)
|
|
1722
|
+
```
|
|
1723
|
+
|
|
1724
|
+
**Use Case**: Historical backfill or testing without affecting incremental sync.
|
|
1725
|
+
|
|
1726
|
+
**Test**:
|
|
1727
|
+
|
|
1728
|
+
```bash
|
|
1729
|
+
curl -X POST https://workspace.versori.run/inventory-positions-adhoc \
|
|
1730
|
+
-H "Content-Type: application/json" \
|
|
1731
|
+
-d '{
|
|
1732
|
+
"fromDate": "2025-01-01T00:00:00Z",
|
|
1733
|
+
"toDate": "2025-01-31T23:59:59Z",
|
|
1734
|
+
"updateState": false
|
|
1735
|
+
}'
|
|
1736
|
+
```
|
|
1737
|
+
|
|
1738
|
+
**Why this matters**:
|
|
1739
|
+
|
|
1740
|
+
- **Incremental sync** relies on state continuity
|
|
1741
|
+
- **Manual overrides** allow catch-up without breaking incremental flow
|
|
1742
|
+
- **Overlap buffer** prevents missed records at time boundaries
|
|
1743
|
+
- **State isolation** lets you test/backfill without affecting production sync
|
|
1744
|
+
|
|
1745
|
+
---
|
|
1746
|
+
|
|
1747
|
+
### Pattern 8: Optional GraphQL Query Logging
|
|
1748
|
+
|
|
1749
|
+
**Use Case**: Debug extraction issues by logging the exact GraphQL query sent to Fluent Commerce API.
|
|
1750
|
+
|
|
1751
|
+
**When to use**:
|
|
1752
|
+
|
|
1753
|
+
- ✅ Debugging pagination issues
|
|
1754
|
+
- ✅ Verifying query variables (dates, filters, limits)
|
|
1755
|
+
- ✅ Development and testing
|
|
1756
|
+
- ❌ Production (verbose logs, potential secrets in variables)
|
|
1757
|
+
|
|
1758
|
+
**How to enable**:
|
|
1759
|
+
|
|
1760
|
+
Set `DEBUG_GRAPHQL=true` environment variable in Versori activation settings.
|
|
1761
|
+
|
|
1762
|
+
**Implementation**:
|
|
1763
|
+
|
|
1764
|
+
```typescript
|
|
1765
|
+
// In your extraction workflow
|
|
1766
|
+
const DEBUG_GRAPHQL = activation?.getVariable('DEBUG_GRAPHQL') === 'true';
|
|
1767
|
+
|
|
1768
|
+
if (DEBUG_GRAPHQL) {
|
|
1769
|
+
log.info('GraphQL Query Debug', {
|
|
1770
|
+
query: INVENTORY_POSITIONS_QUERY,
|
|
1771
|
+
variables: {
|
|
1772
|
+
catalogueRef,
|
|
1773
|
+
dateRangeFilter,
|
|
1774
|
+
first: pageSize,
|
|
1775
|
+
after: null, // First page
|
|
1776
|
+
},
|
|
1777
|
+
pagination: {
|
|
1778
|
+
pageSize,
|
|
1779
|
+
maxRecords,
|
|
1780
|
+
currentPage: 1,
|
|
1781
|
+
},
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const extractionResult = await orchestrator.extract({
|
|
1786
|
+
query: INVENTORY_POSITIONS_QUERY,
|
|
1787
|
+
resultPath: 'inventoryPositions.edges.node',
|
|
1788
|
+
variables: {
|
|
1789
|
+
catalogueRef,
|
|
1790
|
+
dateRangeFilter,
|
|
1791
|
+
},
|
|
1792
|
+
pageSize,
|
|
1793
|
+
maxRecords,
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
if (DEBUG_GRAPHQL) {
|
|
1797
|
+
log.info('GraphQL Response Debug', {
|
|
1798
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1799
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1800
|
+
truncated: extractionResult.stats.truncated,
|
|
1801
|
+
truncationReason: extractionResult.stats.truncationReason,
|
|
1802
|
+
firstRecordId: extractionResult.data[0]?.id,
|
|
1803
|
+
lastRecordId: extractionResult.data[extractionResult.data.length - 1]?.id,
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
```
|
|
1807
|
+
|
|
1808
|
+
**What gets logged**:
|
|
1809
|
+
|
|
1810
|
+
```json
|
|
1811
|
+
{
|
|
1812
|
+
"level": "info",
|
|
1813
|
+
"message": "GraphQL Query Debug",
|
|
1814
|
+
"query": "query GetInventoryPositions($catalogueRef: String, $dateRangeFilter: DateRange, ...)",
|
|
1815
|
+
"variables": {
|
|
1816
|
+
"catalogueRef": "DEFAULT_CATALOGUE",
|
|
1817
|
+
"dateRangeFilter": "2025-01-22T09:59:00Z",
|
|
1818
|
+
"first": 200,
|
|
1819
|
+
"after": null
|
|
1820
|
+
},
|
|
1821
|
+
"pagination": {
|
|
1822
|
+
"pageSize": 200,
|
|
1823
|
+
"maxRecords": 100000,
|
|
1824
|
+
"currentPage": 1
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
```
|
|
1828
|
+
|
|
1829
|
+
**Versori Environment Variables**:
|
|
1830
|
+
|
|
1831
|
+
Add to activation settings:
|
|
1832
|
+
|
|
1833
|
+
```json
|
|
1834
|
+
{
|
|
1835
|
+
"DEBUG_GRAPHQL": "true"
|
|
1836
|
+
}
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
**Testing**:
|
|
1840
|
+
|
|
1841
|
+
```bash
|
|
1842
|
+
# Enable debug logging
|
|
1843
|
+
curl -X POST https://workspace.versori.run/inventory-positions-scheduled
|
|
1844
|
+
|
|
1845
|
+
# Check Versori logs for "GraphQL Query Debug" entries
|
|
1846
|
+
# Verify query structure and variables are correct
|
|
1847
|
+
```
|
|
1848
|
+
|
|
1849
|
+
**Sample Debug Output**:
|
|
1850
|
+
|
|
1851
|
+
```
|
|
1852
|
+
[INFO] GraphQL Query Debug
|
|
1853
|
+
query: "query GetInventoryPositions($catalogueRef: String, $dateRangeFilter: DateRange, ...)"
|
|
1854
|
+
variables: { catalogueRef: "DEFAULT_CATALOGUE", dateRangeFilter: "2025-01-22T09:59:00Z", first: 200, after: null }
|
|
1855
|
+
pagination: { pageSize: 200, maxRecords: 100000, currentPage: 1 }
|
|
1856
|
+
|
|
1857
|
+
[INFO] Extraction complete
|
|
1858
|
+
totalRecords: 1500
|
|
1859
|
+
totalPages: 8
|
|
1860
|
+
truncated: false
|
|
1861
|
+
|
|
1862
|
+
[INFO] GraphQL Response Debug
|
|
1863
|
+
totalRecords: 1500
|
|
1864
|
+
totalPages: 8
|
|
1865
|
+
truncated: false
|
|
1866
|
+
truncationReason: undefined
|
|
1867
|
+
firstRecordId: "ip_001"
|
|
1868
|
+
lastRecordId: "ip_1500"
|
|
1869
|
+
```
|
|
1870
|
+
|
|
1871
|
+
**Key Benefits**:
|
|
1872
|
+
|
|
1873
|
+
- Quickly identify pagination configuration issues
|
|
1874
|
+
- Verify date filters are applied correctly
|
|
1875
|
+
- Debug "no records found" scenarios
|
|
1876
|
+
- Validate ExtractionOrchestrator variable injection
|
|
1877
|
+
|
|
1878
|
+
**Production Best Practice**: Disable `DEBUG_GRAPHQL` in production to reduce log volume and avoid logging sensitive data.
|
|
1879
|
+
|
|
1880
|
+
---
|
|
1881
|
+
|
|
1882
|
+
## 8. Troubleshooting
|
|
1883
|
+
|
|
1884
|
+
**Issue**: "No records extracted"
|
|
1885
|
+
|
|
1886
|
+
- Check dateRange (manual override vs incremental)
|
|
1887
|
+
- Check catalogueRef filter
|
|
1888
|
+
|
|
1889
|
+
**Issue**: "S3 upload failed"
|
|
1890
|
+
|
|
1891
|
+
- Job fails; state not advanced
|
|
1892
|
+
- Next run retries same window
|
|
1893
|
+
- Check S3 credentials and bucket permissions
|
|
1894
|
+
|
|
1895
|
+
**Issue**: "GraphQL pagination error"
|
|
1896
|
+
|
|
1897
|
+
- Ensure edges.cursor and pageInfo.hasNextPage are in query
|
|
1898
|
+
|
|
1899
|
+
**Issue**: "Memory pressure"
|
|
1900
|
+
|
|
1901
|
+
- Lower pageSize or maxRecords
|
|
1902
|
+
- Consider file splitting for large extractions
|
|
1903
|
+
|
|
1904
|
+
---
|
|
1905
|
+
|
|
1906
|
+
## 9. Replication Checklist
|
|
1907
|
+
|
|
1908
|
+
**To replicate this template for other entities/formats:**
|
|
1909
|
+
|
|
1910
|
+
1. **File Naming:** Replace `inventory-positions`, `IP`, `InventoryPosition` with your entity name
|
|
1911
|
+
2. **GraphQL Query:** Update query constant and field selection to match your entity schema
|
|
1912
|
+
3. **Mapping Config:** Create new mapping file in `config/` with correct field paths
|
|
1913
|
+
4. **Workflows:** Rename workflow exports to match entity (e.g., `scheduledOrdersExtraction`)
|
|
1914
|
+
5. **Service Function:** Rename main function (e.g., `executeOrderExtraction`)
|
|
1915
|
+
6. **State Key:** Update KV key (e.g., `lastOrderSync`)
|
|
1916
|
+
7. **Output Format:** For XML use `XMLBuilder`, for JSON use `JSON.stringify()`, for CSV use `CSVParserService`
|
|
1917
|
+
8. **Upload Destination:** For SFTP replace `S3DataSource` with `SftpDataSource` (and add `dispose()` in finally block)
|
|
1918
|
+
9. **Job ID Entity Code:** Update entity abbreviation in generateJobId() (e.g., 'ORD' for orders)
|
|
1919
|
+
|
|
1920
|
+
---
|
|
1921
|
+
|
|
1922
|
+
**Pattern**: Enterprise incremental extraction with overlap buffer for inventory positions
|
|
1923
|
+
**Key Learning**: Use `catalogues` (plural, array) input for filtering
|
|
1924
|
+
**Critical**: Apply 60-second overlap buffer to prevent missed records due to clock skew
|
|
1925
|
+
**Buffer Pattern**: Query WITH buffer (`updatedOn >= lastRunTime - 60s`), save WITHOUT buffer (`MAX(updatedOn)`)
|
|
1926
|
+
**SDK Services**: ExtractionOrchestrator, UniversalMapper, CSVParserService, S3DataSource, JobTracker
|
|
1927
|
+
|
|
1928
|
+
---
|
|
1929
|
+
|
|
1930
|
+
### Pattern 8: Backward Pagination (Optional - Advanced)
|
|
1931
|
+
|
|
1932
|
+
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
1933
|
+
|
|
1934
|
+
**When to Use**:
|
|
1935
|
+
|
|
1936
|
+
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
1937
|
+
- ✅ Time-bounded reverse traversal for auditing
|
|
1938
|
+
- ✅ Display newest-first in UI/reports
|
|
1939
|
+
- ❌ **Don't use for standard incremental sync** - use forward pagination (default)
|
|
1940
|
+
|
|
1941
|
+
**GraphQL Query Requirements**:
|
|
1942
|
+
|
|
1943
|
+
Your query must support backward pagination by including `$last` and `$before`:
|
|
1944
|
+
|
|
1945
|
+
```graphql
|
|
1946
|
+
query GetData(
|
|
1947
|
+
$retailerId: ID!
|
|
1948
|
+
$first: Int # For forward pagination
|
|
1949
|
+
$after: String # For forward pagination
|
|
1950
|
+
$last: Int # For backward pagination
|
|
1951
|
+
$before: String # For backward pagination
|
|
1952
|
+
) {
|
|
1953
|
+
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
1954
|
+
edges {
|
|
1955
|
+
cursor # ✅ REQUIRED
|
|
1956
|
+
node {
|
|
1957
|
+
id
|
|
1958
|
+
createdAt
|
|
1959
|
+
# ... other fields
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
pageInfo {
|
|
1963
|
+
hasNextPage # For forward
|
|
1964
|
+
hasPreviousPage # ✅ REQUIRED for backward
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
```
|
|
1969
|
+
|
|
1970
|
+
**Implementation**:
|
|
1971
|
+
|
|
1972
|
+
```typescript
|
|
1973
|
+
// Backward pagination - newest records first
|
|
1974
|
+
const result = await orchestrator.extract({
|
|
1975
|
+
query: YOUR_QUERY,
|
|
1976
|
+
resultPath: 'data.edges.node',
|
|
1977
|
+
variables: {
|
|
1978
|
+
retailerId,
|
|
1979
|
+
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
1980
|
+
// ❌ Don't include last/before - orchestrator injects them
|
|
1981
|
+
},
|
|
1982
|
+
pageSize: 200,
|
|
1983
|
+
direction: 'backward', // ✅ Enable reverse pagination
|
|
1984
|
+
maxRecords: 10000,
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
// Records are returned in reverse chronological order
|
|
1988
|
+
console.log(result.data[0].createdAt); // Newest
|
|
1989
|
+
console.log(result.data[result.data.length - 1].createdAt); // Oldest (within range)
|
|
1990
|
+
```
|
|
1991
|
+
|
|
1992
|
+
**Key Differences from Forward Pagination**:
|
|
1993
|
+
|
|
1994
|
+
| Aspect | Forward (Default) | Backward |
|
|
1995
|
+
| ---------------------- | -------------------------------- | ----------------------- |
|
|
1996
|
+
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
1997
|
+
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
1998
|
+
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
1999
|
+
| **Cursor Source** | Last edge of page | First edge of page |
|
|
2000
|
+
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
2001
|
+
|
|
2002
|
+
**Important Notes**:
|
|
2003
|
+
|
|
2004
|
+
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
2005
|
+
|
|
2006
|
+
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
2007
|
+
|
|
2008
|
+
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
2009
|
+
|
|
2010
|
+
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
2011
|
+
|
|
2012
|
+
**Example: Extract Latest 1000 Orders**
|
|
2013
|
+
|
|
2014
|
+
```typescript
|
|
2015
|
+
const latestOrders = await orchestrator.extract({
|
|
2016
|
+
query: ORDERS_QUERY,
|
|
2017
|
+
resultPath: 'orders.edges.node',
|
|
2018
|
+
variables: {
|
|
2019
|
+
retailerId,
|
|
2020
|
+
statuses: ['BOOKED', 'ALLOCATED'],
|
|
2021
|
+
},
|
|
2022
|
+
direction: 'backward', // Start from newest
|
|
2023
|
+
maxRecords: 1000, // Stop after 1000 records
|
|
2024
|
+
pageSize: 100, // 100 per page = 10 pages
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
// latestOrders.data[0] is the newest order
|
|
2028
|
+
// latestOrders.data[999] is the 1000th newest order
|
|
2029
|
+
```
|
|
2030
|
+
|
|
2031
|
+
**When to Use Forward vs Backward**:
|
|
2032
|
+
|
|
2033
|
+
```typescript
|
|
2034
|
+
// ✅ Forward (default) - For incremental sync
|
|
2035
|
+
const incrementalData = await orchestrator.extract({
|
|
2036
|
+
query: YOUR_QUERY,
|
|
2037
|
+
resultPath: 'data.edges.node',
|
|
2038
|
+
variables: {
|
|
2039
|
+
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
2040
|
+
},
|
|
2041
|
+
// direction defaults to 'forward'
|
|
2042
|
+
// Processes oldest → newest for proper sequencing
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
// ✅ Backward - For "latest N records" use cases
|
|
2046
|
+
const latestData = await orchestrator.extract({
|
|
2047
|
+
query: YOUR_QUERY,
|
|
2048
|
+
resultPath: 'data.edges.node',
|
|
2049
|
+
direction: 'backward',
|
|
2050
|
+
maxRecords: 100, // Just get latest 100
|
|
2051
|
+
// Gets newest → oldest
|
|
2052
|
+
});
|
|
2053
|
+
```
|
|
2054
|
+
|
|
2055
|
+
**Pagination Variables Reference**:
|
|
2056
|
+
|
|
2057
|
+
| Variable | Forward | Backward | Injected By | Notes |
|
|
2058
|
+
| -------- | ----------- | ----------- | ------------ | ------------------------ |
|
|
2059
|
+
| `first` | ✅ Used | ❌ Not used | Orchestrator | From `pageSize` |
|
|
2060
|
+
| `after` | ✅ Used | ❌ Not used | Orchestrator | From cursor (last edge) |
|
|
2061
|
+
| `last` | ❌ Not used | ✅ Used | Orchestrator | From `pageSize` |
|
|
2062
|
+
| `before` | ❌ Not used | ✅ Used | Orchestrator | From cursor (first edge) |
|
|
2063
|
+
|
|
2064
|
+
**Common Mistakes to Avoid**:
|
|
2065
|
+
|
|
2066
|
+
```typescript
|
|
2067
|
+
// ❌ WRONG - Don't pass pagination variables
|
|
2068
|
+
const result = await orchestrator.extract({
|
|
2069
|
+
variables: {
|
|
2070
|
+
last: 200, // ❌ Orchestrator will override this
|
|
2071
|
+
before: cursor, // ❌ Orchestrator manages cursor
|
|
2072
|
+
},
|
|
2073
|
+
direction: 'backward',
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
// ✅ CORRECT - Let orchestrator inject pagination
|
|
2077
|
+
const result = await orchestrator.extract({
|
|
2078
|
+
variables: {
|
|
2079
|
+
retailerId, // ✅ Your business variables only
|
|
2080
|
+
},
|
|
2081
|
+
pageSize: 200, // ✅ Orchestrator uses this for last/before
|
|
2082
|
+
direction: 'backward',
|
|
2083
|
+
});
|
|
2084
|
+
```
|
|
2085
|
+
|
|
2086
|
+
#### Optional: Reverse Pagination
|
|
2087
|
+
|
|
2088
|
+
- Default: forward ($first/$after) + pageInfo.hasNextPage.
|
|
2089
|
+
- Reverse: use $last/$before and pageInfo.hasPreviousPage in the query, and set direction='backward' in the orchestrator call.
|
|
2090
|
+
|
|
2091
|
+
GraphQL:
|
|
2092
|
+
|
|
2093
|
+
```graphql
|
|
2094
|
+
query GetInventoryPositionsBackward(
|
|
2095
|
+
$catalogues: [InventoryCatalogueKey]
|
|
2096
|
+
$dateRangeFilter: DateRange
|
|
2097
|
+
$last: Int!
|
|
2098
|
+
$before: String
|
|
2099
|
+
) {
|
|
2100
|
+
inventoryPositions(
|
|
2101
|
+
catalogues: $catalogues
|
|
2102
|
+
updatedOn: $dateRangeFilter
|
|
2103
|
+
last: $last
|
|
2104
|
+
before: $before
|
|
2105
|
+
) {
|
|
2106
|
+
edges {
|
|
2107
|
+
cursor
|
|
2108
|
+
node {
|
|
2109
|
+
id
|
|
2110
|
+
ref
|
|
2111
|
+
updatedOn
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
pageInfo {
|
|
2115
|
+
hasPreviousPage
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
```
|
|
2120
|
+
|
|
2121
|
+
SDK:
|
|
2122
|
+
|
|
2123
|
+
```typescript
|
|
2124
|
+
await orchestrator.extract({
|
|
2125
|
+
query: INVENTORY_POSITIONS_BACKWARD_QUERY,
|
|
2126
|
+
resultPath: 'inventoryPositions.edges.node',
|
|
2127
|
+
variables: { catalogues, dateRangeFilter },
|
|
2128
|
+
pageSize,
|
|
2129
|
+
direction: 'backward',
|
|
2130
|
+
});
|
|
2131
|
+
```
|
|
2132
|
+
|
|
2133
|
+
---
|
|
2134
|
+
|
|
2135
|
+
## Testing Checklist
|
|
2136
|
+
|
|
2137
|
+
**Before production deployment:**
|
|
2138
|
+
|
|
2139
|
+
### 1. Schema Validation
|
|
2140
|
+
|
|
2141
|
+
- [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
|
|
2142
|
+
- [ ] Run `npx fc-connect validate-schema --mapping ./config/inventory-positions.export.csv.json --schema ./fluent-schema.json`
|
|
2143
|
+
- [ ] Run `npx fc-connect analyze-coverage --mapping ./config/inventory-positions.export.csv.json --schema ./fluent-schema.json`
|
|
2144
|
+
- [ ] Verify all `source` paths in mapping exist in GraphQL schema
|
|
2145
|
+
- [ ] Verify query structure matches schema (fields, types, filters)
|
|
2146
|
+
|
|
2147
|
+
### 2. Extraction Testing
|
|
2148
|
+
|
|
2149
|
+
- [ ] Test with small dataset first (maxRecords=10)
|
|
2150
|
+
- [ ] Verify ExtractionOrchestrator pagination works correctly
|
|
2151
|
+
- [ ] Test with multiple pages of data (verify cursor handling)
|
|
2152
|
+
- [ ] Verify date range filtering (updatedOn filter)
|
|
2153
|
+
- [ ] Test empty result handling (no records in date range)
|
|
2154
|
+
- [ ] Verify extraction stops at maxRecords limit
|
|
2155
|
+
|
|
2156
|
+
### 3. Mapping Testing
|
|
2157
|
+
|
|
2158
|
+
- [ ] Verify required fields are populated
|
|
2159
|
+
- [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
|
|
2160
|
+
- [ ] Test custom resolvers with edge cases (if any)
|
|
2161
|
+
- [ ] Verify nested field extraction
|
|
2162
|
+
- [ ] Test with null/missing fields
|
|
2163
|
+
- [ ] Verify mapping error collection works
|
|
2164
|
+
|
|
2165
|
+
### 4. CSV Generation Testing
|
|
2166
|
+
|
|
2167
|
+
- [ ] Verify CSV structure matches expected format
|
|
2168
|
+
- [ ] Test CSV validation against schema (if applicable)
|
|
2169
|
+
- [ ] Verify header row is present and correct
|
|
2170
|
+
- [ ] Test with large datasets (>1000 records)
|
|
2171
|
+
- [ ] Verify UTF-8 encoding
|
|
2172
|
+
- [ ] Test special character handling (commas, quotes, newlines)
|
|
2173
|
+
|
|
2174
|
+
### 5. S3 Upload Testing
|
|
2175
|
+
|
|
2176
|
+
- [ ] Test S3 connection and authentication
|
|
2177
|
+
- [ ] Verify file upload to correct bucket and path
|
|
2178
|
+
- [ ] Test file naming convention (timestamp format)
|
|
2179
|
+
- [ ] Verify S3 object metadata
|
|
2180
|
+
- [ ] Test upload retry logic (simulate network failure)
|
|
2181
|
+
- [ ] Verify file permissions and ACLs
|
|
2182
|
+
|
|
2183
|
+
### 6. State Management Testing
|
|
2184
|
+
|
|
2185
|
+
- [ ] Verify overlap buffer prevents missed records (60-second default)
|
|
2186
|
+
- [ ] Test state recovery after extraction failure
|
|
2187
|
+
- [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
|
|
2188
|
+
- [ ] Test first run with no previous state (uses fallbackStartDate)
|
|
2189
|
+
- [ ] Verify state update only happens on successful upload
|
|
2190
|
+
- [ ] Test manual date override (doesn't update state)
|
|
2191
|
+
|
|
2192
|
+
### 7. Job Tracking Testing
|
|
2193
|
+
|
|
2194
|
+
- [ ] Test job creation with JobTracker
|
|
2195
|
+
- [ ] Verify job status updates at each stage
|
|
2196
|
+
- [ ] Test job completion with metadata
|
|
2197
|
+
- [ ] Test job failure handling
|
|
2198
|
+
- [ ] Query job status via webhook endpoint
|
|
2199
|
+
- [ ] Verify job status persists in KV store
|
|
2200
|
+
|
|
2201
|
+
### 8. Error Handling Testing
|
|
2202
|
+
|
|
2203
|
+
- [ ] Test with invalid GraphQL query
|
|
2204
|
+
- [ ] Test with mapping errors (invalid field paths)
|
|
2205
|
+
- [ ] Test with S3 connection failures
|
|
2206
|
+
- [ ] Test with authentication failures
|
|
2207
|
+
- [ ] Test with network timeouts
|
|
2208
|
+
- [ ] Verify error logging includes context (jobId, stage, error details)
|
|
2209
|
+
- [ ] Test error threshold logic (if applicable)
|
|
2210
|
+
|
|
2211
|
+
### 9. Staging Environment Testing
|
|
2212
|
+
|
|
2213
|
+
- [ ] Run full extraction in staging environment
|
|
2214
|
+
- [ ] Verify CSV file format with downstream system
|
|
2215
|
+
- [ ] Monitor extraction duration and resource usage
|
|
2216
|
+
- [ ] Test with production-like data volumes
|
|
2217
|
+
- [ ] Verify no performance degradation over time
|
|
2218
|
+
|
|
2219
|
+
### 10. Integration Testing
|
|
2220
|
+
|
|
2221
|
+
- [ ] Test scheduled workflow (cron trigger)
|
|
2222
|
+
- [ ] Test ad hoc webhook trigger
|
|
2223
|
+
- [ ] Test job status query webhook
|
|
2224
|
+
- [ ] Verify activation variables are read correctly
|
|
2225
|
+
- [ ] Test with different extraction modes (incremental, date range)
|
|
2226
|
+
- [ ] End-to-end test: trigger → extract → transform → upload → verify file
|
|
2227
|
+
|
|
2228
|
+
---
|
|
2229
|
+
|
|
2230
|
+
## Recommended Project Structure
|
|
2231
|
+
|
|
2232
|
+
**Organize workflows by trigger type in separate files:**
|
|
2233
|
+
|
|
2234
|
+
```
|
|
2235
|
+
inventory-positions-extraction/
|
|
2236
|
+
├── index.ts # Entry point - exports all workflows
|
|
2237
|
+
└── src/
|
|
2238
|
+
├── workflows/
|
|
2239
|
+
│ ├── scheduled/
|
|
2240
|
+
│ │ └── daily-inventory-positions-extraction.ts # Scheduled: Daily extraction
|
|
2241
|
+
│ │
|
|
2242
|
+
│ └── webhook/
|
|
2243
|
+
│ ├── adhoc-inventory-positions-extraction.ts # Webhook: Manual trigger
|
|
2244
|
+
│ └── job-status-check.ts # Webhook: Status query
|
|
2245
|
+
│
|
|
2246
|
+
├── services/
|
|
2247
|
+
│ └── inventory-positions-extraction.service.ts # Shared orchestration logic (reusable)
|
|
2248
|
+
│
|
|
2249
|
+
└── config/
|
|
2250
|
+
└── inventory-positions.export.csv.json # Mapping configuration
|
|
2251
|
+
```
|
|
2252
|
+
|
|
2253
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
2254
|
+
|
|
2255
|
+
**Trigger Types:**
|
|
2256
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
2257
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
2258
|
+
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
2259
|
+
|
|
2260
|
+
**Execution Steps (chained to triggers):**
|
|
2261
|
+
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
2262
|
+
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
2263
|
+
|
|
2264
|
+
---
|
|
2265
|
+
|
|
2266
|
+
## Monitoring & Alerting
|
|
2267
|
+
|
|
2268
|
+
### Success Response Example
|
|
2269
|
+
|
|
2270
|
+
```json
|
|
2271
|
+
{
|
|
2272
|
+
"success": true,
|
|
2273
|
+
"jobId": "SCHEDULED_IP_20251102_140000_abc123",
|
|
2274
|
+
"recordsExtracted": 1523,
|
|
2275
|
+
"fileName": "inventory-positions-2025-11-02T14-00-00-000Z.csv",
|
|
2276
|
+
"s3Path": "s3://bucket/inventory-positions/inventory-positions-2025-11-02T14-00-00-000Z.csv",
|
|
2277
|
+
"metrics": {
|
|
2278
|
+
"extractionDurationMs": 12543,
|
|
2279
|
+
"totalPages": 8,
|
|
2280
|
+
"pageSize": 200,
|
|
2281
|
+
"mappingErrors": 0,
|
|
2282
|
+
"fileSizeBytes": 524288,
|
|
2283
|
+
"uploadDurationMs": 1234
|
|
2284
|
+
},
|
|
2285
|
+
"timestamps": {
|
|
2286
|
+
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
2287
|
+
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
2288
|
+
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
2289
|
+
},
|
|
2290
|
+
"state": {
|
|
2291
|
+
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
2292
|
+
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
2293
|
+
"stateUpdated": true,
|
|
2294
|
+
"overlapBufferSeconds": 60
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
```
|
|
2298
|
+
|
|
2299
|
+
### Error Response Example
|
|
2300
|
+
|
|
2301
|
+
```json
|
|
2302
|
+
{
|
|
2303
|
+
"success": false,
|
|
2304
|
+
"jobId": "ADHOC_IP_20251102_140500_xyz789",
|
|
2305
|
+
"error": "S3 upload failed: Connection timeout",
|
|
2306
|
+
"errorCategory": "NETWORK",
|
|
2307
|
+
"recordsExtracted": 0,
|
|
2308
|
+
"stage": "s3_upload",
|
|
2309
|
+
"details": {
|
|
2310
|
+
"message": "Failed to upload file after 3 retry attempts",
|
|
2311
|
+
"retryAttempts": 3,
|
|
2312
|
+
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
2313
|
+
},
|
|
2314
|
+
"state": {
|
|
2315
|
+
"stateUpdated": false,
|
|
2316
|
+
"willRetryNextRun": true,
|
|
2317
|
+
"note": "State not advanced - next extraction will retry same time window"
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
```
|
|
2321
|
+
|
|
2322
|
+
### Key Metrics to Track
|
|
2323
|
+
|
|
2324
|
+
```typescript
|
|
2325
|
+
const METRICS = {
|
|
2326
|
+
// Extraction Performance
|
|
2327
|
+
extractionDurationMs: Date.now() - extractionStart,
|
|
2328
|
+
recordCount: records.length,
|
|
2329
|
+
pageCount: extractionResult.stats.totalPages,
|
|
2330
|
+
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
2331
|
+
|
|
2332
|
+
// Transformation Performance
|
|
2333
|
+
transformedCount: transformedRecords.length,
|
|
2334
|
+
failedCount: mappingErrors.length,
|
|
2335
|
+
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
2336
|
+
|
|
2337
|
+
// File Generation
|
|
2338
|
+
fileSizeMB: (csvContent.length / (1024 * 1024)).toFixed(2),
|
|
2339
|
+
|
|
2340
|
+
// Upload Performance
|
|
2341
|
+
uploadDurationMs: uploadEnd - uploadStart,
|
|
2342
|
+
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
2343
|
+
|
|
2344
|
+
// State Management
|
|
2345
|
+
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
2346
|
+
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
2347
|
+
};
|
|
2348
|
+
|
|
2349
|
+
log.info('Extraction metrics', metrics);
|
|
2350
|
+
```
|
|
2351
|
+
|
|
2352
|
+
### Alert Thresholds
|
|
2353
|
+
|
|
2354
|
+
```typescript
|
|
2355
|
+
const ALERT_THRESHOLDS = {
|
|
2356
|
+
// Duration Alerts
|
|
2357
|
+
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
2358
|
+
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
2359
|
+
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
2360
|
+
|
|
2361
|
+
// Error Rate Alerts
|
|
2362
|
+
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
2363
|
+
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
2364
|
+
|
|
2365
|
+
// Volume Alerts
|
|
2366
|
+
MAX_RECORDS_PER_RUN: 100000,
|
|
2367
|
+
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
2368
|
+
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
2369
|
+
|
|
2370
|
+
// State Alerts
|
|
2371
|
+
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
2372
|
+
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
2373
|
+
};
|
|
2374
|
+
|
|
2375
|
+
// Check thresholds
|
|
2376
|
+
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
2377
|
+
log.warn('Extraction duration exceeded threshold', {
|
|
2378
|
+
duration: metrics.extractionDurationMs,
|
|
2379
|
+
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
2380
|
+
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
```
|
|
2384
|
+
|
|
2385
|
+
### Monitoring Dashboard Queries
|
|
2386
|
+
|
|
2387
|
+
**Versori Platform Logs Query:**
|
|
2388
|
+
|
|
2389
|
+
```
|
|
2390
|
+
# Successful extractions
|
|
2391
|
+
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
2392
|
+
|
|
2393
|
+
# Failed extractions
|
|
2394
|
+
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
2395
|
+
|
|
2396
|
+
# Performance issues
|
|
2397
|
+
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
2398
|
+
|
|
2399
|
+
# High error rates
|
|
2400
|
+
errorRate:>5
|
|
2401
|
+
|
|
2402
|
+
# State management issues
|
|
2403
|
+
stateUpdated:false AND success:true
|
|
2404
|
+
```
|
|
2405
|
+
|
|
2406
|
+
### Common Issues and Solutions
|
|
2407
|
+
|
|
2408
|
+
**Issue**: "Extraction timeout after 10 minutes"
|
|
2409
|
+
|
|
2410
|
+
- **Cause**: Too many records in single extraction
|
|
2411
|
+
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
2412
|
+
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
2413
|
+
|
|
2414
|
+
**Issue**: "Mapping errors for 50% of records"
|
|
2415
|
+
|
|
2416
|
+
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
2417
|
+
- **Fix**: Run schema validation, update mapping config paths
|
|
2418
|
+
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
2419
|
+
|
|
2420
|
+
**Issue**: "S3 connection timeout"
|
|
2421
|
+
|
|
2422
|
+
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
2423
|
+
- **Fix**: Check S3 credentials, verify network connectivity
|
|
2424
|
+
- **Prevention**: Implement connection health checks, monitor connection status
|
|
2425
|
+
|
|
2426
|
+
**Issue**: "State not updating after successful extraction"
|
|
2427
|
+
|
|
2428
|
+
- **Cause**: KV write failure or intentional retry logic
|
|
2429
|
+
- **Fix**: Check KV logs, verify state update code executed
|
|
2430
|
+
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
2431
|
+
|
|
2432
|
+
**Issue**: "First run exceeds record limits"
|
|
2433
|
+
|
|
2434
|
+
- **Cause**: No previous timestamp, fetches all historical records
|
|
2435
|
+
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
2436
|
+
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
2437
|
+
|
|
2438
|
+
**Issue**: "Excessive duplicate records in output"
|
|
2439
|
+
|
|
2440
|
+
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
2441
|
+
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
2442
|
+
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
2443
|
+
|
|
2444
|
+
---
|
|
2445
|
+
|
|
2446
|
+
## Troubleshooting Quick Reference
|
|
2447
|
+
|
|
2448
|
+
| Error Message | Likely Cause | Solution |
|
|
2449
|
+
|--------------|--------------|----------|
|
|
2450
|
+
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
2451
|
+
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
2452
|
+
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
2453
|
+
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
2454
|
+
| "S3 authentication failed" | Invalid credentials | Verify S3 credentials in activation variables |
|
|
2455
|
+
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
2456
|
+
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
2457
|
+
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
2458
|
+
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
2459
|
+
| "CSV generation failed" | Format-specific error | Check CSV generation logic, validate output |
|
|
2460
|
+
|
|
2461
|
+
---
|