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