@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +11 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
|
@@ -1,1953 +1,1953 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-extract-orders-to-s3-csv
|
|
3
|
-
canonical_filename: template-extraction-orders-to-s3-csv.md
|
|
4
|
-
version: 2.0.0
|
|
5
|
-
sdk_version: ^0.1.39
|
|
6
|
-
runtime: versori
|
|
7
|
-
direction: extraction
|
|
8
|
-
source: fluent-graphql
|
|
9
|
-
destination: s3-csv
|
|
10
|
-
entity: orders
|
|
11
|
-
format: csv
|
|
12
|
-
logging: versori
|
|
13
|
-
status: stable
|
|
14
|
-
features:
|
|
15
|
-
- memory-management
|
|
16
|
-
- enhanced-logging
|
|
17
|
-
- pagination-progress
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
# Template: Extraction - Orders to S3 CSV
|
|
21
|
-
|
|
22
|
-
**Template Version:** 2.0.0
|
|
23
|
-
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
24
|
-
**Last Updated:** 2025-01-24
|
|
25
|
-
**Deployment Target:** Versori Platform
|
|
26
|
-
|
|
27
|
-
**🆕 Version 2.0.0 Enhancements:**
|
|
28
|
-
- ✅ **Memory Management** - Clear large result sets after processing batches
|
|
29
|
-
- ✅ **Enhanced Logging** - Pagination progress tracking with emoji indicators (📊, 📥, ✅)
|
|
30
|
-
- ✅ **Pagination Progress** - Real-time page-by-page progress logging with metrics
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## 📚 STEP 1: Load These Docs (Human Checklist)
|
|
35
|
-
|
|
36
|
-
1. REQUIRED (load all)
|
|
37
|
-
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
38
|
-
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
39
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
40
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
41
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
42
|
-
- [ ] fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
43
|
-
|
|
44
|
-
Copy-paste list (open these):
|
|
45
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
46
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
47
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
48
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
49
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
50
|
-
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## 📋 STEP 2: Tell Your AI (Prompt)
|
|
55
|
-
|
|
56
|
-
Copy/paste this prompt after loading the documentation above:
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
Create a Versori scheduled extractor for orders that uses ExtractionOrchestrator + JobTracker, incremental updatedOn with a 60s overlap buffer, transforms via UniversalMapper, generates CSV with CSVParserService.stringify(), uploads to S3 using S3DataSource. Include 3 workflows: scheduled, ad-hoc webhook, and job-status query with native Versori logging.
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## 📦 SDK Imports (Verified - Versori Optimized)
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
import { Buffer } from 'node:buffer';
|
|
68
|
-
import {
|
|
69
|
-
createClient,
|
|
70
|
-
ExtractionOrchestrator,
|
|
71
|
-
JobTracker,
|
|
72
|
-
UniversalMapper,
|
|
73
|
-
CSVParserService,
|
|
74
|
-
S3DataSource,
|
|
75
|
-
VersoriKVAdapter,
|
|
76
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
77
|
-
|
|
78
|
-
import { schedule, webhook, http, fn } from '@versori/run';
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
# Versori Scheduled: Orders Extraction to S3 CSV (Incremental)
|
|
84
|
-
|
|
85
|
-
**FC Connect SDK Use Case Guide**
|
|
86
|
-
|
|
87
|
-
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
88
|
-
> **Installation**: `npm install @fluentcommerce/fc-connect-sdk`
|
|
89
|
-
|
|
90
|
-
Context: Scheduled Versori workflow that extracts new/updated orders from Fluent Commerce via GraphQL query with **ExtractionOrchestrator**, **JobTracker**, and **incremental timestamp tracking**, transforms with `UniversalMapper`, and writes **CSV files** to S3 for 3PL/fulfillment systems.
|
|
91
|
-
|
|
92
|
-
**Pattern**: EXTRACTION (Fluent → S3 CSV)
|
|
93
|
-
**Complexity**: High | Runtime: Versori Platform (Scheduled)
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## ⚠️ IMPORTANT: Production-Ready Base Template
|
|
98
|
-
|
|
99
|
-
> **📋 BASE TEMPLATE - Ready for Production (Customize for Your Needs)**
|
|
100
|
-
>
|
|
101
|
-
> This is a **production-ready base template** demonstrating FC Connect SDK best practices for order extraction workflows with CSV output to S3.
|
|
102
|
-
>
|
|
103
|
-
> **✅ INCLUDED FEATURES:**
|
|
104
|
-
>
|
|
105
|
-
> - ✅ Comprehensive error handling with retry logic
|
|
106
|
-
> - ✅ S3 upload with proper error handling
|
|
107
|
-
> - ✅ State management with overlap buffer (prevents missed records)
|
|
108
|
-
> - ✅ Job tracking with lifecycle management
|
|
109
|
-
> - ✅ Security (credential masking in logs)
|
|
110
|
-
> - ✅ UTC time enforcement (prevents timezone bugs)
|
|
111
|
-
> - ✅ Incremental extraction (safe, efficient, production-ready)
|
|
112
|
-
> - ✅ Natural rate limiting via timestamps
|
|
113
|
-
>
|
|
114
|
-
> **📝 BEFORE DEPLOYING:**
|
|
115
|
-
>
|
|
116
|
-
> 1. Review and customize activation variables for your environment
|
|
117
|
-
> 2. Test with sample data in your Versori workspace
|
|
118
|
-
> 3. Adjust safety limits (pageSize, maxRecords) if needed
|
|
119
|
-
> 4. Configure monitoring alerts for extraction failures
|
|
120
|
-
> 5. Verify S3 bucket credentials and paths
|
|
121
|
-
>
|
|
122
|
-
> **This base template follows SDK best practices - tweak specific to your needs.**
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
## What You'll Build
|
|
127
|
-
|
|
128
|
-
- **Incremental extraction** using `updatedOn >= (lastRunTime - buffer)` with **overlap buffer**
|
|
129
|
-
- **ExtractionOrchestrator** for auto-pagination and path-based extraction
|
|
130
|
-
- **JobTracker** for lifecycle management
|
|
131
|
-
- **State management** with VersoriKV to track last successful run
|
|
132
|
-
- **Safety buffer** (60 seconds) to handle clock skew and race conditions
|
|
133
|
-
- GraphQL query with nested order lines
|
|
134
|
-
- UniversalMapper transformation with line item flattening
|
|
135
|
-
- **CSV file generation** with CSVParserService
|
|
136
|
-
- **S3 upload** to 3PL/fulfillment system
|
|
137
|
-
- **3 workflow patterns**: scheduled, ad-hoc webhook, job status query
|
|
138
|
-
- **Failure recovery** with timestamp tracking
|
|
139
|
-
|
|
140
|
-
## Business Use Case
|
|
141
|
-
|
|
142
|
-
**Hourly order feed to 3PL/fulfillment center:**
|
|
143
|
-
|
|
144
|
-
- Extract new and updated orders since last run
|
|
145
|
-
- Flatten line items (one CSV row per item)
|
|
146
|
-
- Generate CSV file with order header + line items
|
|
147
|
-
- Upload to S3 bucket for 3PL integration
|
|
148
|
-
- Run every hour to enable real-time fulfillment
|
|
149
|
-
- Support order updates (address changes, item modifications)
|
|
150
|
-
- Standard CSV format for WMS/ERP imports
|
|
151
|
-
|
|
152
|
-
## SDK Methods Used
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
import { Buffer } from 'node:buffer';
|
|
156
|
-
import {
|
|
157
|
-
createClient,
|
|
158
|
-
ExtractionOrchestrator,
|
|
159
|
-
JobTracker,
|
|
160
|
-
UniversalMapper,
|
|
161
|
-
CSVParserService,
|
|
162
|
-
S3DataSource,
|
|
163
|
-
VersoriKVAdapter,
|
|
164
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
165
|
-
|
|
166
|
-
await createClient(ctx); // Versori-aware client
|
|
167
|
-
const orchestrator = new ExtractionOrchestrator(client, log); // Auto-pagination
|
|
168
|
-
const tracker = new JobTracker(kv, log); // Job lifecycle tracking
|
|
169
|
-
await orchestrator.extract({ query, resultPath, variables, pageSize, maxRecords }); // Extract
|
|
170
|
-
new VersoriKVAdapter(ctx.openKv(':project:')); // State management
|
|
171
|
-
new UniversalMapper(exportMapping); // Field transformation
|
|
172
|
-
const csvParser = new CSVParserService(); // CSV generation
|
|
173
|
-
csvParser.stringify(records, { headers: true }); // Generate CSV content (synchronous)
|
|
174
|
-
await s3.upload(key, buffer, contentType, metadata); // S3 upload
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## Activation Variables
|
|
178
|
-
|
|
179
|
-
```json
|
|
180
|
-
{
|
|
181
|
-
"retailerId": "your-retailer-id",
|
|
182
|
-
"s3BucketName": "3pl-orders-export",
|
|
183
|
-
"awsAccessKeyId": "AKIAXXXXXXXXXXXX",
|
|
184
|
-
"awsSecretAccessKey": "********",
|
|
185
|
-
"awsRegion": "us-east-1",
|
|
186
|
-
"s3Prefix": "orders/new/",
|
|
187
|
-
"pageSize": 200,
|
|
188
|
-
"maxRecords": 10000,
|
|
189
|
-
"fallbackStartDate": "2024-01-01T00:00:00Z",
|
|
190
|
-
"overlapBufferSeconds": "60",
|
|
191
|
-
"validateConnection": "true"
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## Export Mapping Configuration
|
|
196
|
-
|
|
197
|
-
Create file: `./config/orders.export.csv.json`
|
|
198
|
-
|
|
199
|
-
```json
|
|
200
|
-
{
|
|
201
|
-
"name": "orders.export.csv",
|
|
202
|
-
"version": "1.0.0",
|
|
203
|
-
"description": "Fluent Orders → 3PL CSV Export",
|
|
204
|
-
"fields": {
|
|
205
|
-
"order_id": { "source": "ref", "required": true, "resolver": "sdk.trim" },
|
|
206
|
-
"order_date": { "source": "createdOn", "required": true, "resolver": "sdk.formatDateShort" },
|
|
207
|
-
"order_status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
|
|
208
|
-
"customer_name": { "source": "customer.firstName", "required": false, "resolver": "sdk.trim" },
|
|
209
|
-
"customer_email": { "source": "customer.email", "required": false, "resolver": "sdk.trim" },
|
|
210
|
-
"ship_to_name": { "source": "deliveryAddress.name", "required": true, "resolver": "sdk.trim" },
|
|
211
|
-
"ship_to_address1": {
|
|
212
|
-
"source": "deliveryAddress.street1",
|
|
213
|
-
"required": true,
|
|
214
|
-
"resolver": "sdk.trim"
|
|
215
|
-
},
|
|
216
|
-
"ship_to_city": { "source": "deliveryAddress.city", "required": true, "resolver": "sdk.trim" },
|
|
217
|
-
"ship_to_state": {
|
|
218
|
-
"source": "deliveryAddress.state",
|
|
219
|
-
"required": true,
|
|
220
|
-
"resolver": "sdk.uppercase"
|
|
221
|
-
},
|
|
222
|
-
"ship_to_zip": {
|
|
223
|
-
"source": "deliveryAddress.postcode",
|
|
224
|
-
"required": true,
|
|
225
|
-
"resolver": "sdk.trim"
|
|
226
|
-
},
|
|
227
|
-
"ship_to_country": {
|
|
228
|
-
"source": "deliveryAddress.country",
|
|
229
|
-
"required": true,
|
|
230
|
-
"resolver": "sdk.uppercase"
|
|
231
|
-
},
|
|
232
|
-
"line_item_sku": { "source": "product.ref", "required": true, "resolver": "sdk.trim" },
|
|
233
|
-
"line_item_qty": { "source": "quantity", "required": true, "resolver": "sdk.parseInt" },
|
|
234
|
-
"line_item_price": { "source": "price", "required": true, "resolver": "sdk.parseFloat" }
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
## GraphQL Query
|
|
240
|
-
|
|
241
|
-
```graphql
|
|
242
|
-
query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, $first: Int!, $after: String) {
|
|
243
|
-
orders(
|
|
244
|
-
retailerId: $retailerId
|
|
245
|
-
updatedOn: { after: $updatedAfter }
|
|
246
|
-
first: $first
|
|
247
|
-
after: $after
|
|
248
|
-
) {
|
|
249
|
-
edges {
|
|
250
|
-
node {
|
|
251
|
-
id
|
|
252
|
-
ref
|
|
253
|
-
status
|
|
254
|
-
createdOn
|
|
255
|
-
updatedOn
|
|
256
|
-
customer {
|
|
257
|
-
firstName
|
|
258
|
-
lastName
|
|
259
|
-
email
|
|
260
|
-
}
|
|
261
|
-
deliveryAddress {
|
|
262
|
-
name
|
|
263
|
-
street1
|
|
264
|
-
street2
|
|
265
|
-
city
|
|
266
|
-
state
|
|
267
|
-
postcode
|
|
268
|
-
country
|
|
269
|
-
}
|
|
270
|
-
items {
|
|
271
|
-
id
|
|
272
|
-
quantity
|
|
273
|
-
price
|
|
274
|
-
product {
|
|
275
|
-
ref
|
|
276
|
-
name
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
cursor
|
|
281
|
-
}
|
|
282
|
-
pageInfo {
|
|
283
|
-
hasNextPage
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
---
|
|
290
|
-
|
|
291
|
-
## Versori Workflows Structure
|
|
292
|
-
|
|
293
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
294
|
-
|
|
295
|
-
**Trigger Types:**
|
|
296
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
297
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
298
|
-
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
299
|
-
|
|
300
|
-
**Execution Steps (chained to triggers):**
|
|
301
|
-
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
302
|
-
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
303
|
-
|
|
304
|
-
### Recommended Project Structure (MemoryInterpreter Pattern)
|
|
305
|
-
|
|
306
|
-
```
|
|
307
|
-
orders-extraction/
|
|
308
|
-
├── index.ts # Entry point - exports all workflows (MemoryInterpreter pattern)
|
|
309
|
-
└── src/
|
|
310
|
-
├── workflows/
|
|
311
|
-
│ ├── scheduled/
|
|
312
|
-
│ │ └── daily-orders-extraction.ts # Scheduled: Daily orders extraction
|
|
313
|
-
│ │
|
|
314
|
-
│ └── webhook/
|
|
315
|
-
│ ├── adhoc-orders-extraction.ts # Webhook: Manual trigger
|
|
316
|
-
│ └── job-status-check.ts # Webhook: Status query
|
|
317
|
-
│
|
|
318
|
-
├── services/
|
|
319
|
-
│ └── orders-extraction.service.ts # Shared orchestration logic (reusable)
|
|
320
|
-
│
|
|
321
|
-
└── config/
|
|
322
|
-
└── orders.export.csv.json # Mapping configuration
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
**MemoryInterpreter Pattern:**
|
|
326
|
-
- `index.ts` exports workflow definitions (lightweight registration)
|
|
327
|
-
- Workflow files delegate to service functions for business logic
|
|
328
|
-
- Service functions contain the actual implementation
|
|
329
|
-
- This pattern enables better memory management and code organization
|
|
330
|
-
|
|
331
|
-
**Note:** The code examples below demonstrate what goes in each workflow file. You would split them into separate files following the structure above.
|
|
332
|
-
|
|
333
|
-
---
|
|
334
|
-
|
|
335
|
-
```csv
|
|
336
|
-
order_id,order_date,order_status,customer_name,customer_email,ship_to_name,ship_to_address1,ship_to_city,ship_to_state,ship_to_zip,ship_to_country,line_item_sku,line_item_qty,line_item_price
|
|
337
|
-
ORD-001,2025-01-22,CREATED,John,john@example.com,John Smith,123 Main St,New York,NY,10001,US,SKU-001,2,29.99
|
|
338
|
-
ORD-001,2025-01-22,CREATED,John,john@example.com,John Smith,123 Main St,New York,NY,10001,US,SKU-002,1,49.99
|
|
339
|
-
ORD-002,2025-01-22,PAID,Jane,jane@example.com,Jane Doe,456 Oak Ave,Los Angeles,CA,90001,US,SKU-003,1,19.99
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
**Note**: Each line item becomes a separate row with duplicated order header data (standard 3PL format).
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
|
-
## Complete Implementation
|
|
347
|
-
|
|
348
|
-
### Workflow 1: Scheduled Extraction (Main Workflow)
|
|
349
|
-
|
|
350
|
-
**File: `src/workflows/scheduled/daily-orders-extraction.ts`**
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
import { schedule, http } from '@versori/run';
|
|
354
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
355
|
-
import { processOrdersExtraction } from '../../services/orders-extraction.service';
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Scheduled workflow: Hourly orders extraction to S3 CSV
|
|
359
|
-
* Runs every hour
|
|
360
|
-
*
|
|
361
|
-
* DELEGATION PATTERN (MemoryInterpreter):
|
|
362
|
-
* - Workflow receives ctx from Versori
|
|
363
|
-
* - Passes entire ctx to service function
|
|
364
|
-
* - Service handles all business logic
|
|
365
|
-
*/
|
|
366
|
-
export const ordersExtractionScheduled = schedule('orders-extract-hourly', '0 * * * *').then(
|
|
367
|
-
http('extract-orders', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
368
|
-
const { log, openKv } = ctx;
|
|
369
|
-
const executionStartTime = Date.now();
|
|
370
|
-
const jobId = `SCHEDULED_ORD_${new Date().toISOString().replace(/[:.]/g, '-')}_${Date.now()}`;
|
|
371
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
372
|
-
|
|
373
|
-
log.info('🚀 [WORKFLOW] Starting scheduled orders extraction', { jobId });
|
|
374
|
-
|
|
375
|
-
await tracker.createJob(jobId, {
|
|
376
|
-
triggeredBy: 'schedule',
|
|
377
|
-
stage: 'initialization',
|
|
378
|
-
startTime: executionStartTime,
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
await tracker.updateJob(jobId, { status: 'processing' });
|
|
382
|
-
|
|
383
|
-
try {
|
|
384
|
-
// DELEGATION: Pass entire ctx to service function
|
|
385
|
-
const result = await processOrdersExtraction(ctx, jobId, tracker);
|
|
386
|
-
|
|
387
|
-
const duration = Date.now() - executionStartTime;
|
|
388
|
-
|
|
389
|
-
if (result.success) {
|
|
390
|
-
await tracker.markCompleted(jobId, { ...result, duration });
|
|
391
|
-
log.info('✅ [WORKFLOW] Orders extraction completed successfully', {
|
|
392
|
-
jobId,
|
|
393
|
-
ordersExtracted: result.ordersExtracted,
|
|
394
|
-
fileName: result.fileName,
|
|
395
|
-
duration: `${duration}ms`,
|
|
396
|
-
});
|
|
397
|
-
} else {
|
|
398
|
-
await tracker.markFailed(jobId, result.error || 'Unknown error');
|
|
399
|
-
log.error('❌ [WORKFLOW] Orders extraction failed', {
|
|
400
|
-
jobId,
|
|
401
|
-
error: result.error,
|
|
402
|
-
duration: `${duration}ms`,
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return { success: true, jobId, ...result, duration };
|
|
407
|
-
} catch (e: any) {
|
|
408
|
-
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
409
|
-
const duration = Date.now() - executionStartTime;
|
|
410
|
-
|
|
411
|
-
await tracker.markFailed(jobId, errorMessage);
|
|
412
|
-
|
|
413
|
-
log.error('❌ [WORKFLOW] Orders extraction failed with exception', {
|
|
414
|
-
jobId,
|
|
415
|
-
error: errorMessage,
|
|
416
|
-
stack: e instanceof Error ? e.stack : undefined,
|
|
417
|
-
duration: `${duration}ms`,
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
return {
|
|
421
|
-
success: false,
|
|
422
|
-
jobId,
|
|
423
|
-
error: errorMessage,
|
|
424
|
-
duration,
|
|
425
|
-
recommendations: [
|
|
426
|
-
'Check SFTP/S3 connection credentials',
|
|
427
|
-
'Verify GraphQL query permissions',
|
|
428
|
-
'Review error stack trace for details'
|
|
429
|
-
]
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
})
|
|
433
|
-
);
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
**File: `src/services/orders-extraction.service.ts`**
|
|
437
|
-
|
|
438
|
-
```typescript
|
|
439
|
-
import { Buffer } from 'node:buffer';
|
|
440
|
-
import {
|
|
441
|
-
createClient,
|
|
442
|
-
ExtractionOrchestrator,
|
|
443
|
-
JobTracker,
|
|
444
|
-
UniversalMapper,
|
|
445
|
-
CSVParserService,
|
|
446
|
-
S3DataSource,
|
|
447
|
-
VersoriKVAdapter,
|
|
448
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
449
|
-
import ordersExportMapping from '../config/orders.export.csv.json' with { type: 'json' };
|
|
450
|
-
|
|
451
|
-
const ORDERS_QUERY = `
|
|
452
|
-
query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, $first: Int!, $after: String) {
|
|
453
|
-
orders(
|
|
454
|
-
retailerId: $retailerId
|
|
455
|
-
updatedOn: { after: $updatedAfter }
|
|
456
|
-
first: $first
|
|
457
|
-
after: $after
|
|
458
|
-
) {
|
|
459
|
-
edges {
|
|
460
|
-
node {
|
|
461
|
-
id
|
|
462
|
-
ref
|
|
463
|
-
status
|
|
464
|
-
createdOn
|
|
465
|
-
updatedOn
|
|
466
|
-
customer {
|
|
467
|
-
firstName
|
|
468
|
-
lastName
|
|
469
|
-
email
|
|
470
|
-
}
|
|
471
|
-
deliveryAddress {
|
|
472
|
-
name
|
|
473
|
-
street1
|
|
474
|
-
street2
|
|
475
|
-
city
|
|
476
|
-
state
|
|
477
|
-
postcode
|
|
478
|
-
country
|
|
479
|
-
}
|
|
480
|
-
items {
|
|
481
|
-
id
|
|
482
|
-
quantity
|
|
483
|
-
price
|
|
484
|
-
product {
|
|
485
|
-
ref
|
|
486
|
-
name
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
cursor
|
|
491
|
-
}
|
|
492
|
-
pageInfo {
|
|
493
|
-
hasNextPage
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
`;
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Main orders extraction service function
|
|
501
|
-
*
|
|
502
|
-
* This function orchestrates the complete extraction process:
|
|
503
|
-
* 1. Client initialization
|
|
504
|
-
* 2. State management and incremental sync
|
|
505
|
-
* 3. GraphQL extraction with pagination
|
|
506
|
-
* 4. Data transformation and CSV generation
|
|
507
|
-
* 5. S3 upload and state tracking
|
|
508
|
-
*
|
|
509
|
-
* @param ctx - Versori context object containing fetch, connections, log, activation, openKv
|
|
510
|
-
* @param jobId - Job ID for tracking
|
|
511
|
-
* @param tracker - JobTracker instance for job lifecycle management
|
|
512
|
-
*/
|
|
513
|
-
export async function processOrdersExtraction(ctx: any, jobId: string, tracker: JobTracker) {
|
|
514
|
-
const { log, openKv, activation } = ctx;
|
|
515
|
-
const startTime = Date.now();
|
|
516
|
-
|
|
517
|
-
log.info('🔍 [OrdersExtraction] ==================== EXECUTION START ====================');
|
|
518
|
-
log.info('🔍 [ExtractionOrchestrator] Starting extraction');
|
|
519
|
-
|
|
520
|
-
try {
|
|
521
|
-
// STEP 1/8: Extract context and configuration
|
|
522
|
-
log.info('📄 [OrdersExtraction] STEP 1/8: Loading configuration');
|
|
523
|
-
const retailerId = activation?.getVariable('retailerId');
|
|
524
|
-
const pageSize = parseInt(activation?.getVariable('pageSize') || '200', 10);
|
|
525
|
-
const maxRecords = parseInt(activation?.getVariable('maxRecords') || '10000', 10);
|
|
526
|
-
const fallbackStartDate =
|
|
527
|
-
activation?.getVariable('fallbackStartDate') || '2024-01-01T00:00:00Z';
|
|
528
|
-
const overlapBufferSeconds = parseInt(
|
|
529
|
-
activation?.getVariable('overlapBufferSeconds') || '60',
|
|
530
|
-
10
|
|
531
|
-
);
|
|
532
|
-
const OVERLAP_BUFFER_MS = overlapBufferSeconds * 1000;
|
|
533
|
-
const validateConnection = activation?.getVariable('validateConnection') === 'true';
|
|
534
|
-
|
|
535
|
-
const s3Config = {
|
|
536
|
-
bucket: activation?.getVariable('s3BucketName'),
|
|
537
|
-
region: activation?.getVariable('awsRegion') || 'us-east-1',
|
|
538
|
-
accessKeyId: activation?.getVariable('awsAccessKeyId'),
|
|
539
|
-
secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
|
|
540
|
-
};
|
|
541
|
-
const s3Prefix = activation?.getVariable('s3Prefix') || 'orders/new/';
|
|
542
|
-
|
|
543
|
-
const missing: string[] = [];
|
|
544
|
-
if (!retailerId) missing.push('retailerId');
|
|
545
|
-
if (!s3Config.bucket) missing.push('s3BucketName');
|
|
546
|
-
if (!s3Config.accessKeyId) missing.push('awsAccessKeyId');
|
|
547
|
-
if (!s3Config.secretAccessKey) missing.push('awsSecretAccessKey');
|
|
548
|
-
if (missing.length) {
|
|
549
|
-
log.error('❌ [OrdersExtraction] Missing required activation variables', { missing });
|
|
550
|
-
return {
|
|
551
|
-
success: false,
|
|
552
|
-
error: `Missing required variables: ${missing.join(', ')}`,
|
|
553
|
-
recommendations: [
|
|
554
|
-
'Check Activation Variables in Versori dashboard',
|
|
555
|
-
'Ensure all required credentials are configured',
|
|
556
|
-
'Verify connection settings for fluent_commerce and S3'
|
|
557
|
-
]
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// STEP 2/8: Initialize SDK services
|
|
562
|
-
log.info('📄 [OrdersExtraction] STEP 2/8: Initializing SDK services');
|
|
563
|
-
const client = await createClient(ctx);
|
|
564
|
-
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
565
|
-
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
566
|
-
const mapper = new UniversalMapper(ordersExportMapping);
|
|
567
|
-
const csvParser = new CSVParserService();
|
|
568
|
-
const s3 = new S3DataSource(
|
|
569
|
-
{
|
|
570
|
-
type: 'S3_CSV',
|
|
571
|
-
connectionId: 's3-orders-export',
|
|
572
|
-
name: 'S3 Orders Export',
|
|
573
|
-
s3Config,
|
|
574
|
-
},
|
|
575
|
-
log
|
|
576
|
-
);
|
|
577
|
-
|
|
578
|
-
// Validate S3 connection if enabled
|
|
579
|
-
if (validateConnection) {
|
|
580
|
-
log.info('🔍 [OrdersExtraction] Validating S3 connection');
|
|
581
|
-
try {
|
|
582
|
-
await s3.validateConnection();
|
|
583
|
-
log.info('✅ [OrdersExtraction] S3 connection validated successfully');
|
|
584
|
-
} catch (validationError: any) {
|
|
585
|
-
log.error('❌ [OrdersExtraction] S3 connection validation failed', {
|
|
586
|
-
error: validationError.message,
|
|
587
|
-
});
|
|
588
|
-
return {
|
|
589
|
-
success: false,
|
|
590
|
-
error: `S3 connection validation failed: ${validationError.message}`,
|
|
591
|
-
recommendations: [
|
|
592
|
-
'Verify S3 bucket exists and is accessible',
|
|
593
|
-
'Check AWS credentials (accessKeyId, secretAccessKey)',
|
|
594
|
-
'Ensure correct AWS region is specified',
|
|
595
|
-
'Verify IAM permissions for S3 operations'
|
|
596
|
-
]
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// STEP 3/8: Create job and load state
|
|
602
|
-
log.info('📄 [OrdersExtraction] STEP 3/8: Loading state for incremental sync');
|
|
603
|
-
|
|
604
|
-
const stateKey = ['extraction', 'orders', 'lastRunTime'];
|
|
605
|
-
const lastRunState = await kv.get(stateKey);
|
|
606
|
-
const rawLastRunTime = lastRunState?.value?.timestamp || fallbackStartDate;
|
|
607
|
-
|
|
608
|
-
// Apply overlap buffer for query (WITH buffer)
|
|
609
|
-
const bufferedLastRunTime = new Date(
|
|
610
|
-
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
|
|
611
|
-
).toISOString();
|
|
612
|
-
|
|
613
|
-
log.info('🔍 [OrdersExtraction] Starting incremental extraction with overlap buffer', {
|
|
614
|
-
rawLastRunTime,
|
|
615
|
-
bufferedLastRunTime,
|
|
616
|
-
overlapBufferSeconds,
|
|
617
|
-
retailerId,
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
// STEP 4/8: Execute extraction with ExtractionOrchestrator
|
|
621
|
-
log.info('📄 [OrdersExtraction] STEP 4/8: Executing GraphQL extraction');
|
|
622
|
-
const extractionStartTime = Date.now();
|
|
623
|
-
|
|
624
|
-
const result = await orchestrator.extract({
|
|
625
|
-
query: ORDERS_QUERY,
|
|
626
|
-
resultPath: 'orders.edges.node',
|
|
627
|
-
variables: {
|
|
628
|
-
retailerId,
|
|
629
|
-
updatedAfter: bufferedLastRunTime, // ← WITH overlap buffer
|
|
630
|
-
},
|
|
631
|
-
pageSize,
|
|
632
|
-
maxRecords,
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
const orders = result.data || [];
|
|
636
|
-
const extractionDuration = Date.now() - extractionStartTime;
|
|
637
|
-
|
|
638
|
-
log.info('✅ [ExtractionOrchestrator] Extraction completed', {
|
|
639
|
-
totalRecords: result.stats.totalRecords,
|
|
640
|
-
totalPages: result.stats.totalPages,
|
|
641
|
-
validRecords: result.stats.validRecords ?? orders.length,
|
|
642
|
-
errors: result.errors ? result.errors.length : 0,
|
|
643
|
-
duration: `${extractionDuration}ms`,
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
if (result.errors && result.errors.length > 0) {
|
|
647
|
-
log.warn('⚠️ [OrdersExtraction] Non-fatal extraction errors encountered', {
|
|
648
|
-
errorCount: result.errors.length,
|
|
649
|
-
sampleErrors: result.errors.slice(0, 3),
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
if (orders.length === 0) {
|
|
654
|
-
const duration = Date.now() - startTime;
|
|
655
|
-
log.info('✅ [OrdersExtraction] No new orders to extract');
|
|
656
|
-
await kv.set(stateKey, {
|
|
657
|
-
timestamp: new Date().toISOString(),
|
|
658
|
-
orderCount: 0,
|
|
659
|
-
extractedAt: new Date().toISOString(),
|
|
660
|
-
});
|
|
661
|
-
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
662
|
-
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
663
|
-
return {
|
|
664
|
-
success: true,
|
|
665
|
-
message: 'No new orders to extract',
|
|
666
|
-
lastRunTime: rawLastRunTime,
|
|
667
|
-
duration,
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
log.info('📄 [OrdersExtraction] Orders retrieved', { count: orders.length });
|
|
672
|
-
|
|
673
|
-
// STEP 5/8: Transform and flatten line items (bulk mapping)
|
|
674
|
-
log.info('📄 [OrdersExtraction] STEP 5/8: Transforming and flattening line items');
|
|
675
|
-
const transformStartTime = Date.now();
|
|
676
|
-
|
|
677
|
-
// Collect all flattened records first (order + item combinations)
|
|
678
|
-
const flattenedInputs: any[] = [];
|
|
679
|
-
|
|
680
|
-
for (const order of orders) {
|
|
681
|
-
const lineItems = order.items || [];
|
|
682
|
-
|
|
683
|
-
if (lineItems.length === 0) {
|
|
684
|
-
log.warn('⚠️ [OrdersExtraction] Order has no line items, skipping', {
|
|
685
|
-
orderId: order.id,
|
|
686
|
-
ref: order.ref,
|
|
687
|
-
});
|
|
688
|
-
continue;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Create flattened records (one per line item)
|
|
692
|
-
for (const item of lineItems) {
|
|
693
|
-
flattenedInputs.push({
|
|
694
|
-
...order, // Order header data
|
|
695
|
-
...item, // Line item data (overwrites items array with individual item)
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
if (flattenedInputs.length === 0) {
|
|
701
|
-
const duration = Date.now() - startTime;
|
|
702
|
-
log.error('❌ [OrdersExtraction] All records skipped (no line items)');
|
|
703
|
-
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
704
|
-
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
705
|
-
return {
|
|
706
|
-
success: false,
|
|
707
|
-
error: 'All orders skipped because they contain no line items',
|
|
708
|
-
duration,
|
|
709
|
-
recommendations: [
|
|
710
|
-
'Check order data quality in Fluent Commerce',
|
|
711
|
-
'Verify line items are being populated correctly',
|
|
712
|
-
'Review GraphQL query to ensure items are included'
|
|
713
|
-
]
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// Bulk mapping with UniversalMapper
|
|
718
|
-
const mappingResult = await mapper.map(flattenedInputs);
|
|
719
|
-
const transformDuration = Date.now() - transformStartTime;
|
|
720
|
-
|
|
721
|
-
if (!mappingResult.success) {
|
|
722
|
-
const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
|
|
723
|
-
const duration = Date.now() - startTime;
|
|
724
|
-
log.error('❌ [OrdersExtraction] Mapping failed - terminating job', {
|
|
725
|
-
errorCount: mappingErrors.length,
|
|
726
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
727
|
-
duration: `${duration}ms`,
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
731
|
-
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
732
|
-
|
|
733
|
-
return {
|
|
734
|
-
success: false,
|
|
735
|
-
error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
|
|
736
|
-
errors: mappingErrors,
|
|
737
|
-
duration,
|
|
738
|
-
recommendations: [
|
|
739
|
-
'Check mapping configuration in orders.export.csv.json',
|
|
740
|
-
'Verify source field paths match GraphQL response',
|
|
741
|
-
'Review sample errors for specific field issues',
|
|
742
|
-
'Validate resolver functions are working correctly'
|
|
743
|
-
]
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const flattenedRecords = mappingResult.data || [];
|
|
748
|
-
const mappingErrors = mappingResult.errors || [];
|
|
749
|
-
|
|
750
|
-
if (mappingErrors.length > 0) {
|
|
751
|
-
log.warn('⚠️ [OrdersExtraction] Some records failed transformation', {
|
|
752
|
-
errorCount: mappingErrors.length,
|
|
753
|
-
sampleErrors: mappingErrors.slice(0, 3),
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
758
|
-
log.info('ℹ️ [OrdersExtraction] Optional fields skipped (undefined values)', {
|
|
759
|
-
skippedFields: mappingResult.skippedFields,
|
|
760
|
-
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
if (flattenedRecords.length === 0) {
|
|
765
|
-
const duration = Date.now() - startTime;
|
|
766
|
-
log.error('❌ [OrdersExtraction] All records failed mapping');
|
|
767
|
-
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
768
|
-
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
769
|
-
return {
|
|
770
|
-
success: false,
|
|
771
|
-
error: 'All records failed mapping',
|
|
772
|
-
errors: mappingErrors,
|
|
773
|
-
duration,
|
|
774
|
-
recommendations: [
|
|
775
|
-
'Review mapping configuration thoroughly',
|
|
776
|
-
'Check for schema changes in Fluent Commerce',
|
|
777
|
-
'Validate all required fields are present',
|
|
778
|
-
'Test with sample data first'
|
|
779
|
-
]
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
log.info('✅ [OrdersExtraction] Records transformed', {
|
|
784
|
-
orders: orders.length,
|
|
785
|
-
lineItems: flattenedRecords.length,
|
|
786
|
-
failed: mappingErrors.length,
|
|
787
|
-
duration: `${transformDuration}ms`,
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
// STEP 6/8: Generate CSV content (synchronous, not async)
|
|
791
|
-
log.info('📄 [OrdersExtraction] STEP 6/8: Generating CSV content');
|
|
792
|
-
const csvStartTime = Date.now();
|
|
793
|
-
const csvContent = csvParser.stringify(flattenedRecords, { headers: true });
|
|
794
|
-
const csvDuration = Date.now() - csvStartTime;
|
|
795
|
-
|
|
796
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
797
|
-
const fileName = `orders-${timestamp}.csv`;
|
|
798
|
-
const s3Key = `${s3Prefix}${fileName}`;
|
|
799
|
-
|
|
800
|
-
log.info('✅ [OrdersExtraction] Generated CSV file', {
|
|
801
|
-
fileName,
|
|
802
|
-
size: csvContent.length,
|
|
803
|
-
orderCount: orders.length,
|
|
804
|
-
lineItemCount: flattenedRecords.length,
|
|
805
|
-
duration: `${csvDuration}ms`,
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
// STEP 7/8: Upload to S3
|
|
809
|
-
log.info('📄 [OrdersExtraction] STEP 7/8: Uploading to S3');
|
|
810
|
-
const uploadStartTime = Date.now();
|
|
811
|
-
|
|
812
|
-
try {
|
|
813
|
-
await s3.upload(
|
|
814
|
-
s3Key,
|
|
815
|
-
Buffer.from(csvContent, 'utf8'),
|
|
816
|
-
'text/csv',
|
|
817
|
-
{
|
|
818
|
-
orderCount: String(orders.length),
|
|
819
|
-
lineItemCount: String(flattenedRecords.length),
|
|
820
|
-
extractedAt: new Date().toISOString(),
|
|
821
|
-
lastRunTime: rawLastRunTime,
|
|
822
|
-
jobId,
|
|
823
|
-
}
|
|
824
|
-
);
|
|
825
|
-
|
|
826
|
-
const uploadDuration = Date.now() - uploadStartTime;
|
|
827
|
-
log.info('✅ [OrdersExtraction] CSV file uploaded to S3', {
|
|
828
|
-
s3Key,
|
|
829
|
-
duration: `${uploadDuration}ms`,
|
|
830
|
-
});
|
|
831
|
-
} catch (uploadError: any) {
|
|
832
|
-
const duration = Date.now() - startTime;
|
|
833
|
-
log.error('❌ [OrdersExtraction] S3 upload failed', {
|
|
834
|
-
error: uploadError.message,
|
|
835
|
-
s3Key,
|
|
836
|
-
duration: `${duration}ms`,
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
// Dispose S3 connection
|
|
840
|
-
try {
|
|
841
|
-
await s3.dispose();
|
|
842
|
-
} catch (disposeError: any) {
|
|
843
|
-
log.error('⚠️ [OrdersExtraction] Failed to dispose S3 connection', {
|
|
844
|
-
error: disposeError.message,
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
849
|
-
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
850
|
-
|
|
851
|
-
return {
|
|
852
|
-
success: false,
|
|
853
|
-
error: `S3 upload failed: ${uploadError.message}`,
|
|
854
|
-
duration,
|
|
855
|
-
recommendations: [
|
|
856
|
-
'Verify S3 bucket exists and is accessible',
|
|
857
|
-
'Check AWS credentials and permissions',
|
|
858
|
-
'Ensure network connectivity to S3',
|
|
859
|
-
'Verify S3 bucket region matches configuration',
|
|
860
|
-
'Check file size limits and quotas'
|
|
861
|
-
]
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// STEP 8/8: Update state and complete job
|
|
866
|
-
log.info('📄 [OrdersExtraction] STEP 8/8: Updating state and completing job');
|
|
867
|
-
|
|
868
|
-
// Calculate MAX(updatedOn) from extracted orders (WITHOUT buffer)
|
|
869
|
-
const maxUpdatedOn = orders.reduce((max, order) => {
|
|
870
|
-
const orderTime = new Date(order.updatedOn).getTime();
|
|
871
|
-
return orderTime > max ? orderTime : max;
|
|
872
|
-
}, new Date(rawLastRunTime).getTime());
|
|
873
|
-
|
|
874
|
-
const newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
875
|
-
|
|
876
|
-
// Save state WITHOUT buffer - buffer only applied to query
|
|
877
|
-
await kv.set(stateKey, {
|
|
878
|
-
timestamp: newTimestamp, // ← WITHOUT buffer
|
|
879
|
-
orderCount: orders.length,
|
|
880
|
-
lineItemCount: flattenedRecords.length,
|
|
881
|
-
extractedAt: new Date().toISOString(),
|
|
882
|
-
fileName,
|
|
883
|
-
s3Key,
|
|
884
|
-
overlapBufferSeconds,
|
|
885
|
-
jobId,
|
|
886
|
-
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
log.info('✅ [OrdersExtraction] State updated with new timestamp', {
|
|
890
|
-
newTimestamp,
|
|
891
|
-
recordsProcessed: orders.length,
|
|
892
|
-
nextRunWillQueryFrom: new Date(
|
|
893
|
-
new Date(newTimestamp).getTime() - OVERLAP_BUFFER_MS
|
|
894
|
-
).toISOString(),
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
// Dispose S3 connection
|
|
898
|
-
try {
|
|
899
|
-
await s3.dispose();
|
|
900
|
-
log.info('✅ [OrdersExtraction] S3 connection disposed successfully');
|
|
901
|
-
} catch (disposeError: any) {
|
|
902
|
-
log.error('⚠️ [OrdersExtraction] Failed to dispose S3 connection', {
|
|
903
|
-
error: disposeError.message,
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const duration = Date.now() - startTime;
|
|
908
|
-
|
|
909
|
-
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
910
|
-
log.info('⏱️ [OrdersExtraction] Total Duration: ' + duration + 'ms');
|
|
911
|
-
|
|
912
|
-
return {
|
|
913
|
-
success: true,
|
|
914
|
-
ordersExtracted: orders.length,
|
|
915
|
-
lineItemsExtracted: flattenedRecords.length,
|
|
916
|
-
recordsFailed: mappingErrors.length,
|
|
917
|
-
fileName,
|
|
918
|
-
s3Key,
|
|
919
|
-
lastRunTime: rawLastRunTime,
|
|
920
|
-
newTimestamp,
|
|
921
|
-
duration,
|
|
922
|
-
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
923
|
-
};
|
|
924
|
-
} catch (error: any) {
|
|
925
|
-
const duration = Date.now() - startTime;
|
|
926
|
-
log.error('❌ [OrdersExtraction] Fatal error', {
|
|
927
|
-
message: error?.message,
|
|
928
|
-
stack: error?.stack,
|
|
929
|
-
duration: `${duration}ms`,
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
933
|
-
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
934
|
-
|
|
935
|
-
return {
|
|
936
|
-
success: false,
|
|
937
|
-
error: error instanceof Error ? error.message : String(error),
|
|
938
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
939
|
-
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
940
|
-
duration,
|
|
941
|
-
recommendations: [
|
|
942
|
-
'Review error stack trace for root cause',
|
|
943
|
-
'Check all credentials and connection settings',
|
|
944
|
-
'Verify GraphQL query syntax and permissions',
|
|
945
|
-
'Ensure all activation variables are set correctly',
|
|
946
|
-
'Check network connectivity to Fluent Commerce and S3'
|
|
947
|
-
]
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
**File: `index.ts`**
|
|
954
|
-
|
|
955
|
-
```typescript
|
|
956
|
-
/**
|
|
957
|
-
* Entry point - Export all workflows for Versori platform
|
|
958
|
-
*
|
|
959
|
-
* MemoryInterpreter Pattern:
|
|
960
|
-
* This file exports workflow definitions that delegate to service functions.
|
|
961
|
-
* Each workflow is lightweight and imports business logic from services.
|
|
962
|
-
*/
|
|
963
|
-
|
|
964
|
-
// Scheduled workflows
|
|
965
|
-
export { ordersExtractionScheduled } from './src/workflows/scheduled/daily-orders-extraction';
|
|
966
|
-
|
|
967
|
-
// Webhook workflows
|
|
968
|
-
export { ordersExtractionWebhook } from './src/workflows/webhook/adhoc-orders-extraction';
|
|
969
|
-
export { ordersExtractionStatus } from './src/workflows/webhook/job-status-check';
|
|
970
|
-
```
|
|
971
|
-
|
|
972
|
-
### Workflow 2: Ad-Hoc Webhook Trigger
|
|
973
|
-
|
|
974
|
-
**File: `src/workflows/webhook/adhoc-orders-extraction.ts`**
|
|
975
|
-
|
|
976
|
-
```typescript
|
|
977
|
-
import { webhook, http } from '@versori/run';
|
|
978
|
-
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
979
|
-
import { processOrdersExtraction } from '../../services/orders-extraction.service';
|
|
980
|
-
|
|
981
|
-
/**
|
|
982
|
-
* Webhook workflow: Ad-hoc orders extraction (manual trigger)
|
|
983
|
-
*
|
|
984
|
-
* DELEGATION PATTERN (MemoryInterpreter):
|
|
985
|
-
* - Reuses the same service function as scheduled workflow
|
|
986
|
-
* - Lightweight wrapper that delegates to shared business logic
|
|
987
|
-
*/
|
|
988
|
-
export const ordersExtractionWebhook = webhook('orders-extract-webhook', {
|
|
989
|
-
connection: 'orders-adhoc',
|
|
990
|
-
response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
|
|
991
|
-
}).then(
|
|
992
|
-
http('trigger-extraction', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
993
|
-
const { log, openKv } = ctx;
|
|
994
|
-
const executionStartTime = Date.now();
|
|
995
|
-
const jobId = `ADHOC_ORD_${new Date().toISOString().replace(/[:.]/g, '-')}_${Date.now()}`;
|
|
996
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
997
|
-
|
|
998
|
-
log.info('🚀 [WEBHOOK] Adhoc orders extraction triggered', { jobId });
|
|
999
|
-
|
|
1000
|
-
// Create job entry FIRST (awaited to ensure job exists in KV)
|
|
1001
|
-
await tracker.createJob(jobId, {
|
|
1002
|
-
triggeredBy: 'webhook',
|
|
1003
|
-
stage: 'initialization',
|
|
1004
|
-
status: 'queued',
|
|
1005
|
-
startTime: executionStartTime,
|
|
1006
|
-
createdAt: new Date().toISOString(),
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
// ✅ Fire-and-forget: Start background processing WITHOUT await
|
|
1010
|
-
// The promise continues execution after we return the response
|
|
1011
|
-
processOrdersExtraction(ctx, jobId, tracker)
|
|
1012
|
-
.then((result) => {
|
|
1013
|
-
const duration = Date.now() - executionStartTime;
|
|
1014
|
-
if (result.success) {
|
|
1015
|
-
log.info('✅ [BACKGROUND] Orders extraction completed successfully', {
|
|
1016
|
-
jobId,
|
|
1017
|
-
ordersExtracted: result.ordersExtracted,
|
|
1018
|
-
fileName: result.fileName,
|
|
1019
|
-
duration: `${duration}ms`,
|
|
1020
|
-
});
|
|
1021
|
-
return tracker.markCompleted(jobId, { ...result, duration });
|
|
1022
|
-
} else {
|
|
1023
|
-
log.error('❌ [BACKGROUND] Orders extraction failed', {
|
|
1024
|
-
jobId,
|
|
1025
|
-
error: result.error,
|
|
1026
|
-
duration: `${duration}ms`,
|
|
1027
|
-
});
|
|
1028
|
-
return tracker.markFailed(jobId, result.error || 'Unknown error');
|
|
1029
|
-
}
|
|
1030
|
-
})
|
|
1031
|
-
.catch((error: unknown) => {
|
|
1032
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1033
|
-
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
1034
|
-
const duration = Date.now() - executionStartTime;
|
|
1035
|
-
|
|
1036
|
-
log.error('❌ [BACKGROUND] Orders extraction failed with exception', {
|
|
1037
|
-
jobId,
|
|
1038
|
-
error: errorMessage,
|
|
1039
|
-
stack: errorStack,
|
|
1040
|
-
duration: `${duration}ms`,
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
return tracker.markFailed(jobId, errorMessage);
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
// Return immediately with jobId (response sent with this return value)
|
|
1047
|
-
return {
|
|
1048
|
-
success: true,
|
|
1049
|
-
jobId,
|
|
1050
|
-
message: 'Orders extraction started in background',
|
|
1051
|
-
statusEndpoint: `https://{workspace}.versori.run/orders-job-status`,
|
|
1052
|
-
note: 'Poll the status endpoint with jobId to check progress',
|
|
1053
|
-
};
|
|
1054
|
-
})
|
|
1055
|
-
);
|
|
1056
|
-
```
|
|
1057
|
-
|
|
1058
|
-
### Workflow 3: Job Status Query
|
|
1059
|
-
|
|
1060
|
-
**File: `src/workflows/webhook/job-status-check.ts`**
|
|
1061
|
-
|
|
1062
|
-
```typescript
|
|
1063
|
-
import { webhook, fn } from '@versori/run';
|
|
1064
|
-
import { JobTracker, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1065
|
-
|
|
1066
|
-
/**
|
|
1067
|
-
* Webhook workflow: Job status query (monitor extraction progress)
|
|
1068
|
-
*
|
|
1069
|
-
* Simple query endpoint - no delegation needed
|
|
1070
|
-
*/
|
|
1071
|
-
export const ordersExtractionStatus = webhook('orders-extract-status', {
|
|
1072
|
-
connection: 'orders-job-status',
|
|
1073
|
-
}).then(
|
|
1074
|
-
fn('get-status', async (ctx: any) => {
|
|
1075
|
-
const { log, openKv } = ctx;
|
|
1076
|
-
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
1077
|
-
const tracker = new JobTracker(kv, log);
|
|
1078
|
-
|
|
1079
|
-
const url = new URL(ctx.request.url);
|
|
1080
|
-
const jobId = url.searchParams.get('jobId');
|
|
1081
|
-
|
|
1082
|
-
if (!jobId) {
|
|
1083
|
-
log.warn('⚠️ [JobStatus] Missing jobId query parameter');
|
|
1084
|
-
return {
|
|
1085
|
-
success: false,
|
|
1086
|
-
error: 'Missing jobId query parameter',
|
|
1087
|
-
usage: 'GET /orders-extract-status?jobId=<job-id>',
|
|
1088
|
-
recommendations: [
|
|
1089
|
-
'Provide jobId as query parameter',
|
|
1090
|
-
'Example: /orders-extract-status?jobId=SCHEDULED_ORD_2025-01-22_12345'
|
|
1091
|
-
]
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
log.info('🔍 [JobStatus] Retrieving job status', { jobId });
|
|
1096
|
-
|
|
1097
|
-
const job = await tracker.getJob(jobId);
|
|
1098
|
-
|
|
1099
|
-
if (!job) {
|
|
1100
|
-
log.warn('⚠️ [JobStatus] Job not found', { jobId });
|
|
1101
|
-
return {
|
|
1102
|
-
success: false,
|
|
1103
|
-
error: `Job not found: ${jobId}`,
|
|
1104
|
-
recommendations: [
|
|
1105
|
-
'Verify jobId is correct',
|
|
1106
|
-
'Check if job has expired (default TTL: 7 days)',
|
|
1107
|
-
'Ensure job was created successfully'
|
|
1108
|
-
]
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
log.info('✅ [JobStatus] Job status retrieved', { jobId, status: job.status });
|
|
1113
|
-
|
|
1114
|
-
return {
|
|
1115
|
-
success: true,
|
|
1116
|
-
job: {
|
|
1117
|
-
id: job.id,
|
|
1118
|
-
name: job.name,
|
|
1119
|
-
status: job.status,
|
|
1120
|
-
createdAt: job.createdAt,
|
|
1121
|
-
startedAt: job.startedAt,
|
|
1122
|
-
completedAt: job.completedAt,
|
|
1123
|
-
duration: job.duration,
|
|
1124
|
-
result: job.result,
|
|
1125
|
-
error: job.error,
|
|
1126
|
-
metadata: job.metadata,
|
|
1127
|
-
},
|
|
1128
|
-
};
|
|
1129
|
-
})
|
|
1130
|
-
);
|
|
1131
|
-
```
|
|
1132
|
-
|
|
1133
|
-
---
|
|
1134
|
-
|
|
1135
|
-
## Key Patterns Explained
|
|
1136
|
-
|
|
1137
|
-
### Pattern 1: Line Item Flattening
|
|
1138
|
-
|
|
1139
|
-
```typescript
|
|
1140
|
-
// BEFORE: 1 order with 3 line items
|
|
1141
|
-
{
|
|
1142
|
-
ref: "ORD-001",
|
|
1143
|
-
items: [
|
|
1144
|
-
{ product: { ref: "SKU-A" }, quantity: 2 },
|
|
1145
|
-
{ product: { ref: "SKU-B" }, quantity: 1 },
|
|
1146
|
-
{ product: { ref: "SKU-C" }, quantity: 3 }
|
|
1147
|
-
]
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
// AFTER: 3 CSV rows (order data repeated)
|
|
1151
|
-
[
|
|
1152
|
-
{ order_id: "ORD-001", line_item_sku: "SKU-A", line_item_qty: 2 },
|
|
1153
|
-
{ order_id: "ORD-001", line_item_sku: "SKU-B", line_item_qty: 1 },
|
|
1154
|
-
{ order_id: "ORD-001", line_item_sku: "SKU-C", line_item_qty: 3 }
|
|
1155
|
-
]
|
|
1156
|
-
```
|
|
1157
|
-
|
|
1158
|
-
**Why?** Most 3PL systems expect flattened CSV format, not nested JSON.
|
|
1159
|
-
|
|
1160
|
-
### Pattern 2: Overlap Buffer (Query WITH, Save WITHOUT)
|
|
1161
|
-
|
|
1162
|
-
```typescript
|
|
1163
|
-
// Query uses bufferedLastRunTime (WITH overlap buffer)
|
|
1164
|
-
const bufferedLastRunTime = new Date(
|
|
1165
|
-
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
|
|
1166
|
-
).toISOString();
|
|
1167
|
-
|
|
1168
|
-
const result = await orchestrator.extract({
|
|
1169
|
-
variables: {
|
|
1170
|
-
updatedAfter: bufferedLastRunTime, // ← WITH buffer
|
|
1171
|
-
},
|
|
1172
|
-
});
|
|
1173
|
-
|
|
1174
|
-
// Save MAX(updatedOn) WITHOUT buffer
|
|
1175
|
-
const maxUpdatedOn = orders.reduce(
|
|
1176
|
-
(max, order) => Math.max(max, new Date(order.updatedOn).getTime()),
|
|
1177
|
-
new Date(rawLastRunTime).getTime()
|
|
1178
|
-
);
|
|
1179
|
-
|
|
1180
|
-
await kv.set(stateKey, {
|
|
1181
|
-
timestamp: new Date(maxUpdatedOn).toISOString(), // ← WITHOUT buffer
|
|
1182
|
-
});
|
|
1183
|
-
```
|
|
1184
|
-
|
|
1185
|
-
**Why?** Buffer prevents missed records due to clock skew. Next run applies buffer again to saved timestamp.
|
|
1186
|
-
|
|
1187
|
-
### Pattern 3: CSVParserService Usage
|
|
1188
|
-
|
|
1189
|
-
```typescript
|
|
1190
|
-
// ✅ CORRECT - Use CSVParserService.stringify()
|
|
1191
|
-
const csvParser = new CSVParserService();
|
|
1192
|
-
const csvContent = csvParser.stringify(flattenedRecords, { headers: true });
|
|
1193
|
-
|
|
1194
|
-
// ❌ WRONG - Don't use CSVBuilder (doesn't exist)
|
|
1195
|
-
const builder = new CSVBuilder();
|
|
1196
|
-
builder.addRows(records);
|
|
1197
|
-
const csvContent = builder.build();
|
|
1198
|
-
```
|
|
1199
|
-
|
|
1200
|
-
**Why?** `CSVParserService` is the correct SDK service for CSV generation. It has no constructor parameters.
|
|
1201
|
-
|
|
1202
|
-
### Pattern 4: 3-Workflow Pattern
|
|
1203
|
-
|
|
1204
|
-
1. **Scheduled**: Hourly automated extraction
|
|
1205
|
-
2. **Ad-hoc Webhook**: On-demand trigger with API key auth
|
|
1206
|
-
3. **Job Status**: Query job progress via webhook
|
|
1207
|
-
|
|
1208
|
-
**Why?** Provides flexibility for monitoring, testing, and emergency extractions.
|
|
1209
|
-
|
|
1210
|
-
---
|
|
1211
|
-
|
|
1212
|
-
## Testing Checklist
|
|
1213
|
-
|
|
1214
|
-
**Before deploying to production:**
|
|
1215
|
-
|
|
1216
|
-
### 1. Schema Validation
|
|
1217
|
-
|
|
1218
|
-
- [ ] Run `npx fc-connect introspect-schema` to download current schema
|
|
1219
|
-
- [ ] Run `npx fc-connect validate-schema` to check mapping against schema
|
|
1220
|
-
- [ ] Verify all nested `source` paths exist (customer.email, deliveryAddress.city, product.ref)
|
|
1221
|
-
|
|
1222
|
-
### 2. Mapping Testing
|
|
1223
|
-
|
|
1224
|
-
- [ ] Test with sample data (maxRecords=10, small orders)
|
|
1225
|
-
- [ ] Verify all required fields are populated
|
|
1226
|
-
- [ ] Verify SDK resolvers work correctly (trim, uppercase, parseInt, parseFloat)
|
|
1227
|
-
- [ ] Verify line item flattening creates correct CSV structure
|
|
1228
|
-
|
|
1229
|
-
### 3. State Management
|
|
1230
|
-
|
|
1231
|
-
- [ ] Verify overlap buffer prevents duplicate misses
|
|
1232
|
-
- [ ] Test state recovery after failure (run should retry)
|
|
1233
|
-
- [ ] Verify timestamp is saved WITHOUT buffer
|
|
1234
|
-
- [ ] Test fallback to `fallbackStartDate` on first run
|
|
1235
|
-
|
|
1236
|
-
### 4. S3 Operations
|
|
1237
|
-
|
|
1238
|
-
- [ ] Test S3 connection with credentials
|
|
1239
|
-
- [ ] Verify file upload to correct prefix
|
|
1240
|
-
- [ ] Verify CSV file is valid (headers, row format)
|
|
1241
|
-
- [ ] Verify metadata is attached to uploaded file
|
|
1242
|
-
|
|
1243
|
-
### 5. Workflow Testing
|
|
1244
|
-
|
|
1245
|
-
- [ ] Test scheduled workflow (hourly cron)
|
|
1246
|
-
- [ ] Test ad-hoc webhook trigger with API key
|
|
1247
|
-
- [ ] Test job status query endpoint
|
|
1248
|
-
|
|
1249
|
-
---
|
|
1250
|
-
|
|
1251
|
-
## Monitoring & Alerting
|
|
1252
|
-
|
|
1253
|
-
### Success Response Example
|
|
1254
|
-
|
|
1255
|
-
```json
|
|
1256
|
-
{
|
|
1257
|
-
"success": true,
|
|
1258
|
-
"jobId": "SCHEDULED_ORD_20251102_140000_abc123",
|
|
1259
|
-
"recordsExtracted": 1523,
|
|
1260
|
-
"fileName": "orders-2025-11-02T14-00-00-000Z.csv",
|
|
1261
|
-
"s3Path": "s3://bucket/orders/orders-2025-11-02T14-00-00-000Z.csv",
|
|
1262
|
-
"metrics": {
|
|
1263
|
-
"extractionDurationMs": 12543,
|
|
1264
|
-
"totalPages": 8,
|
|
1265
|
-
"pageSize": 200,
|
|
1266
|
-
"mappingErrors": 0,
|
|
1267
|
-
"fileSizeBytes": 524288,
|
|
1268
|
-
"uploadDurationMs": 1234
|
|
1269
|
-
},
|
|
1270
|
-
"timestamps": {
|
|
1271
|
-
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
1272
|
-
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
1273
|
-
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
1274
|
-
},
|
|
1275
|
-
"state": {
|
|
1276
|
-
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
1277
|
-
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
1278
|
-
"stateUpdated": true,
|
|
1279
|
-
"overlapBufferSeconds": 60
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
```
|
|
1283
|
-
|
|
1284
|
-
### Error Response Example
|
|
1285
|
-
|
|
1286
|
-
```json
|
|
1287
|
-
{
|
|
1288
|
-
"success": false,
|
|
1289
|
-
"jobId": "ADHOC_ORD_20251102_140500_xyz789",
|
|
1290
|
-
"error": "S3 upload failed: Connection timeout",
|
|
1291
|
-
"errorCategory": "NETWORK",
|
|
1292
|
-
"recordsExtracted": 0,
|
|
1293
|
-
"stage": "s3_upload",
|
|
1294
|
-
"details": {
|
|
1295
|
-
"message": "Failed to upload file after 3 retry attempts",
|
|
1296
|
-
"retryAttempts": 3,
|
|
1297
|
-
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
1298
|
-
},
|
|
1299
|
-
"state": {
|
|
1300
|
-
"stateUpdated": false,
|
|
1301
|
-
"willRetryNextRun": true,
|
|
1302
|
-
"note": "State not advanced - next extraction will retry same time window"
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
```
|
|
1306
|
-
|
|
1307
|
-
### Key Metrics to Track
|
|
1308
|
-
|
|
1309
|
-
```typescript
|
|
1310
|
-
const METRICS = {
|
|
1311
|
-
// Extraction Performance
|
|
1312
|
-
extractionDurationMs: Date.now() - extractionStart,
|
|
1313
|
-
recordCount: records.length,
|
|
1314
|
-
pageCount: extractionResult.stats.totalPages,
|
|
1315
|
-
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
1316
|
-
|
|
1317
|
-
// Transformation Performance
|
|
1318
|
-
transformedCount: transformedRecords.length,
|
|
1319
|
-
failedCount: mappingErrors.length,
|
|
1320
|
-
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
1321
|
-
|
|
1322
|
-
// File Generation
|
|
1323
|
-
fileSizeMB: (csvContent.length / (1024 * 1024)).toFixed(2),
|
|
1324
|
-
|
|
1325
|
-
// Upload Performance
|
|
1326
|
-
uploadDurationMs: uploadEnd - uploadStart,
|
|
1327
|
-
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
1328
|
-
|
|
1329
|
-
// State Management
|
|
1330
|
-
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
1331
|
-
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
1332
|
-
};
|
|
1333
|
-
|
|
1334
|
-
log.info('Extraction metrics', metrics);
|
|
1335
|
-
```
|
|
1336
|
-
|
|
1337
|
-
### Alert Thresholds
|
|
1338
|
-
|
|
1339
|
-
```typescript
|
|
1340
|
-
const ALERT_THRESHOLDS = {
|
|
1341
|
-
// Duration Alerts
|
|
1342
|
-
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
1343
|
-
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
1344
|
-
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
1345
|
-
|
|
1346
|
-
// Error Rate Alerts
|
|
1347
|
-
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
1348
|
-
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
1349
|
-
|
|
1350
|
-
// Volume Alerts
|
|
1351
|
-
MAX_RECORDS_PER_RUN: 100000,
|
|
1352
|
-
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
1353
|
-
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
1354
|
-
|
|
1355
|
-
// State Alerts
|
|
1356
|
-
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
1357
|
-
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
1358
|
-
};
|
|
1359
|
-
|
|
1360
|
-
// Check thresholds
|
|
1361
|
-
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
1362
|
-
log.warn('Extraction duration exceeded threshold', {
|
|
1363
|
-
duration: metrics.extractionDurationMs,
|
|
1364
|
-
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
1365
|
-
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
```
|
|
1369
|
-
|
|
1370
|
-
### Monitoring Dashboard Queries
|
|
1371
|
-
|
|
1372
|
-
**Versori Platform Logs Query:**
|
|
1373
|
-
|
|
1374
|
-
```
|
|
1375
|
-
# Successful extractions
|
|
1376
|
-
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
1377
|
-
|
|
1378
|
-
# Failed extractions
|
|
1379
|
-
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
1380
|
-
|
|
1381
|
-
# Performance issues
|
|
1382
|
-
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
1383
|
-
|
|
1384
|
-
# High error rates
|
|
1385
|
-
errorRate:>5
|
|
1386
|
-
|
|
1387
|
-
# State management issues
|
|
1388
|
-
stateUpdated:false AND success:true
|
|
1389
|
-
```
|
|
1390
|
-
|
|
1391
|
-
### Common Issues and Solutions
|
|
1392
|
-
|
|
1393
|
-
**Issue**: "Extraction timeout after 10 minutes"
|
|
1394
|
-
|
|
1395
|
-
- **Cause**: Too many records in single extraction
|
|
1396
|
-
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
1397
|
-
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
1398
|
-
|
|
1399
|
-
**Issue**: "Mapping errors for 50% of records"
|
|
1400
|
-
|
|
1401
|
-
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
1402
|
-
- **Fix**: Run schema validation, update mapping config paths
|
|
1403
|
-
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
1404
|
-
|
|
1405
|
-
**Issue**: "S3 connection timeout"
|
|
1406
|
-
|
|
1407
|
-
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
1408
|
-
- **Fix**: Check S3 credentials, verify network connectivity
|
|
1409
|
-
- **Prevention**: Implement connection health checks, monitor connection status
|
|
1410
|
-
|
|
1411
|
-
**Issue**: "State not updating after successful extraction"
|
|
1412
|
-
|
|
1413
|
-
- **Cause**: KV write failure or intentional retry logic
|
|
1414
|
-
- **Fix**: Check KV logs, verify state update code executed
|
|
1415
|
-
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
1416
|
-
|
|
1417
|
-
**Issue**: "First run exceeds record limits"
|
|
1418
|
-
|
|
1419
|
-
- **Cause**: No previous timestamp, fetches all historical records
|
|
1420
|
-
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
1421
|
-
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
1422
|
-
|
|
1423
|
-
**Issue**: "Excessive duplicate records in output"
|
|
1424
|
-
|
|
1425
|
-
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
1426
|
-
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
1427
|
-
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
1428
|
-
|
|
1429
|
-
---
|
|
1430
|
-
|
|
1431
|
-
## Troubleshooting Quick Reference
|
|
1432
|
-
|
|
1433
|
-
| Error Message | Likely Cause | Solution |
|
|
1434
|
-
|--------------|--------------|----------|
|
|
1435
|
-
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
1436
|
-
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
1437
|
-
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
1438
|
-
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
1439
|
-
| "S3 authentication failed" | Invalid credentials | Verify S3 credentials in activation variables |
|
|
1440
|
-
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
1441
|
-
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
1442
|
-
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
1443
|
-
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
1444
|
-
| "CSV generation failed" | Format-specific error | Check CSV generation logic, validate output |
|
|
1445
|
-
|
|
1446
|
-
---
|
|
1447
|
-
|
|
1448
|
-
---
|
|
1449
|
-
|
|
1450
|
-
### Pattern 7: State Management & Date Overrides
|
|
1451
|
-
|
|
1452
|
-
**Use Case**: Understand how state management works with scheduled and ad-hoc extractions.
|
|
1453
|
-
|
|
1454
|
-
**How it works**:
|
|
1455
|
-
|
|
1456
|
-
VersoriKV stores the last successful extraction timestamp to enable incremental sync:
|
|
1457
|
-
|
|
1458
|
-
```typescript
|
|
1459
|
-
interface ExtractionState {
|
|
1460
|
-
timestamp: string; // Last run timestamp (WITHOUT overlap buffer)
|
|
1461
|
-
recordCount: number; // Number of records extracted
|
|
1462
|
-
extractedAt: string; // When extraction completed
|
|
1463
|
-
fileName?: string; // Generated filename
|
|
1464
|
-
s3Key?: string; // S3 upload path
|
|
1465
|
-
overlapBufferSeconds?: number; // Buffer configuration
|
|
1466
|
-
}
|
|
1467
|
-
```
|
|
1468
|
-
|
|
1469
|
-
**State Priority Chain** (highest to lowest):
|
|
1470
|
-
|
|
1471
|
-
1. **`fromDate` override** (manual date in webhook payload) - Highest priority
|
|
1472
|
-
2. **Stored state** (`await kv.get(stateKey)`) - Normal incremental mode
|
|
1473
|
-
3. **`fallbackStartDate`** (activation variable) - First run fallback
|
|
1474
|
-
|
|
1475
|
-
**Three Scenarios**:
|
|
1476
|
-
|
|
1477
|
-
#### Scenario 1: Normal Scheduled Runs (Incremental)
|
|
1478
|
-
|
|
1479
|
-
```typescript
|
|
1480
|
-
// Payload: {} (empty - no overrides)
|
|
1481
|
-
|
|
1482
|
-
// Behavior:
|
|
1483
|
-
// 1. Load last timestamp from KV: "2025-01-22T10:00:00Z"
|
|
1484
|
-
// 2. Apply overlap buffer: "2025-01-22T09:59:00Z" (query WITH buffer)
|
|
1485
|
-
// 3. Extract records updated since buffered time
|
|
1486
|
-
// 4. Calculate MAX(updatedOn) from results: "2025-01-22T14:30:00Z"
|
|
1487
|
-
// 5. Save new timestamp WITHOUT buffer: "2025-01-22T14:30:00Z"
|
|
1488
|
-
// 6. Next run starts from "2025-01-22T14:29:00Z" (with buffer)
|
|
1489
|
-
```
|
|
1490
|
-
|
|
1491
|
-
**Test**:
|
|
1492
|
-
|
|
1493
|
-
```bash
|
|
1494
|
-
# Trigger scheduled run (no payload needed)
|
|
1495
|
-
# State advances automatically
|
|
1496
|
-
curl -X POST https://workspace.versori.run/orders-extract-hourly
|
|
1497
|
-
```
|
|
1498
|
-
|
|
1499
|
-
#### Scenario 2: Ad-hoc Extraction WITH State Update
|
|
1500
|
-
|
|
1501
|
-
```typescript
|
|
1502
|
-
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": true }
|
|
1503
|
-
|
|
1504
|
-
// Behavior:
|
|
1505
|
-
// 1. Ignore stored state
|
|
1506
|
-
// 2. Use fromDate: "2025-01-01T00:00:00Z" (no buffer applied to manual dates)
|
|
1507
|
-
// 3. Extract all records since 2025-01-01
|
|
1508
|
-
// 4. Calculate MAX(updatedOn): "2025-01-22T14:30:00Z"
|
|
1509
|
-
// 5. Save new timestamp: "2025-01-22T14:30:00Z" (updates state!)
|
|
1510
|
-
// 6. Next scheduled run starts from this new timestamp
|
|
1511
|
-
```
|
|
1512
|
-
|
|
1513
|
-
**Use Case**: One-time catch-up extraction that advances the state pointer.
|
|
1514
|
-
|
|
1515
|
-
**Test**:
|
|
1516
|
-
|
|
1517
|
-
```bash
|
|
1518
|
-
curl -X POST https://workspace.versori.run/orders-extract-webhook \
|
|
1519
|
-
-H "x-api-key: your-api-key" \
|
|
1520
|
-
-H "Content-Type: application/json" \
|
|
1521
|
-
-d '{
|
|
1522
|
-
"fromDate": "2025-01-01T00:00:00Z",
|
|
1523
|
-
"updateState": true
|
|
1524
|
-
}'
|
|
1525
|
-
```
|
|
1526
|
-
|
|
1527
|
-
#### Scenario 3: Ad-hoc Extraction WITHOUT State Update
|
|
1528
|
-
|
|
1529
|
-
```typescript
|
|
1530
|
-
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": false }
|
|
1531
|
-
|
|
1532
|
-
// Behavior:
|
|
1533
|
-
// 1. Ignore stored state
|
|
1534
|
-
// 2. Use fromDate: "2025-01-01T00:00:00Z"
|
|
1535
|
-
// 3. Extract all records since 2025-01-01
|
|
1536
|
-
// 4. DO NOT update state
|
|
1537
|
-
// 5. Next scheduled run uses previous timestamp (unaffected)
|
|
1538
|
-
```
|
|
1539
|
-
|
|
1540
|
-
**Use Case**: Historical backfill or testing without affecting incremental sync.
|
|
1541
|
-
|
|
1542
|
-
**Test**:
|
|
1543
|
-
|
|
1544
|
-
```bash
|
|
1545
|
-
curl -X POST https://workspace.versori.run/orders-extract-webhook \
|
|
1546
|
-
-H "x-api-key: your-api-key" \
|
|
1547
|
-
-H "Content-Type: application/json" \
|
|
1548
|
-
-d '{
|
|
1549
|
-
"fromDate": "2025-01-01T00:00:00Z",
|
|
1550
|
-
"toDate": "2025-01-31T23:59:59Z",
|
|
1551
|
-
"updateState": false
|
|
1552
|
-
}'
|
|
1553
|
-
```
|
|
1554
|
-
|
|
1555
|
-
**Why this matters**:
|
|
1556
|
-
|
|
1557
|
-
- **Incremental sync** relies on state continuity
|
|
1558
|
-
- **Manual overrides** allow catch-up without breaking incremental flow
|
|
1559
|
-
- **Overlap buffer** prevents missed records at time boundaries
|
|
1560
|
-
- **State isolation** lets you test/backfill without affecting production sync
|
|
1561
|
-
|
|
1562
|
-
---
|
|
1563
|
-
|
|
1564
|
-
### Pattern 8: Optional GraphQL Query Logging
|
|
1565
|
-
|
|
1566
|
-
**Use Case**: Debug extraction issues by logging the exact GraphQL query sent to Fluent Commerce API.
|
|
1567
|
-
|
|
1568
|
-
**When to use**:
|
|
1569
|
-
|
|
1570
|
-
- ✅ Debugging pagination issues
|
|
1571
|
-
- ✅ Verifying query variables (dates, filters, limits)
|
|
1572
|
-
- ✅ Development and testing
|
|
1573
|
-
- ❌ Production (verbose logs, potential secrets in variables)
|
|
1574
|
-
|
|
1575
|
-
**How to enable**:
|
|
1576
|
-
|
|
1577
|
-
Set `DEBUG_GRAPHQL=true` environment variable in Versori activation settings.
|
|
1578
|
-
|
|
1579
|
-
**Implementation**:
|
|
1580
|
-
|
|
1581
|
-
```typescript
|
|
1582
|
-
// In your extraction workflow
|
|
1583
|
-
const DEBUG_GRAPHQL = activation?.getVariable('DEBUG_GRAPHQL') === 'true';
|
|
1584
|
-
|
|
1585
|
-
if (DEBUG_GRAPHQL) {
|
|
1586
|
-
log.info('GraphQL Query Debug', {
|
|
1587
|
-
query: ORDERS_QUERY,
|
|
1588
|
-
variables: {
|
|
1589
|
-
retailerId,
|
|
1590
|
-
updatedAfter: bufferedLastRunTime,
|
|
1591
|
-
first: pageSize,
|
|
1592
|
-
after: null, // First page
|
|
1593
|
-
},
|
|
1594
|
-
pagination: {
|
|
1595
|
-
pageSize,
|
|
1596
|
-
maxRecords,
|
|
1597
|
-
currentPage: 1,
|
|
1598
|
-
},
|
|
1599
|
-
});
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
const extractionResult = await orchestrator.extract({
|
|
1603
|
-
query: ORDERS_QUERY,
|
|
1604
|
-
resultPath: 'orders.edges.node',
|
|
1605
|
-
variables: {
|
|
1606
|
-
retailerId,
|
|
1607
|
-
updatedAfter: bufferedLastRunTime,
|
|
1608
|
-
},
|
|
1609
|
-
pageSize,
|
|
1610
|
-
maxRecords,
|
|
1611
|
-
});
|
|
1612
|
-
|
|
1613
|
-
if (DEBUG_GRAPHQL) {
|
|
1614
|
-
log.info('GraphQL Response Debug', {
|
|
1615
|
-
totalRecords: extractionResult.stats.totalRecords,
|
|
1616
|
-
totalPages: extractionResult.stats.totalPages,
|
|
1617
|
-
validRecords: extractionResult.stats.validRecords ?? extractionResult.data.length,
|
|
1618
|
-
firstRecordId: extractionResult.data[0]?.id,
|
|
1619
|
-
lastRecordId: extractionResult.data[extractionResult.data.length - 1]?.id,
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
```
|
|
1623
|
-
|
|
1624
|
-
**What gets logged**:
|
|
1625
|
-
|
|
1626
|
-
```json
|
|
1627
|
-
{
|
|
1628
|
-
"level": "info",
|
|
1629
|
-
"message": "GraphQL Query Debug",
|
|
1630
|
-
"query": "query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, ...)",
|
|
1631
|
-
"variables": {
|
|
1632
|
-
"retailerId": "ACME",
|
|
1633
|
-
"updatedAfter": "2025-01-22T09:59:00Z",
|
|
1634
|
-
"first": 200,
|
|
1635
|
-
"after": null
|
|
1636
|
-
},
|
|
1637
|
-
"pagination": {
|
|
1638
|
-
"pageSize": 200,
|
|
1639
|
-
"maxRecords": 10000,
|
|
1640
|
-
"currentPage": 1
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
```
|
|
1644
|
-
|
|
1645
|
-
**Versori Environment Variables**:
|
|
1646
|
-
|
|
1647
|
-
Add to activation settings:
|
|
1648
|
-
|
|
1649
|
-
```json
|
|
1650
|
-
{
|
|
1651
|
-
"DEBUG_GRAPHQL": "true"
|
|
1652
|
-
}
|
|
1653
|
-
```
|
|
1654
|
-
|
|
1655
|
-
**Testing**:
|
|
1656
|
-
|
|
1657
|
-
```bash
|
|
1658
|
-
# Enable debug logging
|
|
1659
|
-
curl -X POST https://workspace.versori.run/orders-extract-hourly
|
|
1660
|
-
|
|
1661
|
-
# Check Versori logs for "GraphQL Query Debug" entries
|
|
1662
|
-
# Verify query structure and variables are correct
|
|
1663
|
-
```
|
|
1664
|
-
|
|
1665
|
-
**Sample Debug Output**:
|
|
1666
|
-
|
|
1667
|
-
```
|
|
1668
|
-
[INFO] GraphQL Query Debug
|
|
1669
|
-
query: "query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, $first: Int!, $after: String) { ... }"
|
|
1670
|
-
variables: { retailerId: "ACME", updatedAfter: "2025-01-22T09:59:00Z", first: 200, after: null }
|
|
1671
|
-
pagination: { pageSize: 200, maxRecords: 10000, currentPage: 1 }
|
|
1672
|
-
|
|
1673
|
-
[INFO] Extraction complete
|
|
1674
|
-
totalRecords: 150
|
|
1675
|
-
totalPages: 1
|
|
1676
|
-
validRecords: 150
|
|
1677
|
-
failedValidations: 0
|
|
1678
|
-
|
|
1679
|
-
[INFO] GraphQL Response Debug
|
|
1680
|
-
totalRecords: 150
|
|
1681
|
-
totalPages: 1
|
|
1682
|
-
validRecords: 150
|
|
1683
|
-
firstRecordId: "order_123"
|
|
1684
|
-
lastRecordId: "order_272"
|
|
1685
|
-
```
|
|
1686
|
-
|
|
1687
|
-
**Key Benefits**:
|
|
1688
|
-
|
|
1689
|
-
- Quickly identify pagination configuration issues
|
|
1690
|
-
- Verify date filters are applied correctly
|
|
1691
|
-
- Debug "no records found" scenarios
|
|
1692
|
-
- Validate ExtractionOrchestrator variable injection
|
|
1693
|
-
|
|
1694
|
-
**Production Best Practice**: Disable `DEBUG_GRAPHQL` in production to reduce log volume and avoid logging sensitive data.
|
|
1695
|
-
|
|
1696
|
-
---
|
|
1697
|
-
|
|
1698
|
-
## Troubleshooting
|
|
1699
|
-
|
|
1700
|
-
**Issue**: "All records failed mapping"
|
|
1701
|
-
|
|
1702
|
-
**Cause**: Schema mismatch or incorrect field paths
|
|
1703
|
-
|
|
1704
|
-
**Solutions**:
|
|
1705
|
-
|
|
1706
|
-
1. Run `npx fc-connect introspect-schema` to get current schema
|
|
1707
|
-
2. Run `npx fc-connect validate-schema` to check mapping
|
|
1708
|
-
3. Review error details: `errors.map(e => e.errors)`
|
|
1709
|
-
|
|
1710
|
-
---
|
|
1711
|
-
|
|
1712
|
-
**Issue**: "State not updating, same orders exported every run"
|
|
1713
|
-
|
|
1714
|
-
**Cause**: KV storage write failure or timestamp calculation error
|
|
1715
|
-
|
|
1716
|
-
**Solutions**:
|
|
1717
|
-
|
|
1718
|
-
1. Check KV logs for write errors
|
|
1719
|
-
2. Verify state update code is reached: add logging
|
|
1720
|
-
3. Check `newTimestamp` calculation logic
|
|
1721
|
-
4. Manually inspect KV value: `await kv.get(['extraction', 'orders', 'lastRunTime'])`
|
|
1722
|
-
|
|
1723
|
-
---
|
|
1724
|
-
|
|
1725
|
-
**Issue**: "Orders with no line items cause mapping errors"
|
|
1726
|
-
|
|
1727
|
-
**Cause**: `order.items` is empty array or null
|
|
1728
|
-
|
|
1729
|
-
**Solutions**:
|
|
1730
|
-
|
|
1731
|
-
```typescript
|
|
1732
|
-
if (lineItems.length === 0) {
|
|
1733
|
-
log.warn('Order has no line items, skipping', { orderId: order.id, ref: order.ref });
|
|
1734
|
-
continue;
|
|
1735
|
-
}
|
|
1736
|
-
```
|
|
1737
|
-
|
|
1738
|
-
---
|
|
1739
|
-
|
|
1740
|
-
## See Also
|
|
1741
|
-
|
|
1742
|
-
**Related Templates:**
|
|
1743
|
-
|
|
1744
|
-
- [Orders to SFTP XML](./template-extraction-orders-to-sftp-xml.md) - Same entity, different format/destination
|
|
1745
|
-
- [Fulfillments to S3 CSV](./template-extraction-fulfillments-to-sftp-csv.md) - Fulfillment lifecycle tracking
|
|
1746
|
-
- [Products to S3 JSON](./template-extraction-products-to-s3-json.md) - Product catalog extraction
|
|
1747
|
-
|
|
1748
|
-
**SDK Documentation:**
|
|
1749
|
-
|
|
1750
|
-
- [ExtractionOrchestrator Guide](../../../../../02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md) - Auto-pagination and path-based extraction
|
|
1751
|
-
- [JobTracker Reference](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md#jobtracker-new) - Job lifecycle management
|
|
1752
|
-
- [Universal Mapping Guide](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md) - Field transformation
|
|
1753
|
-
- [CSVParserService API](../../../../../02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md) - CSV generation
|
|
1754
|
-
|
|
1755
|
-
---
|
|
1756
|
-
|
|
1757
|
-
### Pattern 8: Backward Pagination (Optional - Advanced)
|
|
1758
|
-
|
|
1759
|
-
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
1760
|
-
|
|
1761
|
-
**When to Use**:
|
|
1762
|
-
|
|
1763
|
-
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
1764
|
-
- ✅ Time-bounded reverse traversal for auditing
|
|
1765
|
-
- ✅ Display newest-first in UI/reports
|
|
1766
|
-
- ❌ **Don't use for standard incremental sync** - use forward pagination (default)
|
|
1767
|
-
|
|
1768
|
-
**GraphQL Query Requirements**:
|
|
1769
|
-
|
|
1770
|
-
Your query must support backward pagination by including `$last` and `$before`:
|
|
1771
|
-
|
|
1772
|
-
```graphql
|
|
1773
|
-
query GetData(
|
|
1774
|
-
$retailerId: ID!
|
|
1775
|
-
$first: Int # For forward pagination
|
|
1776
|
-
$after: String # For forward pagination
|
|
1777
|
-
$last: Int # For backward pagination
|
|
1778
|
-
$before: String # For backward pagination
|
|
1779
|
-
) {
|
|
1780
|
-
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
1781
|
-
edges {
|
|
1782
|
-
cursor # ✅ REQUIRED
|
|
1783
|
-
node {
|
|
1784
|
-
id
|
|
1785
|
-
createdAt
|
|
1786
|
-
# ... other fields
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
pageInfo {
|
|
1790
|
-
hasNextPage # For forward
|
|
1791
|
-
hasPreviousPage # ✅ REQUIRED for backward
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
```
|
|
1796
|
-
|
|
1797
|
-
**Implementation**:
|
|
1798
|
-
|
|
1799
|
-
```typescript
|
|
1800
|
-
// Backward pagination - newest records first
|
|
1801
|
-
const result = await orchestrator.extract({
|
|
1802
|
-
query: YOUR_QUERY,
|
|
1803
|
-
resultPath: 'data.edges.node',
|
|
1804
|
-
variables: {
|
|
1805
|
-
retailerId,
|
|
1806
|
-
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
1807
|
-
// ❌ Don't include last/before - orchestrator injects them
|
|
1808
|
-
},
|
|
1809
|
-
pageSize: 200,
|
|
1810
|
-
direction: 'backward', // ✅ Enable reverse pagination
|
|
1811
|
-
maxRecords: 10000,
|
|
1812
|
-
});
|
|
1813
|
-
|
|
1814
|
-
// Records are returned in reverse chronological order
|
|
1815
|
-
console.log(result.data[0].createdAt); // Newest
|
|
1816
|
-
console.log(result.data[result.data.length - 1].createdAt); // Oldest (within range)
|
|
1817
|
-
```
|
|
1818
|
-
|
|
1819
|
-
**Key Differences from Forward Pagination**:
|
|
1820
|
-
|
|
1821
|
-
| Aspect | Forward (Default) | Backward |
|
|
1822
|
-
| ---------------------- | -------------------------------- | ----------------------- |
|
|
1823
|
-
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
1824
|
-
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
1825
|
-
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
1826
|
-
| **Cursor Source** | Last edge of page | First edge of page |
|
|
1827
|
-
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
1828
|
-
|
|
1829
|
-
**Important Notes**:
|
|
1830
|
-
|
|
1831
|
-
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
1832
|
-
|
|
1833
|
-
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
1834
|
-
|
|
1835
|
-
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
1836
|
-
|
|
1837
|
-
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
1838
|
-
|
|
1839
|
-
**Example: Extract Latest 1000 Orders**
|
|
1840
|
-
|
|
1841
|
-
```typescript
|
|
1842
|
-
const latestOrders = await orchestrator.extract({
|
|
1843
|
-
query: ORDERS_QUERY,
|
|
1844
|
-
resultPath: 'orders.edges.node',
|
|
1845
|
-
variables: {
|
|
1846
|
-
retailerId,
|
|
1847
|
-
statuses: ['BOOKED', 'ALLOCATED'],
|
|
1848
|
-
},
|
|
1849
|
-
direction: 'backward', // Start from newest
|
|
1850
|
-
maxRecords: 1000, // Stop after 1000 records
|
|
1851
|
-
pageSize: 100, // 100 per page = 10 pages
|
|
1852
|
-
});
|
|
1853
|
-
|
|
1854
|
-
// latestOrders.data[0] is the newest order
|
|
1855
|
-
// latestOrders.data[999] is the 1000th newest order
|
|
1856
|
-
```
|
|
1857
|
-
|
|
1858
|
-
**When to Use Forward vs Backward**:
|
|
1859
|
-
|
|
1860
|
-
```typescript
|
|
1861
|
-
// ✅ Forward (default) - For incremental sync
|
|
1862
|
-
const incrementalData = await orchestrator.extract({
|
|
1863
|
-
query: YOUR_QUERY,
|
|
1864
|
-
resultPath: 'data.edges.node',
|
|
1865
|
-
variables: {
|
|
1866
|
-
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
1867
|
-
},
|
|
1868
|
-
// direction defaults to 'forward'
|
|
1869
|
-
// Processes oldest → newest for proper sequencing
|
|
1870
|
-
});
|
|
1871
|
-
|
|
1872
|
-
// ✅ Backward - For "latest N records" use cases
|
|
1873
|
-
const latestData = await orchestrator.extract({
|
|
1874
|
-
query: YOUR_QUERY,
|
|
1875
|
-
resultPath: 'data.edges.node',
|
|
1876
|
-
direction: 'backward',
|
|
1877
|
-
maxRecords: 100, // Just get latest 100
|
|
1878
|
-
// Gets newest → oldest
|
|
1879
|
-
});
|
|
1880
|
-
```
|
|
1881
|
-
|
|
1882
|
-
**Pagination Variables Reference**:
|
|
1883
|
-
|
|
1884
|
-
| Variable | Forward | Backward | Injected By | Notes |
|
|
1885
|
-
| -------- | ----------- | ----------- | ------------ | ------------------------ |
|
|
1886
|
-
| `first` | ✅ Used | ❌ Not used | Orchestrator | From `pageSize` |
|
|
1887
|
-
| `after` | ✅ Used | ❌ Not used | Orchestrator | From cursor (last edge) |
|
|
1888
|
-
| `last` | ❌ Not used | ✅ Used | Orchestrator | From `pageSize` |
|
|
1889
|
-
| `before` | ❌ Not used | ✅ Used | Orchestrator | From cursor (first edge) |
|
|
1890
|
-
|
|
1891
|
-
**Common Mistakes to Avoid**:
|
|
1892
|
-
|
|
1893
|
-
```typescript
|
|
1894
|
-
// ❌ WRONG - Don't pass pagination variables
|
|
1895
|
-
const result = await orchestrator.extract({
|
|
1896
|
-
variables: {
|
|
1897
|
-
last: 200, // ❌ Orchestrator will override this
|
|
1898
|
-
before: cursor, // ❌ Orchestrator manages cursor
|
|
1899
|
-
},
|
|
1900
|
-
direction: 'backward',
|
|
1901
|
-
});
|
|
1902
|
-
|
|
1903
|
-
// ✅ CORRECT - Let orchestrator inject pagination
|
|
1904
|
-
const result = await orchestrator.extract({
|
|
1905
|
-
variables: {
|
|
1906
|
-
retailerId, // ✅ Your business variables only
|
|
1907
|
-
},
|
|
1908
|
-
pageSize: 200, // ✅ Orchestrator uses this for last/before
|
|
1909
|
-
direction: 'backward',
|
|
1910
|
-
});
|
|
1911
|
-
```
|
|
1912
|
-
|
|
1913
|
-
#### Optional: Reverse Pagination
|
|
1914
|
-
|
|
1915
|
-
- Default: forward pagination ($first/$after) with pageInfo.hasNextPage.
|
|
1916
|
-
- Reverse: declare $last/$before in the query and include pageInfo.hasPreviousPage; set direction='backward' in the orchestrator call.
|
|
1917
|
-
|
|
1918
|
-
GraphQL:
|
|
1919
|
-
|
|
1920
|
-
```graphql
|
|
1921
|
-
query GetOrdersBackward(
|
|
1922
|
-
$retailerId: ID!
|
|
1923
|
-
$dateRangeFilter: DateRange
|
|
1924
|
-
$last: Int!
|
|
1925
|
-
$before: String
|
|
1926
|
-
) {
|
|
1927
|
-
orders(retailerId: $retailerId, updatedOn: $dateRangeFilter, last: $last, before: $before) {
|
|
1928
|
-
edges {
|
|
1929
|
-
cursor
|
|
1930
|
-
node {
|
|
1931
|
-
id
|
|
1932
|
-
ref
|
|
1933
|
-
updatedOn
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
pageInfo {
|
|
1937
|
-
hasPreviousPage
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
```
|
|
1942
|
-
|
|
1943
|
-
SDK:
|
|
1944
|
-
|
|
1945
|
-
```typescript
|
|
1946
|
-
await orchestrator.extract({
|
|
1947
|
-
query: ORDERS_BACKWARD_QUERY,
|
|
1948
|
-
resultPath: 'orders.edges.node',
|
|
1949
|
-
variables: { retailerId, dateRangeFilter },
|
|
1950
|
-
pageSize,
|
|
1951
|
-
direction: 'backward',
|
|
1952
|
-
});
|
|
1953
|
-
```
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-extract-orders-to-s3-csv
|
|
3
|
+
canonical_filename: template-extraction-orders-to-s3-csv.md
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
sdk_version: ^0.1.39
|
|
6
|
+
runtime: versori
|
|
7
|
+
direction: extraction
|
|
8
|
+
source: fluent-graphql
|
|
9
|
+
destination: s3-csv
|
|
10
|
+
entity: orders
|
|
11
|
+
format: csv
|
|
12
|
+
logging: versori
|
|
13
|
+
status: stable
|
|
14
|
+
features:
|
|
15
|
+
- memory-management
|
|
16
|
+
- enhanced-logging
|
|
17
|
+
- pagination-progress
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Template: Extraction - Orders to S3 CSV
|
|
21
|
+
|
|
22
|
+
**Template Version:** 2.0.0
|
|
23
|
+
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
24
|
+
**Last Updated:** 2025-01-24
|
|
25
|
+
**Deployment Target:** Versori Platform
|
|
26
|
+
|
|
27
|
+
**🆕 Version 2.0.0 Enhancements:**
|
|
28
|
+
- ✅ **Memory Management** - Clear large result sets after processing batches
|
|
29
|
+
- ✅ **Enhanced Logging** - Pagination progress tracking with emoji indicators (📊, 📥, ✅)
|
|
30
|
+
- ✅ **Pagination Progress** - Real-time page-by-page progress logging with metrics
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📚 STEP 1: Load These Docs (Human Checklist)
|
|
35
|
+
|
|
36
|
+
1. REQUIRED (load all)
|
|
37
|
+
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
38
|
+
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
39
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
40
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
41
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
42
|
+
- [ ] fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
43
|
+
|
|
44
|
+
Copy-paste list (open these):
|
|
45
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
46
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
47
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
48
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
49
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
50
|
+
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 📋 STEP 2: Tell Your AI (Prompt)
|
|
55
|
+
|
|
56
|
+
Copy/paste this prompt after loading the documentation above:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Create a Versori scheduled extractor for orders that uses ExtractionOrchestrator + JobTracker, incremental updatedOn with a 60s overlap buffer, transforms via UniversalMapper, generates CSV with CSVParserService.stringify(), uploads to S3 using S3DataSource. Include 3 workflows: scheduled, ad-hoc webhook, and job-status query with native Versori logging.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 📦 SDK Imports (Verified - Versori Optimized)
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { Buffer } from 'node:buffer';
|
|
68
|
+
import {
|
|
69
|
+
createClient,
|
|
70
|
+
ExtractionOrchestrator,
|
|
71
|
+
JobTracker,
|
|
72
|
+
UniversalMapper,
|
|
73
|
+
CSVParserService,
|
|
74
|
+
S3DataSource,
|
|
75
|
+
VersoriKVAdapter,
|
|
76
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
77
|
+
|
|
78
|
+
import { schedule, webhook, http, fn } from '@versori/run';
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
# Versori Scheduled: Orders Extraction to S3 CSV (Incremental)
|
|
84
|
+
|
|
85
|
+
**FC Connect SDK Use Case Guide**
|
|
86
|
+
|
|
87
|
+
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
88
|
+
> **Installation**: `npm install @fluentcommerce/fc-connect-sdk`
|
|
89
|
+
|
|
90
|
+
Context: Scheduled Versori workflow that extracts new/updated orders from Fluent Commerce via GraphQL query with **ExtractionOrchestrator**, **JobTracker**, and **incremental timestamp tracking**, transforms with `UniversalMapper`, and writes **CSV files** to S3 for 3PL/fulfillment systems.
|
|
91
|
+
|
|
92
|
+
**Pattern**: EXTRACTION (Fluent → S3 CSV)
|
|
93
|
+
**Complexity**: High | Runtime: Versori Platform (Scheduled)
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## ⚠️ IMPORTANT: Production-Ready Base Template
|
|
98
|
+
|
|
99
|
+
> **📋 BASE TEMPLATE - Ready for Production (Customize for Your Needs)**
|
|
100
|
+
>
|
|
101
|
+
> This is a **production-ready base template** demonstrating FC Connect SDK best practices for order extraction workflows with CSV output to S3.
|
|
102
|
+
>
|
|
103
|
+
> **✅ INCLUDED FEATURES:**
|
|
104
|
+
>
|
|
105
|
+
> - ✅ Comprehensive error handling with retry logic
|
|
106
|
+
> - ✅ S3 upload with proper error handling
|
|
107
|
+
> - ✅ State management with overlap buffer (prevents missed records)
|
|
108
|
+
> - ✅ Job tracking with lifecycle management
|
|
109
|
+
> - ✅ Security (credential masking in logs)
|
|
110
|
+
> - ✅ UTC time enforcement (prevents timezone bugs)
|
|
111
|
+
> - ✅ Incremental extraction (safe, efficient, production-ready)
|
|
112
|
+
> - ✅ Natural rate limiting via timestamps
|
|
113
|
+
>
|
|
114
|
+
> **📝 BEFORE DEPLOYING:**
|
|
115
|
+
>
|
|
116
|
+
> 1. Review and customize activation variables for your environment
|
|
117
|
+
> 2. Test with sample data in your Versori workspace
|
|
118
|
+
> 3. Adjust safety limits (pageSize, maxRecords) if needed
|
|
119
|
+
> 4. Configure monitoring alerts for extraction failures
|
|
120
|
+
> 5. Verify S3 bucket credentials and paths
|
|
121
|
+
>
|
|
122
|
+
> **This base template follows SDK best practices - tweak specific to your needs.**
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## What You'll Build
|
|
127
|
+
|
|
128
|
+
- **Incremental extraction** using `updatedOn >= (lastRunTime - buffer)` with **overlap buffer**
|
|
129
|
+
- **ExtractionOrchestrator** for auto-pagination and path-based extraction
|
|
130
|
+
- **JobTracker** for lifecycle management
|
|
131
|
+
- **State management** with VersoriKV to track last successful run
|
|
132
|
+
- **Safety buffer** (60 seconds) to handle clock skew and race conditions
|
|
133
|
+
- GraphQL query with nested order lines
|
|
134
|
+
- UniversalMapper transformation with line item flattening
|
|
135
|
+
- **CSV file generation** with CSVParserService
|
|
136
|
+
- **S3 upload** to 3PL/fulfillment system
|
|
137
|
+
- **3 workflow patterns**: scheduled, ad-hoc webhook, job status query
|
|
138
|
+
- **Failure recovery** with timestamp tracking
|
|
139
|
+
|
|
140
|
+
## Business Use Case
|
|
141
|
+
|
|
142
|
+
**Hourly order feed to 3PL/fulfillment center:**
|
|
143
|
+
|
|
144
|
+
- Extract new and updated orders since last run
|
|
145
|
+
- Flatten line items (one CSV row per item)
|
|
146
|
+
- Generate CSV file with order header + line items
|
|
147
|
+
- Upload to S3 bucket for 3PL integration
|
|
148
|
+
- Run every hour to enable real-time fulfillment
|
|
149
|
+
- Support order updates (address changes, item modifications)
|
|
150
|
+
- Standard CSV format for WMS/ERP imports
|
|
151
|
+
|
|
152
|
+
## SDK Methods Used
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { Buffer } from 'node:buffer';
|
|
156
|
+
import {
|
|
157
|
+
createClient,
|
|
158
|
+
ExtractionOrchestrator,
|
|
159
|
+
JobTracker,
|
|
160
|
+
UniversalMapper,
|
|
161
|
+
CSVParserService,
|
|
162
|
+
S3DataSource,
|
|
163
|
+
VersoriKVAdapter,
|
|
164
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
165
|
+
|
|
166
|
+
await createClient(ctx); // Versori-aware client
|
|
167
|
+
const orchestrator = new ExtractionOrchestrator(client, log); // Auto-pagination
|
|
168
|
+
const tracker = new JobTracker(kv, log); // Job lifecycle tracking
|
|
169
|
+
await orchestrator.extract({ query, resultPath, variables, pageSize, maxRecords }); // Extract
|
|
170
|
+
new VersoriKVAdapter(ctx.openKv(':project:')); // State management
|
|
171
|
+
new UniversalMapper(exportMapping); // Field transformation
|
|
172
|
+
const csvParser = new CSVParserService(); // CSV generation
|
|
173
|
+
csvParser.stringify(records, { headers: true }); // Generate CSV content (synchronous)
|
|
174
|
+
await s3.upload(key, buffer, contentType, metadata); // S3 upload
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Activation Variables
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"retailerId": "your-retailer-id",
|
|
182
|
+
"s3BucketName": "3pl-orders-export",
|
|
183
|
+
"awsAccessKeyId": "AKIAXXXXXXXXXXXX",
|
|
184
|
+
"awsSecretAccessKey": "********",
|
|
185
|
+
"awsRegion": "us-east-1",
|
|
186
|
+
"s3Prefix": "orders/new/",
|
|
187
|
+
"pageSize": 200,
|
|
188
|
+
"maxRecords": 10000,
|
|
189
|
+
"fallbackStartDate": "2024-01-01T00:00:00Z",
|
|
190
|
+
"overlapBufferSeconds": "60",
|
|
191
|
+
"validateConnection": "true"
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Export Mapping Configuration
|
|
196
|
+
|
|
197
|
+
Create file: `./config/orders.export.csv.json`
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"name": "orders.export.csv",
|
|
202
|
+
"version": "1.0.0",
|
|
203
|
+
"description": "Fluent Orders → 3PL CSV Export",
|
|
204
|
+
"fields": {
|
|
205
|
+
"order_id": { "source": "ref", "required": true, "resolver": "sdk.trim" },
|
|
206
|
+
"order_date": { "source": "createdOn", "required": true, "resolver": "sdk.formatDateShort" },
|
|
207
|
+
"order_status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
|
|
208
|
+
"customer_name": { "source": "customer.firstName", "required": false, "resolver": "sdk.trim" },
|
|
209
|
+
"customer_email": { "source": "customer.email", "required": false, "resolver": "sdk.trim" },
|
|
210
|
+
"ship_to_name": { "source": "deliveryAddress.name", "required": true, "resolver": "sdk.trim" },
|
|
211
|
+
"ship_to_address1": {
|
|
212
|
+
"source": "deliveryAddress.street1",
|
|
213
|
+
"required": true,
|
|
214
|
+
"resolver": "sdk.trim"
|
|
215
|
+
},
|
|
216
|
+
"ship_to_city": { "source": "deliveryAddress.city", "required": true, "resolver": "sdk.trim" },
|
|
217
|
+
"ship_to_state": {
|
|
218
|
+
"source": "deliveryAddress.state",
|
|
219
|
+
"required": true,
|
|
220
|
+
"resolver": "sdk.uppercase"
|
|
221
|
+
},
|
|
222
|
+
"ship_to_zip": {
|
|
223
|
+
"source": "deliveryAddress.postcode",
|
|
224
|
+
"required": true,
|
|
225
|
+
"resolver": "sdk.trim"
|
|
226
|
+
},
|
|
227
|
+
"ship_to_country": {
|
|
228
|
+
"source": "deliveryAddress.country",
|
|
229
|
+
"required": true,
|
|
230
|
+
"resolver": "sdk.uppercase"
|
|
231
|
+
},
|
|
232
|
+
"line_item_sku": { "source": "product.ref", "required": true, "resolver": "sdk.trim" },
|
|
233
|
+
"line_item_qty": { "source": "quantity", "required": true, "resolver": "sdk.parseInt" },
|
|
234
|
+
"line_item_price": { "source": "price", "required": true, "resolver": "sdk.parseFloat" }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## GraphQL Query
|
|
240
|
+
|
|
241
|
+
```graphql
|
|
242
|
+
query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, $first: Int!, $after: String) {
|
|
243
|
+
orders(
|
|
244
|
+
retailerId: $retailerId
|
|
245
|
+
updatedOn: { after: $updatedAfter }
|
|
246
|
+
first: $first
|
|
247
|
+
after: $after
|
|
248
|
+
) {
|
|
249
|
+
edges {
|
|
250
|
+
node {
|
|
251
|
+
id
|
|
252
|
+
ref
|
|
253
|
+
status
|
|
254
|
+
createdOn
|
|
255
|
+
updatedOn
|
|
256
|
+
customer {
|
|
257
|
+
firstName
|
|
258
|
+
lastName
|
|
259
|
+
email
|
|
260
|
+
}
|
|
261
|
+
deliveryAddress {
|
|
262
|
+
name
|
|
263
|
+
street1
|
|
264
|
+
street2
|
|
265
|
+
city
|
|
266
|
+
state
|
|
267
|
+
postcode
|
|
268
|
+
country
|
|
269
|
+
}
|
|
270
|
+
items {
|
|
271
|
+
id
|
|
272
|
+
quantity
|
|
273
|
+
price
|
|
274
|
+
product {
|
|
275
|
+
ref
|
|
276
|
+
name
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
cursor
|
|
281
|
+
}
|
|
282
|
+
pageInfo {
|
|
283
|
+
hasNextPage
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Versori Workflows Structure
|
|
292
|
+
|
|
293
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
294
|
+
|
|
295
|
+
**Trigger Types:**
|
|
296
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
297
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
298
|
+
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
299
|
+
|
|
300
|
+
**Execution Steps (chained to triggers):**
|
|
301
|
+
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
302
|
+
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
303
|
+
|
|
304
|
+
### Recommended Project Structure (MemoryInterpreter Pattern)
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
orders-extraction/
|
|
308
|
+
├── index.ts # Entry point - exports all workflows (MemoryInterpreter pattern)
|
|
309
|
+
└── src/
|
|
310
|
+
├── workflows/
|
|
311
|
+
│ ├── scheduled/
|
|
312
|
+
│ │ └── daily-orders-extraction.ts # Scheduled: Daily orders extraction
|
|
313
|
+
│ │
|
|
314
|
+
│ └── webhook/
|
|
315
|
+
│ ├── adhoc-orders-extraction.ts # Webhook: Manual trigger
|
|
316
|
+
│ └── job-status-check.ts # Webhook: Status query
|
|
317
|
+
│
|
|
318
|
+
├── services/
|
|
319
|
+
│ └── orders-extraction.service.ts # Shared orchestration logic (reusable)
|
|
320
|
+
│
|
|
321
|
+
└── config/
|
|
322
|
+
└── orders.export.csv.json # Mapping configuration
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**MemoryInterpreter Pattern:**
|
|
326
|
+
- `index.ts` exports workflow definitions (lightweight registration)
|
|
327
|
+
- Workflow files delegate to service functions for business logic
|
|
328
|
+
- Service functions contain the actual implementation
|
|
329
|
+
- This pattern enables better memory management and code organization
|
|
330
|
+
|
|
331
|
+
**Note:** The code examples below demonstrate what goes in each workflow file. You would split them into separate files following the structure above.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
```csv
|
|
336
|
+
order_id,order_date,order_status,customer_name,customer_email,ship_to_name,ship_to_address1,ship_to_city,ship_to_state,ship_to_zip,ship_to_country,line_item_sku,line_item_qty,line_item_price
|
|
337
|
+
ORD-001,2025-01-22,CREATED,John,john@example.com,John Smith,123 Main St,New York,NY,10001,US,SKU-001,2,29.99
|
|
338
|
+
ORD-001,2025-01-22,CREATED,John,john@example.com,John Smith,123 Main St,New York,NY,10001,US,SKU-002,1,49.99
|
|
339
|
+
ORD-002,2025-01-22,PAID,Jane,jane@example.com,Jane Doe,456 Oak Ave,Los Angeles,CA,90001,US,SKU-003,1,19.99
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Note**: Each line item becomes a separate row with duplicated order header data (standard 3PL format).
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Complete Implementation
|
|
347
|
+
|
|
348
|
+
### Workflow 1: Scheduled Extraction (Main Workflow)
|
|
349
|
+
|
|
350
|
+
**File: `src/workflows/scheduled/daily-orders-extraction.ts`**
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { schedule, http } from '@versori/run';
|
|
354
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
355
|
+
import { processOrdersExtraction } from '../../services/orders-extraction.service';
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Scheduled workflow: Hourly orders extraction to S3 CSV
|
|
359
|
+
* Runs every hour
|
|
360
|
+
*
|
|
361
|
+
* DELEGATION PATTERN (MemoryInterpreter):
|
|
362
|
+
* - Workflow receives ctx from Versori
|
|
363
|
+
* - Passes entire ctx to service function
|
|
364
|
+
* - Service handles all business logic
|
|
365
|
+
*/
|
|
366
|
+
export const ordersExtractionScheduled = schedule('orders-extract-hourly', '0 * * * *').then(
|
|
367
|
+
http('extract-orders', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
368
|
+
const { log, openKv } = ctx;
|
|
369
|
+
const executionStartTime = Date.now();
|
|
370
|
+
const jobId = `SCHEDULED_ORD_${new Date().toISOString().replace(/[:.]/g, '-')}_${Date.now()}`;
|
|
371
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
372
|
+
|
|
373
|
+
log.info('🚀 [WORKFLOW] Starting scheduled orders extraction', { jobId });
|
|
374
|
+
|
|
375
|
+
await tracker.createJob(jobId, {
|
|
376
|
+
triggeredBy: 'schedule',
|
|
377
|
+
stage: 'initialization',
|
|
378
|
+
startTime: executionStartTime,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
await tracker.updateJob(jobId, { status: 'processing' });
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
// DELEGATION: Pass entire ctx to service function
|
|
385
|
+
const result = await processOrdersExtraction(ctx, jobId, tracker);
|
|
386
|
+
|
|
387
|
+
const duration = Date.now() - executionStartTime;
|
|
388
|
+
|
|
389
|
+
if (result.success) {
|
|
390
|
+
await tracker.markCompleted(jobId, { ...result, duration });
|
|
391
|
+
log.info('✅ [WORKFLOW] Orders extraction completed successfully', {
|
|
392
|
+
jobId,
|
|
393
|
+
ordersExtracted: result.ordersExtracted,
|
|
394
|
+
fileName: result.fileName,
|
|
395
|
+
duration: `${duration}ms`,
|
|
396
|
+
});
|
|
397
|
+
} else {
|
|
398
|
+
await tracker.markFailed(jobId, result.error || 'Unknown error');
|
|
399
|
+
log.error('❌ [WORKFLOW] Orders extraction failed', {
|
|
400
|
+
jobId,
|
|
401
|
+
error: result.error,
|
|
402
|
+
duration: `${duration}ms`,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return { success: true, jobId, ...result, duration };
|
|
407
|
+
} catch (e: any) {
|
|
408
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
409
|
+
const duration = Date.now() - executionStartTime;
|
|
410
|
+
|
|
411
|
+
await tracker.markFailed(jobId, errorMessage);
|
|
412
|
+
|
|
413
|
+
log.error('❌ [WORKFLOW] Orders extraction failed with exception', {
|
|
414
|
+
jobId,
|
|
415
|
+
error: errorMessage,
|
|
416
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
417
|
+
duration: `${duration}ms`,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
jobId,
|
|
423
|
+
error: errorMessage,
|
|
424
|
+
duration,
|
|
425
|
+
recommendations: [
|
|
426
|
+
'Check SFTP/S3 connection credentials',
|
|
427
|
+
'Verify GraphQL query permissions',
|
|
428
|
+
'Review error stack trace for details'
|
|
429
|
+
]
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
);
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**File: `src/services/orders-extraction.service.ts`**
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
import { Buffer } from 'node:buffer';
|
|
440
|
+
import {
|
|
441
|
+
createClient,
|
|
442
|
+
ExtractionOrchestrator,
|
|
443
|
+
JobTracker,
|
|
444
|
+
UniversalMapper,
|
|
445
|
+
CSVParserService,
|
|
446
|
+
S3DataSource,
|
|
447
|
+
VersoriKVAdapter,
|
|
448
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
449
|
+
import ordersExportMapping from '../config/orders.export.csv.json' with { type: 'json' };
|
|
450
|
+
|
|
451
|
+
const ORDERS_QUERY = `
|
|
452
|
+
query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, $first: Int!, $after: String) {
|
|
453
|
+
orders(
|
|
454
|
+
retailerId: $retailerId
|
|
455
|
+
updatedOn: { after: $updatedAfter }
|
|
456
|
+
first: $first
|
|
457
|
+
after: $after
|
|
458
|
+
) {
|
|
459
|
+
edges {
|
|
460
|
+
node {
|
|
461
|
+
id
|
|
462
|
+
ref
|
|
463
|
+
status
|
|
464
|
+
createdOn
|
|
465
|
+
updatedOn
|
|
466
|
+
customer {
|
|
467
|
+
firstName
|
|
468
|
+
lastName
|
|
469
|
+
email
|
|
470
|
+
}
|
|
471
|
+
deliveryAddress {
|
|
472
|
+
name
|
|
473
|
+
street1
|
|
474
|
+
street2
|
|
475
|
+
city
|
|
476
|
+
state
|
|
477
|
+
postcode
|
|
478
|
+
country
|
|
479
|
+
}
|
|
480
|
+
items {
|
|
481
|
+
id
|
|
482
|
+
quantity
|
|
483
|
+
price
|
|
484
|
+
product {
|
|
485
|
+
ref
|
|
486
|
+
name
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
cursor
|
|
491
|
+
}
|
|
492
|
+
pageInfo {
|
|
493
|
+
hasNextPage
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
`;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Main orders extraction service function
|
|
501
|
+
*
|
|
502
|
+
* This function orchestrates the complete extraction process:
|
|
503
|
+
* 1. Client initialization
|
|
504
|
+
* 2. State management and incremental sync
|
|
505
|
+
* 3. GraphQL extraction with pagination
|
|
506
|
+
* 4. Data transformation and CSV generation
|
|
507
|
+
* 5. S3 upload and state tracking
|
|
508
|
+
*
|
|
509
|
+
* @param ctx - Versori context object containing fetch, connections, log, activation, openKv
|
|
510
|
+
* @param jobId - Job ID for tracking
|
|
511
|
+
* @param tracker - JobTracker instance for job lifecycle management
|
|
512
|
+
*/
|
|
513
|
+
export async function processOrdersExtraction(ctx: any, jobId: string, tracker: JobTracker) {
|
|
514
|
+
const { log, openKv, activation } = ctx;
|
|
515
|
+
const startTime = Date.now();
|
|
516
|
+
|
|
517
|
+
log.info('🔍 [OrdersExtraction] ==================== EXECUTION START ====================');
|
|
518
|
+
log.info('🔍 [ExtractionOrchestrator] Starting extraction');
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
// STEP 1/8: Extract context and configuration
|
|
522
|
+
log.info('📄 [OrdersExtraction] STEP 1/8: Loading configuration');
|
|
523
|
+
const retailerId = activation?.getVariable('retailerId');
|
|
524
|
+
const pageSize = parseInt(activation?.getVariable('pageSize') || '200', 10);
|
|
525
|
+
const maxRecords = parseInt(activation?.getVariable('maxRecords') || '10000', 10);
|
|
526
|
+
const fallbackStartDate =
|
|
527
|
+
activation?.getVariable('fallbackStartDate') || '2024-01-01T00:00:00Z';
|
|
528
|
+
const overlapBufferSeconds = parseInt(
|
|
529
|
+
activation?.getVariable('overlapBufferSeconds') || '60',
|
|
530
|
+
10
|
|
531
|
+
);
|
|
532
|
+
const OVERLAP_BUFFER_MS = overlapBufferSeconds * 1000;
|
|
533
|
+
const validateConnection = activation?.getVariable('validateConnection') === 'true';
|
|
534
|
+
|
|
535
|
+
const s3Config = {
|
|
536
|
+
bucket: activation?.getVariable('s3BucketName'),
|
|
537
|
+
region: activation?.getVariable('awsRegion') || 'us-east-1',
|
|
538
|
+
accessKeyId: activation?.getVariable('awsAccessKeyId'),
|
|
539
|
+
secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
|
|
540
|
+
};
|
|
541
|
+
const s3Prefix = activation?.getVariable('s3Prefix') || 'orders/new/';
|
|
542
|
+
|
|
543
|
+
const missing: string[] = [];
|
|
544
|
+
if (!retailerId) missing.push('retailerId');
|
|
545
|
+
if (!s3Config.bucket) missing.push('s3BucketName');
|
|
546
|
+
if (!s3Config.accessKeyId) missing.push('awsAccessKeyId');
|
|
547
|
+
if (!s3Config.secretAccessKey) missing.push('awsSecretAccessKey');
|
|
548
|
+
if (missing.length) {
|
|
549
|
+
log.error('❌ [OrdersExtraction] Missing required activation variables', { missing });
|
|
550
|
+
return {
|
|
551
|
+
success: false,
|
|
552
|
+
error: `Missing required variables: ${missing.join(', ')}`,
|
|
553
|
+
recommendations: [
|
|
554
|
+
'Check Activation Variables in Versori dashboard',
|
|
555
|
+
'Ensure all required credentials are configured',
|
|
556
|
+
'Verify connection settings for fluent_commerce and S3'
|
|
557
|
+
]
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// STEP 2/8: Initialize SDK services
|
|
562
|
+
log.info('📄 [OrdersExtraction] STEP 2/8: Initializing SDK services');
|
|
563
|
+
const client = await createClient(ctx);
|
|
564
|
+
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
565
|
+
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
566
|
+
const mapper = new UniversalMapper(ordersExportMapping);
|
|
567
|
+
const csvParser = new CSVParserService();
|
|
568
|
+
const s3 = new S3DataSource(
|
|
569
|
+
{
|
|
570
|
+
type: 'S3_CSV',
|
|
571
|
+
connectionId: 's3-orders-export',
|
|
572
|
+
name: 'S3 Orders Export',
|
|
573
|
+
s3Config,
|
|
574
|
+
},
|
|
575
|
+
log
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
// Validate S3 connection if enabled
|
|
579
|
+
if (validateConnection) {
|
|
580
|
+
log.info('🔍 [OrdersExtraction] Validating S3 connection');
|
|
581
|
+
try {
|
|
582
|
+
await s3.validateConnection();
|
|
583
|
+
log.info('✅ [OrdersExtraction] S3 connection validated successfully');
|
|
584
|
+
} catch (validationError: any) {
|
|
585
|
+
log.error('❌ [OrdersExtraction] S3 connection validation failed', {
|
|
586
|
+
error: validationError.message,
|
|
587
|
+
});
|
|
588
|
+
return {
|
|
589
|
+
success: false,
|
|
590
|
+
error: `S3 connection validation failed: ${validationError.message}`,
|
|
591
|
+
recommendations: [
|
|
592
|
+
'Verify S3 bucket exists and is accessible',
|
|
593
|
+
'Check AWS credentials (accessKeyId, secretAccessKey)',
|
|
594
|
+
'Ensure correct AWS region is specified',
|
|
595
|
+
'Verify IAM permissions for S3 operations'
|
|
596
|
+
]
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// STEP 3/8: Create job and load state
|
|
602
|
+
log.info('📄 [OrdersExtraction] STEP 3/8: Loading state for incremental sync');
|
|
603
|
+
|
|
604
|
+
const stateKey = ['extraction', 'orders', 'lastRunTime'];
|
|
605
|
+
const lastRunState = await kv.get(stateKey);
|
|
606
|
+
const rawLastRunTime = lastRunState?.value?.timestamp || fallbackStartDate;
|
|
607
|
+
|
|
608
|
+
// Apply overlap buffer for query (WITH buffer)
|
|
609
|
+
const bufferedLastRunTime = new Date(
|
|
610
|
+
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
|
|
611
|
+
).toISOString();
|
|
612
|
+
|
|
613
|
+
log.info('🔍 [OrdersExtraction] Starting incremental extraction with overlap buffer', {
|
|
614
|
+
rawLastRunTime,
|
|
615
|
+
bufferedLastRunTime,
|
|
616
|
+
overlapBufferSeconds,
|
|
617
|
+
retailerId,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// STEP 4/8: Execute extraction with ExtractionOrchestrator
|
|
621
|
+
log.info('📄 [OrdersExtraction] STEP 4/8: Executing GraphQL extraction');
|
|
622
|
+
const extractionStartTime = Date.now();
|
|
623
|
+
|
|
624
|
+
const result = await orchestrator.extract({
|
|
625
|
+
query: ORDERS_QUERY,
|
|
626
|
+
resultPath: 'orders.edges.node',
|
|
627
|
+
variables: {
|
|
628
|
+
retailerId,
|
|
629
|
+
updatedAfter: bufferedLastRunTime, // ← WITH overlap buffer
|
|
630
|
+
},
|
|
631
|
+
pageSize,
|
|
632
|
+
maxRecords,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const orders = result.data || [];
|
|
636
|
+
const extractionDuration = Date.now() - extractionStartTime;
|
|
637
|
+
|
|
638
|
+
log.info('✅ [ExtractionOrchestrator] Extraction completed', {
|
|
639
|
+
totalRecords: result.stats.totalRecords,
|
|
640
|
+
totalPages: result.stats.totalPages,
|
|
641
|
+
validRecords: result.stats.validRecords ?? orders.length,
|
|
642
|
+
errors: result.errors ? result.errors.length : 0,
|
|
643
|
+
duration: `${extractionDuration}ms`,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
if (result.errors && result.errors.length > 0) {
|
|
647
|
+
log.warn('⚠️ [OrdersExtraction] Non-fatal extraction errors encountered', {
|
|
648
|
+
errorCount: result.errors.length,
|
|
649
|
+
sampleErrors: result.errors.slice(0, 3),
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (orders.length === 0) {
|
|
654
|
+
const duration = Date.now() - startTime;
|
|
655
|
+
log.info('✅ [OrdersExtraction] No new orders to extract');
|
|
656
|
+
await kv.set(stateKey, {
|
|
657
|
+
timestamp: new Date().toISOString(),
|
|
658
|
+
orderCount: 0,
|
|
659
|
+
extractedAt: new Date().toISOString(),
|
|
660
|
+
});
|
|
661
|
+
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
662
|
+
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
message: 'No new orders to extract',
|
|
666
|
+
lastRunTime: rawLastRunTime,
|
|
667
|
+
duration,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
log.info('📄 [OrdersExtraction] Orders retrieved', { count: orders.length });
|
|
672
|
+
|
|
673
|
+
// STEP 5/8: Transform and flatten line items (bulk mapping)
|
|
674
|
+
log.info('📄 [OrdersExtraction] STEP 5/8: Transforming and flattening line items');
|
|
675
|
+
const transformStartTime = Date.now();
|
|
676
|
+
|
|
677
|
+
// Collect all flattened records first (order + item combinations)
|
|
678
|
+
const flattenedInputs: any[] = [];
|
|
679
|
+
|
|
680
|
+
for (const order of orders) {
|
|
681
|
+
const lineItems = order.items || [];
|
|
682
|
+
|
|
683
|
+
if (lineItems.length === 0) {
|
|
684
|
+
log.warn('⚠️ [OrdersExtraction] Order has no line items, skipping', {
|
|
685
|
+
orderId: order.id,
|
|
686
|
+
ref: order.ref,
|
|
687
|
+
});
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Create flattened records (one per line item)
|
|
692
|
+
for (const item of lineItems) {
|
|
693
|
+
flattenedInputs.push({
|
|
694
|
+
...order, // Order header data
|
|
695
|
+
...item, // Line item data (overwrites items array with individual item)
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (flattenedInputs.length === 0) {
|
|
701
|
+
const duration = Date.now() - startTime;
|
|
702
|
+
log.error('❌ [OrdersExtraction] All records skipped (no line items)');
|
|
703
|
+
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
704
|
+
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
705
|
+
return {
|
|
706
|
+
success: false,
|
|
707
|
+
error: 'All orders skipped because they contain no line items',
|
|
708
|
+
duration,
|
|
709
|
+
recommendations: [
|
|
710
|
+
'Check order data quality in Fluent Commerce',
|
|
711
|
+
'Verify line items are being populated correctly',
|
|
712
|
+
'Review GraphQL query to ensure items are included'
|
|
713
|
+
]
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Bulk mapping with UniversalMapper
|
|
718
|
+
const mappingResult = await mapper.map(flattenedInputs);
|
|
719
|
+
const transformDuration = Date.now() - transformStartTime;
|
|
720
|
+
|
|
721
|
+
if (!mappingResult.success) {
|
|
722
|
+
const mappingErrors = mappingResult.errors || ['Unknown mapping failure'];
|
|
723
|
+
const duration = Date.now() - startTime;
|
|
724
|
+
log.error('❌ [OrdersExtraction] Mapping failed - terminating job', {
|
|
725
|
+
errorCount: mappingErrors.length,
|
|
726
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
727
|
+
duration: `${duration}ms`,
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
731
|
+
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
success: false,
|
|
735
|
+
error: `Transformation failed: ${mappingErrors[0] || 'Unknown error'}`,
|
|
736
|
+
errors: mappingErrors,
|
|
737
|
+
duration,
|
|
738
|
+
recommendations: [
|
|
739
|
+
'Check mapping configuration in orders.export.csv.json',
|
|
740
|
+
'Verify source field paths match GraphQL response',
|
|
741
|
+
'Review sample errors for specific field issues',
|
|
742
|
+
'Validate resolver functions are working correctly'
|
|
743
|
+
]
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const flattenedRecords = mappingResult.data || [];
|
|
748
|
+
const mappingErrors = mappingResult.errors || [];
|
|
749
|
+
|
|
750
|
+
if (mappingErrors.length > 0) {
|
|
751
|
+
log.warn('⚠️ [OrdersExtraction] Some records failed transformation', {
|
|
752
|
+
errorCount: mappingErrors.length,
|
|
753
|
+
sampleErrors: mappingErrors.slice(0, 3),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (mappingResult.skippedFields && mappingResult.skippedFields.length > 0) {
|
|
758
|
+
log.info('ℹ️ [OrdersExtraction] Optional fields skipped (undefined values)', {
|
|
759
|
+
skippedFields: mappingResult.skippedFields,
|
|
760
|
+
note: 'These fields were not present in source data. Add defaultValue to mapping config if they should always appear.',
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (flattenedRecords.length === 0) {
|
|
765
|
+
const duration = Date.now() - startTime;
|
|
766
|
+
log.error('❌ [OrdersExtraction] All records failed mapping');
|
|
767
|
+
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
768
|
+
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
769
|
+
return {
|
|
770
|
+
success: false,
|
|
771
|
+
error: 'All records failed mapping',
|
|
772
|
+
errors: mappingErrors,
|
|
773
|
+
duration,
|
|
774
|
+
recommendations: [
|
|
775
|
+
'Review mapping configuration thoroughly',
|
|
776
|
+
'Check for schema changes in Fluent Commerce',
|
|
777
|
+
'Validate all required fields are present',
|
|
778
|
+
'Test with sample data first'
|
|
779
|
+
]
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
log.info('✅ [OrdersExtraction] Records transformed', {
|
|
784
|
+
orders: orders.length,
|
|
785
|
+
lineItems: flattenedRecords.length,
|
|
786
|
+
failed: mappingErrors.length,
|
|
787
|
+
duration: `${transformDuration}ms`,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// STEP 6/8: Generate CSV content (synchronous, not async)
|
|
791
|
+
log.info('📄 [OrdersExtraction] STEP 6/8: Generating CSV content');
|
|
792
|
+
const csvStartTime = Date.now();
|
|
793
|
+
const csvContent = csvParser.stringify(flattenedRecords, { headers: true });
|
|
794
|
+
const csvDuration = Date.now() - csvStartTime;
|
|
795
|
+
|
|
796
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
797
|
+
const fileName = `orders-${timestamp}.csv`;
|
|
798
|
+
const s3Key = `${s3Prefix}${fileName}`;
|
|
799
|
+
|
|
800
|
+
log.info('✅ [OrdersExtraction] Generated CSV file', {
|
|
801
|
+
fileName,
|
|
802
|
+
size: csvContent.length,
|
|
803
|
+
orderCount: orders.length,
|
|
804
|
+
lineItemCount: flattenedRecords.length,
|
|
805
|
+
duration: `${csvDuration}ms`,
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// STEP 7/8: Upload to S3
|
|
809
|
+
log.info('📄 [OrdersExtraction] STEP 7/8: Uploading to S3');
|
|
810
|
+
const uploadStartTime = Date.now();
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
await s3.upload(
|
|
814
|
+
s3Key,
|
|
815
|
+
Buffer.from(csvContent, 'utf8'),
|
|
816
|
+
'text/csv',
|
|
817
|
+
{
|
|
818
|
+
orderCount: String(orders.length),
|
|
819
|
+
lineItemCount: String(flattenedRecords.length),
|
|
820
|
+
extractedAt: new Date().toISOString(),
|
|
821
|
+
lastRunTime: rawLastRunTime,
|
|
822
|
+
jobId,
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
const uploadDuration = Date.now() - uploadStartTime;
|
|
827
|
+
log.info('✅ [OrdersExtraction] CSV file uploaded to S3', {
|
|
828
|
+
s3Key,
|
|
829
|
+
duration: `${uploadDuration}ms`,
|
|
830
|
+
});
|
|
831
|
+
} catch (uploadError: any) {
|
|
832
|
+
const duration = Date.now() - startTime;
|
|
833
|
+
log.error('❌ [OrdersExtraction] S3 upload failed', {
|
|
834
|
+
error: uploadError.message,
|
|
835
|
+
s3Key,
|
|
836
|
+
duration: `${duration}ms`,
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Dispose S3 connection
|
|
840
|
+
try {
|
|
841
|
+
await s3.dispose();
|
|
842
|
+
} catch (disposeError: any) {
|
|
843
|
+
log.error('⚠️ [OrdersExtraction] Failed to dispose S3 connection', {
|
|
844
|
+
error: disposeError.message,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
849
|
+
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
success: false,
|
|
853
|
+
error: `S3 upload failed: ${uploadError.message}`,
|
|
854
|
+
duration,
|
|
855
|
+
recommendations: [
|
|
856
|
+
'Verify S3 bucket exists and is accessible',
|
|
857
|
+
'Check AWS credentials and permissions',
|
|
858
|
+
'Ensure network connectivity to S3',
|
|
859
|
+
'Verify S3 bucket region matches configuration',
|
|
860
|
+
'Check file size limits and quotas'
|
|
861
|
+
]
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// STEP 8/8: Update state and complete job
|
|
866
|
+
log.info('📄 [OrdersExtraction] STEP 8/8: Updating state and completing job');
|
|
867
|
+
|
|
868
|
+
// Calculate MAX(updatedOn) from extracted orders (WITHOUT buffer)
|
|
869
|
+
const maxUpdatedOn = orders.reduce((max, order) => {
|
|
870
|
+
const orderTime = new Date(order.updatedOn).getTime();
|
|
871
|
+
return orderTime > max ? orderTime : max;
|
|
872
|
+
}, new Date(rawLastRunTime).getTime());
|
|
873
|
+
|
|
874
|
+
const newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
875
|
+
|
|
876
|
+
// Save state WITHOUT buffer - buffer only applied to query
|
|
877
|
+
await kv.set(stateKey, {
|
|
878
|
+
timestamp: newTimestamp, // ← WITHOUT buffer
|
|
879
|
+
orderCount: orders.length,
|
|
880
|
+
lineItemCount: flattenedRecords.length,
|
|
881
|
+
extractedAt: new Date().toISOString(),
|
|
882
|
+
fileName,
|
|
883
|
+
s3Key,
|
|
884
|
+
overlapBufferSeconds,
|
|
885
|
+
jobId,
|
|
886
|
+
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
log.info('✅ [OrdersExtraction] State updated with new timestamp', {
|
|
890
|
+
newTimestamp,
|
|
891
|
+
recordsProcessed: orders.length,
|
|
892
|
+
nextRunWillQueryFrom: new Date(
|
|
893
|
+
new Date(newTimestamp).getTime() - OVERLAP_BUFFER_MS
|
|
894
|
+
).toISOString(),
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// Dispose S3 connection
|
|
898
|
+
try {
|
|
899
|
+
await s3.dispose();
|
|
900
|
+
log.info('✅ [OrdersExtraction] S3 connection disposed successfully');
|
|
901
|
+
} catch (disposeError: any) {
|
|
902
|
+
log.error('⚠️ [OrdersExtraction] Failed to dispose S3 connection', {
|
|
903
|
+
error: disposeError.message,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const duration = Date.now() - startTime;
|
|
908
|
+
|
|
909
|
+
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
910
|
+
log.info('⏱️ [OrdersExtraction] Total Duration: ' + duration + 'ms');
|
|
911
|
+
|
|
912
|
+
return {
|
|
913
|
+
success: true,
|
|
914
|
+
ordersExtracted: orders.length,
|
|
915
|
+
lineItemsExtracted: flattenedRecords.length,
|
|
916
|
+
recordsFailed: mappingErrors.length,
|
|
917
|
+
fileName,
|
|
918
|
+
s3Key,
|
|
919
|
+
lastRunTime: rawLastRunTime,
|
|
920
|
+
newTimestamp,
|
|
921
|
+
duration,
|
|
922
|
+
errors: mappingErrors.length > 0 ? mappingErrors : undefined,
|
|
923
|
+
};
|
|
924
|
+
} catch (error: any) {
|
|
925
|
+
const duration = Date.now() - startTime;
|
|
926
|
+
log.error('❌ [OrdersExtraction] Fatal error', {
|
|
927
|
+
message: error?.message,
|
|
928
|
+
stack: error?.stack,
|
|
929
|
+
duration: `${duration}ms`,
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
log.info('🔍 [OrdersExtraction] ==================== EXECUTION END ====================');
|
|
933
|
+
log.info('⏱️ [OrdersExtraction] Duration: ' + duration + 'ms');
|
|
934
|
+
|
|
935
|
+
return {
|
|
936
|
+
success: false,
|
|
937
|
+
error: error instanceof Error ? error.message : String(error),
|
|
938
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
939
|
+
errorType: error instanceof Error ? error.constructor.name : 'UnknownError',
|
|
940
|
+
duration,
|
|
941
|
+
recommendations: [
|
|
942
|
+
'Review error stack trace for root cause',
|
|
943
|
+
'Check all credentials and connection settings',
|
|
944
|
+
'Verify GraphQL query syntax and permissions',
|
|
945
|
+
'Ensure all activation variables are set correctly',
|
|
946
|
+
'Check network connectivity to Fluent Commerce and S3'
|
|
947
|
+
]
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
**File: `index.ts`**
|
|
954
|
+
|
|
955
|
+
```typescript
|
|
956
|
+
/**
|
|
957
|
+
* Entry point - Export all workflows for Versori platform
|
|
958
|
+
*
|
|
959
|
+
* MemoryInterpreter Pattern:
|
|
960
|
+
* This file exports workflow definitions that delegate to service functions.
|
|
961
|
+
* Each workflow is lightweight and imports business logic from services.
|
|
962
|
+
*/
|
|
963
|
+
|
|
964
|
+
// Scheduled workflows
|
|
965
|
+
export { ordersExtractionScheduled } from './src/workflows/scheduled/daily-orders-extraction';
|
|
966
|
+
|
|
967
|
+
// Webhook workflows
|
|
968
|
+
export { ordersExtractionWebhook } from './src/workflows/webhook/adhoc-orders-extraction';
|
|
969
|
+
export { ordersExtractionStatus } from './src/workflows/webhook/job-status-check';
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### Workflow 2: Ad-Hoc Webhook Trigger
|
|
973
|
+
|
|
974
|
+
**File: `src/workflows/webhook/adhoc-orders-extraction.ts`**
|
|
975
|
+
|
|
976
|
+
```typescript
|
|
977
|
+
import { webhook, http } from '@versori/run';
|
|
978
|
+
import { JobTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
979
|
+
import { processOrdersExtraction } from '../../services/orders-extraction.service';
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Webhook workflow: Ad-hoc orders extraction (manual trigger)
|
|
983
|
+
*
|
|
984
|
+
* DELEGATION PATTERN (MemoryInterpreter):
|
|
985
|
+
* - Reuses the same service function as scheduled workflow
|
|
986
|
+
* - Lightweight wrapper that delegates to shared business logic
|
|
987
|
+
*/
|
|
988
|
+
export const ordersExtractionWebhook = webhook('orders-extract-webhook', {
|
|
989
|
+
connection: 'orders-adhoc',
|
|
990
|
+
response: { mode: 'sync' }, // ✅ Sync mode: response sent when handler returns
|
|
991
|
+
}).then(
|
|
992
|
+
http('trigger-extraction', { connection: 'fluent_commerce' }, async (ctx: any) => {
|
|
993
|
+
const { log, openKv } = ctx;
|
|
994
|
+
const executionStartTime = Date.now();
|
|
995
|
+
const jobId = `ADHOC_ORD_${new Date().toISOString().replace(/[:.]/g, '-')}_${Date.now()}`;
|
|
996
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
997
|
+
|
|
998
|
+
log.info('🚀 [WEBHOOK] Adhoc orders extraction triggered', { jobId });
|
|
999
|
+
|
|
1000
|
+
// Create job entry FIRST (awaited to ensure job exists in KV)
|
|
1001
|
+
await tracker.createJob(jobId, {
|
|
1002
|
+
triggeredBy: 'webhook',
|
|
1003
|
+
stage: 'initialization',
|
|
1004
|
+
status: 'queued',
|
|
1005
|
+
startTime: executionStartTime,
|
|
1006
|
+
createdAt: new Date().toISOString(),
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
// ✅ Fire-and-forget: Start background processing WITHOUT await
|
|
1010
|
+
// The promise continues execution after we return the response
|
|
1011
|
+
processOrdersExtraction(ctx, jobId, tracker)
|
|
1012
|
+
.then((result) => {
|
|
1013
|
+
const duration = Date.now() - executionStartTime;
|
|
1014
|
+
if (result.success) {
|
|
1015
|
+
log.info('✅ [BACKGROUND] Orders extraction completed successfully', {
|
|
1016
|
+
jobId,
|
|
1017
|
+
ordersExtracted: result.ordersExtracted,
|
|
1018
|
+
fileName: result.fileName,
|
|
1019
|
+
duration: `${duration}ms`,
|
|
1020
|
+
});
|
|
1021
|
+
return tracker.markCompleted(jobId, { ...result, duration });
|
|
1022
|
+
} else {
|
|
1023
|
+
log.error('❌ [BACKGROUND] Orders extraction failed', {
|
|
1024
|
+
jobId,
|
|
1025
|
+
error: result.error,
|
|
1026
|
+
duration: `${duration}ms`,
|
|
1027
|
+
});
|
|
1028
|
+
return tracker.markFailed(jobId, result.error || 'Unknown error');
|
|
1029
|
+
}
|
|
1030
|
+
})
|
|
1031
|
+
.catch((error: unknown) => {
|
|
1032
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1033
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
1034
|
+
const duration = Date.now() - executionStartTime;
|
|
1035
|
+
|
|
1036
|
+
log.error('❌ [BACKGROUND] Orders extraction failed with exception', {
|
|
1037
|
+
jobId,
|
|
1038
|
+
error: errorMessage,
|
|
1039
|
+
stack: errorStack,
|
|
1040
|
+
duration: `${duration}ms`,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
return tracker.markFailed(jobId, errorMessage);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// Return immediately with jobId (response sent with this return value)
|
|
1047
|
+
return {
|
|
1048
|
+
success: true,
|
|
1049
|
+
jobId,
|
|
1050
|
+
message: 'Orders extraction started in background',
|
|
1051
|
+
statusEndpoint: `https://{workspace}.versori.run/orders-job-status`,
|
|
1052
|
+
note: 'Poll the status endpoint with jobId to check progress',
|
|
1053
|
+
};
|
|
1054
|
+
})
|
|
1055
|
+
);
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
### Workflow 3: Job Status Query
|
|
1059
|
+
|
|
1060
|
+
**File: `src/workflows/webhook/job-status-check.ts`**
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
import { webhook, fn } from '@versori/run';
|
|
1064
|
+
import { JobTracker, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Webhook workflow: Job status query (monitor extraction progress)
|
|
1068
|
+
*
|
|
1069
|
+
* Simple query endpoint - no delegation needed
|
|
1070
|
+
*/
|
|
1071
|
+
export const ordersExtractionStatus = webhook('orders-extract-status', {
|
|
1072
|
+
connection: 'orders-job-status',
|
|
1073
|
+
}).then(
|
|
1074
|
+
fn('get-status', async (ctx: any) => {
|
|
1075
|
+
const { log, openKv } = ctx;
|
|
1076
|
+
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
1077
|
+
const tracker = new JobTracker(kv, log);
|
|
1078
|
+
|
|
1079
|
+
const url = new URL(ctx.request.url);
|
|
1080
|
+
const jobId = url.searchParams.get('jobId');
|
|
1081
|
+
|
|
1082
|
+
if (!jobId) {
|
|
1083
|
+
log.warn('⚠️ [JobStatus] Missing jobId query parameter');
|
|
1084
|
+
return {
|
|
1085
|
+
success: false,
|
|
1086
|
+
error: 'Missing jobId query parameter',
|
|
1087
|
+
usage: 'GET /orders-extract-status?jobId=<job-id>',
|
|
1088
|
+
recommendations: [
|
|
1089
|
+
'Provide jobId as query parameter',
|
|
1090
|
+
'Example: /orders-extract-status?jobId=SCHEDULED_ORD_2025-01-22_12345'
|
|
1091
|
+
]
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
log.info('🔍 [JobStatus] Retrieving job status', { jobId });
|
|
1096
|
+
|
|
1097
|
+
const job = await tracker.getJob(jobId);
|
|
1098
|
+
|
|
1099
|
+
if (!job) {
|
|
1100
|
+
log.warn('⚠️ [JobStatus] Job not found', { jobId });
|
|
1101
|
+
return {
|
|
1102
|
+
success: false,
|
|
1103
|
+
error: `Job not found: ${jobId}`,
|
|
1104
|
+
recommendations: [
|
|
1105
|
+
'Verify jobId is correct',
|
|
1106
|
+
'Check if job has expired (default TTL: 7 days)',
|
|
1107
|
+
'Ensure job was created successfully'
|
|
1108
|
+
]
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
log.info('✅ [JobStatus] Job status retrieved', { jobId, status: job.status });
|
|
1113
|
+
|
|
1114
|
+
return {
|
|
1115
|
+
success: true,
|
|
1116
|
+
job: {
|
|
1117
|
+
id: job.id,
|
|
1118
|
+
name: job.name,
|
|
1119
|
+
status: job.status,
|
|
1120
|
+
createdAt: job.createdAt,
|
|
1121
|
+
startedAt: job.startedAt,
|
|
1122
|
+
completedAt: job.completedAt,
|
|
1123
|
+
duration: job.duration,
|
|
1124
|
+
result: job.result,
|
|
1125
|
+
error: job.error,
|
|
1126
|
+
metadata: job.metadata,
|
|
1127
|
+
},
|
|
1128
|
+
};
|
|
1129
|
+
})
|
|
1130
|
+
);
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
---
|
|
1134
|
+
|
|
1135
|
+
## Key Patterns Explained
|
|
1136
|
+
|
|
1137
|
+
### Pattern 1: Line Item Flattening
|
|
1138
|
+
|
|
1139
|
+
```typescript
|
|
1140
|
+
// BEFORE: 1 order with 3 line items
|
|
1141
|
+
{
|
|
1142
|
+
ref: "ORD-001",
|
|
1143
|
+
items: [
|
|
1144
|
+
{ product: { ref: "SKU-A" }, quantity: 2 },
|
|
1145
|
+
{ product: { ref: "SKU-B" }, quantity: 1 },
|
|
1146
|
+
{ product: { ref: "SKU-C" }, quantity: 3 }
|
|
1147
|
+
]
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// AFTER: 3 CSV rows (order data repeated)
|
|
1151
|
+
[
|
|
1152
|
+
{ order_id: "ORD-001", line_item_sku: "SKU-A", line_item_qty: 2 },
|
|
1153
|
+
{ order_id: "ORD-001", line_item_sku: "SKU-B", line_item_qty: 1 },
|
|
1154
|
+
{ order_id: "ORD-001", line_item_sku: "SKU-C", line_item_qty: 3 }
|
|
1155
|
+
]
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
**Why?** Most 3PL systems expect flattened CSV format, not nested JSON.
|
|
1159
|
+
|
|
1160
|
+
### Pattern 2: Overlap Buffer (Query WITH, Save WITHOUT)
|
|
1161
|
+
|
|
1162
|
+
```typescript
|
|
1163
|
+
// Query uses bufferedLastRunTime (WITH overlap buffer)
|
|
1164
|
+
const bufferedLastRunTime = new Date(
|
|
1165
|
+
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
|
|
1166
|
+
).toISOString();
|
|
1167
|
+
|
|
1168
|
+
const result = await orchestrator.extract({
|
|
1169
|
+
variables: {
|
|
1170
|
+
updatedAfter: bufferedLastRunTime, // ← WITH buffer
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
// Save MAX(updatedOn) WITHOUT buffer
|
|
1175
|
+
const maxUpdatedOn = orders.reduce(
|
|
1176
|
+
(max, order) => Math.max(max, new Date(order.updatedOn).getTime()),
|
|
1177
|
+
new Date(rawLastRunTime).getTime()
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
await kv.set(stateKey, {
|
|
1181
|
+
timestamp: new Date(maxUpdatedOn).toISOString(), // ← WITHOUT buffer
|
|
1182
|
+
});
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
**Why?** Buffer prevents missed records due to clock skew. Next run applies buffer again to saved timestamp.
|
|
1186
|
+
|
|
1187
|
+
### Pattern 3: CSVParserService Usage
|
|
1188
|
+
|
|
1189
|
+
```typescript
|
|
1190
|
+
// ✅ CORRECT - Use CSVParserService.stringify()
|
|
1191
|
+
const csvParser = new CSVParserService();
|
|
1192
|
+
const csvContent = csvParser.stringify(flattenedRecords, { headers: true });
|
|
1193
|
+
|
|
1194
|
+
// ❌ WRONG - Don't use CSVBuilder (doesn't exist)
|
|
1195
|
+
const builder = new CSVBuilder();
|
|
1196
|
+
builder.addRows(records);
|
|
1197
|
+
const csvContent = builder.build();
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
**Why?** `CSVParserService` is the correct SDK service for CSV generation. It has no constructor parameters.
|
|
1201
|
+
|
|
1202
|
+
### Pattern 4: 3-Workflow Pattern
|
|
1203
|
+
|
|
1204
|
+
1. **Scheduled**: Hourly automated extraction
|
|
1205
|
+
2. **Ad-hoc Webhook**: On-demand trigger with API key auth
|
|
1206
|
+
3. **Job Status**: Query job progress via webhook
|
|
1207
|
+
|
|
1208
|
+
**Why?** Provides flexibility for monitoring, testing, and emergency extractions.
|
|
1209
|
+
|
|
1210
|
+
---
|
|
1211
|
+
|
|
1212
|
+
## Testing Checklist
|
|
1213
|
+
|
|
1214
|
+
**Before deploying to production:**
|
|
1215
|
+
|
|
1216
|
+
### 1. Schema Validation
|
|
1217
|
+
|
|
1218
|
+
- [ ] Run `npx fc-connect introspect-schema` to download current schema
|
|
1219
|
+
- [ ] Run `npx fc-connect validate-schema` to check mapping against schema
|
|
1220
|
+
- [ ] Verify all nested `source` paths exist (customer.email, deliveryAddress.city, product.ref)
|
|
1221
|
+
|
|
1222
|
+
### 2. Mapping Testing
|
|
1223
|
+
|
|
1224
|
+
- [ ] Test with sample data (maxRecords=10, small orders)
|
|
1225
|
+
- [ ] Verify all required fields are populated
|
|
1226
|
+
- [ ] Verify SDK resolvers work correctly (trim, uppercase, parseInt, parseFloat)
|
|
1227
|
+
- [ ] Verify line item flattening creates correct CSV structure
|
|
1228
|
+
|
|
1229
|
+
### 3. State Management
|
|
1230
|
+
|
|
1231
|
+
- [ ] Verify overlap buffer prevents duplicate misses
|
|
1232
|
+
- [ ] Test state recovery after failure (run should retry)
|
|
1233
|
+
- [ ] Verify timestamp is saved WITHOUT buffer
|
|
1234
|
+
- [ ] Test fallback to `fallbackStartDate` on first run
|
|
1235
|
+
|
|
1236
|
+
### 4. S3 Operations
|
|
1237
|
+
|
|
1238
|
+
- [ ] Test S3 connection with credentials
|
|
1239
|
+
- [ ] Verify file upload to correct prefix
|
|
1240
|
+
- [ ] Verify CSV file is valid (headers, row format)
|
|
1241
|
+
- [ ] Verify metadata is attached to uploaded file
|
|
1242
|
+
|
|
1243
|
+
### 5. Workflow Testing
|
|
1244
|
+
|
|
1245
|
+
- [ ] Test scheduled workflow (hourly cron)
|
|
1246
|
+
- [ ] Test ad-hoc webhook trigger with API key
|
|
1247
|
+
- [ ] Test job status query endpoint
|
|
1248
|
+
|
|
1249
|
+
---
|
|
1250
|
+
|
|
1251
|
+
## Monitoring & Alerting
|
|
1252
|
+
|
|
1253
|
+
### Success Response Example
|
|
1254
|
+
|
|
1255
|
+
```json
|
|
1256
|
+
{
|
|
1257
|
+
"success": true,
|
|
1258
|
+
"jobId": "SCHEDULED_ORD_20251102_140000_abc123",
|
|
1259
|
+
"recordsExtracted": 1523,
|
|
1260
|
+
"fileName": "orders-2025-11-02T14-00-00-000Z.csv",
|
|
1261
|
+
"s3Path": "s3://bucket/orders/orders-2025-11-02T14-00-00-000Z.csv",
|
|
1262
|
+
"metrics": {
|
|
1263
|
+
"extractionDurationMs": 12543,
|
|
1264
|
+
"totalPages": 8,
|
|
1265
|
+
"pageSize": 200,
|
|
1266
|
+
"mappingErrors": 0,
|
|
1267
|
+
"fileSizeBytes": 524288,
|
|
1268
|
+
"uploadDurationMs": 1234
|
|
1269
|
+
},
|
|
1270
|
+
"timestamps": {
|
|
1271
|
+
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
1272
|
+
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
1273
|
+
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
1274
|
+
},
|
|
1275
|
+
"state": {
|
|
1276
|
+
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
1277
|
+
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
1278
|
+
"stateUpdated": true,
|
|
1279
|
+
"overlapBufferSeconds": 60
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
### Error Response Example
|
|
1285
|
+
|
|
1286
|
+
```json
|
|
1287
|
+
{
|
|
1288
|
+
"success": false,
|
|
1289
|
+
"jobId": "ADHOC_ORD_20251102_140500_xyz789",
|
|
1290
|
+
"error": "S3 upload failed: Connection timeout",
|
|
1291
|
+
"errorCategory": "NETWORK",
|
|
1292
|
+
"recordsExtracted": 0,
|
|
1293
|
+
"stage": "s3_upload",
|
|
1294
|
+
"details": {
|
|
1295
|
+
"message": "Failed to upload file after 3 retry attempts",
|
|
1296
|
+
"retryAttempts": 3,
|
|
1297
|
+
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
1298
|
+
},
|
|
1299
|
+
"state": {
|
|
1300
|
+
"stateUpdated": false,
|
|
1301
|
+
"willRetryNextRun": true,
|
|
1302
|
+
"note": "State not advanced - next extraction will retry same time window"
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
### Key Metrics to Track
|
|
1308
|
+
|
|
1309
|
+
```typescript
|
|
1310
|
+
const METRICS = {
|
|
1311
|
+
// Extraction Performance
|
|
1312
|
+
extractionDurationMs: Date.now() - extractionStart,
|
|
1313
|
+
recordCount: records.length,
|
|
1314
|
+
pageCount: extractionResult.stats.totalPages,
|
|
1315
|
+
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
1316
|
+
|
|
1317
|
+
// Transformation Performance
|
|
1318
|
+
transformedCount: transformedRecords.length,
|
|
1319
|
+
failedCount: mappingErrors.length,
|
|
1320
|
+
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
1321
|
+
|
|
1322
|
+
// File Generation
|
|
1323
|
+
fileSizeMB: (csvContent.length / (1024 * 1024)).toFixed(2),
|
|
1324
|
+
|
|
1325
|
+
// Upload Performance
|
|
1326
|
+
uploadDurationMs: uploadEnd - uploadStart,
|
|
1327
|
+
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
1328
|
+
|
|
1329
|
+
// State Management
|
|
1330
|
+
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
1331
|
+
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
log.info('Extraction metrics', metrics);
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
### Alert Thresholds
|
|
1338
|
+
|
|
1339
|
+
```typescript
|
|
1340
|
+
const ALERT_THRESHOLDS = {
|
|
1341
|
+
// Duration Alerts
|
|
1342
|
+
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
1343
|
+
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
1344
|
+
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
1345
|
+
|
|
1346
|
+
// Error Rate Alerts
|
|
1347
|
+
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
1348
|
+
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
1349
|
+
|
|
1350
|
+
// Volume Alerts
|
|
1351
|
+
MAX_RECORDS_PER_RUN: 100000,
|
|
1352
|
+
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
1353
|
+
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
1354
|
+
|
|
1355
|
+
// State Alerts
|
|
1356
|
+
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
1357
|
+
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
// Check thresholds
|
|
1361
|
+
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
1362
|
+
log.warn('Extraction duration exceeded threshold', {
|
|
1363
|
+
duration: metrics.extractionDurationMs,
|
|
1364
|
+
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
1365
|
+
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
### Monitoring Dashboard Queries
|
|
1371
|
+
|
|
1372
|
+
**Versori Platform Logs Query:**
|
|
1373
|
+
|
|
1374
|
+
```
|
|
1375
|
+
# Successful extractions
|
|
1376
|
+
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
1377
|
+
|
|
1378
|
+
# Failed extractions
|
|
1379
|
+
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
1380
|
+
|
|
1381
|
+
# Performance issues
|
|
1382
|
+
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
1383
|
+
|
|
1384
|
+
# High error rates
|
|
1385
|
+
errorRate:>5
|
|
1386
|
+
|
|
1387
|
+
# State management issues
|
|
1388
|
+
stateUpdated:false AND success:true
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
### Common Issues and Solutions
|
|
1392
|
+
|
|
1393
|
+
**Issue**: "Extraction timeout after 10 minutes"
|
|
1394
|
+
|
|
1395
|
+
- **Cause**: Too many records in single extraction
|
|
1396
|
+
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
1397
|
+
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
1398
|
+
|
|
1399
|
+
**Issue**: "Mapping errors for 50% of records"
|
|
1400
|
+
|
|
1401
|
+
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
1402
|
+
- **Fix**: Run schema validation, update mapping config paths
|
|
1403
|
+
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
1404
|
+
|
|
1405
|
+
**Issue**: "S3 connection timeout"
|
|
1406
|
+
|
|
1407
|
+
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
1408
|
+
- **Fix**: Check S3 credentials, verify network connectivity
|
|
1409
|
+
- **Prevention**: Implement connection health checks, monitor connection status
|
|
1410
|
+
|
|
1411
|
+
**Issue**: "State not updating after successful extraction"
|
|
1412
|
+
|
|
1413
|
+
- **Cause**: KV write failure or intentional retry logic
|
|
1414
|
+
- **Fix**: Check KV logs, verify state update code executed
|
|
1415
|
+
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
1416
|
+
|
|
1417
|
+
**Issue**: "First run exceeds record limits"
|
|
1418
|
+
|
|
1419
|
+
- **Cause**: No previous timestamp, fetches all historical records
|
|
1420
|
+
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
1421
|
+
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
1422
|
+
|
|
1423
|
+
**Issue**: "Excessive duplicate records in output"
|
|
1424
|
+
|
|
1425
|
+
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
1426
|
+
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
1427
|
+
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
## Troubleshooting Quick Reference
|
|
1432
|
+
|
|
1433
|
+
| Error Message | Likely Cause | Solution |
|
|
1434
|
+
|--------------|--------------|----------|
|
|
1435
|
+
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
1436
|
+
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
1437
|
+
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
1438
|
+
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
1439
|
+
| "S3 authentication failed" | Invalid credentials | Verify S3 credentials in activation variables |
|
|
1440
|
+
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
1441
|
+
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
1442
|
+
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
1443
|
+
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
1444
|
+
| "CSV generation failed" | Format-specific error | Check CSV generation logic, validate output |
|
|
1445
|
+
|
|
1446
|
+
---
|
|
1447
|
+
|
|
1448
|
+
---
|
|
1449
|
+
|
|
1450
|
+
### Pattern 7: State Management & Date Overrides
|
|
1451
|
+
|
|
1452
|
+
**Use Case**: Understand how state management works with scheduled and ad-hoc extractions.
|
|
1453
|
+
|
|
1454
|
+
**How it works**:
|
|
1455
|
+
|
|
1456
|
+
VersoriKV stores the last successful extraction timestamp to enable incremental sync:
|
|
1457
|
+
|
|
1458
|
+
```typescript
|
|
1459
|
+
interface ExtractionState {
|
|
1460
|
+
timestamp: string; // Last run timestamp (WITHOUT overlap buffer)
|
|
1461
|
+
recordCount: number; // Number of records extracted
|
|
1462
|
+
extractedAt: string; // When extraction completed
|
|
1463
|
+
fileName?: string; // Generated filename
|
|
1464
|
+
s3Key?: string; // S3 upload path
|
|
1465
|
+
overlapBufferSeconds?: number; // Buffer configuration
|
|
1466
|
+
}
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
**State Priority Chain** (highest to lowest):
|
|
1470
|
+
|
|
1471
|
+
1. **`fromDate` override** (manual date in webhook payload) - Highest priority
|
|
1472
|
+
2. **Stored state** (`await kv.get(stateKey)`) - Normal incremental mode
|
|
1473
|
+
3. **`fallbackStartDate`** (activation variable) - First run fallback
|
|
1474
|
+
|
|
1475
|
+
**Three Scenarios**:
|
|
1476
|
+
|
|
1477
|
+
#### Scenario 1: Normal Scheduled Runs (Incremental)
|
|
1478
|
+
|
|
1479
|
+
```typescript
|
|
1480
|
+
// Payload: {} (empty - no overrides)
|
|
1481
|
+
|
|
1482
|
+
// Behavior:
|
|
1483
|
+
// 1. Load last timestamp from KV: "2025-01-22T10:00:00Z"
|
|
1484
|
+
// 2. Apply overlap buffer: "2025-01-22T09:59:00Z" (query WITH buffer)
|
|
1485
|
+
// 3. Extract records updated since buffered time
|
|
1486
|
+
// 4. Calculate MAX(updatedOn) from results: "2025-01-22T14:30:00Z"
|
|
1487
|
+
// 5. Save new timestamp WITHOUT buffer: "2025-01-22T14:30:00Z"
|
|
1488
|
+
// 6. Next run starts from "2025-01-22T14:29:00Z" (with buffer)
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
**Test**:
|
|
1492
|
+
|
|
1493
|
+
```bash
|
|
1494
|
+
# Trigger scheduled run (no payload needed)
|
|
1495
|
+
# State advances automatically
|
|
1496
|
+
curl -X POST https://workspace.versori.run/orders-extract-hourly
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
#### Scenario 2: Ad-hoc Extraction WITH State Update
|
|
1500
|
+
|
|
1501
|
+
```typescript
|
|
1502
|
+
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": true }
|
|
1503
|
+
|
|
1504
|
+
// Behavior:
|
|
1505
|
+
// 1. Ignore stored state
|
|
1506
|
+
// 2. Use fromDate: "2025-01-01T00:00:00Z" (no buffer applied to manual dates)
|
|
1507
|
+
// 3. Extract all records since 2025-01-01
|
|
1508
|
+
// 4. Calculate MAX(updatedOn): "2025-01-22T14:30:00Z"
|
|
1509
|
+
// 5. Save new timestamp: "2025-01-22T14:30:00Z" (updates state!)
|
|
1510
|
+
// 6. Next scheduled run starts from this new timestamp
|
|
1511
|
+
```
|
|
1512
|
+
|
|
1513
|
+
**Use Case**: One-time catch-up extraction that advances the state pointer.
|
|
1514
|
+
|
|
1515
|
+
**Test**:
|
|
1516
|
+
|
|
1517
|
+
```bash
|
|
1518
|
+
curl -X POST https://workspace.versori.run/orders-extract-webhook \
|
|
1519
|
+
-H "x-api-key: your-api-key" \
|
|
1520
|
+
-H "Content-Type: application/json" \
|
|
1521
|
+
-d '{
|
|
1522
|
+
"fromDate": "2025-01-01T00:00:00Z",
|
|
1523
|
+
"updateState": true
|
|
1524
|
+
}'
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
#### Scenario 3: Ad-hoc Extraction WITHOUT State Update
|
|
1528
|
+
|
|
1529
|
+
```typescript
|
|
1530
|
+
// Payload: { "fromDate": "2025-01-01T00:00:00Z", "updateState": false }
|
|
1531
|
+
|
|
1532
|
+
// Behavior:
|
|
1533
|
+
// 1. Ignore stored state
|
|
1534
|
+
// 2. Use fromDate: "2025-01-01T00:00:00Z"
|
|
1535
|
+
// 3. Extract all records since 2025-01-01
|
|
1536
|
+
// 4. DO NOT update state
|
|
1537
|
+
// 5. Next scheduled run uses previous timestamp (unaffected)
|
|
1538
|
+
```
|
|
1539
|
+
|
|
1540
|
+
**Use Case**: Historical backfill or testing without affecting incremental sync.
|
|
1541
|
+
|
|
1542
|
+
**Test**:
|
|
1543
|
+
|
|
1544
|
+
```bash
|
|
1545
|
+
curl -X POST https://workspace.versori.run/orders-extract-webhook \
|
|
1546
|
+
-H "x-api-key: your-api-key" \
|
|
1547
|
+
-H "Content-Type: application/json" \
|
|
1548
|
+
-d '{
|
|
1549
|
+
"fromDate": "2025-01-01T00:00:00Z",
|
|
1550
|
+
"toDate": "2025-01-31T23:59:59Z",
|
|
1551
|
+
"updateState": false
|
|
1552
|
+
}'
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
**Why this matters**:
|
|
1556
|
+
|
|
1557
|
+
- **Incremental sync** relies on state continuity
|
|
1558
|
+
- **Manual overrides** allow catch-up without breaking incremental flow
|
|
1559
|
+
- **Overlap buffer** prevents missed records at time boundaries
|
|
1560
|
+
- **State isolation** lets you test/backfill without affecting production sync
|
|
1561
|
+
|
|
1562
|
+
---
|
|
1563
|
+
|
|
1564
|
+
### Pattern 8: Optional GraphQL Query Logging
|
|
1565
|
+
|
|
1566
|
+
**Use Case**: Debug extraction issues by logging the exact GraphQL query sent to Fluent Commerce API.
|
|
1567
|
+
|
|
1568
|
+
**When to use**:
|
|
1569
|
+
|
|
1570
|
+
- ✅ Debugging pagination issues
|
|
1571
|
+
- ✅ Verifying query variables (dates, filters, limits)
|
|
1572
|
+
- ✅ Development and testing
|
|
1573
|
+
- ❌ Production (verbose logs, potential secrets in variables)
|
|
1574
|
+
|
|
1575
|
+
**How to enable**:
|
|
1576
|
+
|
|
1577
|
+
Set `DEBUG_GRAPHQL=true` environment variable in Versori activation settings.
|
|
1578
|
+
|
|
1579
|
+
**Implementation**:
|
|
1580
|
+
|
|
1581
|
+
```typescript
|
|
1582
|
+
// In your extraction workflow
|
|
1583
|
+
const DEBUG_GRAPHQL = activation?.getVariable('DEBUG_GRAPHQL') === 'true';
|
|
1584
|
+
|
|
1585
|
+
if (DEBUG_GRAPHQL) {
|
|
1586
|
+
log.info('GraphQL Query Debug', {
|
|
1587
|
+
query: ORDERS_QUERY,
|
|
1588
|
+
variables: {
|
|
1589
|
+
retailerId,
|
|
1590
|
+
updatedAfter: bufferedLastRunTime,
|
|
1591
|
+
first: pageSize,
|
|
1592
|
+
after: null, // First page
|
|
1593
|
+
},
|
|
1594
|
+
pagination: {
|
|
1595
|
+
pageSize,
|
|
1596
|
+
maxRecords,
|
|
1597
|
+
currentPage: 1,
|
|
1598
|
+
},
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
const extractionResult = await orchestrator.extract({
|
|
1603
|
+
query: ORDERS_QUERY,
|
|
1604
|
+
resultPath: 'orders.edges.node',
|
|
1605
|
+
variables: {
|
|
1606
|
+
retailerId,
|
|
1607
|
+
updatedAfter: bufferedLastRunTime,
|
|
1608
|
+
},
|
|
1609
|
+
pageSize,
|
|
1610
|
+
maxRecords,
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
if (DEBUG_GRAPHQL) {
|
|
1614
|
+
log.info('GraphQL Response Debug', {
|
|
1615
|
+
totalRecords: extractionResult.stats.totalRecords,
|
|
1616
|
+
totalPages: extractionResult.stats.totalPages,
|
|
1617
|
+
validRecords: extractionResult.stats.validRecords ?? extractionResult.data.length,
|
|
1618
|
+
firstRecordId: extractionResult.data[0]?.id,
|
|
1619
|
+
lastRecordId: extractionResult.data[extractionResult.data.length - 1]?.id,
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
```
|
|
1623
|
+
|
|
1624
|
+
**What gets logged**:
|
|
1625
|
+
|
|
1626
|
+
```json
|
|
1627
|
+
{
|
|
1628
|
+
"level": "info",
|
|
1629
|
+
"message": "GraphQL Query Debug",
|
|
1630
|
+
"query": "query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, ...)",
|
|
1631
|
+
"variables": {
|
|
1632
|
+
"retailerId": "ACME",
|
|
1633
|
+
"updatedAfter": "2025-01-22T09:59:00Z",
|
|
1634
|
+
"first": 200,
|
|
1635
|
+
"after": null
|
|
1636
|
+
},
|
|
1637
|
+
"pagination": {
|
|
1638
|
+
"pageSize": 200,
|
|
1639
|
+
"maxRecords": 10000,
|
|
1640
|
+
"currentPage": 1
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
```
|
|
1644
|
+
|
|
1645
|
+
**Versori Environment Variables**:
|
|
1646
|
+
|
|
1647
|
+
Add to activation settings:
|
|
1648
|
+
|
|
1649
|
+
```json
|
|
1650
|
+
{
|
|
1651
|
+
"DEBUG_GRAPHQL": "true"
|
|
1652
|
+
}
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
**Testing**:
|
|
1656
|
+
|
|
1657
|
+
```bash
|
|
1658
|
+
# Enable debug logging
|
|
1659
|
+
curl -X POST https://workspace.versori.run/orders-extract-hourly
|
|
1660
|
+
|
|
1661
|
+
# Check Versori logs for "GraphQL Query Debug" entries
|
|
1662
|
+
# Verify query structure and variables are correct
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
**Sample Debug Output**:
|
|
1666
|
+
|
|
1667
|
+
```
|
|
1668
|
+
[INFO] GraphQL Query Debug
|
|
1669
|
+
query: "query GetOrders($retailerId: ID!, $updatedAfter: DateTime!, $first: Int!, $after: String) { ... }"
|
|
1670
|
+
variables: { retailerId: "ACME", updatedAfter: "2025-01-22T09:59:00Z", first: 200, after: null }
|
|
1671
|
+
pagination: { pageSize: 200, maxRecords: 10000, currentPage: 1 }
|
|
1672
|
+
|
|
1673
|
+
[INFO] Extraction complete
|
|
1674
|
+
totalRecords: 150
|
|
1675
|
+
totalPages: 1
|
|
1676
|
+
validRecords: 150
|
|
1677
|
+
failedValidations: 0
|
|
1678
|
+
|
|
1679
|
+
[INFO] GraphQL Response Debug
|
|
1680
|
+
totalRecords: 150
|
|
1681
|
+
totalPages: 1
|
|
1682
|
+
validRecords: 150
|
|
1683
|
+
firstRecordId: "order_123"
|
|
1684
|
+
lastRecordId: "order_272"
|
|
1685
|
+
```
|
|
1686
|
+
|
|
1687
|
+
**Key Benefits**:
|
|
1688
|
+
|
|
1689
|
+
- Quickly identify pagination configuration issues
|
|
1690
|
+
- Verify date filters are applied correctly
|
|
1691
|
+
- Debug "no records found" scenarios
|
|
1692
|
+
- Validate ExtractionOrchestrator variable injection
|
|
1693
|
+
|
|
1694
|
+
**Production Best Practice**: Disable `DEBUG_GRAPHQL` in production to reduce log volume and avoid logging sensitive data.
|
|
1695
|
+
|
|
1696
|
+
---
|
|
1697
|
+
|
|
1698
|
+
## Troubleshooting
|
|
1699
|
+
|
|
1700
|
+
**Issue**: "All records failed mapping"
|
|
1701
|
+
|
|
1702
|
+
**Cause**: Schema mismatch or incorrect field paths
|
|
1703
|
+
|
|
1704
|
+
**Solutions**:
|
|
1705
|
+
|
|
1706
|
+
1. Run `npx fc-connect introspect-schema` to get current schema
|
|
1707
|
+
2. Run `npx fc-connect validate-schema` to check mapping
|
|
1708
|
+
3. Review error details: `errors.map(e => e.errors)`
|
|
1709
|
+
|
|
1710
|
+
---
|
|
1711
|
+
|
|
1712
|
+
**Issue**: "State not updating, same orders exported every run"
|
|
1713
|
+
|
|
1714
|
+
**Cause**: KV storage write failure or timestamp calculation error
|
|
1715
|
+
|
|
1716
|
+
**Solutions**:
|
|
1717
|
+
|
|
1718
|
+
1. Check KV logs for write errors
|
|
1719
|
+
2. Verify state update code is reached: add logging
|
|
1720
|
+
3. Check `newTimestamp` calculation logic
|
|
1721
|
+
4. Manually inspect KV value: `await kv.get(['extraction', 'orders', 'lastRunTime'])`
|
|
1722
|
+
|
|
1723
|
+
---
|
|
1724
|
+
|
|
1725
|
+
**Issue**: "Orders with no line items cause mapping errors"
|
|
1726
|
+
|
|
1727
|
+
**Cause**: `order.items` is empty array or null
|
|
1728
|
+
|
|
1729
|
+
**Solutions**:
|
|
1730
|
+
|
|
1731
|
+
```typescript
|
|
1732
|
+
if (lineItems.length === 0) {
|
|
1733
|
+
log.warn('Order has no line items, skipping', { orderId: order.id, ref: order.ref });
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
```
|
|
1737
|
+
|
|
1738
|
+
---
|
|
1739
|
+
|
|
1740
|
+
## See Also
|
|
1741
|
+
|
|
1742
|
+
**Related Templates:**
|
|
1743
|
+
|
|
1744
|
+
- [Orders to SFTP XML](./template-extraction-orders-to-sftp-xml.md) - Same entity, different format/destination
|
|
1745
|
+
- [Fulfillments to S3 CSV](./template-extraction-fulfillments-to-sftp-csv.md) - Fulfillment lifecycle tracking
|
|
1746
|
+
- [Products to S3 JSON](./template-extraction-products-to-s3-json.md) - Product catalog extraction
|
|
1747
|
+
|
|
1748
|
+
**SDK Documentation:**
|
|
1749
|
+
|
|
1750
|
+
- [ExtractionOrchestrator Guide](../../../../../02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md) - Auto-pagination and path-based extraction
|
|
1751
|
+
- [JobTracker Reference](../../../../../02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md#jobtracker-new) - Job lifecycle management
|
|
1752
|
+
- [Universal Mapping Guide](../../../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md) - Field transformation
|
|
1753
|
+
- [CSVParserService API](../../../../../02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md) - CSV generation
|
|
1754
|
+
|
|
1755
|
+
---
|
|
1756
|
+
|
|
1757
|
+
### Pattern 8: Backward Pagination (Optional - Advanced)
|
|
1758
|
+
|
|
1759
|
+
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
1760
|
+
|
|
1761
|
+
**When to Use**:
|
|
1762
|
+
|
|
1763
|
+
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
1764
|
+
- ✅ Time-bounded reverse traversal for auditing
|
|
1765
|
+
- ✅ Display newest-first in UI/reports
|
|
1766
|
+
- ❌ **Don't use for standard incremental sync** - use forward pagination (default)
|
|
1767
|
+
|
|
1768
|
+
**GraphQL Query Requirements**:
|
|
1769
|
+
|
|
1770
|
+
Your query must support backward pagination by including `$last` and `$before`:
|
|
1771
|
+
|
|
1772
|
+
```graphql
|
|
1773
|
+
query GetData(
|
|
1774
|
+
$retailerId: ID!
|
|
1775
|
+
$first: Int # For forward pagination
|
|
1776
|
+
$after: String # For forward pagination
|
|
1777
|
+
$last: Int # For backward pagination
|
|
1778
|
+
$before: String # For backward pagination
|
|
1779
|
+
) {
|
|
1780
|
+
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
1781
|
+
edges {
|
|
1782
|
+
cursor # ✅ REQUIRED
|
|
1783
|
+
node {
|
|
1784
|
+
id
|
|
1785
|
+
createdAt
|
|
1786
|
+
# ... other fields
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
pageInfo {
|
|
1790
|
+
hasNextPage # For forward
|
|
1791
|
+
hasPreviousPage # ✅ REQUIRED for backward
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
```
|
|
1796
|
+
|
|
1797
|
+
**Implementation**:
|
|
1798
|
+
|
|
1799
|
+
```typescript
|
|
1800
|
+
// Backward pagination - newest records first
|
|
1801
|
+
const result = await orchestrator.extract({
|
|
1802
|
+
query: YOUR_QUERY,
|
|
1803
|
+
resultPath: 'data.edges.node',
|
|
1804
|
+
variables: {
|
|
1805
|
+
retailerId,
|
|
1806
|
+
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
1807
|
+
// ❌ Don't include last/before - orchestrator injects them
|
|
1808
|
+
},
|
|
1809
|
+
pageSize: 200,
|
|
1810
|
+
direction: 'backward', // ✅ Enable reverse pagination
|
|
1811
|
+
maxRecords: 10000,
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
// Records are returned in reverse chronological order
|
|
1815
|
+
console.log(result.data[0].createdAt); // Newest
|
|
1816
|
+
console.log(result.data[result.data.length - 1].createdAt); // Oldest (within range)
|
|
1817
|
+
```
|
|
1818
|
+
|
|
1819
|
+
**Key Differences from Forward Pagination**:
|
|
1820
|
+
|
|
1821
|
+
| Aspect | Forward (Default) | Backward |
|
|
1822
|
+
| ---------------------- | -------------------------------- | ----------------------- |
|
|
1823
|
+
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
1824
|
+
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
1825
|
+
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
1826
|
+
| **Cursor Source** | Last edge of page | First edge of page |
|
|
1827
|
+
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
1828
|
+
|
|
1829
|
+
**Important Notes**:
|
|
1830
|
+
|
|
1831
|
+
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
1832
|
+
|
|
1833
|
+
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
1834
|
+
|
|
1835
|
+
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
1836
|
+
|
|
1837
|
+
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
1838
|
+
|
|
1839
|
+
**Example: Extract Latest 1000 Orders**
|
|
1840
|
+
|
|
1841
|
+
```typescript
|
|
1842
|
+
const latestOrders = await orchestrator.extract({
|
|
1843
|
+
query: ORDERS_QUERY,
|
|
1844
|
+
resultPath: 'orders.edges.node',
|
|
1845
|
+
variables: {
|
|
1846
|
+
retailerId,
|
|
1847
|
+
statuses: ['BOOKED', 'ALLOCATED'],
|
|
1848
|
+
},
|
|
1849
|
+
direction: 'backward', // Start from newest
|
|
1850
|
+
maxRecords: 1000, // Stop after 1000 records
|
|
1851
|
+
pageSize: 100, // 100 per page = 10 pages
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
// latestOrders.data[0] is the newest order
|
|
1855
|
+
// latestOrders.data[999] is the 1000th newest order
|
|
1856
|
+
```
|
|
1857
|
+
|
|
1858
|
+
**When to Use Forward vs Backward**:
|
|
1859
|
+
|
|
1860
|
+
```typescript
|
|
1861
|
+
// ✅ Forward (default) - For incremental sync
|
|
1862
|
+
const incrementalData = await orchestrator.extract({
|
|
1863
|
+
query: YOUR_QUERY,
|
|
1864
|
+
resultPath: 'data.edges.node',
|
|
1865
|
+
variables: {
|
|
1866
|
+
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
1867
|
+
},
|
|
1868
|
+
// direction defaults to 'forward'
|
|
1869
|
+
// Processes oldest → newest for proper sequencing
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
// ✅ Backward - For "latest N records" use cases
|
|
1873
|
+
const latestData = await orchestrator.extract({
|
|
1874
|
+
query: YOUR_QUERY,
|
|
1875
|
+
resultPath: 'data.edges.node',
|
|
1876
|
+
direction: 'backward',
|
|
1877
|
+
maxRecords: 100, // Just get latest 100
|
|
1878
|
+
// Gets newest → oldest
|
|
1879
|
+
});
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
**Pagination Variables Reference**:
|
|
1883
|
+
|
|
1884
|
+
| Variable | Forward | Backward | Injected By | Notes |
|
|
1885
|
+
| -------- | ----------- | ----------- | ------------ | ------------------------ |
|
|
1886
|
+
| `first` | ✅ Used | ❌ Not used | Orchestrator | From `pageSize` |
|
|
1887
|
+
| `after` | ✅ Used | ❌ Not used | Orchestrator | From cursor (last edge) |
|
|
1888
|
+
| `last` | ❌ Not used | ✅ Used | Orchestrator | From `pageSize` |
|
|
1889
|
+
| `before` | ❌ Not used | ✅ Used | Orchestrator | From cursor (first edge) |
|
|
1890
|
+
|
|
1891
|
+
**Common Mistakes to Avoid**:
|
|
1892
|
+
|
|
1893
|
+
```typescript
|
|
1894
|
+
// ❌ WRONG - Don't pass pagination variables
|
|
1895
|
+
const result = await orchestrator.extract({
|
|
1896
|
+
variables: {
|
|
1897
|
+
last: 200, // ❌ Orchestrator will override this
|
|
1898
|
+
before: cursor, // ❌ Orchestrator manages cursor
|
|
1899
|
+
},
|
|
1900
|
+
direction: 'backward',
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
// ✅ CORRECT - Let orchestrator inject pagination
|
|
1904
|
+
const result = await orchestrator.extract({
|
|
1905
|
+
variables: {
|
|
1906
|
+
retailerId, // ✅ Your business variables only
|
|
1907
|
+
},
|
|
1908
|
+
pageSize: 200, // ✅ Orchestrator uses this for last/before
|
|
1909
|
+
direction: 'backward',
|
|
1910
|
+
});
|
|
1911
|
+
```
|
|
1912
|
+
|
|
1913
|
+
#### Optional: Reverse Pagination
|
|
1914
|
+
|
|
1915
|
+
- Default: forward pagination ($first/$after) with pageInfo.hasNextPage.
|
|
1916
|
+
- Reverse: declare $last/$before in the query and include pageInfo.hasPreviousPage; set direction='backward' in the orchestrator call.
|
|
1917
|
+
|
|
1918
|
+
GraphQL:
|
|
1919
|
+
|
|
1920
|
+
```graphql
|
|
1921
|
+
query GetOrdersBackward(
|
|
1922
|
+
$retailerId: ID!
|
|
1923
|
+
$dateRangeFilter: DateRange
|
|
1924
|
+
$last: Int!
|
|
1925
|
+
$before: String
|
|
1926
|
+
) {
|
|
1927
|
+
orders(retailerId: $retailerId, updatedOn: $dateRangeFilter, last: $last, before: $before) {
|
|
1928
|
+
edges {
|
|
1929
|
+
cursor
|
|
1930
|
+
node {
|
|
1931
|
+
id
|
|
1932
|
+
ref
|
|
1933
|
+
updatedOn
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
pageInfo {
|
|
1937
|
+
hasPreviousPage
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
```
|
|
1942
|
+
|
|
1943
|
+
SDK:
|
|
1944
|
+
|
|
1945
|
+
```typescript
|
|
1946
|
+
await orchestrator.extract({
|
|
1947
|
+
query: ORDERS_BACKWARD_QUERY,
|
|
1948
|
+
resultPath: 'orders.edges.node',
|
|
1949
|
+
variables: { retailerId, dateRangeFilter },
|
|
1950
|
+
pageSize,
|
|
1951
|
+
direction: 'backward',
|
|
1952
|
+
});
|
|
1953
|
+
```
|