@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,2042 +1,2042 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-extract-virtual-positions-graphql-to-s3-json
|
|
3
|
-
canonical_filename: template-extraction-virtual-positions-to-s3-json.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-json
|
|
10
|
-
entity: virtual-positions
|
|
11
|
-
format: json
|
|
12
|
-
logging: versori
|
|
13
|
-
status: stable
|
|
14
|
-
features:
|
|
15
|
-
- memory-management
|
|
16
|
-
- enhanced-logging
|
|
17
|
-
- pagination-progress
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
# Template: Extraction - Virtual Positions GraphQL to S3 JSON
|
|
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
|
-
Create a Versori scheduled extractor for virtualPositions using ExtractionOrchestrator with JobTracker for job lifecycle management, supports incremental runs with overlap buffer, transforms via UniversalMapper, and uploads JSON to S3 using S3DataSource. Use native Versori logging and KV state. Include three workflows: scheduled incremental, ad hoc manual trigger, and job status query.
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## 📦 SDK Imports (Verified - Versori Optimized)
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
import { Buffer } from 'node:buffer';
|
|
66
|
-
import {
|
|
67
|
-
createClient,
|
|
68
|
-
ExtractionOrchestrator,
|
|
69
|
-
JobTracker,
|
|
70
|
-
UniversalMapper,
|
|
71
|
-
S3DataSource,
|
|
72
|
-
JSONBuilder,
|
|
73
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
74
|
-
|
|
75
|
-
import { schedule, webhook, http } from '@versori/run';
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
# Versori Scheduled: Virtual Positions Extraction to S3 JSON (ATP/Allocation)
|
|
81
|
-
|
|
82
|
-
**FC Connect SDK Use Case Guide**
|
|
83
|
-
|
|
84
|
-
> SDK: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
85
|
-
> Version: Use ^0.1.39 - `npm install @fluentcommerce/fc-connect-sdk@^0.1.39`
|
|
86
|
-
|
|
87
|
-
Context: Scheduled Versori workflow that extracts virtual positions (ATP/allocated inventory) from Fluent Commerce via GraphQL query using **ExtractionOrchestrator with JobTracker**, transforms with `UniversalMapper`, and writes JSON files to S3 for API consumption and real-time order promising.
|
|
88
|
-
|
|
89
|
-
**Pattern**: EXTRACTION (Fluent → S3 JSON)
|
|
90
|
-
**Complexity**: Medium | Runtime: Versori Platform (Scheduled)
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## ⚠️ IMPORTANT: Production-Ready Base Template
|
|
95
|
-
|
|
96
|
-
> **📋 BASE TEMPLATE - Ready for Production (Customize for Your Needs)**
|
|
97
|
-
>
|
|
98
|
-
> This is a **production-ready base template** demonstrating FC Connect SDK best practices for virtual position extraction workflows with JSON output to S3.
|
|
99
|
-
>
|
|
100
|
-
> **✅ INCLUDED FEATURES:**
|
|
101
|
-
>
|
|
102
|
-
> - ✅ Comprehensive error handling with retry logic
|
|
103
|
-
> - ✅ S3 upload with proper error handling
|
|
104
|
-
> - ✅ State management with overlap buffer (prevents missed records)
|
|
105
|
-
> - ✅ Job tracking with lifecycle management
|
|
106
|
-
> - ✅ Security (credential masking in logs)
|
|
107
|
-
> - ✅ UTC time enforcement (prevents timezone bugs)
|
|
108
|
-
> - ✅ Incremental extraction (safe, efficient, production-ready)
|
|
109
|
-
> - ✅ Natural rate limiting via timestamps
|
|
110
|
-
>
|
|
111
|
-
> **📝 BEFORE DEPLOYING:**
|
|
112
|
-
>
|
|
113
|
-
> 1. Review and customize activation variables for your environment
|
|
114
|
-
> 2. Test with sample data in your Versori workspace
|
|
115
|
-
> 3. Adjust safety limits (pageSize, maxRecords) if needed
|
|
116
|
-
> 4. Configure monitoring alerts for extraction failures
|
|
117
|
-
> 5. Verify S3 bucket credentials and paths
|
|
118
|
-
>
|
|
119
|
-
> **This base template follows SDK best practices - tweak specific to your needs.**
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
## What You'll Build
|
|
124
|
-
|
|
125
|
-
- **Three Versori workflows**: Scheduled incremental, ad hoc manual trigger, job status query
|
|
126
|
-
- **ExtractionOrchestrator** for high-level extraction orchestration
|
|
127
|
-
- **JobTracker** for job lifecycle and state management
|
|
128
|
-
- **Incremental extraction** using `updatedOn > lastRunTime` filter with overlap buffer
|
|
129
|
-
- **State management** with VersoriKV to track last successful run
|
|
130
|
-
- GraphQL query with auto-pagination
|
|
131
|
-
- UniversalMapper transformation with nested locationLink data
|
|
132
|
-
- JSON file generation with metadata wrapper
|
|
133
|
-
- S3 upload for API consumption
|
|
134
|
-
- **Pretty print option** for human-readable JSON
|
|
135
|
-
- **Failure recovery** with timestamp tracking
|
|
136
|
-
|
|
137
|
-
## Business Use Case
|
|
138
|
-
|
|
139
|
-
**Real-time ATP API for external systems:**
|
|
140
|
-
|
|
141
|
-
- Extract ATP calculations every 15 minutes
|
|
142
|
-
- Export as JSON for REST API consumption
|
|
143
|
-
- Support webhooks/polling by downstream systems
|
|
144
|
-
- Lightweight incremental updates for order promising
|
|
145
|
-
- Enable real-time inventory allocation decisions
|
|
146
|
-
- Feed to order management and ecommerce platforms
|
|
147
|
-
|
|
148
|
-
## Virtual Positions Explained
|
|
149
|
-
|
|
150
|
-
**VirtualPosition** = ATP (Available To Promise) calculation
|
|
151
|
-
|
|
152
|
-
- Represents virtual/allocated inventory for promising
|
|
153
|
-
- Calculated quantity available for new orders
|
|
154
|
-
- Grouped by location or category
|
|
155
|
-
- Used for: Order promising, allocation tracking, ATP feeds
|
|
156
|
-
|
|
157
|
-
**vs InventoryPosition** = Physical on-hand calculation
|
|
158
|
-
|
|
159
|
-
- Actual stock in warehouse
|
|
160
|
-
- Used for: Stock reporting
|
|
161
|
-
|
|
162
|
-
**vs InventoryQuantity** = Detailed quantity records
|
|
163
|
-
|
|
164
|
-
- Individual quantity records by type
|
|
165
|
-
- Used for: Audit trails
|
|
166
|
-
|
|
167
|
-
## SDK Methods Used
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
import { Buffer } from 'node:buffer';
|
|
171
|
-
import {
|
|
172
|
-
createClient,
|
|
173
|
-
ExtractionOrchestrator,
|
|
174
|
-
JobTracker,
|
|
175
|
-
UniversalMapper,
|
|
176
|
-
S3DataSource,
|
|
177
|
-
JSONBuilder,
|
|
178
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
179
|
-
|
|
180
|
-
await createClient(ctx); // Versori-aware client
|
|
181
|
-
new ExtractionOrchestrator(client, log); // High-level orchestration
|
|
182
|
-
new JobTracker(ctx.openKv(':project:'), log); // Job tracking
|
|
183
|
-
await orchestrator.extractToS3Json({ query, mapping, s3Config, ... }); // Auto-pagination + mapping + upload
|
|
184
|
-
new UniversalMapper(exportMapping); // Field transformation
|
|
185
|
-
const jsonBuilder = new JSONBuilder({ prettyPrint: true, indent: 2 });
|
|
186
|
-
const jsonContent = jsonBuilder.build(dataObject); // JSON generation
|
|
187
|
-
await s3.uploadFile(key, Buffer.from(jsonContent, 'utf8'), options); // S3 upload
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
## Activation Variables
|
|
191
|
-
|
|
192
|
-
```json
|
|
193
|
-
{
|
|
194
|
-
"catalogueRefs": "DEFAULT_VIRTUAL_CATALOGUE",
|
|
195
|
-
"s3BucketName": "atp-api-exports",
|
|
196
|
-
"awsAccessKeyId": "AKIAXXXXXXXXXXXX",
|
|
197
|
-
"awsSecretAccessKey": "********",
|
|
198
|
-
"awsRegion": "us-east-1",
|
|
199
|
-
"s3Prefix": "virtual-positions/api/",
|
|
200
|
-
"pageSize": 200,
|
|
201
|
-
"maxRecords": 100000,
|
|
202
|
-
"fallbackStartDate": "2024-01-01T00:00:00Z",
|
|
203
|
-
"overlapBufferSeconds": "60",
|
|
204
|
-
"positionTypes": "",
|
|
205
|
-
"groupRefs": "",
|
|
206
|
-
"productRefs": "",
|
|
207
|
-
"prettyPrint": "true",
|
|
208
|
-
"DEBUG_GRAPHQL": "false"
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
## Package Configuration
|
|
213
|
-
|
|
214
|
-
Create file: `package.json`
|
|
215
|
-
|
|
216
|
-
```json
|
|
217
|
-
{
|
|
218
|
-
"name": "virtual-positions-extraction-json",
|
|
219
|
-
"version": "1.0.0",
|
|
220
|
-
"type": "module",
|
|
221
|
-
"dependencies": {
|
|
222
|
-
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
223
|
-
"@versori/run": "latest"
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
## Export Mapping Configuration
|
|
229
|
-
|
|
230
|
-
Create file: `./config/virtual-positions.export.json`
|
|
231
|
-
|
|
232
|
-
```json
|
|
233
|
-
{
|
|
234
|
-
"name": "virtual-positions.export",
|
|
235
|
-
"version": "1.0.0",
|
|
236
|
-
"description": "Fluent Virtual Positions → JSON API Export (with nested locationLink)",
|
|
237
|
-
"fields": {
|
|
238
|
-
"positionRef": { "source": "ref", "required": true, "resolver": "sdk.trim" },
|
|
239
|
-
"productRef": { "source": "productRef", "required": true, "resolver": "sdk.trim" },
|
|
240
|
-
"locationRef": { "source": "locationLink.ref", "required": false, "resolver": "sdk.trim" },
|
|
241
|
-
"locationName": { "source": "locationLink.name", "required": false, "resolver": "sdk.trim" },
|
|
242
|
-
"quantity": { "source": "quantity", "required": true, "resolver": "sdk.parseInt" },
|
|
243
|
-
"type": { "source": "type", "required": true, "resolver": "sdk.uppercase" },
|
|
244
|
-
"groupRef": { "source": "groupRef", "required": false, "resolver": "sdk.trim" },
|
|
245
|
-
"status": { "source": "status", "required": false, "resolver": "sdk.uppercase" },
|
|
246
|
-
"catalogueRef": { "source": "catalogue.ref", "required": false, "resolver": "sdk.trim" },
|
|
247
|
-
"catalogueName": { "source": "catalogue.name", "required": false, "resolver": "sdk.trim" },
|
|
248
|
-
"createdOn": { "source": "createdOn", "required": true, "resolver": "sdk.toString" },
|
|
249
|
-
"updatedOn": { "source": "updatedOn", "required": true, "resolver": "sdk.toString" }
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## GraphQL Query
|
|
255
|
-
|
|
256
|
-
**Note**: This query is verified against Fluent Commerce schema introspection.
|
|
257
|
-
|
|
258
|
-
```graphql
|
|
259
|
-
query GetVirtualPositions(
|
|
260
|
-
$catalogues: [VirtualCatalogueKey]
|
|
261
|
-
$dateRangeFilter: DateRange
|
|
262
|
-
$productRefs: [String!]
|
|
263
|
-
$types: [String!]
|
|
264
|
-
$groupRefs: [String]
|
|
265
|
-
$first: Int!
|
|
266
|
-
$after: String
|
|
267
|
-
) {
|
|
268
|
-
virtualPositions(
|
|
269
|
-
catalogues: $catalogues
|
|
270
|
-
updatedOn: $dateRangeFilter
|
|
271
|
-
productRef: $productRefs
|
|
272
|
-
type: $types
|
|
273
|
-
groupRef: $groupRefs
|
|
274
|
-
first: $first
|
|
275
|
-
after: $after
|
|
276
|
-
) {
|
|
277
|
-
edges {
|
|
278
|
-
node {
|
|
279
|
-
id
|
|
280
|
-
ref
|
|
281
|
-
productRef
|
|
282
|
-
quantity
|
|
283
|
-
type
|
|
284
|
-
groupRef
|
|
285
|
-
status
|
|
286
|
-
catalogue {
|
|
287
|
-
ref
|
|
288
|
-
name
|
|
289
|
-
}
|
|
290
|
-
createdOn
|
|
291
|
-
updatedOn
|
|
292
|
-
locationLink {
|
|
293
|
-
ref
|
|
294
|
-
name
|
|
295
|
-
status
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
cursor
|
|
299
|
-
}
|
|
300
|
-
pageInfo {
|
|
301
|
-
hasNextPage
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
---
|
|
308
|
-
|
|
309
|
-
## Versori Workflows Structure
|
|
310
|
-
|
|
311
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
312
|
-
|
|
313
|
-
**Trigger Types:**
|
|
314
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
315
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
316
|
-
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
317
|
-
|
|
318
|
-
**Execution Steps (chained to triggers):**
|
|
319
|
-
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
320
|
-
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
321
|
-
|
|
322
|
-
### Recommended Project Structure
|
|
323
|
-
|
|
324
|
-
```
|
|
325
|
-
virtual-positions-extraction/
|
|
326
|
-
├── index.ts # Entry point - exports all workflows
|
|
327
|
-
└── src/
|
|
328
|
-
├── workflows/
|
|
329
|
-
│ ├── scheduled/
|
|
330
|
-
│ │ └── daily-virtual-positions-extraction.ts # Scheduled: Daily extraction
|
|
331
|
-
│ │
|
|
332
|
-
│ └── webhook/
|
|
333
|
-
│ ├── adhoc-virtual-positions-extraction.ts # Webhook: Manual trigger
|
|
334
|
-
│ └── job-status-check.ts # Webhook: Status query
|
|
335
|
-
│
|
|
336
|
-
├── services/
|
|
337
|
-
│ └── virtual-positions-extraction.service.ts # Shared orchestration logic (reusable)
|
|
338
|
-
│
|
|
339
|
-
└── config/
|
|
340
|
-
└── virtual-positions.export.json # Mapping configuration
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
---
|
|
344
|
-
|
|
345
|
-
```json
|
|
346
|
-
{
|
|
347
|
-
"metadata": {
|
|
348
|
-
"extractedAt": "2025-01-22T14:30:00.000Z",
|
|
349
|
-
"recordCount": 3,
|
|
350
|
-
"incrementalFrom": "2025-01-22T10:00:00.000Z",
|
|
351
|
-
"incrementalTo": "2025-01-22T14:30:00.000Z",
|
|
352
|
-
"jobId": "job_abc123",
|
|
353
|
-
"extractionMode": "incremental"
|
|
354
|
-
},
|
|
355
|
-
"data": [
|
|
356
|
-
{
|
|
357
|
-
"positionRef": "VPOS-001",
|
|
358
|
-
"productRef": "PROD-SKU-001",
|
|
359
|
-
"locationRef": "LOC-001",
|
|
360
|
-
"locationName": "NYC Warehouse",
|
|
361
|
-
"quantity": 75,
|
|
362
|
-
"type": "DEFAULT",
|
|
363
|
-
"groupRef": "GROUP-EAST",
|
|
364
|
-
"status": "ACTIVE",
|
|
365
|
-
"catalogueRef": "DEFAULT_VIRTUAL_CATALOGUE",
|
|
366
|
-
"catalogueName": "Default Virtual",
|
|
367
|
-
"createdOn": "2025-01-15T10:00:00Z",
|
|
368
|
-
"updatedOn": "2025-01-22T08:30:00Z"
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
"positionRef": "VPOS-002",
|
|
372
|
-
"productRef": "PROD-SKU-002",
|
|
373
|
-
"locationRef": "LOC-002",
|
|
374
|
-
"locationName": "LA Warehouse",
|
|
375
|
-
"quantity": 50,
|
|
376
|
-
"type": "DEFAULT",
|
|
377
|
-
"groupRef": "GROUP-WEST",
|
|
378
|
-
"status": "ACTIVE",
|
|
379
|
-
"catalogueRef": "DEFAULT_VIRTUAL_CATALOGUE",
|
|
380
|
-
"catalogueName": "Default Virtual",
|
|
381
|
-
"createdOn": "2025-01-16T11:00:00Z",
|
|
382
|
-
"updatedOn": "2025-01-22T09:15:00Z"
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
"positionRef": "VPOS-003",
|
|
386
|
-
"productRef": "PROD-SKU-003",
|
|
387
|
-
"locationRef": "LOC-001",
|
|
388
|
-
"locationName": "NYC Warehouse",
|
|
389
|
-
"quantity": 100,
|
|
390
|
-
"type": "SEASONAL",
|
|
391
|
-
"groupRef": "GROUP-EAST",
|
|
392
|
-
"status": "ACTIVE",
|
|
393
|
-
"catalogueRef": "SEASONAL_VIRTUAL_CATALOGUE",
|
|
394
|
-
"catalogueName": "Seasonal Virtual",
|
|
395
|
-
"createdOn": "2025-01-17T12:00:00Z",
|
|
396
|
-
"updatedOn": "2025-01-22T10:00:00Z"
|
|
397
|
-
}
|
|
398
|
-
]
|
|
399
|
-
}
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
## Complete Workflow Implementation
|
|
403
|
-
|
|
404
|
-
The following sections demonstrate the three workflows. Each workflow should be in its own file following the modular project structure shown above.
|
|
405
|
-
|
|
406
|
-
### Entry Point: index.ts
|
|
407
|
-
|
|
408
|
-
**File:** `index.ts`
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
/**
|
|
412
|
-
* Entry point - Export all workflows for Versori platform
|
|
413
|
-
*
|
|
414
|
-
* This file exports all workflows to be registered with Versori.
|
|
415
|
-
* Each workflow is defined in its own file for better organization.
|
|
416
|
-
*/
|
|
417
|
-
|
|
418
|
-
// Scheduled workflows
|
|
419
|
-
export { virtualPositionsExtractionJson } from './src/workflows/scheduled/daily-virtual-positions-extraction';
|
|
420
|
-
|
|
421
|
-
// Webhook workflows
|
|
422
|
-
export { virtualPositionsManualExtraction } from './src/workflows/webhook/adhoc-virtual-positions-extraction';
|
|
423
|
-
export { virtualPositionsJobStatus } from './src/workflows/webhook/job-status-check';
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
### Workflow 1: Scheduled Incremental Extraction
|
|
427
|
-
|
|
428
|
-
**File:** `src/workflows/scheduled/daily-virtual-positions-extraction.ts`
|
|
429
|
-
|
|
430
|
-
```typescript
|
|
431
|
-
import { schedule, http } from '@versori/run';
|
|
432
|
-
import { Buffer } from 'node:buffer';
|
|
433
|
-
import {
|
|
434
|
-
createClient,
|
|
435
|
-
ExtractionOrchestrator,
|
|
436
|
-
JobTracker,
|
|
437
|
-
UniversalMapper,
|
|
438
|
-
S3DataSource,
|
|
439
|
-
JSONBuilder,
|
|
440
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
441
|
-
import virtualPositionsExportMapping from './config/virtual-positions.export.json' with { type: 'json' };
|
|
442
|
-
|
|
443
|
-
// GraphQL query
|
|
444
|
-
const VIRTUAL_POSITIONS_QUERY = `
|
|
445
|
-
query GetVirtualPositions(
|
|
446
|
-
$catalogues: [VirtualCatalogueKey]
|
|
447
|
-
$dateRangeFilter: DateRange
|
|
448
|
-
$productRefs: [String!]
|
|
449
|
-
$types: [String!]
|
|
450
|
-
$groupRefs: [String]
|
|
451
|
-
$first: Int!
|
|
452
|
-
$after: String
|
|
453
|
-
) {
|
|
454
|
-
virtualPositions(
|
|
455
|
-
catalogues: $catalogues
|
|
456
|
-
updatedOn: $dateRangeFilter
|
|
457
|
-
productRef: $productRefs
|
|
458
|
-
type: $types
|
|
459
|
-
groupRef: $groupRefs
|
|
460
|
-
first: $first
|
|
461
|
-
after: $after
|
|
462
|
-
) {
|
|
463
|
-
edges {
|
|
464
|
-
node {
|
|
465
|
-
id
|
|
466
|
-
ref
|
|
467
|
-
productRef
|
|
468
|
-
quantity
|
|
469
|
-
type
|
|
470
|
-
groupRef
|
|
471
|
-
status
|
|
472
|
-
catalogue {
|
|
473
|
-
ref
|
|
474
|
-
name
|
|
475
|
-
}
|
|
476
|
-
createdOn
|
|
477
|
-
updatedOn
|
|
478
|
-
locationLink {
|
|
479
|
-
ref
|
|
480
|
-
name
|
|
481
|
-
status
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
cursor
|
|
485
|
-
}
|
|
486
|
-
pageInfo {
|
|
487
|
-
hasNextPage
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
`;
|
|
492
|
-
|
|
493
|
-
export const virtualPositionsExtractionJson = schedule(
|
|
494
|
-
'virtual-positions-extract-json-15min',
|
|
495
|
-
'*/15 * * * *'
|
|
496
|
-
).then(
|
|
497
|
-
http('extract-virtual-positions-json', { connection: 'fluent_commerce' }, async ctx => {
|
|
498
|
-
const { log, openKv, activation } = ctx;
|
|
499
|
-
const executionStartTime = Date.now();
|
|
500
|
-
|
|
501
|
-
log.info('🚀 [EXTRACTION] Starting virtual positions extraction to S3 JSON', {
|
|
502
|
-
timestamp: new Date().toISOString(),
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// STEP 1/8: Parse activation variables
|
|
506
|
-
const catalogueRefsStr =
|
|
507
|
-
activation?.getVariable('catalogueRefs') || 'DEFAULT_VIRTUAL_CATALOGUE';
|
|
508
|
-
const catalogueRefsList = catalogueRefsStr
|
|
509
|
-
.split(',')
|
|
510
|
-
.map(ref => ref.trim())
|
|
511
|
-
.filter(Boolean);
|
|
512
|
-
const catalogues = catalogueRefsList.map(ref => ({ ref }));
|
|
513
|
-
|
|
514
|
-
const pageSize = parseInt(activation?.getVariable('pageSize') || '200', 10);
|
|
515
|
-
const maxRecords = parseInt(activation?.getVariable('maxRecords') || '100000', 10);
|
|
516
|
-
const fallbackStartDate =
|
|
517
|
-
activation?.getVariable('fallbackStartDate') || '2024-01-01T00:00:00Z';
|
|
518
|
-
const prettyPrint = activation?.getVariable('prettyPrint') === 'true';
|
|
519
|
-
const overlapBufferSeconds = parseInt(
|
|
520
|
-
activation?.getVariable('overlapBufferSeconds') || '60',
|
|
521
|
-
10
|
|
522
|
-
);
|
|
523
|
-
|
|
524
|
-
const s3Config = {
|
|
525
|
-
bucket: activation?.getVariable('s3BucketName'),
|
|
526
|
-
region: activation?.getVariable('awsRegion') || 'us-east-1',
|
|
527
|
-
accessKeyId: activation?.getVariable('awsAccessKeyId'),
|
|
528
|
-
secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
|
|
529
|
-
};
|
|
530
|
-
const s3Prefix = activation?.getVariable('s3Prefix') || 'virtual-positions/api/';
|
|
531
|
-
|
|
532
|
-
// Optional filtering
|
|
533
|
-
const positionTypesStr = activation?.getVariable('positionTypes') || '';
|
|
534
|
-
const groupRefsStr = activation?.getVariable('groupRefs') || '';
|
|
535
|
-
const productRefsStr = activation?.getVariable('productRefs') || '';
|
|
536
|
-
|
|
537
|
-
const positionTypes = positionTypesStr
|
|
538
|
-
? positionTypesStr
|
|
539
|
-
.split(',')
|
|
540
|
-
.map(t => t.trim())
|
|
541
|
-
.filter(Boolean)
|
|
542
|
-
: undefined;
|
|
543
|
-
const groupRefs = groupRefsStr
|
|
544
|
-
? groupRefsStr
|
|
545
|
-
.split(',')
|
|
546
|
-
.map(g => g.trim())
|
|
547
|
-
.filter(Boolean)
|
|
548
|
-
: undefined;
|
|
549
|
-
const productRefs = productRefsStr
|
|
550
|
-
? productRefsStr
|
|
551
|
-
.split(',')
|
|
552
|
-
.map(p => p.trim())
|
|
553
|
-
.filter(Boolean)
|
|
554
|
-
: undefined;
|
|
555
|
-
|
|
556
|
-
// Validate required variables
|
|
557
|
-
const missing: string[] = [];
|
|
558
|
-
if (catalogues.length === 0) missing.push('catalogueRefs');
|
|
559
|
-
if (!s3Config.bucket) missing.push('s3BucketName');
|
|
560
|
-
if (!s3Config.accessKeyId) missing.push('awsAccessKeyId');
|
|
561
|
-
if (!s3Config.secretAccessKey) missing.push('awsSecretAccessKey');
|
|
562
|
-
if (missing.length)
|
|
563
|
-
return { success: false, error: `Missing required variables: ${missing.join(', ')}` };
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
// STEP 2/8: Initialize SDK services
|
|
567
|
-
log.info('📦 [INITIALIZATION] Creating Fluent Commerce client');
|
|
568
|
-
const client = await createClient(ctx);
|
|
569
|
-
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
570
|
-
const jobTracker = new JobTracker(openKv(':project:'), log);
|
|
571
|
-
log.info('✅ [INITIALIZATION] SDK services initialized successfully');
|
|
572
|
-
|
|
573
|
-
// STEP 3/8: Determine incremental date range with overlap buffer
|
|
574
|
-
log.info('📅 [STATE] Loading extraction state');
|
|
575
|
-
const stateKey = ['extraction', 'virtual-positions-json', 'lastRunTime'];
|
|
576
|
-
const kv = openKv(':project:');
|
|
577
|
-
const lastRunState = await kv.get(stateKey);
|
|
578
|
-
const rawLastRunTime = lastRunState?.value?.timestamp || fallbackStartDate;
|
|
579
|
-
|
|
580
|
-
// Apply overlap buffer for query (safety window)
|
|
581
|
-
const OVERLAP_BUFFER_MS = overlapBufferSeconds * 1000;
|
|
582
|
-
const bufferedLastRunTime = new Date(
|
|
583
|
-
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
|
|
584
|
-
).toISOString();
|
|
585
|
-
|
|
586
|
-
const effectiveEndTime = new Date().toISOString();
|
|
587
|
-
|
|
588
|
-
const dateRangeFilter = {
|
|
589
|
-
from: bufferedLastRunTime,
|
|
590
|
-
to: effectiveEndTime, // End of extraction window
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
log.info('✅ [STATE] Incremental extraction mode with overlap buffer', {
|
|
594
|
-
rawLastRunTime,
|
|
595
|
-
bufferedLastRunTime,
|
|
596
|
-
effectiveEndTime,
|
|
597
|
-
overlapBufferSeconds,
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
// STEP 4/8: Create extraction job
|
|
601
|
-
log.info('📝 [JOB] Creating extraction job');
|
|
602
|
-
const jobId = `virtual-positions-json-${Date.now()}`;
|
|
603
|
-
await jobTracker.createJob(jobId, {
|
|
604
|
-
type: 'extraction',
|
|
605
|
-
entity: 'virtualPositions',
|
|
606
|
-
mode: 'incremental',
|
|
607
|
-
dateRangeFilter,
|
|
608
|
-
startTime: executionStartTime,
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
log.info('✅ [JOB] Extraction job created', {
|
|
612
|
-
jobId,
|
|
613
|
-
catalogues,
|
|
614
|
-
dateRangeFilter,
|
|
615
|
-
maxRecords,
|
|
616
|
-
positionTypes,
|
|
617
|
-
groupRefs,
|
|
618
|
-
productRefs,
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
// ? Enhanced: Extract context for progress logging
|
|
622
|
-
const dateRangeInfo = {
|
|
623
|
-
start: dateRangeFilter?.from || 'N/A',
|
|
624
|
-
end: dateRangeFilter?.to || 'N/A',
|
|
625
|
-
catalogues: catalogues.map((c: any) => c.ref || c).join(', ') || 'all',
|
|
626
|
-
types: positionTypes?.join(', ') || 'all',
|
|
627
|
-
groups: groupRefs?.join(', ') || 'none',
|
|
628
|
-
products: productRefs?.join(', ') || 'none'
|
|
629
|
-
};
|
|
630
|
-
|
|
631
|
-
// ? Enhanced: Start logging with context
|
|
632
|
-
log.info(`🔍 [EXTRACTION] Starting GraphQL extraction`, {
|
|
633
|
-
query: 'virtualPositions',
|
|
634
|
-
pageSize,
|
|
635
|
-
maxRecords,
|
|
636
|
-
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
637
|
-
catalogues: dateRangeInfo.catalogues,
|
|
638
|
-
positionTypes: dateRangeInfo.types,
|
|
639
|
-
groupRefs: dateRangeInfo.groups,
|
|
640
|
-
productRefs: dateRangeInfo.products,
|
|
641
|
-
jobId
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
// STEP 5/8: Execute extraction with auto-pagination (WITH overlap buffer)
|
|
645
|
-
const extractionStartTime = Date.now();
|
|
646
|
-
const extractionResult = await orchestrator.extract({
|
|
647
|
-
query: VIRTUAL_POSITIONS_QUERY,
|
|
648
|
-
resultPath: 'virtualPositions.edges.node',
|
|
649
|
-
variables: {
|
|
650
|
-
catalogues,
|
|
651
|
-
dateRangeFilter,
|
|
652
|
-
types: positionTypes,
|
|
653
|
-
groupRefs,
|
|
654
|
-
productRefs,
|
|
655
|
-
},
|
|
656
|
-
pageSize,
|
|
657
|
-
maxRecords,
|
|
658
|
-
validateItem: item => !!(item.ref && item.productRef),
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
const rawRecords = extractionResult.data;
|
|
662
|
-
const extractionDuration = Date.now() - extractionStartTime;
|
|
663
|
-
|
|
664
|
-
log.info('✅ [EXTRACTION] Virtual position extraction completed', {
|
|
665
|
-
jobId,
|
|
666
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
667
|
-
totalPages: extractionResult.stats.totalPages,
|
|
668
|
-
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
669
|
-
errors: extractionResult.errors ? extractionResult.errors.length : 0,
|
|
670
|
-
duration: `${extractionDuration}ms`,
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
// ? Enhanced: Completion logging with summary
|
|
674
|
-
log.info(`✅ [STATS] Extraction statistics`, {
|
|
675
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
676
|
-
totalPages: extractionResult.stats.totalPages,
|
|
677
|
-
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
678
|
-
failedValidations: extractionResult.stats.failedValidations,
|
|
679
|
-
truncated: extractionResult.stats.truncated,
|
|
680
|
-
truncationReason: extractionResult.stats.truncationReason,
|
|
681
|
-
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
682
|
-
duration: `${extractionDuration}ms`,
|
|
683
|
-
jobId
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
if (extractionResult.errors && extractionResult.errors.length > 0) {
|
|
687
|
-
log.warn('Non-fatal extraction errors encountered', {
|
|
688
|
-
jobId,
|
|
689
|
-
errorCount: extractionResult.errors.length,
|
|
690
|
-
sampleErrors: extractionResult.errors.slice(0, 3),
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
if (rawRecords.length === 0) {
|
|
695
|
-
log.info('ℹ️ [EXTRACTION] No virtual position records to extract');
|
|
696
|
-
await jobTracker.markCompleted(jobId, { recordCount: 0 });
|
|
697
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
698
|
-
log.info('✅ [COMPLETE] Extraction workflow completed', {
|
|
699
|
-
duration: `${totalDuration}ms`,
|
|
700
|
-
recordCount: 0,
|
|
701
|
-
});
|
|
702
|
-
return {
|
|
703
|
-
success: true,
|
|
704
|
-
message: 'No records to extract',
|
|
705
|
-
jobId,
|
|
706
|
-
dateRangeFilter,
|
|
707
|
-
duration: totalDuration,
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
log.info('📦 [DATA] Virtual position records retrieved', { count: rawRecords.length, jobId });
|
|
712
|
-
|
|
713
|
-
// STEP 6/8: Transform with UniversalMapper
|
|
714
|
-
log.info('🔄 [MAPPING] Starting field transformation');
|
|
715
|
-
const mapper = new UniversalMapper(virtualPositionsExportMapping);
|
|
716
|
-
const mappingResult = await mapper.map(rawRecords);
|
|
717
|
-
|
|
718
|
-
if (!mappingResult.success) {
|
|
719
|
-
const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
|
|
720
|
-
log.error('❌ [MAPPING] Transformation failed', {
|
|
721
|
-
errorCount: mappingErrors.length,
|
|
722
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
723
|
-
});
|
|
724
|
-
await jobTracker.markFailed(
|
|
725
|
-
jobId,
|
|
726
|
-
mappingErrors[0] || 'UniversalMapper returned unsuccessful result',
|
|
727
|
-
{
|
|
728
|
-
errors: mappingErrors,
|
|
729
|
-
}
|
|
730
|
-
);
|
|
731
|
-
return {
|
|
732
|
-
success: false,
|
|
733
|
-
error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
|
|
734
|
-
jobId,
|
|
735
|
-
errors: mappingErrors,
|
|
736
|
-
recommendation: 'Check mapping configuration in config/virtual-positions.export.json. Verify all source paths exist in GraphQL response.',
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
const transformedRecords = Array.isArray(mappingResult.data) ? mappingResult.data : [];
|
|
741
|
-
const mappingErrors = mappingResult.errors || [];
|
|
742
|
-
|
|
743
|
-
if (mappingErrors.length > 0) {
|
|
744
|
-
log.warn('Some records failed transformation', {
|
|
745
|
-
jobId,
|
|
746
|
-
errorCount: mappingErrors.length,
|
|
747
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
752
|
-
log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
753
|
-
jobId,
|
|
754
|
-
skippedFields: mappingResult.skippedFields,
|
|
755
|
-
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
if (transformedRecords.length === 0) {
|
|
760
|
-
log.error('❌ [MAPPING] All records failed mapping', {
|
|
761
|
-
totalRecords: rawRecords.length,
|
|
762
|
-
errorCount: mappingErrors.length,
|
|
763
|
-
});
|
|
764
|
-
await jobTracker.markFailed(jobId, 'All records failed mapping', { errors: mappingErrors });
|
|
765
|
-
return {
|
|
766
|
-
success: false,
|
|
767
|
-
error: 'All records failed mapping',
|
|
768
|
-
jobId,
|
|
769
|
-
errors: mappingErrors,
|
|
770
|
-
recommendation: 'Review mapping errors above. Common issues: missing required fields, invalid resolver names, incorrect source paths.',
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
log.info('✅ [MAPPING] Records transformed successfully', {
|
|
775
|
-
successful: transformedRecords.length,
|
|
776
|
-
skippedRecords: rawRecords.length - transformedRecords.length,
|
|
777
|
-
errorRate: ((mappingErrors.length / rawRecords.length) * 100).toFixed(2) + '%',
|
|
778
|
-
jobId,
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
// STEP 7/8: Calculate max updatedOn for next incremental run (WITHOUT buffer)
|
|
782
|
-
const maxUpdatedOn = transformedRecords.reduce((max, record) => {
|
|
783
|
-
const recordTime = new Date(record.updatedOn).getTime();
|
|
784
|
-
return recordTime > max ? recordTime : max;
|
|
785
|
-
}, new Date(rawLastRunTime).getTime());
|
|
786
|
-
const newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
787
|
-
|
|
788
|
-
// Build JSON with metadata
|
|
789
|
-
const jsonOutput = {
|
|
790
|
-
metadata: {
|
|
791
|
-
extractedAt: new Date().toISOString(),
|
|
792
|
-
recordCount: transformedRecords.length,
|
|
793
|
-
incrementalFrom: rawLastRunTime,
|
|
794
|
-
incrementalTo: newTimestamp,
|
|
795
|
-
jobId,
|
|
796
|
-
extractionMode: 'incremental',
|
|
797
|
-
},
|
|
798
|
-
data: transformedRecords,
|
|
799
|
-
};
|
|
800
|
-
|
|
801
|
-
// Use JSONBuilder for consistent JSON generation
|
|
802
|
-
const jsonBuilder = new JSONBuilder({
|
|
803
|
-
prettyPrint,
|
|
804
|
-
indent: 2,
|
|
805
|
-
});
|
|
806
|
-
const jsonContent = jsonBuilder.build(jsonOutput);
|
|
807
|
-
|
|
808
|
-
// Generate timestamped filename
|
|
809
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
810
|
-
const fileName = `virtual-positions-${timestamp}.json`;
|
|
811
|
-
const s3Key = `${s3Prefix}${fileName}`;
|
|
812
|
-
|
|
813
|
-
log.info('📄 [JSON] Generated JSON file', {
|
|
814
|
-
fileName,
|
|
815
|
-
size: `${(jsonContent.length / 1024).toFixed(2)} KB`,
|
|
816
|
-
recordCount: transformedRecords.length,
|
|
817
|
-
jobId,
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
// STEP 8/8: Upload to S3
|
|
821
|
-
log.info('☁️ [S3] Starting S3 upload');
|
|
822
|
-
const s3 = new S3DataSource(
|
|
823
|
-
{
|
|
824
|
-
type: 'S3_JSON',
|
|
825
|
-
connectionId: 's3-virtual-positions-json-export',
|
|
826
|
-
name: 'S3 Virtual Positions JSON Export',
|
|
827
|
-
s3Config,
|
|
828
|
-
},
|
|
829
|
-
log
|
|
830
|
-
);
|
|
831
|
-
|
|
832
|
-
await s3.uploadFile(s3Key, Buffer.from(jsonContent, 'utf8'), {
|
|
833
|
-
contentType: 'application/json',
|
|
834
|
-
metadata: {
|
|
835
|
-
recordCount: String(transformedRecords.length),
|
|
836
|
-
extractedAt: new Date().toISOString(),
|
|
837
|
-
jobId,
|
|
838
|
-
incrementalFrom: rawLastRunTime,
|
|
839
|
-
incrementalTo: newTimestamp,
|
|
840
|
-
},
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
log.info('✅ [S3] JSON file uploaded successfully', { s3Key, jobId });
|
|
844
|
-
|
|
845
|
-
// Update state with new timestamp (WITHOUT buffer - critical!)
|
|
846
|
-
await kv.set(stateKey, {
|
|
847
|
-
timestamp: newTimestamp, // ← NO buffer applied
|
|
848
|
-
recordCount: transformedRecords.length,
|
|
849
|
-
extractedAt: new Date().toISOString(),
|
|
850
|
-
overlapBufferSeconds, // Track buffer config
|
|
851
|
-
fileName,
|
|
852
|
-
s3Key,
|
|
853
|
-
jobId,
|
|
854
|
-
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
855
|
-
});
|
|
856
|
-
log.info('💾 [STATE] State updated with new timestamp (without buffer)', {
|
|
857
|
-
newTimestamp,
|
|
858
|
-
overlapBufferSeconds,
|
|
859
|
-
jobId,
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
await jobTracker.markCompleted(jobId, {
|
|
863
|
-
recordCount: transformedRecords.length,
|
|
864
|
-
fileName,
|
|
865
|
-
s3Key,
|
|
866
|
-
errors: mappingErrors,
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
870
|
-
log.info('✅ [COMPLETE] Extraction workflow completed successfully', {
|
|
871
|
-
duration: `${totalDuration}ms`,
|
|
872
|
-
extractionDuration: `${extractionDuration}ms`,
|
|
873
|
-
recordsExtracted: transformedRecords.length,
|
|
874
|
-
recordsFailed: mappingErrors.length,
|
|
875
|
-
s3Key,
|
|
876
|
-
jobId,
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
return {
|
|
880
|
-
success: true,
|
|
881
|
-
recordsExtracted: transformedRecords.length,
|
|
882
|
-
recordsFailed: mappingErrors.length,
|
|
883
|
-
fileName,
|
|
884
|
-
s3Key,
|
|
885
|
-
jobId,
|
|
886
|
-
newTimestamp,
|
|
887
|
-
duration: totalDuration,
|
|
888
|
-
extractionDuration,
|
|
889
|
-
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
890
|
-
};
|
|
891
|
-
} catch (error: any) {
|
|
892
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
893
|
-
log.error('❌ [FAILED] Extraction workflow failed', {
|
|
894
|
-
message: error?.message,
|
|
895
|
-
duration: `${totalDuration}ms`,
|
|
896
|
-
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
897
|
-
});
|
|
898
|
-
|
|
899
|
-
return {
|
|
900
|
-
success: false,
|
|
901
|
-
message: error instanceof Error ? error.message : String(error),
|
|
902
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
903
|
-
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
904
|
-
duration: totalDuration,
|
|
905
|
-
recommendation: 'Check error message above. Common issues: connection failures, invalid GraphQL query, S3 credentials, or mapping configuration errors.',
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
})
|
|
909
|
-
);
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
### Workflow 2: Ad Hoc Manual Trigger
|
|
913
|
-
|
|
914
|
-
**File:** `src/workflows/webhook/adhoc-virtual-positions-extraction.ts`
|
|
915
|
-
|
|
916
|
-
```typescript
|
|
917
|
-
import { webhook, http } from '@versori/run';
|
|
918
|
-
import { Buffer } from 'node:buffer';
|
|
919
|
-
import {
|
|
920
|
-
createClient,
|
|
921
|
-
ExtractionOrchestrator,
|
|
922
|
-
JobTracker,
|
|
923
|
-
UniversalMapper,
|
|
924
|
-
S3DataSource,
|
|
925
|
-
JSONBuilder,
|
|
926
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
927
|
-
import virtualPositionsExportMapping from './config/virtual-positions.export.json' with { type: 'json' };
|
|
928
|
-
|
|
929
|
-
export const virtualPositionsManualExtraction = webhook('manual-extract-virtual-positions', {
|
|
930
|
-
connection: 'virtual-positions-adhoc',
|
|
931
|
-
response: {
|
|
932
|
-
mode: 'sync',
|
|
933
|
-
onSuccess: ctx =>
|
|
934
|
-
new Response(JSON.stringify(ctx.data), {
|
|
935
|
-
status: 200,
|
|
936
|
-
headers: { 'Content-Type': 'application/json' },
|
|
937
|
-
}),
|
|
938
|
-
onError: ctx =>
|
|
939
|
-
new Response(JSON.stringify({ error: ctx.data }), {
|
|
940
|
-
status: 500,
|
|
941
|
-
headers: { 'Content-Type': 'application/json' },
|
|
942
|
-
}),
|
|
943
|
-
},
|
|
944
|
-
}).then(
|
|
945
|
-
http('trigger-extraction', { connection: 'fluent_commerce' }, async ctx => {
|
|
946
|
-
const { log, openKv, activation } = ctx;
|
|
947
|
-
const executionStartTime = Date.now();
|
|
948
|
-
|
|
949
|
-
log.info('🚀 [EXTRACTION] Starting ad hoc virtual positions extraction', {
|
|
950
|
-
timestamp: new Date().toISOString(),
|
|
951
|
-
});
|
|
952
|
-
|
|
953
|
-
// STEP 1/8: Parse request body for custom date range
|
|
954
|
-
const body = ctx.request?.body || {};
|
|
955
|
-
const startDate = body.startDate || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); // Last 24 hours
|
|
956
|
-
const endDate = body.endDate || new Date().toISOString();
|
|
957
|
-
const catalogueRefs = body.catalogueRefs || 'DEFAULT_VIRTUAL_CATALOGUE';
|
|
958
|
-
|
|
959
|
-
const catalogues = catalogueRefs.split(',').map((ref: string) => ({ ref: ref.trim() }));
|
|
960
|
-
|
|
961
|
-
const s3Config = {
|
|
962
|
-
bucket: activation?.getVariable('s3BucketName'),
|
|
963
|
-
region: activation?.getVariable('awsRegion') || 'us-east-1',
|
|
964
|
-
accessKeyId: activation?.getVariable('awsAccessKeyId'),
|
|
965
|
-
secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
|
|
966
|
-
};
|
|
967
|
-
const s3Prefix = activation?.getVariable('s3Prefix') || 'virtual-positions/adhoc/';
|
|
968
|
-
|
|
969
|
-
log.info('📋 [CONFIG] Manual extraction triggered', { startDate, endDate, catalogues });
|
|
970
|
-
|
|
971
|
-
// STEP 2/8: Initialize SDK services
|
|
972
|
-
log.info('📦 [INITIALIZATION] Creating Fluent Commerce client');
|
|
973
|
-
const client = await createClient(ctx);
|
|
974
|
-
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
975
|
-
const jobTracker = new JobTracker(openKv(':project:'), log);
|
|
976
|
-
log.info('✅ [INITIALIZATION] SDK services initialized successfully');
|
|
977
|
-
|
|
978
|
-
// STEP 3/8: Create extraction job
|
|
979
|
-
log.info('📝 [JOB] Creating ad hoc extraction job');
|
|
980
|
-
const jobId = `virtual-positions-adhoc-${Date.now()}`;
|
|
981
|
-
await jobTracker.createJob(jobId, {
|
|
982
|
-
type: 'extraction',
|
|
983
|
-
entity: 'virtualPositions',
|
|
984
|
-
mode: 'adhoc',
|
|
985
|
-
dateRange: { from: startDate, to: endDate },
|
|
986
|
-
startTime: executionStartTime,
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
// ? Enhanced: Extract context for progress logging
|
|
990
|
-
const dateRangeInfo = {
|
|
991
|
-
start: startDate || 'N/A',
|
|
992
|
-
end: endDate || 'N/A',
|
|
993
|
-
catalogues: catalogues?.map((c: any) => c.ref || c).join(', ') || 'all'
|
|
994
|
-
};
|
|
995
|
-
|
|
996
|
-
// ? Enhanced: Start logging with context
|
|
997
|
-
log.info(`🔍 [EXTRACTION] Starting GraphQL extraction`, {
|
|
998
|
-
query: 'virtualPositions',
|
|
999
|
-
pageSize: 200,
|
|
1000
|
-
maxRecords: 100000,
|
|
1001
|
-
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1002
|
-
catalogues: dateRangeInfo.catalogues,
|
|
1003
|
-
jobId
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
// STEP 4/8: Execute extraction with orchestrator
|
|
1007
|
-
const extractionStartTime = Date.now();
|
|
1008
|
-
const extractionResult = await orchestrator.extract({
|
|
1009
|
-
query: VIRTUAL_POSITIONS_QUERY,
|
|
1010
|
-
resultPath: 'virtualPositions.edges.node',
|
|
1011
|
-
variables: {
|
|
1012
|
-
catalogues,
|
|
1013
|
-
dateRangeFilter: { from: startDate, to: endDate },
|
|
1014
|
-
},
|
|
1015
|
-
pageSize: 200,
|
|
1016
|
-
maxRecords: 100000,
|
|
1017
|
-
validateItem: (item: any) => !!(item.ref && item.productRef),
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
const rawRecords = extractionResult.data;
|
|
1021
|
-
const extractionDuration = Date.now() - extractionStartTime;
|
|
1022
|
-
|
|
1023
|
-
log.info('✅ [EXTRACTION] Manual extraction completed', {
|
|
1024
|
-
jobId,
|
|
1025
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1026
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1027
|
-
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1028
|
-
errors: extractionResult.errors ? extractionResult.errors.length : 0,
|
|
1029
|
-
duration: `${extractionDuration}ms`,
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
// ? Enhanced: Completion logging with summary
|
|
1033
|
-
log.info(`✅ [STATS] Extraction statistics`, {
|
|
1034
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1035
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1036
|
-
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1037
|
-
failedValidations: extractionResult.stats.failedValidations,
|
|
1038
|
-
truncated: extractionResult.stats.truncated,
|
|
1039
|
-
truncationReason: extractionResult.stats.truncationReason,
|
|
1040
|
-
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1041
|
-
duration: `${extractionDuration}ms`,
|
|
1042
|
-
jobId
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
if (extractionResult.errors && extractionResult.errors.length > 0) {
|
|
1046
|
-
log.warn('Non-fatal extraction errors encountered', {
|
|
1047
|
-
jobId,
|
|
1048
|
-
errorCount: extractionResult.errors.length,
|
|
1049
|
-
sampleErrors: extractionResult.errors.slice(0, 3),
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (rawRecords.length === 0) {
|
|
1054
|
-
log.info('ℹ️ [EXTRACTION] No records found for specified date range');
|
|
1055
|
-
await jobTracker.markCompleted(jobId, {
|
|
1056
|
-
recordCount: 0,
|
|
1057
|
-
message: 'No records to extract',
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
1061
|
-
log.info('✅ [COMPLETE] Extraction workflow completed', {
|
|
1062
|
-
duration: `${totalDuration}ms`,
|
|
1063
|
-
recordCount: 0,
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
return {
|
|
1067
|
-
success: true,
|
|
1068
|
-
jobId,
|
|
1069
|
-
recordCount: 0,
|
|
1070
|
-
fileName: undefined,
|
|
1071
|
-
s3Key: undefined,
|
|
1072
|
-
duration: totalDuration,
|
|
1073
|
-
};
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
// STEP 5/8: Transform records
|
|
1077
|
-
log.info('🔄 [MAPPING] Starting field transformation');
|
|
1078
|
-
const mapper = new UniversalMapper(virtualPositionsExportMapping);
|
|
1079
|
-
const mappingResult = await mapper.map(rawRecords);
|
|
1080
|
-
|
|
1081
|
-
if (!mappingResult.success) {
|
|
1082
|
-
const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
|
|
1083
|
-
log.error('❌ [MAPPING] Transformation failed', {
|
|
1084
|
-
errorCount: mappingErrors.length,
|
|
1085
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
1086
|
-
});
|
|
1087
|
-
await jobTracker.markFailed(
|
|
1088
|
-
jobId,
|
|
1089
|
-
mappingErrors[0] || 'UniversalMapper returned unsuccessful result',
|
|
1090
|
-
{
|
|
1091
|
-
errors: mappingErrors,
|
|
1092
|
-
}
|
|
1093
|
-
);
|
|
1094
|
-
return {
|
|
1095
|
-
success: false,
|
|
1096
|
-
jobId,
|
|
1097
|
-
error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
|
|
1098
|
-
errors: mappingErrors,
|
|
1099
|
-
recommendation: 'Check mapping configuration in config/virtual-positions.export.json. Verify all source paths exist in GraphQL response.',
|
|
1100
|
-
};
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
const transformedRecords = Array.isArray(mappingResult.data) ? mappingResult.data : [];
|
|
1104
|
-
const mappingErrors = mappingResult.errors || [];
|
|
1105
|
-
|
|
1106
|
-
if (mappingErrors.length > 0) {
|
|
1107
|
-
log.warn('Manual extraction mapping warnings', {
|
|
1108
|
-
jobId,
|
|
1109
|
-
errorCount: mappingErrors.length,
|
|
1110
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
1115
|
-
log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
1116
|
-
jobId,
|
|
1117
|
-
skippedFields: mappingResult.skippedFields,
|
|
1118
|
-
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
if (transformedRecords.length === 0) {
|
|
1123
|
-
log.error('❌ [MAPPING] All records failed mapping', {
|
|
1124
|
-
totalRecords: rawRecords.length,
|
|
1125
|
-
errorCount: mappingErrors.length,
|
|
1126
|
-
});
|
|
1127
|
-
await jobTracker.markFailed(jobId, 'All records failed mapping', { errors: mappingErrors });
|
|
1128
|
-
return {
|
|
1129
|
-
success: false,
|
|
1130
|
-
jobId,
|
|
1131
|
-
error: 'All records failed mapping',
|
|
1132
|
-
errors: mappingErrors,
|
|
1133
|
-
recommendation: 'Review mapping errors above. Common issues: missing required fields, invalid resolver names, incorrect source paths.',
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
log.info('✅ [MAPPING] Records transformed successfully', {
|
|
1138
|
-
successful: transformedRecords.length,
|
|
1139
|
-
skippedRecords: rawRecords.length - transformedRecords.length,
|
|
1140
|
-
errorRate: ((mappingErrors.length / rawRecords.length) * 100).toFixed(2) + '%',
|
|
1141
|
-
jobId,
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
// STEP 6/8: Build JSON output
|
|
1145
|
-
log.info('📄 [JSON] Building JSON output');
|
|
1146
|
-
const jsonOutput = {
|
|
1147
|
-
metadata: {
|
|
1148
|
-
extractedAt: new Date().toISOString(),
|
|
1149
|
-
recordCount: transformedRecords.length,
|
|
1150
|
-
dateRangeFrom: startDate,
|
|
1151
|
-
dateRangeTo: endDate,
|
|
1152
|
-
jobId,
|
|
1153
|
-
extractionMode: 'adhoc',
|
|
1154
|
-
},
|
|
1155
|
-
data: transformedRecords,
|
|
1156
|
-
};
|
|
1157
|
-
|
|
1158
|
-
// Use JSONBuilder for consistent JSON generation
|
|
1159
|
-
const jsonBuilder = new JSONBuilder({
|
|
1160
|
-
prettyPrint: true,
|
|
1161
|
-
indent: 2,
|
|
1162
|
-
});
|
|
1163
|
-
const jsonContent = jsonBuilder.build(jsonOutput);
|
|
1164
|
-
|
|
1165
|
-
// STEP 7/8: Upload to S3
|
|
1166
|
-
log.info('☁️ [S3] Starting S3 upload');
|
|
1167
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1168
|
-
const fileName = `virtual-positions-adhoc-${timestamp}.json`;
|
|
1169
|
-
const s3Key = `${s3Prefix}${fileName}`;
|
|
1170
|
-
|
|
1171
|
-
const s3 = new S3DataSource(
|
|
1172
|
-
{
|
|
1173
|
-
type: 'S3_JSON',
|
|
1174
|
-
connectionId: 's3-virtual-positions-json-export',
|
|
1175
|
-
name: 'S3 Virtual Positions JSON Export',
|
|
1176
|
-
s3Config,
|
|
1177
|
-
},
|
|
1178
|
-
log
|
|
1179
|
-
);
|
|
1180
|
-
|
|
1181
|
-
await s3.uploadFile(s3Key, Buffer.from(jsonContent, 'utf8'), {
|
|
1182
|
-
contentType: 'application/json',
|
|
1183
|
-
metadata: {
|
|
1184
|
-
recordCount: String(transformedRecords.length),
|
|
1185
|
-
extractedAt: new Date().toISOString(),
|
|
1186
|
-
jobId,
|
|
1187
|
-
},
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
log.info('✅ [S3] JSON file uploaded successfully', { s3Key, jobId });
|
|
1191
|
-
|
|
1192
|
-
// STEP 8/8: Complete job
|
|
1193
|
-
await jobTracker.markCompleted(jobId, {
|
|
1194
|
-
recordCount: transformedRecords.length,
|
|
1195
|
-
fileName,
|
|
1196
|
-
s3Key,
|
|
1197
|
-
errors: mappingErrors,
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
1201
|
-
log.info('✅ [COMPLETE] Ad hoc extraction workflow completed successfully', {
|
|
1202
|
-
duration: `${totalDuration}ms`,
|
|
1203
|
-
extractionDuration: `${extractionDuration}ms`,
|
|
1204
|
-
jobId,
|
|
1205
|
-
recordCount: transformedRecords.length,
|
|
1206
|
-
s3Key,
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
return {
|
|
1210
|
-
success: true,
|
|
1211
|
-
jobId,
|
|
1212
|
-
recordCount: transformedRecords.length,
|
|
1213
|
-
fileName,
|
|
1214
|
-
s3Key,
|
|
1215
|
-
duration: totalDuration,
|
|
1216
|
-
extractionDuration,
|
|
1217
|
-
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1218
|
-
};
|
|
1219
|
-
})
|
|
1220
|
-
);
|
|
1221
|
-
```
|
|
1222
|
-
|
|
1223
|
-
### Workflow 3: Job Status Query
|
|
1224
|
-
|
|
1225
|
-
**File:** `src/workflows/webhook/job-status-check.ts`
|
|
1226
|
-
|
|
1227
|
-
```typescript
|
|
1228
|
-
import { webhook, http } from '@versori/run';
|
|
1229
|
-
import { Buffer } from 'node:buffer';
|
|
1230
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
1231
|
-
|
|
1232
|
-
export const virtualPositionsJobStatus = webhook('job-status', {
|
|
1233
|
-
connection: 'virtual-positions-job-status',
|
|
1234
|
-
response: {
|
|
1235
|
-
mode: 'sync',
|
|
1236
|
-
onSuccess: ctx =>
|
|
1237
|
-
new Response(JSON.stringify(ctx.data), {
|
|
1238
|
-
status: 200,
|
|
1239
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1240
|
-
}),
|
|
1241
|
-
onError: ctx =>
|
|
1242
|
-
new Response(JSON.stringify({ error: ctx.data }), {
|
|
1243
|
-
status: 404,
|
|
1244
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1245
|
-
}),
|
|
1246
|
-
},
|
|
1247
|
-
}).then(
|
|
1248
|
-
http('query-job-status', async ctx => {
|
|
1249
|
-
const { log, openKv, activation } = ctx;
|
|
1250
|
-
const jobId = ctx.request?.query?.jobId;
|
|
1251
|
-
|
|
1252
|
-
if (!jobId) {
|
|
1253
|
-
return { error: 'Missing jobId query parameter' };
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
const jobTracker = new JobTracker(openKv(':project:'), log);
|
|
1257
|
-
const jobState = await jobTracker.getJob(jobId);
|
|
1258
|
-
|
|
1259
|
-
if (!jobState) {
|
|
1260
|
-
return { error: `Job not found: ${jobId}` };
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
return {
|
|
1264
|
-
success: true,
|
|
1265
|
-
job: jobState,
|
|
1266
|
-
};
|
|
1267
|
-
})
|
|
1268
|
-
);
|
|
1269
|
-
```
|
|
1270
|
-
|
|
1271
|
-
## Key Differences from Manual Implementation
|
|
1272
|
-
|
|
1273
|
-
1. **ExtractionOrchestrator**: High-level orchestration vs manual steps
|
|
1274
|
-
2. **JobTracker**: Automatic job lifecycle tracking
|
|
1275
|
-
3. **Three Workflows**: Scheduled, ad hoc, and status query patterns
|
|
1276
|
-
4. **STEP X/8 Comments**: Clear orchestration progression
|
|
1277
|
-
5. **Overlap Buffer Pattern**: Query WITH buffer, save WITHOUT buffer
|
|
1278
|
-
|
|
1279
|
-
## Production Checklist
|
|
1280
|
-
|
|
1281
|
-
- [ ] Set `catalogueRefs` to correct virtual catalogue
|
|
1282
|
-
- [ ] Configure extraction schedule (15min for real-time, hourly for standard)
|
|
1283
|
-
- [ ] Set `maxRecords` based on ATP volume (100k+ for large retailers)
|
|
1284
|
-
- [ ] Set `pageSize` to balance throughput and memory (200-500 recommended)
|
|
1285
|
-
- [ ] Enable/disable `prettyPrint` based on use case (false for production)
|
|
1286
|
-
- [ ] Verify S3 bucket permissions and CORS configuration
|
|
1287
|
-
- [ ] Set up S3 lifecycle policy to archive old JSON files
|
|
1288
|
-
- [ ] Test with incremental mode first (validates state management)
|
|
1289
|
-
- [ ] Document JSON schema for API consumers
|
|
1290
|
-
- [ ] Test failure recovery (state rollback on error)
|
|
1291
|
-
- [ ] Set up alerts for extraction failures
|
|
1292
|
-
- [ ] Test with real-time incremental changes (15min schedule)
|
|
1293
|
-
- [ ] Test ad hoc manual trigger workflow
|
|
1294
|
-
- [ ] Test job status query workflow
|
|
1295
|
-
|
|
1296
|
-
---
|
|
1297
|
-
|
|
1298
|
-
---
|
|
1299
|
-
|
|
1300
|
-
### Pattern 7: State Management & Date Overrides
|
|
1301
|
-
|
|
1302
|
-
**Use Case**: Understand how state management works with scheduled and ad-hoc extractions.
|
|
1303
|
-
|
|
1304
|
-
**How it works**:
|
|
1305
|
-
|
|
1306
|
-
VersoriKV stores the last successful extraction timestamp to enable incremental sync:
|
|
1307
|
-
|
|
1308
|
-
```typescript
|
|
1309
|
-
interface ExtractionState {
|
|
1310
|
-
timestamp: string; // Last run timestamp (WITHOUT overlap buffer)
|
|
1311
|
-
recordCount: number; // Number of records extracted
|
|
1312
|
-
extractedAt: string; // When extraction completed
|
|
1313
|
-
fileName?: string; // Generated filename
|
|
1314
|
-
s3Key?: string; // S3 upload path
|
|
1315
|
-
overlapBufferSeconds?: number; // Buffer configuration
|
|
1316
|
-
}
|
|
1317
|
-
```
|
|
1318
|
-
|
|
1319
|
-
**State Priority Chain** (highest to lowest):
|
|
1320
|
-
|
|
1321
|
-
1. **`fromDate` override** (manual date in webhook payload) - Highest priority
|
|
1322
|
-
2. **Stored state** (`await kv.get(stateKey)`) - Normal incremental mode
|
|
1323
|
-
3. **`fallbackStartDate`** (activation variable) - First run fallback
|
|
1324
|
-
|
|
1325
|
-
**Three Scenarios**:
|
|
1326
|
-
|
|
1327
|
-
#### Scenario 1: Normal Scheduled Runs (Incremental)
|
|
1328
|
-
|
|
1329
|
-
```typescript
|
|
1330
|
-
// Payload: {} (empty - no overrides)
|
|
1331
|
-
|
|
1332
|
-
// Behavior:
|
|
1333
|
-
// 1. Load last timestamp from KV: "2025-01-22T10:00:00Z"
|
|
1334
|
-
// 2. Apply overlap buffer: "2025-01-22T09:59:00Z" (query WITH buffer)
|
|
1335
|
-
// 3. Extract records updated since buffered time
|
|
1336
|
-
// 4. Calculate MAX(updatedOn) from results: "2025-01-22T14:30:00Z"
|
|
1337
|
-
// 5. Save new timestamp WITHOUT buffer: "2025-01-22T14:30:00Z"
|
|
1338
|
-
// 6. Next run starts from "2025-01-22T14:29:00Z" (with buffer)
|
|
1339
|
-
```
|
|
1340
|
-
|
|
1341
|
-
**Test**:
|
|
1342
|
-
|
|
1343
|
-
```bash
|
|
1344
|
-
# Trigger scheduled run (no payload needed)
|
|
1345
|
-
# State advances automatically
|
|
1346
|
-
curl -X POST https://workspace.versori.run/virtual-positions-extract-json-15min
|
|
1347
|
-
```
|
|
1348
|
-
|
|
1349
|
-
#### Scenario 2: Ad-hoc Extraction WITH State Update
|
|
1350
|
-
|
|
1351
|
-
```typescript
|
|
1352
|
-
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": true }
|
|
1353
|
-
|
|
1354
|
-
// Behavior:
|
|
1355
|
-
// 1. Ignore stored state
|
|
1356
|
-
// 2. Use fromDate: "2025-01-01T00:00:00Z" (no buffer applied to manual dates)
|
|
1357
|
-
// 3. Extract all records since 2025-01-01
|
|
1358
|
-
// 4. Calculate MAX(updatedOn): "2025-01-22T14:30:00Z"
|
|
1359
|
-
// 5. Save new timestamp: "2025-01-22T14:30:00Z" (updates state!)
|
|
1360
|
-
// 6. Next scheduled run starts from this new timestamp
|
|
1361
|
-
```
|
|
1362
|
-
|
|
1363
|
-
**Use Case**: One-time catch-up extraction that advances the state pointer.
|
|
1364
|
-
|
|
1365
|
-
**Test**:
|
|
1366
|
-
|
|
1367
|
-
```bash
|
|
1368
|
-
curl -X POST https://workspace.versori.run/manual-extract-virtual-positions \
|
|
1369
|
-
-H "Content-Type: application/json" \
|
|
1370
|
-
-d '{
|
|
1371
|
-
"startDate": "2025-01-01T00:00:00Z",
|
|
1372
|
-
"catalogueRefs": "DEFAULT_VIRTUAL_CATALOGUE"
|
|
1373
|
-
}'
|
|
1374
|
-
```
|
|
1375
|
-
|
|
1376
|
-
#### Scenario 3: Ad-hoc Extraction WITHOUT State Update
|
|
1377
|
-
|
|
1378
|
-
```typescript
|
|
1379
|
-
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": false }
|
|
1380
|
-
|
|
1381
|
-
// Behavior:
|
|
1382
|
-
// 1. Ignore stored state
|
|
1383
|
-
// 2. Use fromDate: "2025-01-01T00:00:00Z"
|
|
1384
|
-
// 3. Extract all records since 2025-01-01
|
|
1385
|
-
// 4. DO NOT update state
|
|
1386
|
-
// 5. Next scheduled run uses previous timestamp (unaffected)
|
|
1387
|
-
```
|
|
1388
|
-
|
|
1389
|
-
**Use Case**: Historical backfill or testing without affecting incremental sync.
|
|
1390
|
-
|
|
1391
|
-
**Test**:
|
|
1392
|
-
|
|
1393
|
-
```bash
|
|
1394
|
-
curl -X POST https://workspace.versori.run/manual-extract-virtual-positions \
|
|
1395
|
-
-H "Content-Type: application/json" \
|
|
1396
|
-
-d '{
|
|
1397
|
-
"startDate": "2025-01-01T00:00:00Z",
|
|
1398
|
-
"endDate": "2025-01-31T23:59:59Z",
|
|
1399
|
-
"catalogueRefs": "DEFAULT_VIRTUAL_CATALOGUE"
|
|
1400
|
-
}'
|
|
1401
|
-
```
|
|
1402
|
-
|
|
1403
|
-
**Why this matters**:
|
|
1404
|
-
|
|
1405
|
-
- **Incremental sync** relies on state continuity
|
|
1406
|
-
- **Manual overrides** allow catch-up without breaking incremental flow
|
|
1407
|
-
- **Overlap buffer** prevents missed records at time boundaries
|
|
1408
|
-
- **State isolation** lets you test/backfill without affecting production sync
|
|
1409
|
-
|
|
1410
|
-
---
|
|
1411
|
-
|
|
1412
|
-
### Pattern 8: Optional GraphQL Query Logging
|
|
1413
|
-
|
|
1414
|
-
**Use Case**: Debug extraction issues by logging the exact GraphQL query sent to Fluent Commerce API.
|
|
1415
|
-
|
|
1416
|
-
**When to use**:
|
|
1417
|
-
|
|
1418
|
-
- ✅ Debugging pagination issues
|
|
1419
|
-
- ✅ Verifying query variables (dates, filters, limits)
|
|
1420
|
-
- ✅ Development and testing
|
|
1421
|
-
- ❌ Production (verbose logs, potential secrets in variables)
|
|
1422
|
-
|
|
1423
|
-
**How to enable**:
|
|
1424
|
-
|
|
1425
|
-
Set `DEBUG_GRAPHQL=true` environment variable in Versori activation settings.
|
|
1426
|
-
|
|
1427
|
-
**Implementation**:
|
|
1428
|
-
|
|
1429
|
-
```typescript
|
|
1430
|
-
// In your extraction workflow
|
|
1431
|
-
const DEBUG_GRAPHQL = activation?.getVariable('DEBUG_GRAPHQL') === 'true';
|
|
1432
|
-
|
|
1433
|
-
if (DEBUG_GRAPHQL) {
|
|
1434
|
-
log.info('GraphQL Query Debug', {
|
|
1435
|
-
query: VIRTUAL_POSITIONS_QUERY,
|
|
1436
|
-
variables: {
|
|
1437
|
-
catalogues,
|
|
1438
|
-
dateRangeFilter,
|
|
1439
|
-
first: pageSize,
|
|
1440
|
-
after: null, // First page
|
|
1441
|
-
},
|
|
1442
|
-
pagination: {
|
|
1443
|
-
pageSize,
|
|
1444
|
-
maxRecords,
|
|
1445
|
-
currentPage: 1,
|
|
1446
|
-
},
|
|
1447
|
-
});
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
const extractionResult = await orchestrator.extract({
|
|
1451
|
-
query: VIRTUAL_POSITIONS_QUERY,
|
|
1452
|
-
resultPath: 'virtualPositions.edges.node',
|
|
1453
|
-
variables: {
|
|
1454
|
-
catalogues,
|
|
1455
|
-
dateRangeFilter,
|
|
1456
|
-
},
|
|
1457
|
-
pageSize,
|
|
1458
|
-
maxRecords,
|
|
1459
|
-
});
|
|
1460
|
-
|
|
1461
|
-
if (DEBUG_GRAPHQL) {
|
|
1462
|
-
log.info('GraphQL Response Debug', {
|
|
1463
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1464
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1465
|
-
truncated: extractionResult.stats.truncated,
|
|
1466
|
-
truncationReason: extractionResult.stats.truncationReason,
|
|
1467
|
-
firstRecordId: extractionResult.data[0]?.id,
|
|
1468
|
-
lastRecordId: extractionResult.data[extractionResult.data.length - 1]?.id,
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
```
|
|
1472
|
-
|
|
1473
|
-
**What gets logged**:
|
|
1474
|
-
|
|
1475
|
-
```json
|
|
1476
|
-
{
|
|
1477
|
-
"level": "info",
|
|
1478
|
-
"message": "GraphQL Query Debug",
|
|
1479
|
-
"query": "query GetVirtualPositions($catalogues: [VirtualCatalogueKey], $dateRangeFilter: DateRange, ...)",
|
|
1480
|
-
"variables": {
|
|
1481
|
-
"catalogues": [{ "ref": "DEFAULT_VIRTUAL_CATALOGUE" }],
|
|
1482
|
-
"dateRangeFilter": "2025-01-22T09:59:00Z",
|
|
1483
|
-
"first": 200,
|
|
1484
|
-
"after": null
|
|
1485
|
-
},
|
|
1486
|
-
"pagination": {
|
|
1487
|
-
"pageSize": 200,
|
|
1488
|
-
"maxRecords": 100000,
|
|
1489
|
-
"currentPage": 1
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
```
|
|
1493
|
-
|
|
1494
|
-
**Versori Environment Variables**:
|
|
1495
|
-
|
|
1496
|
-
Add to activation settings:
|
|
1497
|
-
|
|
1498
|
-
```json
|
|
1499
|
-
{
|
|
1500
|
-
"DEBUG_GRAPHQL": "true"
|
|
1501
|
-
}
|
|
1502
|
-
```
|
|
1503
|
-
|
|
1504
|
-
**Testing**:
|
|
1505
|
-
|
|
1506
|
-
```bash
|
|
1507
|
-
# Enable debug logging
|
|
1508
|
-
curl -X POST https://workspace.versori.run/virtual-positions-extract-json-15min
|
|
1509
|
-
|
|
1510
|
-
# Check Versori logs for "GraphQL Query Debug" entries
|
|
1511
|
-
# Verify query structure and variables are correct
|
|
1512
|
-
```
|
|
1513
|
-
|
|
1514
|
-
**Sample Debug Output**:
|
|
1515
|
-
|
|
1516
|
-
```
|
|
1517
|
-
[INFO] GraphQL Query Debug
|
|
1518
|
-
query: "query GetVirtualPositions($catalogues: [VirtualCatalogueKey], $dateRangeFilter: DateRange, ...)"
|
|
1519
|
-
variables: { catalogues: [{ ref: "DEFAULT_VIRTUAL_CATALOGUE" }], dateRangeFilter: "2025-01-22T09:59:00Z", first: 200, after: null }
|
|
1520
|
-
pagination: { pageSize: 200, maxRecords: 100000, currentPage: 1 }
|
|
1521
|
-
|
|
1522
|
-
[INFO] Extraction complete
|
|
1523
|
-
totalRecords: 850
|
|
1524
|
-
totalPages: 5
|
|
1525
|
-
truncated: false
|
|
1526
|
-
|
|
1527
|
-
[INFO] GraphQL Response Debug
|
|
1528
|
-
totalRecords: 850
|
|
1529
|
-
totalPages: 5
|
|
1530
|
-
truncated: false
|
|
1531
|
-
truncationReason: undefined
|
|
1532
|
-
firstRecordId: "vp_001"
|
|
1533
|
-
lastRecordId: "vp_850"
|
|
1534
|
-
```
|
|
1535
|
-
|
|
1536
|
-
**Key Benefits**:
|
|
1537
|
-
|
|
1538
|
-
- Quickly identify pagination configuration issues
|
|
1539
|
-
- Verify date filters are applied correctly
|
|
1540
|
-
- Debug "no records found" scenarios
|
|
1541
|
-
- Validate ExtractionOrchestrator variable injection
|
|
1542
|
-
|
|
1543
|
-
**Production Best Practice**: Disable `DEBUG_GRAPHQL` in production to reduce log volume and avoid logging sensitive data.
|
|
1544
|
-
|
|
1545
|
-
---
|
|
1546
|
-
|
|
1547
|
-
**Pattern**: Enterprise incremental extraction with overlap buffer for virtual positions (ATP) - JSON format with ExtractionOrchestrator
|
|
1548
|
-
**Key Learning**: Use `catalogues` (plural, array) input for the Fluent API
|
|
1549
|
-
**Critical**: Apply 60-second overlap buffer to prevent missed records due to clock skew
|
|
1550
|
-
**Buffer Pattern**: Query WITH buffer (`updatedOn >= lastRunTime - 60s`), save WITHOUT buffer (`MAX(updatedOn)`)
|
|
1551
|
-
**Orchestration**: ExtractionOrchestrator + JobTracker for production-grade extraction workflows
|
|
1552
|
-
**Schema**: virtualPositions accepts `catalogues: [VirtualCatalogueKey]` where VirtualCatalogueKey = `{ ref: String! }`
|
|
1553
|
-
**Use Case**: Real-time ATP updates for order management systems; use camelCase for JSON fields
|
|
1554
|
-
|
|
1555
|
-
---
|
|
1556
|
-
|
|
1557
|
-
### Pattern 8: Backward Pagination (Optional - Advanced)
|
|
1558
|
-
|
|
1559
|
-
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
1560
|
-
|
|
1561
|
-
**When to Use**:
|
|
1562
|
-
|
|
1563
|
-
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
1564
|
-
- ✅ Time-bounded reverse traversal for auditing
|
|
1565
|
-
- ✅ Display newest-first in UI/reports
|
|
1566
|
-
- ❌ **Don't use for standard incremental sync** - use forward pagination (default)
|
|
1567
|
-
|
|
1568
|
-
**GraphQL Query Requirements**:
|
|
1569
|
-
|
|
1570
|
-
Your query must support backward pagination by including `$last` and `$before`:
|
|
1571
|
-
|
|
1572
|
-
```graphql
|
|
1573
|
-
query GetData(
|
|
1574
|
-
$retailerId: ID!
|
|
1575
|
-
$first: Int # For forward pagination
|
|
1576
|
-
$after: String # For forward pagination
|
|
1577
|
-
$last: Int # For backward pagination
|
|
1578
|
-
$before: String # For backward pagination
|
|
1579
|
-
) {
|
|
1580
|
-
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
1581
|
-
edges {
|
|
1582
|
-
cursor # ✅ REQUIRED
|
|
1583
|
-
node {
|
|
1584
|
-
id
|
|
1585
|
-
createdAt
|
|
1586
|
-
# ... other fields
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
pageInfo {
|
|
1590
|
-
hasNextPage # For forward
|
|
1591
|
-
hasPreviousPage # ✅ REQUIRED for backward
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
```
|
|
1596
|
-
|
|
1597
|
-
**Implementation**:
|
|
1598
|
-
|
|
1599
|
-
```typescript
|
|
1600
|
-
// Backward pagination - newest records first
|
|
1601
|
-
const result = await orchestrator.extract({
|
|
1602
|
-
query: YOUR_QUERY,
|
|
1603
|
-
resultPath: 'data.edges.node',
|
|
1604
|
-
variables: {
|
|
1605
|
-
retailerId,
|
|
1606
|
-
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
1607
|
-
// ❌ Don't include last/before - orchestrator injects them
|
|
1608
|
-
},
|
|
1609
|
-
pageSize: 200,
|
|
1610
|
-
direction: 'backward', // ✅ Enable reverse pagination
|
|
1611
|
-
maxRecords: 10000,
|
|
1612
|
-
});
|
|
1613
|
-
|
|
1614
|
-
// Records are returned in reverse chronological order
|
|
1615
|
-
log.info('Record order', {
|
|
1616
|
-
newestRecord: result.data[0].createdAt,
|
|
1617
|
-
oldestRecord: result.data[result.data.length - 1].createdAt
|
|
1618
|
-
});
|
|
1619
|
-
```
|
|
1620
|
-
|
|
1621
|
-
**Key Differences from Forward Pagination**:
|
|
1622
|
-
|
|
1623
|
-
| Aspect | Forward (Default) | Backward |
|
|
1624
|
-
| ---------------------- | -------------------------------- | ----------------------- |
|
|
1625
|
-
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
1626
|
-
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
1627
|
-
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
1628
|
-
| **Cursor Source** | Last edge of page | First edge of page |
|
|
1629
|
-
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
1630
|
-
|
|
1631
|
-
**Important Notes**:
|
|
1632
|
-
|
|
1633
|
-
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
1634
|
-
|
|
1635
|
-
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
1636
|
-
|
|
1637
|
-
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
1638
|
-
|
|
1639
|
-
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
1640
|
-
|
|
1641
|
-
**Example: Extract Latest 1000 Orders**
|
|
1642
|
-
|
|
1643
|
-
```typescript
|
|
1644
|
-
const latestOrders = await orchestrator.extract({
|
|
1645
|
-
query: ORDERS_QUERY,
|
|
1646
|
-
resultPath: 'orders.edges.node',
|
|
1647
|
-
variables: {
|
|
1648
|
-
retailerId,
|
|
1649
|
-
statuses: ['BOOKED', 'ALLOCATED'],
|
|
1650
|
-
},
|
|
1651
|
-
direction: 'backward', // Start from newest
|
|
1652
|
-
maxRecords: 1000, // Stop after 1000 records
|
|
1653
|
-
pageSize: 100, // 100 per page = 10 pages
|
|
1654
|
-
});
|
|
1655
|
-
|
|
1656
|
-
// latestOrders.data[0] is the newest order
|
|
1657
|
-
// latestOrders.data[999] is the 1000th newest order
|
|
1658
|
-
```
|
|
1659
|
-
|
|
1660
|
-
**When to Use Forward vs Backward**:
|
|
1661
|
-
|
|
1662
|
-
```typescript
|
|
1663
|
-
// ✅ Forward (default) - For incremental sync
|
|
1664
|
-
const incrementalData = await orchestrator.extract({
|
|
1665
|
-
query: YOUR_QUERY,
|
|
1666
|
-
resultPath: 'data.edges.node',
|
|
1667
|
-
variables: {
|
|
1668
|
-
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
1669
|
-
},
|
|
1670
|
-
// direction defaults to 'forward'
|
|
1671
|
-
// Processes oldest → newest for proper sequencing
|
|
1672
|
-
});
|
|
1673
|
-
|
|
1674
|
-
// ✅ Backward - For "latest N records" use cases
|
|
1675
|
-
const latestData = await orchestrator.extract({
|
|
1676
|
-
query: YOUR_QUERY,
|
|
1677
|
-
resultPath: 'data.edges.node',
|
|
1678
|
-
direction: 'backward',
|
|
1679
|
-
maxRecords: 100, // Just get latest 100
|
|
1680
|
-
// Gets newest → oldest
|
|
1681
|
-
});
|
|
1682
|
-
```
|
|
1683
|
-
|
|
1684
|
-
**Pagination Variables Reference**:
|
|
1685
|
-
|
|
1686
|
-
| Variable | Forward | Backward | Injected By | Notes |
|
|
1687
|
-
| -------- | ----------- | ----------- | ------------ | ------------------------ |
|
|
1688
|
-
| `first` | ✅ Used | ❌ Not used | Orchestrator | From `pageSize` |
|
|
1689
|
-
| `after` | ✅ Used | ❌ Not used | Orchestrator | From cursor (last edge) |
|
|
1690
|
-
| `last` | ❌ Not used | ✅ Used | Orchestrator | From `pageSize` |
|
|
1691
|
-
| `before` | ❌ Not used | ✅ Used | Orchestrator | From cursor (first edge) |
|
|
1692
|
-
|
|
1693
|
-
**Common Mistakes to Avoid**:
|
|
1694
|
-
|
|
1695
|
-
```typescript
|
|
1696
|
-
// ❌ WRONG - Don't pass pagination variables
|
|
1697
|
-
const result = await orchestrator.extract({
|
|
1698
|
-
variables: {
|
|
1699
|
-
last: 200, // ❌ Orchestrator will override this
|
|
1700
|
-
before: cursor, // ❌ Orchestrator manages cursor
|
|
1701
|
-
},
|
|
1702
|
-
direction: 'backward',
|
|
1703
|
-
});
|
|
1704
|
-
|
|
1705
|
-
// ✅ CORRECT - Let orchestrator inject pagination
|
|
1706
|
-
const result = await orchestrator.extract({
|
|
1707
|
-
variables: {
|
|
1708
|
-
retailerId, // ✅ Your business variables only
|
|
1709
|
-
},
|
|
1710
|
-
pageSize: 200, // ✅ Orchestrator uses this for last/before
|
|
1711
|
-
direction: 'backward',
|
|
1712
|
-
});
|
|
1713
|
-
```
|
|
1714
|
-
|
|
1715
|
-
#### Optional: Reverse Pagination
|
|
1716
|
-
|
|
1717
|
-
- Forward default; reverse optional with $last/$before + pageInfo.hasPreviousPage.
|
|
1718
|
-
|
|
1719
|
-
GraphQL:
|
|
1720
|
-
|
|
1721
|
-
```graphql
|
|
1722
|
-
query GetVirtualPositionsBackward($last: Int!, $before: String) {
|
|
1723
|
-
virtualPositions(last: $last, before: $before) {
|
|
1724
|
-
edges {
|
|
1725
|
-
cursor
|
|
1726
|
-
node {
|
|
1727
|
-
id
|
|
1728
|
-
ref
|
|
1729
|
-
updatedOn
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
pageInfo {
|
|
1733
|
-
hasPreviousPage
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
```
|
|
1738
|
-
|
|
1739
|
-
SDK:
|
|
1740
|
-
|
|
1741
|
-
```typescript
|
|
1742
|
-
await orchestrator.extract({
|
|
1743
|
-
query: VIRTUAL_POSITIONS_BACKWARD_QUERY,
|
|
1744
|
-
resultPath: 'virtualPositions.edges.node',
|
|
1745
|
-
variables: {},
|
|
1746
|
-
pageSize,
|
|
1747
|
-
direction: 'backward',
|
|
1748
|
-
});
|
|
1749
|
-
```
|
|
1750
|
-
|
|
1751
|
-
---
|
|
1752
|
-
|
|
1753
|
-
## Testing Checklist
|
|
1754
|
-
|
|
1755
|
-
**Before production deployment:**
|
|
1756
|
-
|
|
1757
|
-
### 1. Schema Validation
|
|
1758
|
-
|
|
1759
|
-
- [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
|
|
1760
|
-
- [ ] Run `npx fc-connect validate-schema --mapping ./config/virtual-positions.export.json --schema ./fluent-schema.json`
|
|
1761
|
-
- [ ] Run `npx fc-connect analyze-coverage --mapping ./config/virtual-positions.export.json --schema ./fluent-schema.json`
|
|
1762
|
-
- [ ] Verify all `source` paths in mapping exist in GraphQL schema
|
|
1763
|
-
- [ ] Verify query structure matches schema (fields, types, filters)
|
|
1764
|
-
|
|
1765
|
-
### 2. Extraction Testing
|
|
1766
|
-
|
|
1767
|
-
- [ ] Test with small dataset first (maxRecords=10)
|
|
1768
|
-
- [ ] Verify ExtractionOrchestrator pagination works correctly
|
|
1769
|
-
- [ ] Test with multiple pages of data (verify cursor handling)
|
|
1770
|
-
- [ ] Verify date range filtering (updatedOn filter)
|
|
1771
|
-
- [ ] Test empty result handling (no records in date range)
|
|
1772
|
-
- [ ] Verify extraction stops at maxRecords limit
|
|
1773
|
-
|
|
1774
|
-
### 3. Mapping Testing
|
|
1775
|
-
|
|
1776
|
-
- [ ] Verify required fields are populated
|
|
1777
|
-
- [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
|
|
1778
|
-
- [ ] Test custom resolvers with edge cases (if any)
|
|
1779
|
-
- [ ] Verify nested field extraction
|
|
1780
|
-
- [ ] Test with null/missing fields
|
|
1781
|
-
- [ ] Verify mapping error collection works
|
|
1782
|
-
|
|
1783
|
-
### 4. JSON Generation Testing
|
|
1784
|
-
|
|
1785
|
-
- [ ] Verify JSON structure matches expected format
|
|
1786
|
-
- [ ] Test JSON validation against schema (if applicable)
|
|
1787
|
-
- [ ] Verify proper nesting and structure
|
|
1788
|
-
- [ ] Test with large datasets (>1000 records)
|
|
1789
|
-
- [ ] Verify UTF-8 encoding
|
|
1790
|
-
- [ ] Test special character escaping
|
|
1791
|
-
|
|
1792
|
-
### 5. S3 Upload Testing
|
|
1793
|
-
|
|
1794
|
-
- [ ] Test S3 connection and authentication
|
|
1795
|
-
- [ ] Verify file upload to correct bucket and path
|
|
1796
|
-
- [ ] Test file naming convention (timestamp format)
|
|
1797
|
-
- [ ] Verify S3 object metadata
|
|
1798
|
-
- [ ] Test upload retry logic (simulate network failure)
|
|
1799
|
-
- [ ] Verify file permissions and ACLs
|
|
1800
|
-
|
|
1801
|
-
### 6. State Management Testing
|
|
1802
|
-
|
|
1803
|
-
- [ ] Verify overlap buffer prevents missed records (60-second default)
|
|
1804
|
-
- [ ] Test state recovery after extraction failure
|
|
1805
|
-
- [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
|
|
1806
|
-
- [ ] Test first run with no previous state (uses fallbackStartDate)
|
|
1807
|
-
- [ ] Verify state update only happens on successful upload
|
|
1808
|
-
- [ ] Test manual date override (doesn't update state)
|
|
1809
|
-
|
|
1810
|
-
### 7. Job Tracking Testing
|
|
1811
|
-
|
|
1812
|
-
- [ ] Test job creation with JobTracker
|
|
1813
|
-
- [ ] Verify job status updates at each stage
|
|
1814
|
-
- [ ] Test job completion with metadata
|
|
1815
|
-
- [ ] Test job failure handling
|
|
1816
|
-
- [ ] Query job status via webhook endpoint
|
|
1817
|
-
- [ ] Verify job status persists in KV store
|
|
1818
|
-
|
|
1819
|
-
### 8. Error Handling Testing
|
|
1820
|
-
|
|
1821
|
-
- [ ] Test with invalid GraphQL query
|
|
1822
|
-
- [ ] Test with mapping errors (invalid field paths)
|
|
1823
|
-
- [ ] Test with S3 connection failures
|
|
1824
|
-
- [ ] Test with authentication failures
|
|
1825
|
-
- [ ] Test with network timeouts
|
|
1826
|
-
- [ ] Verify error logging includes context (jobId, stage, error details)
|
|
1827
|
-
- [ ] Test error threshold logic (if applicable)
|
|
1828
|
-
|
|
1829
|
-
### 9. Staging Environment Testing
|
|
1830
|
-
|
|
1831
|
-
- [ ] Run full extraction in staging environment
|
|
1832
|
-
- [ ] Verify JSON file format with downstream system
|
|
1833
|
-
- [ ] Monitor extraction duration and resource usage
|
|
1834
|
-
- [ ] Test with production-like data volumes
|
|
1835
|
-
- [ ] Verify no performance degradation over time
|
|
1836
|
-
|
|
1837
|
-
### 10. Integration Testing
|
|
1838
|
-
|
|
1839
|
-
- [ ] Test scheduled workflow (cron trigger)
|
|
1840
|
-
- [ ] Test ad hoc webhook trigger
|
|
1841
|
-
- [ ] Test job status query webhook
|
|
1842
|
-
- [ ] Verify activation variables are read correctly
|
|
1843
|
-
- [ ] Test with different extraction modes (incremental, date range)
|
|
1844
|
-
- [ ] End-to-end test: trigger → extract → transform → upload → verify file
|
|
1845
|
-
|
|
1846
|
-
---
|
|
1847
|
-
## Monitoring & Alerting
|
|
1848
|
-
|
|
1849
|
-
### Success Response Example
|
|
1850
|
-
|
|
1851
|
-
```json
|
|
1852
|
-
{
|
|
1853
|
-
"success": true,
|
|
1854
|
-
"jobId": "SCHEDULED_VP_20251102_140000_abc123",
|
|
1855
|
-
"recordsExtracted": 1523,
|
|
1856
|
-
"fileName": "virtual-positions-2025-11-02T14-00-00-000Z.json",
|
|
1857
|
-
"s3Path": "s3://bucket/virtual-positions/virtual-positions-2025-11-02T14-00-00-000Z.json",
|
|
1858
|
-
"metrics": {
|
|
1859
|
-
"extractionDurationMs": 12543,
|
|
1860
|
-
"totalPages": 8,
|
|
1861
|
-
"pageSize": 200,
|
|
1862
|
-
"mappingErrors": 0,
|
|
1863
|
-
"fileSizeBytes": 524288,
|
|
1864
|
-
"uploadDurationMs": 1234
|
|
1865
|
-
},
|
|
1866
|
-
"timestamps": {
|
|
1867
|
-
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
1868
|
-
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
1869
|
-
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
1870
|
-
},
|
|
1871
|
-
"state": {
|
|
1872
|
-
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
1873
|
-
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
1874
|
-
"stateUpdated": true,
|
|
1875
|
-
"overlapBufferSeconds": 60
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
```
|
|
1879
|
-
|
|
1880
|
-
### Error Response Example
|
|
1881
|
-
|
|
1882
|
-
```json
|
|
1883
|
-
{
|
|
1884
|
-
"success": false,
|
|
1885
|
-
"jobId": "ADHOC_VP_20251102_140500_xyz789",
|
|
1886
|
-
"error": "S3 upload failed: Connection timeout",
|
|
1887
|
-
"errorCategory": "NETWORK",
|
|
1888
|
-
"recordsExtracted": 0,
|
|
1889
|
-
"stage": "s3_upload",
|
|
1890
|
-
"details": {
|
|
1891
|
-
"message": "Failed to upload file after 3 retry attempts",
|
|
1892
|
-
"retryAttempts": 3,
|
|
1893
|
-
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
1894
|
-
},
|
|
1895
|
-
"state": {
|
|
1896
|
-
"stateUpdated": false,
|
|
1897
|
-
"willRetryNextRun": true,
|
|
1898
|
-
"note": "State not advanced - next extraction will retry same time window"
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
```
|
|
1902
|
-
|
|
1903
|
-
### Key Metrics to Track
|
|
1904
|
-
|
|
1905
|
-
```typescript
|
|
1906
|
-
const METRICS = {
|
|
1907
|
-
// Extraction Performance
|
|
1908
|
-
extractionDurationMs: Date.now() - extractionStart,
|
|
1909
|
-
recordCount: records.length,
|
|
1910
|
-
pageCount: extractionResult.stats.totalPages,
|
|
1911
|
-
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
1912
|
-
|
|
1913
|
-
// Transformation Performance
|
|
1914
|
-
transformedCount: transformedRecords.length,
|
|
1915
|
-
failedCount: mappingErrors.length,
|
|
1916
|
-
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
1917
|
-
|
|
1918
|
-
// File Generation
|
|
1919
|
-
fileSizeMB: (jsonContent.length / (1024 * 1024)).toFixed(2),
|
|
1920
|
-
|
|
1921
|
-
// Upload Performance
|
|
1922
|
-
uploadDurationMs: uploadEnd - uploadStart,
|
|
1923
|
-
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
1924
|
-
|
|
1925
|
-
// State Management
|
|
1926
|
-
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
1927
|
-
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
1928
|
-
};
|
|
1929
|
-
|
|
1930
|
-
log.info('Extraction metrics', metrics);
|
|
1931
|
-
```
|
|
1932
|
-
|
|
1933
|
-
### Alert Thresholds
|
|
1934
|
-
|
|
1935
|
-
```typescript
|
|
1936
|
-
const ALERT_THRESHOLDS = {
|
|
1937
|
-
// Duration Alerts
|
|
1938
|
-
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
1939
|
-
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
1940
|
-
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
1941
|
-
|
|
1942
|
-
// Error Rate Alerts
|
|
1943
|
-
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
1944
|
-
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
1945
|
-
|
|
1946
|
-
// Volume Alerts
|
|
1947
|
-
MAX_RECORDS_PER_RUN: 100000,
|
|
1948
|
-
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
1949
|
-
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
1950
|
-
|
|
1951
|
-
// State Alerts
|
|
1952
|
-
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
1953
|
-
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
1954
|
-
};
|
|
1955
|
-
|
|
1956
|
-
// Check thresholds
|
|
1957
|
-
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
1958
|
-
log.warn('Extraction duration exceeded threshold', {
|
|
1959
|
-
duration: metrics.extractionDurationMs,
|
|
1960
|
-
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
1961
|
-
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
1962
|
-
});
|
|
1963
|
-
}
|
|
1964
|
-
```
|
|
1965
|
-
|
|
1966
|
-
### Monitoring Dashboard Queries
|
|
1967
|
-
|
|
1968
|
-
**Versori Platform Logs Query:**
|
|
1969
|
-
|
|
1970
|
-
```
|
|
1971
|
-
# Successful extractions
|
|
1972
|
-
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
1973
|
-
|
|
1974
|
-
# Failed extractions
|
|
1975
|
-
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
1976
|
-
|
|
1977
|
-
# Performance issues
|
|
1978
|
-
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
1979
|
-
|
|
1980
|
-
# High error rates
|
|
1981
|
-
errorRate:>5
|
|
1982
|
-
|
|
1983
|
-
# State management issues
|
|
1984
|
-
stateUpdated:false AND success:true
|
|
1985
|
-
```
|
|
1986
|
-
|
|
1987
|
-
### Common Issues and Solutions
|
|
1988
|
-
|
|
1989
|
-
**Issue**: "Extraction timeout after 10 minutes"
|
|
1990
|
-
|
|
1991
|
-
- **Cause**: Too many records in single extraction
|
|
1992
|
-
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
1993
|
-
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
1994
|
-
|
|
1995
|
-
**Issue**: "Mapping errors for 50% of records"
|
|
1996
|
-
|
|
1997
|
-
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
1998
|
-
- **Fix**: Run schema validation, update mapping config paths
|
|
1999
|
-
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
2000
|
-
|
|
2001
|
-
**Issue**: "S3 connection timeout"
|
|
2002
|
-
|
|
2003
|
-
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
2004
|
-
- **Fix**: Check S3 credentials, verify network connectivity
|
|
2005
|
-
- **Prevention**: Implement connection health checks, monitor connection status
|
|
2006
|
-
|
|
2007
|
-
**Issue**: "State not updating after successful extraction"
|
|
2008
|
-
|
|
2009
|
-
- **Cause**: KV write failure or intentional retry logic
|
|
2010
|
-
- **Fix**: Check KV logs, verify state update code executed
|
|
2011
|
-
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
2012
|
-
|
|
2013
|
-
**Issue**: "First run exceeds record limits"
|
|
2014
|
-
|
|
2015
|
-
- **Cause**: No previous timestamp, fetches all historical records
|
|
2016
|
-
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
2017
|
-
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
2018
|
-
|
|
2019
|
-
**Issue**: "Excessive duplicate records in output"
|
|
2020
|
-
|
|
2021
|
-
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
2022
|
-
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
2023
|
-
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
2024
|
-
|
|
2025
|
-
---
|
|
2026
|
-
|
|
2027
|
-
## Troubleshooting Quick Reference
|
|
2028
|
-
|
|
2029
|
-
| Error Message | Likely Cause | Solution |
|
|
2030
|
-
|--------------|--------------|----------|
|
|
2031
|
-
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
2032
|
-
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
2033
|
-
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
2034
|
-
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
2035
|
-
| "S3 authentication failed" | Invalid credentials | Verify S3 credentials in activation variables |
|
|
2036
|
-
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
2037
|
-
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
2038
|
-
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
2039
|
-
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
2040
|
-
| "JSON generation failed" | Format-specific error | Check JSON generation logic, validate output |
|
|
2041
|
-
|
|
2042
|
-
---
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-extract-virtual-positions-graphql-to-s3-json
|
|
3
|
+
canonical_filename: template-extraction-virtual-positions-to-s3-json.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-json
|
|
10
|
+
entity: virtual-positions
|
|
11
|
+
format: json
|
|
12
|
+
logging: versori
|
|
13
|
+
status: stable
|
|
14
|
+
features:
|
|
15
|
+
- memory-management
|
|
16
|
+
- enhanced-logging
|
|
17
|
+
- pagination-progress
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Template: Extraction - Virtual Positions GraphQL to S3 JSON
|
|
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
|
+
Create a Versori scheduled extractor for virtualPositions using ExtractionOrchestrator with JobTracker for job lifecycle management, supports incremental runs with overlap buffer, transforms via UniversalMapper, and uploads JSON to S3 using S3DataSource. Use native Versori logging and KV state. Include three workflows: scheduled incremental, ad hoc manual trigger, and job status query.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 📦 SDK Imports (Verified - Versori Optimized)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { Buffer } from 'node:buffer';
|
|
66
|
+
import {
|
|
67
|
+
createClient,
|
|
68
|
+
ExtractionOrchestrator,
|
|
69
|
+
JobTracker,
|
|
70
|
+
UniversalMapper,
|
|
71
|
+
S3DataSource,
|
|
72
|
+
JSONBuilder,
|
|
73
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
74
|
+
|
|
75
|
+
import { schedule, webhook, http } from '@versori/run';
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
# Versori Scheduled: Virtual Positions Extraction to S3 JSON (ATP/Allocation)
|
|
81
|
+
|
|
82
|
+
**FC Connect SDK Use Case Guide**
|
|
83
|
+
|
|
84
|
+
> SDK: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
85
|
+
> Version: Use ^0.1.39 - `npm install @fluentcommerce/fc-connect-sdk@^0.1.39`
|
|
86
|
+
|
|
87
|
+
Context: Scheduled Versori workflow that extracts virtual positions (ATP/allocated inventory) from Fluent Commerce via GraphQL query using **ExtractionOrchestrator with JobTracker**, transforms with `UniversalMapper`, and writes JSON files to S3 for API consumption and real-time order promising.
|
|
88
|
+
|
|
89
|
+
**Pattern**: EXTRACTION (Fluent → S3 JSON)
|
|
90
|
+
**Complexity**: Medium | Runtime: Versori Platform (Scheduled)
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## ⚠️ IMPORTANT: Production-Ready Base Template
|
|
95
|
+
|
|
96
|
+
> **📋 BASE TEMPLATE - Ready for Production (Customize for Your Needs)**
|
|
97
|
+
>
|
|
98
|
+
> This is a **production-ready base template** demonstrating FC Connect SDK best practices for virtual position extraction workflows with JSON output to S3.
|
|
99
|
+
>
|
|
100
|
+
> **✅ INCLUDED FEATURES:**
|
|
101
|
+
>
|
|
102
|
+
> - ✅ Comprehensive error handling with retry logic
|
|
103
|
+
> - ✅ S3 upload with proper error handling
|
|
104
|
+
> - ✅ State management with overlap buffer (prevents missed records)
|
|
105
|
+
> - ✅ Job tracking with lifecycle management
|
|
106
|
+
> - ✅ Security (credential masking in logs)
|
|
107
|
+
> - ✅ UTC time enforcement (prevents timezone bugs)
|
|
108
|
+
> - ✅ Incremental extraction (safe, efficient, production-ready)
|
|
109
|
+
> - ✅ Natural rate limiting via timestamps
|
|
110
|
+
>
|
|
111
|
+
> **📝 BEFORE DEPLOYING:**
|
|
112
|
+
>
|
|
113
|
+
> 1. Review and customize activation variables for your environment
|
|
114
|
+
> 2. Test with sample data in your Versori workspace
|
|
115
|
+
> 3. Adjust safety limits (pageSize, maxRecords) if needed
|
|
116
|
+
> 4. Configure monitoring alerts for extraction failures
|
|
117
|
+
> 5. Verify S3 bucket credentials and paths
|
|
118
|
+
>
|
|
119
|
+
> **This base template follows SDK best practices - tweak specific to your needs.**
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## What You'll Build
|
|
124
|
+
|
|
125
|
+
- **Three Versori workflows**: Scheduled incremental, ad hoc manual trigger, job status query
|
|
126
|
+
- **ExtractionOrchestrator** for high-level extraction orchestration
|
|
127
|
+
- **JobTracker** for job lifecycle and state management
|
|
128
|
+
- **Incremental extraction** using `updatedOn > lastRunTime` filter with overlap buffer
|
|
129
|
+
- **State management** with VersoriKV to track last successful run
|
|
130
|
+
- GraphQL query with auto-pagination
|
|
131
|
+
- UniversalMapper transformation with nested locationLink data
|
|
132
|
+
- JSON file generation with metadata wrapper
|
|
133
|
+
- S3 upload for API consumption
|
|
134
|
+
- **Pretty print option** for human-readable JSON
|
|
135
|
+
- **Failure recovery** with timestamp tracking
|
|
136
|
+
|
|
137
|
+
## Business Use Case
|
|
138
|
+
|
|
139
|
+
**Real-time ATP API for external systems:**
|
|
140
|
+
|
|
141
|
+
- Extract ATP calculations every 15 minutes
|
|
142
|
+
- Export as JSON for REST API consumption
|
|
143
|
+
- Support webhooks/polling by downstream systems
|
|
144
|
+
- Lightweight incremental updates for order promising
|
|
145
|
+
- Enable real-time inventory allocation decisions
|
|
146
|
+
- Feed to order management and ecommerce platforms
|
|
147
|
+
|
|
148
|
+
## Virtual Positions Explained
|
|
149
|
+
|
|
150
|
+
**VirtualPosition** = ATP (Available To Promise) calculation
|
|
151
|
+
|
|
152
|
+
- Represents virtual/allocated inventory for promising
|
|
153
|
+
- Calculated quantity available for new orders
|
|
154
|
+
- Grouped by location or category
|
|
155
|
+
- Used for: Order promising, allocation tracking, ATP feeds
|
|
156
|
+
|
|
157
|
+
**vs InventoryPosition** = Physical on-hand calculation
|
|
158
|
+
|
|
159
|
+
- Actual stock in warehouse
|
|
160
|
+
- Used for: Stock reporting
|
|
161
|
+
|
|
162
|
+
**vs InventoryQuantity** = Detailed quantity records
|
|
163
|
+
|
|
164
|
+
- Individual quantity records by type
|
|
165
|
+
- Used for: Audit trails
|
|
166
|
+
|
|
167
|
+
## SDK Methods Used
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { Buffer } from 'node:buffer';
|
|
171
|
+
import {
|
|
172
|
+
createClient,
|
|
173
|
+
ExtractionOrchestrator,
|
|
174
|
+
JobTracker,
|
|
175
|
+
UniversalMapper,
|
|
176
|
+
S3DataSource,
|
|
177
|
+
JSONBuilder,
|
|
178
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
179
|
+
|
|
180
|
+
await createClient(ctx); // Versori-aware client
|
|
181
|
+
new ExtractionOrchestrator(client, log); // High-level orchestration
|
|
182
|
+
new JobTracker(ctx.openKv(':project:'), log); // Job tracking
|
|
183
|
+
await orchestrator.extractToS3Json({ query, mapping, s3Config, ... }); // Auto-pagination + mapping + upload
|
|
184
|
+
new UniversalMapper(exportMapping); // Field transformation
|
|
185
|
+
const jsonBuilder = new JSONBuilder({ prettyPrint: true, indent: 2 });
|
|
186
|
+
const jsonContent = jsonBuilder.build(dataObject); // JSON generation
|
|
187
|
+
await s3.uploadFile(key, Buffer.from(jsonContent, 'utf8'), options); // S3 upload
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Activation Variables
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"catalogueRefs": "DEFAULT_VIRTUAL_CATALOGUE",
|
|
195
|
+
"s3BucketName": "atp-api-exports",
|
|
196
|
+
"awsAccessKeyId": "AKIAXXXXXXXXXXXX",
|
|
197
|
+
"awsSecretAccessKey": "********",
|
|
198
|
+
"awsRegion": "us-east-1",
|
|
199
|
+
"s3Prefix": "virtual-positions/api/",
|
|
200
|
+
"pageSize": 200,
|
|
201
|
+
"maxRecords": 100000,
|
|
202
|
+
"fallbackStartDate": "2024-01-01T00:00:00Z",
|
|
203
|
+
"overlapBufferSeconds": "60",
|
|
204
|
+
"positionTypes": "",
|
|
205
|
+
"groupRefs": "",
|
|
206
|
+
"productRefs": "",
|
|
207
|
+
"prettyPrint": "true",
|
|
208
|
+
"DEBUG_GRAPHQL": "false"
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Package Configuration
|
|
213
|
+
|
|
214
|
+
Create file: `package.json`
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"name": "virtual-positions-extraction-json",
|
|
219
|
+
"version": "1.0.0",
|
|
220
|
+
"type": "module",
|
|
221
|
+
"dependencies": {
|
|
222
|
+
"@fluentcommerce/fc-connect-sdk": "^0.1.39",
|
|
223
|
+
"@versori/run": "latest"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Export Mapping Configuration
|
|
229
|
+
|
|
230
|
+
Create file: `./config/virtual-positions.export.json`
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"name": "virtual-positions.export",
|
|
235
|
+
"version": "1.0.0",
|
|
236
|
+
"description": "Fluent Virtual Positions → JSON API Export (with nested locationLink)",
|
|
237
|
+
"fields": {
|
|
238
|
+
"positionRef": { "source": "ref", "required": true, "resolver": "sdk.trim" },
|
|
239
|
+
"productRef": { "source": "productRef", "required": true, "resolver": "sdk.trim" },
|
|
240
|
+
"locationRef": { "source": "locationLink.ref", "required": false, "resolver": "sdk.trim" },
|
|
241
|
+
"locationName": { "source": "locationLink.name", "required": false, "resolver": "sdk.trim" },
|
|
242
|
+
"quantity": { "source": "quantity", "required": true, "resolver": "sdk.parseInt" },
|
|
243
|
+
"type": { "source": "type", "required": true, "resolver": "sdk.uppercase" },
|
|
244
|
+
"groupRef": { "source": "groupRef", "required": false, "resolver": "sdk.trim" },
|
|
245
|
+
"status": { "source": "status", "required": false, "resolver": "sdk.uppercase" },
|
|
246
|
+
"catalogueRef": { "source": "catalogue.ref", "required": false, "resolver": "sdk.trim" },
|
|
247
|
+
"catalogueName": { "source": "catalogue.name", "required": false, "resolver": "sdk.trim" },
|
|
248
|
+
"createdOn": { "source": "createdOn", "required": true, "resolver": "sdk.toString" },
|
|
249
|
+
"updatedOn": { "source": "updatedOn", "required": true, "resolver": "sdk.toString" }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## GraphQL Query
|
|
255
|
+
|
|
256
|
+
**Note**: This query is verified against Fluent Commerce schema introspection.
|
|
257
|
+
|
|
258
|
+
```graphql
|
|
259
|
+
query GetVirtualPositions(
|
|
260
|
+
$catalogues: [VirtualCatalogueKey]
|
|
261
|
+
$dateRangeFilter: DateRange
|
|
262
|
+
$productRefs: [String!]
|
|
263
|
+
$types: [String!]
|
|
264
|
+
$groupRefs: [String]
|
|
265
|
+
$first: Int!
|
|
266
|
+
$after: String
|
|
267
|
+
) {
|
|
268
|
+
virtualPositions(
|
|
269
|
+
catalogues: $catalogues
|
|
270
|
+
updatedOn: $dateRangeFilter
|
|
271
|
+
productRef: $productRefs
|
|
272
|
+
type: $types
|
|
273
|
+
groupRef: $groupRefs
|
|
274
|
+
first: $first
|
|
275
|
+
after: $after
|
|
276
|
+
) {
|
|
277
|
+
edges {
|
|
278
|
+
node {
|
|
279
|
+
id
|
|
280
|
+
ref
|
|
281
|
+
productRef
|
|
282
|
+
quantity
|
|
283
|
+
type
|
|
284
|
+
groupRef
|
|
285
|
+
status
|
|
286
|
+
catalogue {
|
|
287
|
+
ref
|
|
288
|
+
name
|
|
289
|
+
}
|
|
290
|
+
createdOn
|
|
291
|
+
updatedOn
|
|
292
|
+
locationLink {
|
|
293
|
+
ref
|
|
294
|
+
name
|
|
295
|
+
status
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
cursor
|
|
299
|
+
}
|
|
300
|
+
pageInfo {
|
|
301
|
+
hasNextPage
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Versori Workflows Structure
|
|
310
|
+
|
|
311
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
312
|
+
|
|
313
|
+
**Trigger Types:**
|
|
314
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
315
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
316
|
+
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
317
|
+
|
|
318
|
+
**Execution Steps (chained to triggers):**
|
|
319
|
+
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
320
|
+
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
321
|
+
|
|
322
|
+
### Recommended Project Structure
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
virtual-positions-extraction/
|
|
326
|
+
├── index.ts # Entry point - exports all workflows
|
|
327
|
+
└── src/
|
|
328
|
+
├── workflows/
|
|
329
|
+
│ ├── scheduled/
|
|
330
|
+
│ │ └── daily-virtual-positions-extraction.ts # Scheduled: Daily extraction
|
|
331
|
+
│ │
|
|
332
|
+
│ └── webhook/
|
|
333
|
+
│ ├── adhoc-virtual-positions-extraction.ts # Webhook: Manual trigger
|
|
334
|
+
│ └── job-status-check.ts # Webhook: Status query
|
|
335
|
+
│
|
|
336
|
+
├── services/
|
|
337
|
+
│ └── virtual-positions-extraction.service.ts # Shared orchestration logic (reusable)
|
|
338
|
+
│
|
|
339
|
+
└── config/
|
|
340
|
+
└── virtual-positions.export.json # Mapping configuration
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"metadata": {
|
|
348
|
+
"extractedAt": "2025-01-22T14:30:00.000Z",
|
|
349
|
+
"recordCount": 3,
|
|
350
|
+
"incrementalFrom": "2025-01-22T10:00:00.000Z",
|
|
351
|
+
"incrementalTo": "2025-01-22T14:30:00.000Z",
|
|
352
|
+
"jobId": "job_abc123",
|
|
353
|
+
"extractionMode": "incremental"
|
|
354
|
+
},
|
|
355
|
+
"data": [
|
|
356
|
+
{
|
|
357
|
+
"positionRef": "VPOS-001",
|
|
358
|
+
"productRef": "PROD-SKU-001",
|
|
359
|
+
"locationRef": "LOC-001",
|
|
360
|
+
"locationName": "NYC Warehouse",
|
|
361
|
+
"quantity": 75,
|
|
362
|
+
"type": "DEFAULT",
|
|
363
|
+
"groupRef": "GROUP-EAST",
|
|
364
|
+
"status": "ACTIVE",
|
|
365
|
+
"catalogueRef": "DEFAULT_VIRTUAL_CATALOGUE",
|
|
366
|
+
"catalogueName": "Default Virtual",
|
|
367
|
+
"createdOn": "2025-01-15T10:00:00Z",
|
|
368
|
+
"updatedOn": "2025-01-22T08:30:00Z"
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
"positionRef": "VPOS-002",
|
|
372
|
+
"productRef": "PROD-SKU-002",
|
|
373
|
+
"locationRef": "LOC-002",
|
|
374
|
+
"locationName": "LA Warehouse",
|
|
375
|
+
"quantity": 50,
|
|
376
|
+
"type": "DEFAULT",
|
|
377
|
+
"groupRef": "GROUP-WEST",
|
|
378
|
+
"status": "ACTIVE",
|
|
379
|
+
"catalogueRef": "DEFAULT_VIRTUAL_CATALOGUE",
|
|
380
|
+
"catalogueName": "Default Virtual",
|
|
381
|
+
"createdOn": "2025-01-16T11:00:00Z",
|
|
382
|
+
"updatedOn": "2025-01-22T09:15:00Z"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"positionRef": "VPOS-003",
|
|
386
|
+
"productRef": "PROD-SKU-003",
|
|
387
|
+
"locationRef": "LOC-001",
|
|
388
|
+
"locationName": "NYC Warehouse",
|
|
389
|
+
"quantity": 100,
|
|
390
|
+
"type": "SEASONAL",
|
|
391
|
+
"groupRef": "GROUP-EAST",
|
|
392
|
+
"status": "ACTIVE",
|
|
393
|
+
"catalogueRef": "SEASONAL_VIRTUAL_CATALOGUE",
|
|
394
|
+
"catalogueName": "Seasonal Virtual",
|
|
395
|
+
"createdOn": "2025-01-17T12:00:00Z",
|
|
396
|
+
"updatedOn": "2025-01-22T10:00:00Z"
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Complete Workflow Implementation
|
|
403
|
+
|
|
404
|
+
The following sections demonstrate the three workflows. Each workflow should be in its own file following the modular project structure shown above.
|
|
405
|
+
|
|
406
|
+
### Entry Point: index.ts
|
|
407
|
+
|
|
408
|
+
**File:** `index.ts`
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
/**
|
|
412
|
+
* Entry point - Export all workflows for Versori platform
|
|
413
|
+
*
|
|
414
|
+
* This file exports all workflows to be registered with Versori.
|
|
415
|
+
* Each workflow is defined in its own file for better organization.
|
|
416
|
+
*/
|
|
417
|
+
|
|
418
|
+
// Scheduled workflows
|
|
419
|
+
export { virtualPositionsExtractionJson } from './src/workflows/scheduled/daily-virtual-positions-extraction';
|
|
420
|
+
|
|
421
|
+
// Webhook workflows
|
|
422
|
+
export { virtualPositionsManualExtraction } from './src/workflows/webhook/adhoc-virtual-positions-extraction';
|
|
423
|
+
export { virtualPositionsJobStatus } from './src/workflows/webhook/job-status-check';
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Workflow 1: Scheduled Incremental Extraction
|
|
427
|
+
|
|
428
|
+
**File:** `src/workflows/scheduled/daily-virtual-positions-extraction.ts`
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { schedule, http } from '@versori/run';
|
|
432
|
+
import { Buffer } from 'node:buffer';
|
|
433
|
+
import {
|
|
434
|
+
createClient,
|
|
435
|
+
ExtractionOrchestrator,
|
|
436
|
+
JobTracker,
|
|
437
|
+
UniversalMapper,
|
|
438
|
+
S3DataSource,
|
|
439
|
+
JSONBuilder,
|
|
440
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
441
|
+
import virtualPositionsExportMapping from './config/virtual-positions.export.json' with { type: 'json' };
|
|
442
|
+
|
|
443
|
+
// GraphQL query
|
|
444
|
+
const VIRTUAL_POSITIONS_QUERY = `
|
|
445
|
+
query GetVirtualPositions(
|
|
446
|
+
$catalogues: [VirtualCatalogueKey]
|
|
447
|
+
$dateRangeFilter: DateRange
|
|
448
|
+
$productRefs: [String!]
|
|
449
|
+
$types: [String!]
|
|
450
|
+
$groupRefs: [String]
|
|
451
|
+
$first: Int!
|
|
452
|
+
$after: String
|
|
453
|
+
) {
|
|
454
|
+
virtualPositions(
|
|
455
|
+
catalogues: $catalogues
|
|
456
|
+
updatedOn: $dateRangeFilter
|
|
457
|
+
productRef: $productRefs
|
|
458
|
+
type: $types
|
|
459
|
+
groupRef: $groupRefs
|
|
460
|
+
first: $first
|
|
461
|
+
after: $after
|
|
462
|
+
) {
|
|
463
|
+
edges {
|
|
464
|
+
node {
|
|
465
|
+
id
|
|
466
|
+
ref
|
|
467
|
+
productRef
|
|
468
|
+
quantity
|
|
469
|
+
type
|
|
470
|
+
groupRef
|
|
471
|
+
status
|
|
472
|
+
catalogue {
|
|
473
|
+
ref
|
|
474
|
+
name
|
|
475
|
+
}
|
|
476
|
+
createdOn
|
|
477
|
+
updatedOn
|
|
478
|
+
locationLink {
|
|
479
|
+
ref
|
|
480
|
+
name
|
|
481
|
+
status
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
cursor
|
|
485
|
+
}
|
|
486
|
+
pageInfo {
|
|
487
|
+
hasNextPage
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
`;
|
|
492
|
+
|
|
493
|
+
export const virtualPositionsExtractionJson = schedule(
|
|
494
|
+
'virtual-positions-extract-json-15min',
|
|
495
|
+
'*/15 * * * *'
|
|
496
|
+
).then(
|
|
497
|
+
http('extract-virtual-positions-json', { connection: 'fluent_commerce' }, async ctx => {
|
|
498
|
+
const { log, openKv, activation } = ctx;
|
|
499
|
+
const executionStartTime = Date.now();
|
|
500
|
+
|
|
501
|
+
log.info('🚀 [EXTRACTION] Starting virtual positions extraction to S3 JSON', {
|
|
502
|
+
timestamp: new Date().toISOString(),
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// STEP 1/8: Parse activation variables
|
|
506
|
+
const catalogueRefsStr =
|
|
507
|
+
activation?.getVariable('catalogueRefs') || 'DEFAULT_VIRTUAL_CATALOGUE';
|
|
508
|
+
const catalogueRefsList = catalogueRefsStr
|
|
509
|
+
.split(',')
|
|
510
|
+
.map(ref => ref.trim())
|
|
511
|
+
.filter(Boolean);
|
|
512
|
+
const catalogues = catalogueRefsList.map(ref => ({ ref }));
|
|
513
|
+
|
|
514
|
+
const pageSize = parseInt(activation?.getVariable('pageSize') || '200', 10);
|
|
515
|
+
const maxRecords = parseInt(activation?.getVariable('maxRecords') || '100000', 10);
|
|
516
|
+
const fallbackStartDate =
|
|
517
|
+
activation?.getVariable('fallbackStartDate') || '2024-01-01T00:00:00Z';
|
|
518
|
+
const prettyPrint = activation?.getVariable('prettyPrint') === 'true';
|
|
519
|
+
const overlapBufferSeconds = parseInt(
|
|
520
|
+
activation?.getVariable('overlapBufferSeconds') || '60',
|
|
521
|
+
10
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const s3Config = {
|
|
525
|
+
bucket: activation?.getVariable('s3BucketName'),
|
|
526
|
+
region: activation?.getVariable('awsRegion') || 'us-east-1',
|
|
527
|
+
accessKeyId: activation?.getVariable('awsAccessKeyId'),
|
|
528
|
+
secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
|
|
529
|
+
};
|
|
530
|
+
const s3Prefix = activation?.getVariable('s3Prefix') || 'virtual-positions/api/';
|
|
531
|
+
|
|
532
|
+
// Optional filtering
|
|
533
|
+
const positionTypesStr = activation?.getVariable('positionTypes') || '';
|
|
534
|
+
const groupRefsStr = activation?.getVariable('groupRefs') || '';
|
|
535
|
+
const productRefsStr = activation?.getVariable('productRefs') || '';
|
|
536
|
+
|
|
537
|
+
const positionTypes = positionTypesStr
|
|
538
|
+
? positionTypesStr
|
|
539
|
+
.split(',')
|
|
540
|
+
.map(t => t.trim())
|
|
541
|
+
.filter(Boolean)
|
|
542
|
+
: undefined;
|
|
543
|
+
const groupRefs = groupRefsStr
|
|
544
|
+
? groupRefsStr
|
|
545
|
+
.split(',')
|
|
546
|
+
.map(g => g.trim())
|
|
547
|
+
.filter(Boolean)
|
|
548
|
+
: undefined;
|
|
549
|
+
const productRefs = productRefsStr
|
|
550
|
+
? productRefsStr
|
|
551
|
+
.split(',')
|
|
552
|
+
.map(p => p.trim())
|
|
553
|
+
.filter(Boolean)
|
|
554
|
+
: undefined;
|
|
555
|
+
|
|
556
|
+
// Validate required variables
|
|
557
|
+
const missing: string[] = [];
|
|
558
|
+
if (catalogues.length === 0) missing.push('catalogueRefs');
|
|
559
|
+
if (!s3Config.bucket) missing.push('s3BucketName');
|
|
560
|
+
if (!s3Config.accessKeyId) missing.push('awsAccessKeyId');
|
|
561
|
+
if (!s3Config.secretAccessKey) missing.push('awsSecretAccessKey');
|
|
562
|
+
if (missing.length)
|
|
563
|
+
return { success: false, error: `Missing required variables: ${missing.join(', ')}` };
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
// STEP 2/8: Initialize SDK services
|
|
567
|
+
log.info('📦 [INITIALIZATION] Creating Fluent Commerce client');
|
|
568
|
+
const client = await createClient(ctx);
|
|
569
|
+
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
570
|
+
const jobTracker = new JobTracker(openKv(':project:'), log);
|
|
571
|
+
log.info('✅ [INITIALIZATION] SDK services initialized successfully');
|
|
572
|
+
|
|
573
|
+
// STEP 3/8: Determine incremental date range with overlap buffer
|
|
574
|
+
log.info('📅 [STATE] Loading extraction state');
|
|
575
|
+
const stateKey = ['extraction', 'virtual-positions-json', 'lastRunTime'];
|
|
576
|
+
const kv = openKv(':project:');
|
|
577
|
+
const lastRunState = await kv.get(stateKey);
|
|
578
|
+
const rawLastRunTime = lastRunState?.value?.timestamp || fallbackStartDate;
|
|
579
|
+
|
|
580
|
+
// Apply overlap buffer for query (safety window)
|
|
581
|
+
const OVERLAP_BUFFER_MS = overlapBufferSeconds * 1000;
|
|
582
|
+
const bufferedLastRunTime = new Date(
|
|
583
|
+
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
|
|
584
|
+
).toISOString();
|
|
585
|
+
|
|
586
|
+
const effectiveEndTime = new Date().toISOString();
|
|
587
|
+
|
|
588
|
+
const dateRangeFilter = {
|
|
589
|
+
from: bufferedLastRunTime,
|
|
590
|
+
to: effectiveEndTime, // End of extraction window
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
log.info('✅ [STATE] Incremental extraction mode with overlap buffer', {
|
|
594
|
+
rawLastRunTime,
|
|
595
|
+
bufferedLastRunTime,
|
|
596
|
+
effectiveEndTime,
|
|
597
|
+
overlapBufferSeconds,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// STEP 4/8: Create extraction job
|
|
601
|
+
log.info('📝 [JOB] Creating extraction job');
|
|
602
|
+
const jobId = `virtual-positions-json-${Date.now()}`;
|
|
603
|
+
await jobTracker.createJob(jobId, {
|
|
604
|
+
type: 'extraction',
|
|
605
|
+
entity: 'virtualPositions',
|
|
606
|
+
mode: 'incremental',
|
|
607
|
+
dateRangeFilter,
|
|
608
|
+
startTime: executionStartTime,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
log.info('✅ [JOB] Extraction job created', {
|
|
612
|
+
jobId,
|
|
613
|
+
catalogues,
|
|
614
|
+
dateRangeFilter,
|
|
615
|
+
maxRecords,
|
|
616
|
+
positionTypes,
|
|
617
|
+
groupRefs,
|
|
618
|
+
productRefs,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// ? Enhanced: Extract context for progress logging
|
|
622
|
+
const dateRangeInfo = {
|
|
623
|
+
start: dateRangeFilter?.from || 'N/A',
|
|
624
|
+
end: dateRangeFilter?.to || 'N/A',
|
|
625
|
+
catalogues: catalogues.map((c: any) => c.ref || c).join(', ') || 'all',
|
|
626
|
+
types: positionTypes?.join(', ') || 'all',
|
|
627
|
+
groups: groupRefs?.join(', ') || 'none',
|
|
628
|
+
products: productRefs?.join(', ') || 'none'
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// ? Enhanced: Start logging with context
|
|
632
|
+
log.info(`🔍 [EXTRACTION] Starting GraphQL extraction`, {
|
|
633
|
+
query: 'virtualPositions',
|
|
634
|
+
pageSize,
|
|
635
|
+
maxRecords,
|
|
636
|
+
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
637
|
+
catalogues: dateRangeInfo.catalogues,
|
|
638
|
+
positionTypes: dateRangeInfo.types,
|
|
639
|
+
groupRefs: dateRangeInfo.groups,
|
|
640
|
+
productRefs: dateRangeInfo.products,
|
|
641
|
+
jobId
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// STEP 5/8: Execute extraction with auto-pagination (WITH overlap buffer)
|
|
645
|
+
const extractionStartTime = Date.now();
|
|
646
|
+
const extractionResult = await orchestrator.extract({
|
|
647
|
+
query: VIRTUAL_POSITIONS_QUERY,
|
|
648
|
+
resultPath: 'virtualPositions.edges.node',
|
|
649
|
+
variables: {
|
|
650
|
+
catalogues,
|
|
651
|
+
dateRangeFilter,
|
|
652
|
+
types: positionTypes,
|
|
653
|
+
groupRefs,
|
|
654
|
+
productRefs,
|
|
655
|
+
},
|
|
656
|
+
pageSize,
|
|
657
|
+
maxRecords,
|
|
658
|
+
validateItem: item => !!(item.ref && item.productRef),
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const rawRecords = extractionResult.data;
|
|
662
|
+
const extractionDuration = Date.now() - extractionStartTime;
|
|
663
|
+
|
|
664
|
+
log.info('✅ [EXTRACTION] Virtual position extraction completed', {
|
|
665
|
+
jobId,
|
|
666
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
667
|
+
totalPages: extractionResult.stats.totalPages,
|
|
668
|
+
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
669
|
+
errors: extractionResult.errors ? extractionResult.errors.length : 0,
|
|
670
|
+
duration: `${extractionDuration}ms`,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// ? Enhanced: Completion logging with summary
|
|
674
|
+
log.info(`✅ [STATS] Extraction statistics`, {
|
|
675
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
676
|
+
totalPages: extractionResult.stats.totalPages,
|
|
677
|
+
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
678
|
+
failedValidations: extractionResult.stats.failedValidations,
|
|
679
|
+
truncated: extractionResult.stats.truncated,
|
|
680
|
+
truncationReason: extractionResult.stats.truncationReason,
|
|
681
|
+
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
682
|
+
duration: `${extractionDuration}ms`,
|
|
683
|
+
jobId
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
if (extractionResult.errors && extractionResult.errors.length > 0) {
|
|
687
|
+
log.warn('Non-fatal extraction errors encountered', {
|
|
688
|
+
jobId,
|
|
689
|
+
errorCount: extractionResult.errors.length,
|
|
690
|
+
sampleErrors: extractionResult.errors.slice(0, 3),
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (rawRecords.length === 0) {
|
|
695
|
+
log.info('ℹ️ [EXTRACTION] No virtual position records to extract');
|
|
696
|
+
await jobTracker.markCompleted(jobId, { recordCount: 0 });
|
|
697
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
698
|
+
log.info('✅ [COMPLETE] Extraction workflow completed', {
|
|
699
|
+
duration: `${totalDuration}ms`,
|
|
700
|
+
recordCount: 0,
|
|
701
|
+
});
|
|
702
|
+
return {
|
|
703
|
+
success: true,
|
|
704
|
+
message: 'No records to extract',
|
|
705
|
+
jobId,
|
|
706
|
+
dateRangeFilter,
|
|
707
|
+
duration: totalDuration,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
log.info('📦 [DATA] Virtual position records retrieved', { count: rawRecords.length, jobId });
|
|
712
|
+
|
|
713
|
+
// STEP 6/8: Transform with UniversalMapper
|
|
714
|
+
log.info('🔄 [MAPPING] Starting field transformation');
|
|
715
|
+
const mapper = new UniversalMapper(virtualPositionsExportMapping);
|
|
716
|
+
const mappingResult = await mapper.map(rawRecords);
|
|
717
|
+
|
|
718
|
+
if (!mappingResult.success) {
|
|
719
|
+
const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
|
|
720
|
+
log.error('❌ [MAPPING] Transformation failed', {
|
|
721
|
+
errorCount: mappingErrors.length,
|
|
722
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
723
|
+
});
|
|
724
|
+
await jobTracker.markFailed(
|
|
725
|
+
jobId,
|
|
726
|
+
mappingErrors[0] || 'UniversalMapper returned unsuccessful result',
|
|
727
|
+
{
|
|
728
|
+
errors: mappingErrors,
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
return {
|
|
732
|
+
success: false,
|
|
733
|
+
error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
|
|
734
|
+
jobId,
|
|
735
|
+
errors: mappingErrors,
|
|
736
|
+
recommendation: 'Check mapping configuration in config/virtual-positions.export.json. Verify all source paths exist in GraphQL response.',
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const transformedRecords = Array.isArray(mappingResult.data) ? mappingResult.data : [];
|
|
741
|
+
const mappingErrors = mappingResult.errors || [];
|
|
742
|
+
|
|
743
|
+
if (mappingErrors.length > 0) {
|
|
744
|
+
log.warn('Some records failed transformation', {
|
|
745
|
+
jobId,
|
|
746
|
+
errorCount: mappingErrors.length,
|
|
747
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
752
|
+
log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
753
|
+
jobId,
|
|
754
|
+
skippedFields: mappingResult.skippedFields,
|
|
755
|
+
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (transformedRecords.length === 0) {
|
|
760
|
+
log.error('❌ [MAPPING] All records failed mapping', {
|
|
761
|
+
totalRecords: rawRecords.length,
|
|
762
|
+
errorCount: mappingErrors.length,
|
|
763
|
+
});
|
|
764
|
+
await jobTracker.markFailed(jobId, 'All records failed mapping', { errors: mappingErrors });
|
|
765
|
+
return {
|
|
766
|
+
success: false,
|
|
767
|
+
error: 'All records failed mapping',
|
|
768
|
+
jobId,
|
|
769
|
+
errors: mappingErrors,
|
|
770
|
+
recommendation: 'Review mapping errors above. Common issues: missing required fields, invalid resolver names, incorrect source paths.',
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
log.info('✅ [MAPPING] Records transformed successfully', {
|
|
775
|
+
successful: transformedRecords.length,
|
|
776
|
+
skippedRecords: rawRecords.length - transformedRecords.length,
|
|
777
|
+
errorRate: ((mappingErrors.length / rawRecords.length) * 100).toFixed(2) + '%',
|
|
778
|
+
jobId,
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// STEP 7/8: Calculate max updatedOn for next incremental run (WITHOUT buffer)
|
|
782
|
+
const maxUpdatedOn = transformedRecords.reduce((max, record) => {
|
|
783
|
+
const recordTime = new Date(record.updatedOn).getTime();
|
|
784
|
+
return recordTime > max ? recordTime : max;
|
|
785
|
+
}, new Date(rawLastRunTime).getTime());
|
|
786
|
+
const newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
787
|
+
|
|
788
|
+
// Build JSON with metadata
|
|
789
|
+
const jsonOutput = {
|
|
790
|
+
metadata: {
|
|
791
|
+
extractedAt: new Date().toISOString(),
|
|
792
|
+
recordCount: transformedRecords.length,
|
|
793
|
+
incrementalFrom: rawLastRunTime,
|
|
794
|
+
incrementalTo: newTimestamp,
|
|
795
|
+
jobId,
|
|
796
|
+
extractionMode: 'incremental',
|
|
797
|
+
},
|
|
798
|
+
data: transformedRecords,
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// Use JSONBuilder for consistent JSON generation
|
|
802
|
+
const jsonBuilder = new JSONBuilder({
|
|
803
|
+
prettyPrint,
|
|
804
|
+
indent: 2,
|
|
805
|
+
});
|
|
806
|
+
const jsonContent = jsonBuilder.build(jsonOutput);
|
|
807
|
+
|
|
808
|
+
// Generate timestamped filename
|
|
809
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
810
|
+
const fileName = `virtual-positions-${timestamp}.json`;
|
|
811
|
+
const s3Key = `${s3Prefix}${fileName}`;
|
|
812
|
+
|
|
813
|
+
log.info('📄 [JSON] Generated JSON file', {
|
|
814
|
+
fileName,
|
|
815
|
+
size: `${(jsonContent.length / 1024).toFixed(2)} KB`,
|
|
816
|
+
recordCount: transformedRecords.length,
|
|
817
|
+
jobId,
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
// STEP 8/8: Upload to S3
|
|
821
|
+
log.info('☁️ [S3] Starting S3 upload');
|
|
822
|
+
const s3 = new S3DataSource(
|
|
823
|
+
{
|
|
824
|
+
type: 'S3_JSON',
|
|
825
|
+
connectionId: 's3-virtual-positions-json-export',
|
|
826
|
+
name: 'S3 Virtual Positions JSON Export',
|
|
827
|
+
s3Config,
|
|
828
|
+
},
|
|
829
|
+
log
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
await s3.uploadFile(s3Key, Buffer.from(jsonContent, 'utf8'), {
|
|
833
|
+
contentType: 'application/json',
|
|
834
|
+
metadata: {
|
|
835
|
+
recordCount: String(transformedRecords.length),
|
|
836
|
+
extractedAt: new Date().toISOString(),
|
|
837
|
+
jobId,
|
|
838
|
+
incrementalFrom: rawLastRunTime,
|
|
839
|
+
incrementalTo: newTimestamp,
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
log.info('✅ [S3] JSON file uploaded successfully', { s3Key, jobId });
|
|
844
|
+
|
|
845
|
+
// Update state with new timestamp (WITHOUT buffer - critical!)
|
|
846
|
+
await kv.set(stateKey, {
|
|
847
|
+
timestamp: newTimestamp, // ← NO buffer applied
|
|
848
|
+
recordCount: transformedRecords.length,
|
|
849
|
+
extractedAt: new Date().toISOString(),
|
|
850
|
+
overlapBufferSeconds, // Track buffer config
|
|
851
|
+
fileName,
|
|
852
|
+
s3Key,
|
|
853
|
+
jobId,
|
|
854
|
+
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
855
|
+
});
|
|
856
|
+
log.info('💾 [STATE] State updated with new timestamp (without buffer)', {
|
|
857
|
+
newTimestamp,
|
|
858
|
+
overlapBufferSeconds,
|
|
859
|
+
jobId,
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
await jobTracker.markCompleted(jobId, {
|
|
863
|
+
recordCount: transformedRecords.length,
|
|
864
|
+
fileName,
|
|
865
|
+
s3Key,
|
|
866
|
+
errors: mappingErrors,
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
870
|
+
log.info('✅ [COMPLETE] Extraction workflow completed successfully', {
|
|
871
|
+
duration: `${totalDuration}ms`,
|
|
872
|
+
extractionDuration: `${extractionDuration}ms`,
|
|
873
|
+
recordsExtracted: transformedRecords.length,
|
|
874
|
+
recordsFailed: mappingErrors.length,
|
|
875
|
+
s3Key,
|
|
876
|
+
jobId,
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
return {
|
|
880
|
+
success: true,
|
|
881
|
+
recordsExtracted: transformedRecords.length,
|
|
882
|
+
recordsFailed: mappingErrors.length,
|
|
883
|
+
fileName,
|
|
884
|
+
s3Key,
|
|
885
|
+
jobId,
|
|
886
|
+
newTimestamp,
|
|
887
|
+
duration: totalDuration,
|
|
888
|
+
extractionDuration,
|
|
889
|
+
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
890
|
+
};
|
|
891
|
+
} catch (error: any) {
|
|
892
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
893
|
+
log.error('❌ [FAILED] Extraction workflow failed', {
|
|
894
|
+
message: error?.message,
|
|
895
|
+
duration: `${totalDuration}ms`,
|
|
896
|
+
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
return {
|
|
900
|
+
success: false,
|
|
901
|
+
message: error instanceof Error ? error.message : String(error),
|
|
902
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
903
|
+
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
904
|
+
duration: totalDuration,
|
|
905
|
+
recommendation: 'Check error message above. Common issues: connection failures, invalid GraphQL query, S3 credentials, or mapping configuration errors.',
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### Workflow 2: Ad Hoc Manual Trigger
|
|
913
|
+
|
|
914
|
+
**File:** `src/workflows/webhook/adhoc-virtual-positions-extraction.ts`
|
|
915
|
+
|
|
916
|
+
```typescript
|
|
917
|
+
import { webhook, http } from '@versori/run';
|
|
918
|
+
import { Buffer } from 'node:buffer';
|
|
919
|
+
import {
|
|
920
|
+
createClient,
|
|
921
|
+
ExtractionOrchestrator,
|
|
922
|
+
JobTracker,
|
|
923
|
+
UniversalMapper,
|
|
924
|
+
S3DataSource,
|
|
925
|
+
JSONBuilder,
|
|
926
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
927
|
+
import virtualPositionsExportMapping from './config/virtual-positions.export.json' with { type: 'json' };
|
|
928
|
+
|
|
929
|
+
export const virtualPositionsManualExtraction = webhook('manual-extract-virtual-positions', {
|
|
930
|
+
connection: 'virtual-positions-adhoc',
|
|
931
|
+
response: {
|
|
932
|
+
mode: 'sync',
|
|
933
|
+
onSuccess: ctx =>
|
|
934
|
+
new Response(JSON.stringify(ctx.data), {
|
|
935
|
+
status: 200,
|
|
936
|
+
headers: { 'Content-Type': 'application/json' },
|
|
937
|
+
}),
|
|
938
|
+
onError: ctx =>
|
|
939
|
+
new Response(JSON.stringify({ error: ctx.data }), {
|
|
940
|
+
status: 500,
|
|
941
|
+
headers: { 'Content-Type': 'application/json' },
|
|
942
|
+
}),
|
|
943
|
+
},
|
|
944
|
+
}).then(
|
|
945
|
+
http('trigger-extraction', { connection: 'fluent_commerce' }, async ctx => {
|
|
946
|
+
const { log, openKv, activation } = ctx;
|
|
947
|
+
const executionStartTime = Date.now();
|
|
948
|
+
|
|
949
|
+
log.info('🚀 [EXTRACTION] Starting ad hoc virtual positions extraction', {
|
|
950
|
+
timestamp: new Date().toISOString(),
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
// STEP 1/8: Parse request body for custom date range
|
|
954
|
+
const body = ctx.request?.body || {};
|
|
955
|
+
const startDate = body.startDate || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); // Last 24 hours
|
|
956
|
+
const endDate = body.endDate || new Date().toISOString();
|
|
957
|
+
const catalogueRefs = body.catalogueRefs || 'DEFAULT_VIRTUAL_CATALOGUE';
|
|
958
|
+
|
|
959
|
+
const catalogues = catalogueRefs.split(',').map((ref: string) => ({ ref: ref.trim() }));
|
|
960
|
+
|
|
961
|
+
const s3Config = {
|
|
962
|
+
bucket: activation?.getVariable('s3BucketName'),
|
|
963
|
+
region: activation?.getVariable('awsRegion') || 'us-east-1',
|
|
964
|
+
accessKeyId: activation?.getVariable('awsAccessKeyId'),
|
|
965
|
+
secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
|
|
966
|
+
};
|
|
967
|
+
const s3Prefix = activation?.getVariable('s3Prefix') || 'virtual-positions/adhoc/';
|
|
968
|
+
|
|
969
|
+
log.info('📋 [CONFIG] Manual extraction triggered', { startDate, endDate, catalogues });
|
|
970
|
+
|
|
971
|
+
// STEP 2/8: Initialize SDK services
|
|
972
|
+
log.info('📦 [INITIALIZATION] Creating Fluent Commerce client');
|
|
973
|
+
const client = await createClient(ctx);
|
|
974
|
+
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
975
|
+
const jobTracker = new JobTracker(openKv(':project:'), log);
|
|
976
|
+
log.info('✅ [INITIALIZATION] SDK services initialized successfully');
|
|
977
|
+
|
|
978
|
+
// STEP 3/8: Create extraction job
|
|
979
|
+
log.info('📝 [JOB] Creating ad hoc extraction job');
|
|
980
|
+
const jobId = `virtual-positions-adhoc-${Date.now()}`;
|
|
981
|
+
await jobTracker.createJob(jobId, {
|
|
982
|
+
type: 'extraction',
|
|
983
|
+
entity: 'virtualPositions',
|
|
984
|
+
mode: 'adhoc',
|
|
985
|
+
dateRange: { from: startDate, to: endDate },
|
|
986
|
+
startTime: executionStartTime,
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// ? Enhanced: Extract context for progress logging
|
|
990
|
+
const dateRangeInfo = {
|
|
991
|
+
start: startDate || 'N/A',
|
|
992
|
+
end: endDate || 'N/A',
|
|
993
|
+
catalogues: catalogues?.map((c: any) => c.ref || c).join(', ') || 'all'
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// ? Enhanced: Start logging with context
|
|
997
|
+
log.info(`🔍 [EXTRACTION] Starting GraphQL extraction`, {
|
|
998
|
+
query: 'virtualPositions',
|
|
999
|
+
pageSize: 200,
|
|
1000
|
+
maxRecords: 100000,
|
|
1001
|
+
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1002
|
+
catalogues: dateRangeInfo.catalogues,
|
|
1003
|
+
jobId
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
// STEP 4/8: Execute extraction with orchestrator
|
|
1007
|
+
const extractionStartTime = Date.now();
|
|
1008
|
+
const extractionResult = await orchestrator.extract({
|
|
1009
|
+
query: VIRTUAL_POSITIONS_QUERY,
|
|
1010
|
+
resultPath: 'virtualPositions.edges.node',
|
|
1011
|
+
variables: {
|
|
1012
|
+
catalogues,
|
|
1013
|
+
dateRangeFilter: { from: startDate, to: endDate },
|
|
1014
|
+
},
|
|
1015
|
+
pageSize: 200,
|
|
1016
|
+
maxRecords: 100000,
|
|
1017
|
+
validateItem: (item: any) => !!(item.ref && item.productRef),
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
const rawRecords = extractionResult.data;
|
|
1021
|
+
const extractionDuration = Date.now() - extractionStartTime;
|
|
1022
|
+
|
|
1023
|
+
log.info('✅ [EXTRACTION] Manual extraction completed', {
|
|
1024
|
+
jobId,
|
|
1025
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1026
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1027
|
+
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1028
|
+
errors: extractionResult.errors ? extractionResult.errors.length : 0,
|
|
1029
|
+
duration: `${extractionDuration}ms`,
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
// ? Enhanced: Completion logging with summary
|
|
1033
|
+
log.info(`✅ [STATS] Extraction statistics`, {
|
|
1034
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1035
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1036
|
+
validRecords: extractionResult.stats.validRecords ?? rawRecords.length,
|
|
1037
|
+
failedValidations: extractionResult.stats.failedValidations,
|
|
1038
|
+
truncated: extractionResult.stats.truncated,
|
|
1039
|
+
truncationReason: extractionResult.stats.truncationReason,
|
|
1040
|
+
dateRange: `${dateRangeInfo.start} to ${dateRangeInfo.end}`,
|
|
1041
|
+
duration: `${extractionDuration}ms`,
|
|
1042
|
+
jobId
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
if (extractionResult.errors && extractionResult.errors.length > 0) {
|
|
1046
|
+
log.warn('Non-fatal extraction errors encountered', {
|
|
1047
|
+
jobId,
|
|
1048
|
+
errorCount: extractionResult.errors.length,
|
|
1049
|
+
sampleErrors: extractionResult.errors.slice(0, 3),
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (rawRecords.length === 0) {
|
|
1054
|
+
log.info('ℹ️ [EXTRACTION] No records found for specified date range');
|
|
1055
|
+
await jobTracker.markCompleted(jobId, {
|
|
1056
|
+
recordCount: 0,
|
|
1057
|
+
message: 'No records to extract',
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
1061
|
+
log.info('✅ [COMPLETE] Extraction workflow completed', {
|
|
1062
|
+
duration: `${totalDuration}ms`,
|
|
1063
|
+
recordCount: 0,
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
return {
|
|
1067
|
+
success: true,
|
|
1068
|
+
jobId,
|
|
1069
|
+
recordCount: 0,
|
|
1070
|
+
fileName: undefined,
|
|
1071
|
+
s3Key: undefined,
|
|
1072
|
+
duration: totalDuration,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// STEP 5/8: Transform records
|
|
1077
|
+
log.info('🔄 [MAPPING] Starting field transformation');
|
|
1078
|
+
const mapper = new UniversalMapper(virtualPositionsExportMapping);
|
|
1079
|
+
const mappingResult = await mapper.map(rawRecords);
|
|
1080
|
+
|
|
1081
|
+
if (!mappingResult.success) {
|
|
1082
|
+
const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
|
|
1083
|
+
log.error('❌ [MAPPING] Transformation failed', {
|
|
1084
|
+
errorCount: mappingErrors.length,
|
|
1085
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
1086
|
+
});
|
|
1087
|
+
await jobTracker.markFailed(
|
|
1088
|
+
jobId,
|
|
1089
|
+
mappingErrors[0] || 'UniversalMapper returned unsuccessful result',
|
|
1090
|
+
{
|
|
1091
|
+
errors: mappingErrors,
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
return {
|
|
1095
|
+
success: false,
|
|
1096
|
+
jobId,
|
|
1097
|
+
error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
|
|
1098
|
+
errors: mappingErrors,
|
|
1099
|
+
recommendation: 'Check mapping configuration in config/virtual-positions.export.json. Verify all source paths exist in GraphQL response.',
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const transformedRecords = Array.isArray(mappingResult.data) ? mappingResult.data : [];
|
|
1104
|
+
const mappingErrors = mappingResult.errors || [];
|
|
1105
|
+
|
|
1106
|
+
if (mappingErrors.length > 0) {
|
|
1107
|
+
log.warn('Manual extraction mapping warnings', {
|
|
1108
|
+
jobId,
|
|
1109
|
+
errorCount: mappingErrors.length,
|
|
1110
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
1115
|
+
log.info('ℹ️ [MAPPING] Optional fields skipped (undefined values)', {
|
|
1116
|
+
jobId,
|
|
1117
|
+
skippedFields: mappingResult.skippedFields,
|
|
1118
|
+
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (transformedRecords.length === 0) {
|
|
1123
|
+
log.error('❌ [MAPPING] All records failed mapping', {
|
|
1124
|
+
totalRecords: rawRecords.length,
|
|
1125
|
+
errorCount: mappingErrors.length,
|
|
1126
|
+
});
|
|
1127
|
+
await jobTracker.markFailed(jobId, 'All records failed mapping', { errors: mappingErrors });
|
|
1128
|
+
return {
|
|
1129
|
+
success: false,
|
|
1130
|
+
jobId,
|
|
1131
|
+
error: 'All records failed mapping',
|
|
1132
|
+
errors: mappingErrors,
|
|
1133
|
+
recommendation: 'Review mapping errors above. Common issues: missing required fields, invalid resolver names, incorrect source paths.',
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
log.info('✅ [MAPPING] Records transformed successfully', {
|
|
1138
|
+
successful: transformedRecords.length,
|
|
1139
|
+
skippedRecords: rawRecords.length - transformedRecords.length,
|
|
1140
|
+
errorRate: ((mappingErrors.length / rawRecords.length) * 100).toFixed(2) + '%',
|
|
1141
|
+
jobId,
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// STEP 6/8: Build JSON output
|
|
1145
|
+
log.info('📄 [JSON] Building JSON output');
|
|
1146
|
+
const jsonOutput = {
|
|
1147
|
+
metadata: {
|
|
1148
|
+
extractedAt: new Date().toISOString(),
|
|
1149
|
+
recordCount: transformedRecords.length,
|
|
1150
|
+
dateRangeFrom: startDate,
|
|
1151
|
+
dateRangeTo: endDate,
|
|
1152
|
+
jobId,
|
|
1153
|
+
extractionMode: 'adhoc',
|
|
1154
|
+
},
|
|
1155
|
+
data: transformedRecords,
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// Use JSONBuilder for consistent JSON generation
|
|
1159
|
+
const jsonBuilder = new JSONBuilder({
|
|
1160
|
+
prettyPrint: true,
|
|
1161
|
+
indent: 2,
|
|
1162
|
+
});
|
|
1163
|
+
const jsonContent = jsonBuilder.build(jsonOutput);
|
|
1164
|
+
|
|
1165
|
+
// STEP 7/8: Upload to S3
|
|
1166
|
+
log.info('☁️ [S3] Starting S3 upload');
|
|
1167
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1168
|
+
const fileName = `virtual-positions-adhoc-${timestamp}.json`;
|
|
1169
|
+
const s3Key = `${s3Prefix}${fileName}`;
|
|
1170
|
+
|
|
1171
|
+
const s3 = new S3DataSource(
|
|
1172
|
+
{
|
|
1173
|
+
type: 'S3_JSON',
|
|
1174
|
+
connectionId: 's3-virtual-positions-json-export',
|
|
1175
|
+
name: 'S3 Virtual Positions JSON Export',
|
|
1176
|
+
s3Config,
|
|
1177
|
+
},
|
|
1178
|
+
log
|
|
1179
|
+
);
|
|
1180
|
+
|
|
1181
|
+
await s3.uploadFile(s3Key, Buffer.from(jsonContent, 'utf8'), {
|
|
1182
|
+
contentType: 'application/json',
|
|
1183
|
+
metadata: {
|
|
1184
|
+
recordCount: String(transformedRecords.length),
|
|
1185
|
+
extractedAt: new Date().toISOString(),
|
|
1186
|
+
jobId,
|
|
1187
|
+
},
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
log.info('✅ [S3] JSON file uploaded successfully', { s3Key, jobId });
|
|
1191
|
+
|
|
1192
|
+
// STEP 8/8: Complete job
|
|
1193
|
+
await jobTracker.markCompleted(jobId, {
|
|
1194
|
+
recordCount: transformedRecords.length,
|
|
1195
|
+
fileName,
|
|
1196
|
+
s3Key,
|
|
1197
|
+
errors: mappingErrors,
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
1201
|
+
log.info('✅ [COMPLETE] Ad hoc extraction workflow completed successfully', {
|
|
1202
|
+
duration: `${totalDuration}ms`,
|
|
1203
|
+
extractionDuration: `${extractionDuration}ms`,
|
|
1204
|
+
jobId,
|
|
1205
|
+
recordCount: transformedRecords.length,
|
|
1206
|
+
s3Key,
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
return {
|
|
1210
|
+
success: true,
|
|
1211
|
+
jobId,
|
|
1212
|
+
recordCount: transformedRecords.length,
|
|
1213
|
+
fileName,
|
|
1214
|
+
s3Key,
|
|
1215
|
+
duration: totalDuration,
|
|
1216
|
+
extractionDuration,
|
|
1217
|
+
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
1218
|
+
};
|
|
1219
|
+
})
|
|
1220
|
+
);
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
### Workflow 3: Job Status Query
|
|
1224
|
+
|
|
1225
|
+
**File:** `src/workflows/webhook/job-status-check.ts`
|
|
1226
|
+
|
|
1227
|
+
```typescript
|
|
1228
|
+
import { webhook, http } from '@versori/run';
|
|
1229
|
+
import { Buffer } from 'node:buffer';
|
|
1230
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
1231
|
+
|
|
1232
|
+
export const virtualPositionsJobStatus = webhook('job-status', {
|
|
1233
|
+
connection: 'virtual-positions-job-status',
|
|
1234
|
+
response: {
|
|
1235
|
+
mode: 'sync',
|
|
1236
|
+
onSuccess: ctx =>
|
|
1237
|
+
new Response(JSON.stringify(ctx.data), {
|
|
1238
|
+
status: 200,
|
|
1239
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1240
|
+
}),
|
|
1241
|
+
onError: ctx =>
|
|
1242
|
+
new Response(JSON.stringify({ error: ctx.data }), {
|
|
1243
|
+
status: 404,
|
|
1244
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1245
|
+
}),
|
|
1246
|
+
},
|
|
1247
|
+
}).then(
|
|
1248
|
+
http('query-job-status', async ctx => {
|
|
1249
|
+
const { log, openKv, activation } = ctx;
|
|
1250
|
+
const jobId = ctx.request?.query?.jobId;
|
|
1251
|
+
|
|
1252
|
+
if (!jobId) {
|
|
1253
|
+
return { error: 'Missing jobId query parameter' };
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const jobTracker = new JobTracker(openKv(':project:'), log);
|
|
1257
|
+
const jobState = await jobTracker.getJob(jobId);
|
|
1258
|
+
|
|
1259
|
+
if (!jobState) {
|
|
1260
|
+
return { error: `Job not found: ${jobId}` };
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return {
|
|
1264
|
+
success: true,
|
|
1265
|
+
job: jobState,
|
|
1266
|
+
};
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
## Key Differences from Manual Implementation
|
|
1272
|
+
|
|
1273
|
+
1. **ExtractionOrchestrator**: High-level orchestration vs manual steps
|
|
1274
|
+
2. **JobTracker**: Automatic job lifecycle tracking
|
|
1275
|
+
3. **Three Workflows**: Scheduled, ad hoc, and status query patterns
|
|
1276
|
+
4. **STEP X/8 Comments**: Clear orchestration progression
|
|
1277
|
+
5. **Overlap Buffer Pattern**: Query WITH buffer, save WITHOUT buffer
|
|
1278
|
+
|
|
1279
|
+
## Production Checklist
|
|
1280
|
+
|
|
1281
|
+
- [ ] Set `catalogueRefs` to correct virtual catalogue
|
|
1282
|
+
- [ ] Configure extraction schedule (15min for real-time, hourly for standard)
|
|
1283
|
+
- [ ] Set `maxRecords` based on ATP volume (100k+ for large retailers)
|
|
1284
|
+
- [ ] Set `pageSize` to balance throughput and memory (200-500 recommended)
|
|
1285
|
+
- [ ] Enable/disable `prettyPrint` based on use case (false for production)
|
|
1286
|
+
- [ ] Verify S3 bucket permissions and CORS configuration
|
|
1287
|
+
- [ ] Set up S3 lifecycle policy to archive old JSON files
|
|
1288
|
+
- [ ] Test with incremental mode first (validates state management)
|
|
1289
|
+
- [ ] Document JSON schema for API consumers
|
|
1290
|
+
- [ ] Test failure recovery (state rollback on error)
|
|
1291
|
+
- [ ] Set up alerts for extraction failures
|
|
1292
|
+
- [ ] Test with real-time incremental changes (15min schedule)
|
|
1293
|
+
- [ ] Test ad hoc manual trigger workflow
|
|
1294
|
+
- [ ] Test job status query workflow
|
|
1295
|
+
|
|
1296
|
+
---
|
|
1297
|
+
|
|
1298
|
+
---
|
|
1299
|
+
|
|
1300
|
+
### Pattern 7: State Management & Date Overrides
|
|
1301
|
+
|
|
1302
|
+
**Use Case**: Understand how state management works with scheduled and ad-hoc extractions.
|
|
1303
|
+
|
|
1304
|
+
**How it works**:
|
|
1305
|
+
|
|
1306
|
+
VersoriKV stores the last successful extraction timestamp to enable incremental sync:
|
|
1307
|
+
|
|
1308
|
+
```typescript
|
|
1309
|
+
interface ExtractionState {
|
|
1310
|
+
timestamp: string; // Last run timestamp (WITHOUT overlap buffer)
|
|
1311
|
+
recordCount: number; // Number of records extracted
|
|
1312
|
+
extractedAt: string; // When extraction completed
|
|
1313
|
+
fileName?: string; // Generated filename
|
|
1314
|
+
s3Key?: string; // S3 upload path
|
|
1315
|
+
overlapBufferSeconds?: number; // Buffer configuration
|
|
1316
|
+
}
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1319
|
+
**State Priority Chain** (highest to lowest):
|
|
1320
|
+
|
|
1321
|
+
1. **`fromDate` override** (manual date in webhook payload) - Highest priority
|
|
1322
|
+
2. **Stored state** (`await kv.get(stateKey)`) - Normal incremental mode
|
|
1323
|
+
3. **`fallbackStartDate`** (activation variable) - First run fallback
|
|
1324
|
+
|
|
1325
|
+
**Three Scenarios**:
|
|
1326
|
+
|
|
1327
|
+
#### Scenario 1: Normal Scheduled Runs (Incremental)
|
|
1328
|
+
|
|
1329
|
+
```typescript
|
|
1330
|
+
// Payload: {} (empty - no overrides)
|
|
1331
|
+
|
|
1332
|
+
// Behavior:
|
|
1333
|
+
// 1. Load last timestamp from KV: "2025-01-22T10:00:00Z"
|
|
1334
|
+
// 2. Apply overlap buffer: "2025-01-22T09:59:00Z" (query WITH buffer)
|
|
1335
|
+
// 3. Extract records updated since buffered time
|
|
1336
|
+
// 4. Calculate MAX(updatedOn) from results: "2025-01-22T14:30:00Z"
|
|
1337
|
+
// 5. Save new timestamp WITHOUT buffer: "2025-01-22T14:30:00Z"
|
|
1338
|
+
// 6. Next run starts from "2025-01-22T14:29:00Z" (with buffer)
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
**Test**:
|
|
1342
|
+
|
|
1343
|
+
```bash
|
|
1344
|
+
# Trigger scheduled run (no payload needed)
|
|
1345
|
+
# State advances automatically
|
|
1346
|
+
curl -X POST https://workspace.versori.run/virtual-positions-extract-json-15min
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
#### Scenario 2: Ad-hoc Extraction WITH State Update
|
|
1350
|
+
|
|
1351
|
+
```typescript
|
|
1352
|
+
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": true }
|
|
1353
|
+
|
|
1354
|
+
// Behavior:
|
|
1355
|
+
// 1. Ignore stored state
|
|
1356
|
+
// 2. Use fromDate: "2025-01-01T00:00:00Z" (no buffer applied to manual dates)
|
|
1357
|
+
// 3. Extract all records since 2025-01-01
|
|
1358
|
+
// 4. Calculate MAX(updatedOn): "2025-01-22T14:30:00Z"
|
|
1359
|
+
// 5. Save new timestamp: "2025-01-22T14:30:00Z" (updates state!)
|
|
1360
|
+
// 6. Next scheduled run starts from this new timestamp
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
**Use Case**: One-time catch-up extraction that advances the state pointer.
|
|
1364
|
+
|
|
1365
|
+
**Test**:
|
|
1366
|
+
|
|
1367
|
+
```bash
|
|
1368
|
+
curl -X POST https://workspace.versori.run/manual-extract-virtual-positions \
|
|
1369
|
+
-H "Content-Type: application/json" \
|
|
1370
|
+
-d '{
|
|
1371
|
+
"startDate": "2025-01-01T00:00:00Z",
|
|
1372
|
+
"catalogueRefs": "DEFAULT_VIRTUAL_CATALOGUE"
|
|
1373
|
+
}'
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
#### Scenario 3: Ad-hoc Extraction WITHOUT State Update
|
|
1377
|
+
|
|
1378
|
+
```typescript
|
|
1379
|
+
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": false }
|
|
1380
|
+
|
|
1381
|
+
// Behavior:
|
|
1382
|
+
// 1. Ignore stored state
|
|
1383
|
+
// 2. Use fromDate: "2025-01-01T00:00:00Z"
|
|
1384
|
+
// 3. Extract all records since 2025-01-01
|
|
1385
|
+
// 4. DO NOT update state
|
|
1386
|
+
// 5. Next scheduled run uses previous timestamp (unaffected)
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
**Use Case**: Historical backfill or testing without affecting incremental sync.
|
|
1390
|
+
|
|
1391
|
+
**Test**:
|
|
1392
|
+
|
|
1393
|
+
```bash
|
|
1394
|
+
curl -X POST https://workspace.versori.run/manual-extract-virtual-positions \
|
|
1395
|
+
-H "Content-Type: application/json" \
|
|
1396
|
+
-d '{
|
|
1397
|
+
"startDate": "2025-01-01T00:00:00Z",
|
|
1398
|
+
"endDate": "2025-01-31T23:59:59Z",
|
|
1399
|
+
"catalogueRefs": "DEFAULT_VIRTUAL_CATALOGUE"
|
|
1400
|
+
}'
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
**Why this matters**:
|
|
1404
|
+
|
|
1405
|
+
- **Incremental sync** relies on state continuity
|
|
1406
|
+
- **Manual overrides** allow catch-up without breaking incremental flow
|
|
1407
|
+
- **Overlap buffer** prevents missed records at time boundaries
|
|
1408
|
+
- **State isolation** lets you test/backfill without affecting production sync
|
|
1409
|
+
|
|
1410
|
+
---
|
|
1411
|
+
|
|
1412
|
+
### Pattern 8: Optional GraphQL Query Logging
|
|
1413
|
+
|
|
1414
|
+
**Use Case**: Debug extraction issues by logging the exact GraphQL query sent to Fluent Commerce API.
|
|
1415
|
+
|
|
1416
|
+
**When to use**:
|
|
1417
|
+
|
|
1418
|
+
- ✅ Debugging pagination issues
|
|
1419
|
+
- ✅ Verifying query variables (dates, filters, limits)
|
|
1420
|
+
- ✅ Development and testing
|
|
1421
|
+
- ❌ Production (verbose logs, potential secrets in variables)
|
|
1422
|
+
|
|
1423
|
+
**How to enable**:
|
|
1424
|
+
|
|
1425
|
+
Set `DEBUG_GRAPHQL=true` environment variable in Versori activation settings.
|
|
1426
|
+
|
|
1427
|
+
**Implementation**:
|
|
1428
|
+
|
|
1429
|
+
```typescript
|
|
1430
|
+
// In your extraction workflow
|
|
1431
|
+
const DEBUG_GRAPHQL = activation?.getVariable('DEBUG_GRAPHQL') === 'true';
|
|
1432
|
+
|
|
1433
|
+
if (DEBUG_GRAPHQL) {
|
|
1434
|
+
log.info('GraphQL Query Debug', {
|
|
1435
|
+
query: VIRTUAL_POSITIONS_QUERY,
|
|
1436
|
+
variables: {
|
|
1437
|
+
catalogues,
|
|
1438
|
+
dateRangeFilter,
|
|
1439
|
+
first: pageSize,
|
|
1440
|
+
after: null, // First page
|
|
1441
|
+
},
|
|
1442
|
+
pagination: {
|
|
1443
|
+
pageSize,
|
|
1444
|
+
maxRecords,
|
|
1445
|
+
currentPage: 1,
|
|
1446
|
+
},
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const extractionResult = await orchestrator.extract({
|
|
1451
|
+
query: VIRTUAL_POSITIONS_QUERY,
|
|
1452
|
+
resultPath: 'virtualPositions.edges.node',
|
|
1453
|
+
variables: {
|
|
1454
|
+
catalogues,
|
|
1455
|
+
dateRangeFilter,
|
|
1456
|
+
},
|
|
1457
|
+
pageSize,
|
|
1458
|
+
maxRecords,
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
if (DEBUG_GRAPHQL) {
|
|
1462
|
+
log.info('GraphQL Response Debug', {
|
|
1463
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1464
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1465
|
+
truncated: extractionResult.stats.truncated,
|
|
1466
|
+
truncationReason: extractionResult.stats.truncationReason,
|
|
1467
|
+
firstRecordId: extractionResult.data[0]?.id,
|
|
1468
|
+
lastRecordId: extractionResult.data[extractionResult.data.length - 1]?.id,
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
**What gets logged**:
|
|
1474
|
+
|
|
1475
|
+
```json
|
|
1476
|
+
{
|
|
1477
|
+
"level": "info",
|
|
1478
|
+
"message": "GraphQL Query Debug",
|
|
1479
|
+
"query": "query GetVirtualPositions($catalogues: [VirtualCatalogueKey], $dateRangeFilter: DateRange, ...)",
|
|
1480
|
+
"variables": {
|
|
1481
|
+
"catalogues": [{ "ref": "DEFAULT_VIRTUAL_CATALOGUE" }],
|
|
1482
|
+
"dateRangeFilter": "2025-01-22T09:59:00Z",
|
|
1483
|
+
"first": 200,
|
|
1484
|
+
"after": null
|
|
1485
|
+
},
|
|
1486
|
+
"pagination": {
|
|
1487
|
+
"pageSize": 200,
|
|
1488
|
+
"maxRecords": 100000,
|
|
1489
|
+
"currentPage": 1
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
**Versori Environment Variables**:
|
|
1495
|
+
|
|
1496
|
+
Add to activation settings:
|
|
1497
|
+
|
|
1498
|
+
```json
|
|
1499
|
+
{
|
|
1500
|
+
"DEBUG_GRAPHQL": "true"
|
|
1501
|
+
}
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
**Testing**:
|
|
1505
|
+
|
|
1506
|
+
```bash
|
|
1507
|
+
# Enable debug logging
|
|
1508
|
+
curl -X POST https://workspace.versori.run/virtual-positions-extract-json-15min
|
|
1509
|
+
|
|
1510
|
+
# Check Versori logs for "GraphQL Query Debug" entries
|
|
1511
|
+
# Verify query structure and variables are correct
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
**Sample Debug Output**:
|
|
1515
|
+
|
|
1516
|
+
```
|
|
1517
|
+
[INFO] GraphQL Query Debug
|
|
1518
|
+
query: "query GetVirtualPositions($catalogues: [VirtualCatalogueKey], $dateRangeFilter: DateRange, ...)"
|
|
1519
|
+
variables: { catalogues: [{ ref: "DEFAULT_VIRTUAL_CATALOGUE" }], dateRangeFilter: "2025-01-22T09:59:00Z", first: 200, after: null }
|
|
1520
|
+
pagination: { pageSize: 200, maxRecords: 100000, currentPage: 1 }
|
|
1521
|
+
|
|
1522
|
+
[INFO] Extraction complete
|
|
1523
|
+
totalRecords: 850
|
|
1524
|
+
totalPages: 5
|
|
1525
|
+
truncated: false
|
|
1526
|
+
|
|
1527
|
+
[INFO] GraphQL Response Debug
|
|
1528
|
+
totalRecords: 850
|
|
1529
|
+
totalPages: 5
|
|
1530
|
+
truncated: false
|
|
1531
|
+
truncationReason: undefined
|
|
1532
|
+
firstRecordId: "vp_001"
|
|
1533
|
+
lastRecordId: "vp_850"
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
**Key Benefits**:
|
|
1537
|
+
|
|
1538
|
+
- Quickly identify pagination configuration issues
|
|
1539
|
+
- Verify date filters are applied correctly
|
|
1540
|
+
- Debug "no records found" scenarios
|
|
1541
|
+
- Validate ExtractionOrchestrator variable injection
|
|
1542
|
+
|
|
1543
|
+
**Production Best Practice**: Disable `DEBUG_GRAPHQL` in production to reduce log volume and avoid logging sensitive data.
|
|
1544
|
+
|
|
1545
|
+
---
|
|
1546
|
+
|
|
1547
|
+
**Pattern**: Enterprise incremental extraction with overlap buffer for virtual positions (ATP) - JSON format with ExtractionOrchestrator
|
|
1548
|
+
**Key Learning**: Use `catalogues` (plural, array) input for the Fluent API
|
|
1549
|
+
**Critical**: Apply 60-second overlap buffer to prevent missed records due to clock skew
|
|
1550
|
+
**Buffer Pattern**: Query WITH buffer (`updatedOn >= lastRunTime - 60s`), save WITHOUT buffer (`MAX(updatedOn)`)
|
|
1551
|
+
**Orchestration**: ExtractionOrchestrator + JobTracker for production-grade extraction workflows
|
|
1552
|
+
**Schema**: virtualPositions accepts `catalogues: [VirtualCatalogueKey]` where VirtualCatalogueKey = `{ ref: String! }`
|
|
1553
|
+
**Use Case**: Real-time ATP updates for order management systems; use camelCase for JSON fields
|
|
1554
|
+
|
|
1555
|
+
---
|
|
1556
|
+
|
|
1557
|
+
### Pattern 8: Backward Pagination (Optional - Advanced)
|
|
1558
|
+
|
|
1559
|
+
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
1560
|
+
|
|
1561
|
+
**When to Use**:
|
|
1562
|
+
|
|
1563
|
+
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
1564
|
+
- ✅ Time-bounded reverse traversal for auditing
|
|
1565
|
+
- ✅ Display newest-first in UI/reports
|
|
1566
|
+
- ❌ **Don't use for standard incremental sync** - use forward pagination (default)
|
|
1567
|
+
|
|
1568
|
+
**GraphQL Query Requirements**:
|
|
1569
|
+
|
|
1570
|
+
Your query must support backward pagination by including `$last` and `$before`:
|
|
1571
|
+
|
|
1572
|
+
```graphql
|
|
1573
|
+
query GetData(
|
|
1574
|
+
$retailerId: ID!
|
|
1575
|
+
$first: Int # For forward pagination
|
|
1576
|
+
$after: String # For forward pagination
|
|
1577
|
+
$last: Int # For backward pagination
|
|
1578
|
+
$before: String # For backward pagination
|
|
1579
|
+
) {
|
|
1580
|
+
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
1581
|
+
edges {
|
|
1582
|
+
cursor # ✅ REQUIRED
|
|
1583
|
+
node {
|
|
1584
|
+
id
|
|
1585
|
+
createdAt
|
|
1586
|
+
# ... other fields
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
pageInfo {
|
|
1590
|
+
hasNextPage # For forward
|
|
1591
|
+
hasPreviousPage # ✅ REQUIRED for backward
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
```
|
|
1596
|
+
|
|
1597
|
+
**Implementation**:
|
|
1598
|
+
|
|
1599
|
+
```typescript
|
|
1600
|
+
// Backward pagination - newest records first
|
|
1601
|
+
const result = await orchestrator.extract({
|
|
1602
|
+
query: YOUR_QUERY,
|
|
1603
|
+
resultPath: 'data.edges.node',
|
|
1604
|
+
variables: {
|
|
1605
|
+
retailerId,
|
|
1606
|
+
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
1607
|
+
// ❌ Don't include last/before - orchestrator injects them
|
|
1608
|
+
},
|
|
1609
|
+
pageSize: 200,
|
|
1610
|
+
direction: 'backward', // ✅ Enable reverse pagination
|
|
1611
|
+
maxRecords: 10000,
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
// Records are returned in reverse chronological order
|
|
1615
|
+
log.info('Record order', {
|
|
1616
|
+
newestRecord: result.data[0].createdAt,
|
|
1617
|
+
oldestRecord: result.data[result.data.length - 1].createdAt
|
|
1618
|
+
});
|
|
1619
|
+
```
|
|
1620
|
+
|
|
1621
|
+
**Key Differences from Forward Pagination**:
|
|
1622
|
+
|
|
1623
|
+
| Aspect | Forward (Default) | Backward |
|
|
1624
|
+
| ---------------------- | -------------------------------- | ----------------------- |
|
|
1625
|
+
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
1626
|
+
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
1627
|
+
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
1628
|
+
| **Cursor Source** | Last edge of page | First edge of page |
|
|
1629
|
+
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
1630
|
+
|
|
1631
|
+
**Important Notes**:
|
|
1632
|
+
|
|
1633
|
+
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
1634
|
+
|
|
1635
|
+
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
1636
|
+
|
|
1637
|
+
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
1638
|
+
|
|
1639
|
+
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
1640
|
+
|
|
1641
|
+
**Example: Extract Latest 1000 Orders**
|
|
1642
|
+
|
|
1643
|
+
```typescript
|
|
1644
|
+
const latestOrders = await orchestrator.extract({
|
|
1645
|
+
query: ORDERS_QUERY,
|
|
1646
|
+
resultPath: 'orders.edges.node',
|
|
1647
|
+
variables: {
|
|
1648
|
+
retailerId,
|
|
1649
|
+
statuses: ['BOOKED', 'ALLOCATED'],
|
|
1650
|
+
},
|
|
1651
|
+
direction: 'backward', // Start from newest
|
|
1652
|
+
maxRecords: 1000, // Stop after 1000 records
|
|
1653
|
+
pageSize: 100, // 100 per page = 10 pages
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
// latestOrders.data[0] is the newest order
|
|
1657
|
+
// latestOrders.data[999] is the 1000th newest order
|
|
1658
|
+
```
|
|
1659
|
+
|
|
1660
|
+
**When to Use Forward vs Backward**:
|
|
1661
|
+
|
|
1662
|
+
```typescript
|
|
1663
|
+
// ✅ Forward (default) - For incremental sync
|
|
1664
|
+
const incrementalData = await orchestrator.extract({
|
|
1665
|
+
query: YOUR_QUERY,
|
|
1666
|
+
resultPath: 'data.edges.node',
|
|
1667
|
+
variables: {
|
|
1668
|
+
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
1669
|
+
},
|
|
1670
|
+
// direction defaults to 'forward'
|
|
1671
|
+
// Processes oldest → newest for proper sequencing
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
// ✅ Backward - For "latest N records" use cases
|
|
1675
|
+
const latestData = await orchestrator.extract({
|
|
1676
|
+
query: YOUR_QUERY,
|
|
1677
|
+
resultPath: 'data.edges.node',
|
|
1678
|
+
direction: 'backward',
|
|
1679
|
+
maxRecords: 100, // Just get latest 100
|
|
1680
|
+
// Gets newest → oldest
|
|
1681
|
+
});
|
|
1682
|
+
```
|
|
1683
|
+
|
|
1684
|
+
**Pagination Variables Reference**:
|
|
1685
|
+
|
|
1686
|
+
| Variable | Forward | Backward | Injected By | Notes |
|
|
1687
|
+
| -------- | ----------- | ----------- | ------------ | ------------------------ |
|
|
1688
|
+
| `first` | ✅ Used | ❌ Not used | Orchestrator | From `pageSize` |
|
|
1689
|
+
| `after` | ✅ Used | ❌ Not used | Orchestrator | From cursor (last edge) |
|
|
1690
|
+
| `last` | ❌ Not used | ✅ Used | Orchestrator | From `pageSize` |
|
|
1691
|
+
| `before` | ❌ Not used | ✅ Used | Orchestrator | From cursor (first edge) |
|
|
1692
|
+
|
|
1693
|
+
**Common Mistakes to Avoid**:
|
|
1694
|
+
|
|
1695
|
+
```typescript
|
|
1696
|
+
// ❌ WRONG - Don't pass pagination variables
|
|
1697
|
+
const result = await orchestrator.extract({
|
|
1698
|
+
variables: {
|
|
1699
|
+
last: 200, // ❌ Orchestrator will override this
|
|
1700
|
+
before: cursor, // ❌ Orchestrator manages cursor
|
|
1701
|
+
},
|
|
1702
|
+
direction: 'backward',
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
// ✅ CORRECT - Let orchestrator inject pagination
|
|
1706
|
+
const result = await orchestrator.extract({
|
|
1707
|
+
variables: {
|
|
1708
|
+
retailerId, // ✅ Your business variables only
|
|
1709
|
+
},
|
|
1710
|
+
pageSize: 200, // ✅ Orchestrator uses this for last/before
|
|
1711
|
+
direction: 'backward',
|
|
1712
|
+
});
|
|
1713
|
+
```
|
|
1714
|
+
|
|
1715
|
+
#### Optional: Reverse Pagination
|
|
1716
|
+
|
|
1717
|
+
- Forward default; reverse optional with $last/$before + pageInfo.hasPreviousPage.
|
|
1718
|
+
|
|
1719
|
+
GraphQL:
|
|
1720
|
+
|
|
1721
|
+
```graphql
|
|
1722
|
+
query GetVirtualPositionsBackward($last: Int!, $before: String) {
|
|
1723
|
+
virtualPositions(last: $last, before: $before) {
|
|
1724
|
+
edges {
|
|
1725
|
+
cursor
|
|
1726
|
+
node {
|
|
1727
|
+
id
|
|
1728
|
+
ref
|
|
1729
|
+
updatedOn
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
pageInfo {
|
|
1733
|
+
hasPreviousPage
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
SDK:
|
|
1740
|
+
|
|
1741
|
+
```typescript
|
|
1742
|
+
await orchestrator.extract({
|
|
1743
|
+
query: VIRTUAL_POSITIONS_BACKWARD_QUERY,
|
|
1744
|
+
resultPath: 'virtualPositions.edges.node',
|
|
1745
|
+
variables: {},
|
|
1746
|
+
pageSize,
|
|
1747
|
+
direction: 'backward',
|
|
1748
|
+
});
|
|
1749
|
+
```
|
|
1750
|
+
|
|
1751
|
+
---
|
|
1752
|
+
|
|
1753
|
+
## Testing Checklist
|
|
1754
|
+
|
|
1755
|
+
**Before production deployment:**
|
|
1756
|
+
|
|
1757
|
+
### 1. Schema Validation
|
|
1758
|
+
|
|
1759
|
+
- [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
|
|
1760
|
+
- [ ] Run `npx fc-connect validate-schema --mapping ./config/virtual-positions.export.json --schema ./fluent-schema.json`
|
|
1761
|
+
- [ ] Run `npx fc-connect analyze-coverage --mapping ./config/virtual-positions.export.json --schema ./fluent-schema.json`
|
|
1762
|
+
- [ ] Verify all `source` paths in mapping exist in GraphQL schema
|
|
1763
|
+
- [ ] Verify query structure matches schema (fields, types, filters)
|
|
1764
|
+
|
|
1765
|
+
### 2. Extraction Testing
|
|
1766
|
+
|
|
1767
|
+
- [ ] Test with small dataset first (maxRecords=10)
|
|
1768
|
+
- [ ] Verify ExtractionOrchestrator pagination works correctly
|
|
1769
|
+
- [ ] Test with multiple pages of data (verify cursor handling)
|
|
1770
|
+
- [ ] Verify date range filtering (updatedOn filter)
|
|
1771
|
+
- [ ] Test empty result handling (no records in date range)
|
|
1772
|
+
- [ ] Verify extraction stops at maxRecords limit
|
|
1773
|
+
|
|
1774
|
+
### 3. Mapping Testing
|
|
1775
|
+
|
|
1776
|
+
- [ ] Verify required fields are populated
|
|
1777
|
+
- [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
|
|
1778
|
+
- [ ] Test custom resolvers with edge cases (if any)
|
|
1779
|
+
- [ ] Verify nested field extraction
|
|
1780
|
+
- [ ] Test with null/missing fields
|
|
1781
|
+
- [ ] Verify mapping error collection works
|
|
1782
|
+
|
|
1783
|
+
### 4. JSON Generation Testing
|
|
1784
|
+
|
|
1785
|
+
- [ ] Verify JSON structure matches expected format
|
|
1786
|
+
- [ ] Test JSON validation against schema (if applicable)
|
|
1787
|
+
- [ ] Verify proper nesting and structure
|
|
1788
|
+
- [ ] Test with large datasets (>1000 records)
|
|
1789
|
+
- [ ] Verify UTF-8 encoding
|
|
1790
|
+
- [ ] Test special character escaping
|
|
1791
|
+
|
|
1792
|
+
### 5. S3 Upload Testing
|
|
1793
|
+
|
|
1794
|
+
- [ ] Test S3 connection and authentication
|
|
1795
|
+
- [ ] Verify file upload to correct bucket and path
|
|
1796
|
+
- [ ] Test file naming convention (timestamp format)
|
|
1797
|
+
- [ ] Verify S3 object metadata
|
|
1798
|
+
- [ ] Test upload retry logic (simulate network failure)
|
|
1799
|
+
- [ ] Verify file permissions and ACLs
|
|
1800
|
+
|
|
1801
|
+
### 6. State Management Testing
|
|
1802
|
+
|
|
1803
|
+
- [ ] Verify overlap buffer prevents missed records (60-second default)
|
|
1804
|
+
- [ ] Test state recovery after extraction failure
|
|
1805
|
+
- [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
|
|
1806
|
+
- [ ] Test first run with no previous state (uses fallbackStartDate)
|
|
1807
|
+
- [ ] Verify state update only happens on successful upload
|
|
1808
|
+
- [ ] Test manual date override (doesn't update state)
|
|
1809
|
+
|
|
1810
|
+
### 7. Job Tracking Testing
|
|
1811
|
+
|
|
1812
|
+
- [ ] Test job creation with JobTracker
|
|
1813
|
+
- [ ] Verify job status updates at each stage
|
|
1814
|
+
- [ ] Test job completion with metadata
|
|
1815
|
+
- [ ] Test job failure handling
|
|
1816
|
+
- [ ] Query job status via webhook endpoint
|
|
1817
|
+
- [ ] Verify job status persists in KV store
|
|
1818
|
+
|
|
1819
|
+
### 8. Error Handling Testing
|
|
1820
|
+
|
|
1821
|
+
- [ ] Test with invalid GraphQL query
|
|
1822
|
+
- [ ] Test with mapping errors (invalid field paths)
|
|
1823
|
+
- [ ] Test with S3 connection failures
|
|
1824
|
+
- [ ] Test with authentication failures
|
|
1825
|
+
- [ ] Test with network timeouts
|
|
1826
|
+
- [ ] Verify error logging includes context (jobId, stage, error details)
|
|
1827
|
+
- [ ] Test error threshold logic (if applicable)
|
|
1828
|
+
|
|
1829
|
+
### 9. Staging Environment Testing
|
|
1830
|
+
|
|
1831
|
+
- [ ] Run full extraction in staging environment
|
|
1832
|
+
- [ ] Verify JSON file format with downstream system
|
|
1833
|
+
- [ ] Monitor extraction duration and resource usage
|
|
1834
|
+
- [ ] Test with production-like data volumes
|
|
1835
|
+
- [ ] Verify no performance degradation over time
|
|
1836
|
+
|
|
1837
|
+
### 10. Integration Testing
|
|
1838
|
+
|
|
1839
|
+
- [ ] Test scheduled workflow (cron trigger)
|
|
1840
|
+
- [ ] Test ad hoc webhook trigger
|
|
1841
|
+
- [ ] Test job status query webhook
|
|
1842
|
+
- [ ] Verify activation variables are read correctly
|
|
1843
|
+
- [ ] Test with different extraction modes (incremental, date range)
|
|
1844
|
+
- [ ] End-to-end test: trigger → extract → transform → upload → verify file
|
|
1845
|
+
|
|
1846
|
+
---
|
|
1847
|
+
## Monitoring & Alerting
|
|
1848
|
+
|
|
1849
|
+
### Success Response Example
|
|
1850
|
+
|
|
1851
|
+
```json
|
|
1852
|
+
{
|
|
1853
|
+
"success": true,
|
|
1854
|
+
"jobId": "SCHEDULED_VP_20251102_140000_abc123",
|
|
1855
|
+
"recordsExtracted": 1523,
|
|
1856
|
+
"fileName": "virtual-positions-2025-11-02T14-00-00-000Z.json",
|
|
1857
|
+
"s3Path": "s3://bucket/virtual-positions/virtual-positions-2025-11-02T14-00-00-000Z.json",
|
|
1858
|
+
"metrics": {
|
|
1859
|
+
"extractionDurationMs": 12543,
|
|
1860
|
+
"totalPages": 8,
|
|
1861
|
+
"pageSize": 200,
|
|
1862
|
+
"mappingErrors": 0,
|
|
1863
|
+
"fileSizeBytes": 524288,
|
|
1864
|
+
"uploadDurationMs": 1234
|
|
1865
|
+
},
|
|
1866
|
+
"timestamps": {
|
|
1867
|
+
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
1868
|
+
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
1869
|
+
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
1870
|
+
},
|
|
1871
|
+
"state": {
|
|
1872
|
+
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
1873
|
+
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
1874
|
+
"stateUpdated": true,
|
|
1875
|
+
"overlapBufferSeconds": 60
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
```
|
|
1879
|
+
|
|
1880
|
+
### Error Response Example
|
|
1881
|
+
|
|
1882
|
+
```json
|
|
1883
|
+
{
|
|
1884
|
+
"success": false,
|
|
1885
|
+
"jobId": "ADHOC_VP_20251102_140500_xyz789",
|
|
1886
|
+
"error": "S3 upload failed: Connection timeout",
|
|
1887
|
+
"errorCategory": "NETWORK",
|
|
1888
|
+
"recordsExtracted": 0,
|
|
1889
|
+
"stage": "s3_upload",
|
|
1890
|
+
"details": {
|
|
1891
|
+
"message": "Failed to upload file after 3 retry attempts",
|
|
1892
|
+
"retryAttempts": 3,
|
|
1893
|
+
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
1894
|
+
},
|
|
1895
|
+
"state": {
|
|
1896
|
+
"stateUpdated": false,
|
|
1897
|
+
"willRetryNextRun": true,
|
|
1898
|
+
"note": "State not advanced - next extraction will retry same time window"
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
```
|
|
1902
|
+
|
|
1903
|
+
### Key Metrics to Track
|
|
1904
|
+
|
|
1905
|
+
```typescript
|
|
1906
|
+
const METRICS = {
|
|
1907
|
+
// Extraction Performance
|
|
1908
|
+
extractionDurationMs: Date.now() - extractionStart,
|
|
1909
|
+
recordCount: records.length,
|
|
1910
|
+
pageCount: extractionResult.stats.totalPages,
|
|
1911
|
+
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
1912
|
+
|
|
1913
|
+
// Transformation Performance
|
|
1914
|
+
transformedCount: transformedRecords.length,
|
|
1915
|
+
failedCount: mappingErrors.length,
|
|
1916
|
+
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
1917
|
+
|
|
1918
|
+
// File Generation
|
|
1919
|
+
fileSizeMB: (jsonContent.length / (1024 * 1024)).toFixed(2),
|
|
1920
|
+
|
|
1921
|
+
// Upload Performance
|
|
1922
|
+
uploadDurationMs: uploadEnd - uploadStart,
|
|
1923
|
+
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
1924
|
+
|
|
1925
|
+
// State Management
|
|
1926
|
+
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
1927
|
+
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
log.info('Extraction metrics', metrics);
|
|
1931
|
+
```
|
|
1932
|
+
|
|
1933
|
+
### Alert Thresholds
|
|
1934
|
+
|
|
1935
|
+
```typescript
|
|
1936
|
+
const ALERT_THRESHOLDS = {
|
|
1937
|
+
// Duration Alerts
|
|
1938
|
+
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
1939
|
+
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
1940
|
+
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
1941
|
+
|
|
1942
|
+
// Error Rate Alerts
|
|
1943
|
+
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
1944
|
+
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
1945
|
+
|
|
1946
|
+
// Volume Alerts
|
|
1947
|
+
MAX_RECORDS_PER_RUN: 100000,
|
|
1948
|
+
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
1949
|
+
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
1950
|
+
|
|
1951
|
+
// State Alerts
|
|
1952
|
+
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
1953
|
+
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1956
|
+
// Check thresholds
|
|
1957
|
+
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
1958
|
+
log.warn('Extraction duration exceeded threshold', {
|
|
1959
|
+
duration: metrics.extractionDurationMs,
|
|
1960
|
+
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
1961
|
+
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
```
|
|
1965
|
+
|
|
1966
|
+
### Monitoring Dashboard Queries
|
|
1967
|
+
|
|
1968
|
+
**Versori Platform Logs Query:**
|
|
1969
|
+
|
|
1970
|
+
```
|
|
1971
|
+
# Successful extractions
|
|
1972
|
+
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
1973
|
+
|
|
1974
|
+
# Failed extractions
|
|
1975
|
+
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
1976
|
+
|
|
1977
|
+
# Performance issues
|
|
1978
|
+
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
1979
|
+
|
|
1980
|
+
# High error rates
|
|
1981
|
+
errorRate:>5
|
|
1982
|
+
|
|
1983
|
+
# State management issues
|
|
1984
|
+
stateUpdated:false AND success:true
|
|
1985
|
+
```
|
|
1986
|
+
|
|
1987
|
+
### Common Issues and Solutions
|
|
1988
|
+
|
|
1989
|
+
**Issue**: "Extraction timeout after 10 minutes"
|
|
1990
|
+
|
|
1991
|
+
- **Cause**: Too many records in single extraction
|
|
1992
|
+
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
1993
|
+
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
1994
|
+
|
|
1995
|
+
**Issue**: "Mapping errors for 50% of records"
|
|
1996
|
+
|
|
1997
|
+
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
1998
|
+
- **Fix**: Run schema validation, update mapping config paths
|
|
1999
|
+
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
2000
|
+
|
|
2001
|
+
**Issue**: "S3 connection timeout"
|
|
2002
|
+
|
|
2003
|
+
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
2004
|
+
- **Fix**: Check S3 credentials, verify network connectivity
|
|
2005
|
+
- **Prevention**: Implement connection health checks, monitor connection status
|
|
2006
|
+
|
|
2007
|
+
**Issue**: "State not updating after successful extraction"
|
|
2008
|
+
|
|
2009
|
+
- **Cause**: KV write failure or intentional retry logic
|
|
2010
|
+
- **Fix**: Check KV logs, verify state update code executed
|
|
2011
|
+
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
2012
|
+
|
|
2013
|
+
**Issue**: "First run exceeds record limits"
|
|
2014
|
+
|
|
2015
|
+
- **Cause**: No previous timestamp, fetches all historical records
|
|
2016
|
+
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
2017
|
+
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
2018
|
+
|
|
2019
|
+
**Issue**: "Excessive duplicate records in output"
|
|
2020
|
+
|
|
2021
|
+
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
2022
|
+
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
2023
|
+
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
2024
|
+
|
|
2025
|
+
---
|
|
2026
|
+
|
|
2027
|
+
## Troubleshooting Quick Reference
|
|
2028
|
+
|
|
2029
|
+
| Error Message | Likely Cause | Solution |
|
|
2030
|
+
|--------------|--------------|----------|
|
|
2031
|
+
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
2032
|
+
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
2033
|
+
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
2034
|
+
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
2035
|
+
| "S3 authentication failed" | Invalid credentials | Verify S3 credentials in activation variables |
|
|
2036
|
+
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
2037
|
+
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
2038
|
+
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
2039
|
+
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
2040
|
+
| "JSON generation failed" | Format-specific error | Check JSON generation logic, validate output |
|
|
2041
|
+
|
|
2042
|
+
---
|