@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
|
@@ -1,1959 +1,1959 @@
|
|
|
1
|
-
---
|
|
2
|
-
template_id: tpl-extract-inventory-quantities-graphql-to-s3-json
|
|
3
|
-
canonical_filename: template-extraction-inventory-quantities-to-s3-json.md
|
|
4
|
-
version: 2.0.0
|
|
5
|
-
sdk_version: ^0.1.39
|
|
6
|
-
runtime: versori
|
|
7
|
-
direction: extraction
|
|
8
|
-
source: fluent-graphql
|
|
9
|
-
destination: s3-json
|
|
10
|
-
entity: inventory-quantities
|
|
11
|
-
format: json
|
|
12
|
-
logging: versori
|
|
13
|
-
status: stable
|
|
14
|
-
features:
|
|
15
|
-
- memory-management
|
|
16
|
-
- enhanced-logging
|
|
17
|
-
- pagination-progress
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
# Template: Extraction - Inventory Quantities GraphQL to S3 JSON
|
|
21
|
-
|
|
22
|
-
**Template Version:** 2.0.0
|
|
23
|
-
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
24
|
-
**Last Updated:** 2025-01-24
|
|
25
|
-
**Deployment Target:** Versori Platform
|
|
26
|
-
|
|
27
|
-
**🆕 Version 2.0.0 Enhancements:**
|
|
28
|
-
- ✅ **Memory Management** - Clear large result sets after processing batches
|
|
29
|
-
- ✅ **Enhanced Logging** - Pagination progress tracking with emoji indicators (📊, 📥, ✅)
|
|
30
|
-
- ✅ **Pagination Progress** - Real-time page-by-page progress logging with metrics
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## 📚 STEP 1: Load These Docs (Human Checklist)
|
|
35
|
-
|
|
36
|
-
1. REQUIRED (load all)
|
|
37
|
-
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
38
|
-
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
39
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
40
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
41
|
-
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
42
|
-
- [ ] fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
43
|
-
|
|
44
|
-
Copy-paste list (open these):
|
|
45
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
46
|
-
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
47
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
48
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
49
|
-
fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
50
|
-
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## 📋 STEP 2: Tell Your AI (Prompt)
|
|
55
|
-
|
|
56
|
-
Copy/paste the standardized prompt from `docs/template-loading-matrix.md#prompts`.
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## 💻 STEP 3: Implementation (Verified Imports)
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
import { Buffer } from 'node:buffer';
|
|
64
|
-
import {
|
|
65
|
-
createClient,
|
|
66
|
-
UniversalMapper,
|
|
67
|
-
S3DataSource,
|
|
68
|
-
JSONBuilder,
|
|
69
|
-
VersoriKVAdapter,
|
|
70
|
-
ExtractionOrchestrator,
|
|
71
|
-
JobTracker,
|
|
72
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
These are the only SDK imports required for this template. Keep type-only imports out of code samples. Prefer the orchestrator-based flow shown below for pagination and stats.
|
|
76
|
-
|
|
77
|
-
Note on ad hoc runs: Use `fromDate`/`toDate` in webhook payloads. Default `updateState` is `false` for ad hoc; set to `true` only when you want the run to advance the saved timestamp used by scheduled runs.
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
# Versori Scheduled: Inventory Quantities Extraction to S3 JSON (Detailed Records)
|
|
82
|
-
|
|
83
|
-
**FC Connect SDK Use Case Guide**
|
|
84
|
-
|
|
85
|
-
> SDK: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
86
|
-
> Version: ^0.1.39
|
|
87
|
-
|
|
88
|
-
Context: Scheduled Versori workflow that extracts inventory quantities (detailed quantity records) from Fluent Commerce via GraphQL query with **incremental timestamp tracking**, transforms with `UniversalMapper`, and writes JSON files to S3 for API consumption and audit trail systems.
|
|
89
|
-
|
|
90
|
-
**Pattern**: EXTRACTION (Fluent → S3 JSON)
|
|
91
|
-
**Complexity**: Medium | Runtime: Versori Platform (Scheduled)
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## ⚠️ IMPORTANT: Sample Code for SDK Demonstration Only
|
|
96
|
-
|
|
97
|
-
> **🔴 PRODUCTION RECOMMENDATION**
|
|
98
|
-
>
|
|
99
|
-
> This guide demonstrates FC Connect SDK capabilities for **extraction and mapping workflows**. This is a **Versori sample connector** showing how to build inventory quantity extraction workflows using the SDK.
|
|
100
|
-
>
|
|
101
|
-
> **✅ FOR PRODUCTION IMPLEMENTATIONS:**
|
|
102
|
-
>
|
|
103
|
-
> - **ONLY use INCREMENTAL mode with scheduled runs** (e.g., every 15 minutes for real-time)
|
|
104
|
-
> - Incremental mode is safe, efficient, and production-ready
|
|
105
|
-
> - Uses overlap buffer to prevent missed records
|
|
106
|
-
> - Natural rate limiting via timestamps
|
|
107
|
-
> - JSON format ideal for API consumption
|
|
108
|
-
>
|
|
109
|
-
> **🎯 RECOMMENDED SCHEDULE:**
|
|
110
|
-
>
|
|
111
|
-
> - **Every 15 minutes** for real-time inventory APIs
|
|
112
|
-
> - **Hourly** for standard inventory feeds
|
|
113
|
-
> - **Daily** for analytics/reporting systems
|
|
114
|
-
>
|
|
115
|
-
> **⚠️ NOTE:** This sample shows incremental mode implementation. For your production Versori connector, adapt this pattern with proper error handling, monitoring, and testing for your specific use case.
|
|
116
|
-
>
|
|
117
|
-
> **This is a reference implementation showing HOW to use the SDK - adapt for your needs.**
|
|
118
|
-
|
|
119
|
-
---
|
|
120
|
-
|
|
121
|
-
## What You'll Build
|
|
122
|
-
|
|
123
|
-
- **Three workflows**: Scheduled extraction, ad hoc extraction, job status lookup
|
|
124
|
-
- **Incremental extraction** using `updatedOn > lastRunTime` filter
|
|
125
|
-
- **State management** with VersoriKVAdapter to track last successful run
|
|
126
|
-
- **JobTracker integration** for KV-backed job tracking
|
|
127
|
-
- GraphQL query with auto-pagination
|
|
128
|
-
- UniversalMapper transformation for export schema
|
|
129
|
-
- JSON file generation with proper structure
|
|
130
|
-
- S3 upload to target bucket
|
|
131
|
-
- **Failure recovery** - maintains last successful timestamp on errors
|
|
132
|
-
|
|
133
|
-
## Business Use Case
|
|
134
|
-
|
|
135
|
-
**Real-time inventory API for external systems:**
|
|
136
|
-
|
|
137
|
-
- Extract inventory quantity changes every 15 minutes
|
|
138
|
-
- Export as JSON for REST API consumption
|
|
139
|
-
- Support for webhooks/polling by downstream systems
|
|
140
|
-
- Lightweight incremental updates
|
|
141
|
-
- Detailed audit trail with quantity types (AVAILABLE, RESERVED, EXPECTED, etc.)
|
|
142
|
-
- Feed to order management and ecommerce platforms
|
|
143
|
-
|
|
144
|
-
## Inventory Quantities Explained
|
|
145
|
-
|
|
146
|
-
**InventoryQuantity** = Detailed quantity record with type breakdown
|
|
147
|
-
|
|
148
|
-
- Individual quantity records by type (AVAILABLE, RESERVED, EXPECTED, etc.)
|
|
149
|
-
- SKU + Location + Type combination
|
|
150
|
-
- Retailer-defined types for specific tracking needs
|
|
151
|
-
- Used for: Audit trails, detailed inventory tracking, compliance
|
|
152
|
-
|
|
153
|
-
**vs InventoryPosition** = Physical on-hand calculation
|
|
154
|
-
|
|
155
|
-
- Aggregated stock in warehouse
|
|
156
|
-
- Used for: Stock reporting
|
|
157
|
-
|
|
158
|
-
**vs VirtualPosition** = ATP (Available To Promise) calculation
|
|
159
|
-
|
|
160
|
-
- Calculated quantity available for orders
|
|
161
|
-
- Used for: Order promising
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## SDK Methods Used
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
import { Buffer } from 'node:buffer';
|
|
169
|
-
import {
|
|
170
|
-
createClient,
|
|
171
|
-
UniversalMapper,
|
|
172
|
-
S3DataSource,
|
|
173
|
-
JSONBuilder,
|
|
174
|
-
VersoriKVAdapter,
|
|
175
|
-
ExtractionOrchestrator,
|
|
176
|
-
JobTracker,
|
|
177
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
178
|
-
|
|
179
|
-
// STEP 1: Create client
|
|
180
|
-
const client = await createClient(ctx);
|
|
181
|
-
|
|
182
|
-
// STEP 2: Setup job tracking
|
|
183
|
-
const kv = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
184
|
-
const tracker = new JobTracker(kv, log);
|
|
185
|
-
|
|
186
|
-
// STEP 3: Create extraction job
|
|
187
|
-
const job = await tracker.createJob({
|
|
188
|
-
type: 'extraction',
|
|
189
|
-
entity: 'inventoryQuantities',
|
|
190
|
-
config: { extractionMode: 'incremental' },
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// STEP 4: Initialize orchestrator
|
|
194
|
-
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
195
|
-
|
|
196
|
-
// STEP 5: Execute extraction with auto-pagination
|
|
197
|
-
const result = await orchestrator.extract({
|
|
198
|
-
query: INVENTORY_QUANTITIES_QUERY,
|
|
199
|
-
resultPath: 'inventoryQuantities.edges.node',
|
|
200
|
-
variables: { retailerId, updatedAfter },
|
|
201
|
-
maxRecords: 20000,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// STEP 6: Transform with UniversalMapper
|
|
205
|
-
const mapper = new UniversalMapper(exportMapping);
|
|
206
|
-
const transformed = result.data.map(r => mapper.map(r));
|
|
207
|
-
|
|
208
|
-
// STEP 7: Build JSON and upload to S3
|
|
209
|
-
const jsonBuilder = new JSONBuilder({
|
|
210
|
-
prettyPrint: true,
|
|
211
|
-
indent: 2,
|
|
212
|
-
});
|
|
213
|
-
const jsonOutput = {
|
|
214
|
-
metadata: { extractedAt: new Date().toISOString(), recordCount: transformed.length },
|
|
215
|
-
data: transformed,
|
|
216
|
-
};
|
|
217
|
-
const jsonContent = jsonBuilder.build(jsonOutput);
|
|
218
|
-
const s3 = new S3DataSource(config, log);
|
|
219
|
-
await s3.upload({
|
|
220
|
-
key: s3Key,
|
|
221
|
-
body: Buffer.from(jsonContent, 'utf8'),
|
|
222
|
-
contentType: 'application/json'
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// STEP 8: Update job status
|
|
226
|
-
await tracker.markCompleted(job.id, { recordsExtracted: transformed.length, s3Key });
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
---
|
|
230
|
-
|
|
231
|
-
## Activation Variables
|
|
232
|
-
|
|
233
|
-
Configure these variables in your Versori connector's Activation Variables section:
|
|
234
|
-
|
|
235
|
-
```json
|
|
236
|
-
{
|
|
237
|
-
"retailerId": "your-retailer-id",
|
|
238
|
-
"s3BucketName": "api-inventory-exports",
|
|
239
|
-
"awsAccessKeyId": "AKIAXXXXXXXXXXXX",
|
|
240
|
-
"awsSecretAccessKey": "********",
|
|
241
|
-
"awsRegion": "us-east-1",
|
|
242
|
-
"s3Prefix": "inventory/quantities/",
|
|
243
|
-
"pageSize": 500,
|
|
244
|
-
"maxRecords": 20000,
|
|
245
|
-
"fallbackStartDate": "2024-01-01T00:00:00Z",
|
|
246
|
-
"overlapBufferSeconds": "60",
|
|
247
|
-
"prettyPrint": "true"
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
**Variable Descriptions:**
|
|
252
|
-
|
|
253
|
-
| Variable | Required | Description | Default | Example |
|
|
254
|
-
|----------|----------|-------------|---------|---------|
|
|
255
|
-
| `retailerId` | ✅ Yes | Fluent Commerce retailer ID for GraphQL queries | - | `"ACME_CORP"` |
|
|
256
|
-
| `s3BucketName` | ✅ Yes | Target S3 bucket for JSON exports | - | `"api-inventory-exports"` |
|
|
257
|
-
| `awsAccessKeyId` | ✅ Yes | AWS access key ID for S3 uploads | - | `"AKIAIOSFODNN7EXAMPLE"` |
|
|
258
|
-
| `awsSecretAccessKey` | ✅ Yes | AWS secret access key for S3 uploads | - | `"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"` |
|
|
259
|
-
| `awsRegion` | No | AWS region for S3 bucket | `"us-east-1"` | `"us-west-2"` |
|
|
260
|
-
| `s3Prefix` | No | S3 key prefix for file organization | `"inventory/quantities/"` | `"exports/inventory/"` |
|
|
261
|
-
| `pageSize` | No | GraphQL pagination page size | `500` | `200` |
|
|
262
|
-
| `maxRecords` | No | Maximum records per extraction run | `20000` | `50000` |
|
|
263
|
-
| `fallbackStartDate` | No | Initial timestamp for first run | `"2024-01-01T00:00:00Z"` | `"2025-01-01T00:00:00Z"` |
|
|
264
|
-
| `overlapBufferSeconds` | No | Safety buffer to prevent missed records (seconds) | `60` | `120` |
|
|
265
|
-
| `prettyPrint` | No | Pretty-print JSON output (`"true"` or `"false"`) | `"true"` | `"false"` |
|
|
266
|
-
|
|
267
|
-
**🔒 Security Notes:**
|
|
268
|
-
- Store credentials securely in Versori's encrypted variable storage
|
|
269
|
-
- Never commit credentials to source control
|
|
270
|
-
- Rotate AWS keys regularly
|
|
271
|
-
- Use IAM roles with minimal required permissions
|
|
272
|
-
|
|
273
|
-
**⚙️ Performance Tuning:**
|
|
274
|
-
- **pageSize**: Lower values (100-200) for slower networks, higher values (500-1000) for faster connections
|
|
275
|
-
- **maxRecords**: Adjust based on expected change volume - 20K is safe for 15-minute incremental runs
|
|
276
|
-
- **overlapBufferSeconds**: Increase if you see missed records, decrease if seeing too many duplicates
|
|
277
|
-
|
|
278
|
-
---
|
|
279
|
-
|
|
280
|
-
## Export Mapping Configuration
|
|
281
|
-
|
|
282
|
-
Create file: `./config/inventory-quantities.export.json`
|
|
283
|
-
|
|
284
|
-
```json
|
|
285
|
-
{
|
|
286
|
-
"name": "inventory-quantities.export",
|
|
287
|
-
"version": "1.0.0",
|
|
288
|
-
"description": "Fluent Inventory Quantities → JSON API Export Mapping",
|
|
289
|
-
"fields": {
|
|
290
|
-
"ref": { "source": "ref", "required": true, "resolver": "sdk.trim" },
|
|
291
|
-
"location": { "source": "locationRef", "required": true, "resolver": "sdk.trim" },
|
|
292
|
-
"sku": { "source": "articleRef", "required": true, "resolver": "sdk.trim" },
|
|
293
|
-
"quantity": { "source": "qty", "required": true, "resolver": "sdk.parseInt" },
|
|
294
|
-
"type": { "source": "type", "required": true, "resolver": "sdk.uppercase" },
|
|
295
|
-
"condition": { "source": "condition", "required": false, "resolver": "sdk.uppercase" },
|
|
296
|
-
"status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
|
|
297
|
-
"storageArea": { "source": "storageAreaRef", "required": false, "resolver": "sdk.trim" },
|
|
298
|
-
"catalogueRef": { "source": "catalogue.ref", "required": false, "resolver": "sdk.trim" },
|
|
299
|
-
"catalogueName": { "source": "catalogue.name", "required": false, "resolver": "sdk.trim" },
|
|
300
|
-
"expectedDate": { "source": "expectedOn", "required": false, "resolver": "sdk.formatDate" },
|
|
301
|
-
"createdOn": { "source": "createdOn", "required": true, "resolver": "sdk.toString" },
|
|
302
|
-
"updatedOn": { "source": "updatedOn", "required": true, "resolver": "sdk.toString" }
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
---
|
|
308
|
-
|
|
309
|
-
## Mapping & Resolvers Explained
|
|
310
|
-
|
|
311
|
-
This section explains how the SDK transforms raw GraphQL data into your JSON export format using **UniversalMapper** and **SDK resolvers**.
|
|
312
|
-
|
|
313
|
-
### SDK Resolvers Used
|
|
314
|
-
|
|
315
|
-
| Field | Resolver | Why? | Example Transformation |
|
|
316
|
-
| --------------- | ---------------- | --------------------------------------------- | ----------------------------------------------------------- |
|
|
317
|
-
| `ref` | `sdk.trim` | Clean quantity record references | `" QTY-001 "` → `"QTY-001"` |
|
|
318
|
-
| `location` | `sdk.trim` | Clean location references | `" DC01 "` → `"DC01"` |
|
|
319
|
-
| `sku` | `sdk.trim` | Clean SKU references | `" SKU-001 "` → `"SKU-001"` |
|
|
320
|
-
| `quantity` | `sdk.parseInt` | Parse quantity as integer for JSON APIs | `"150"` → `150` |
|
|
321
|
-
| `type` | `sdk.uppercase` | Normalize type codes for consistency | `"available"` → `"AVAILABLE"` |
|
|
322
|
-
| `condition` | `sdk.uppercase` | Normalize condition codes | `"good"` → `"GOOD"` |
|
|
323
|
-
| `status` | `sdk.uppercase` | Normalize status codes | `"active"` → `"ACTIVE"` |
|
|
324
|
-
| `storageArea` | `sdk.trim` | Clean storage area references | `" ZONE-A "` → `"ZONE-A"` |
|
|
325
|
-
| `catalogueRef` | `sdk.trim` | Clean catalogue references from nested object | `" DEFAULT_CATALOGUE "` → `"DEFAULT_CATALOGUE"` |
|
|
326
|
-
| `catalogueName` | `sdk.trim` | Clean catalogue names from nested object | `" Default Inventory "` → `"Default Inventory"` |
|
|
327
|
-
| `expectedDate` | `sdk.formatDate` | Format dates for JSON APIs | `"2025-01-30T00:00:00.000Z"` → `"2025-01-30"` |
|
|
328
|
-
| `createdOn` | `sdk.toString` | Preserve ISO 8601 timestamp string | `"2025-01-22T10:00:00.000Z"` → `"2025-01-22T10:00:00.000Z"` |
|
|
329
|
-
| `updatedOn` | `sdk.toString` | Preserve ISO 8601 timestamp for tracking | `"2025-01-22T10:30:00.000Z"` → `"2025-01-22T10:30:00.000Z"` |
|
|
330
|
-
|
|
331
|
-
### Transformation Flow
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
// 1. GraphQL Response (raw data from Fluent Commerce)
|
|
335
|
-
const rawQuantity = {
|
|
336
|
-
ref: ' QTY-001 ',
|
|
337
|
-
locationRef: ' DC01 ',
|
|
338
|
-
articleRef: ' SKU-001 ',
|
|
339
|
-
qty: '150',
|
|
340
|
-
type: 'available',
|
|
341
|
-
condition: 'good',
|
|
342
|
-
status: 'active',
|
|
343
|
-
storageAreaRef: ' ZONE-A ',
|
|
344
|
-
catalogue: {
|
|
345
|
-
ref: ' DEFAULT_CATALOGUE ',
|
|
346
|
-
name: ' Default Inventory ',
|
|
347
|
-
},
|
|
348
|
-
expectedOn: '2025-01-25T00:00:00.000Z',
|
|
349
|
-
createdOn: '2025-01-20T10:00:00.000Z',
|
|
350
|
-
updatedOn: '2025-01-22T10:30:00.000Z',
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
// 2. UniversalMapper applies SDK resolvers
|
|
354
|
-
const mapper = new UniversalMapper(inventoryQuantitiesExportMapping);
|
|
355
|
-
const result = await mapper.map(rawQuantity);
|
|
356
|
-
|
|
357
|
-
// 3. Transformed Output (JSON-friendly camelCase)
|
|
358
|
-
const transformedQuantity = {
|
|
359
|
-
ref: 'QTY-001',
|
|
360
|
-
location: 'DC01',
|
|
361
|
-
sku: 'SKU-001',
|
|
362
|
-
quantity: 150, // Parsed as number for JSON
|
|
363
|
-
type: 'AVAILABLE',
|
|
364
|
-
condition: 'GOOD',
|
|
365
|
-
status: 'ACTIVE',
|
|
366
|
-
storageArea: 'ZONE-A',
|
|
367
|
-
catalogueRef: 'DEFAULT_CATALOGUE',
|
|
368
|
-
catalogueName: 'Default Inventory',
|
|
369
|
-
expectedDate: '2025-01-25',
|
|
370
|
-
createdOn: '2025-01-20T10:00:00.000Z',
|
|
371
|
-
updatedOn: '2025-01-22T10:30:00.000Z', // ISO 8601 for APIs
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
// 4. Final JSON Structure with Metadata
|
|
375
|
-
const jsonOutput = {
|
|
376
|
-
metadata: {
|
|
377
|
-
extractedAt: '2025-01-22T14:30:00.000Z',
|
|
378
|
-
recordCount: 1,
|
|
379
|
-
incrementalFrom: '2025-01-22T10:00:00.000Z',
|
|
380
|
-
incrementalTo: '2025-01-22T14:30:00.000Z',
|
|
381
|
-
},
|
|
382
|
-
data: [transformedQuantity],
|
|
383
|
-
};
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
### Custom Resolvers for Inventory Quantity-Specific Logic
|
|
387
|
-
|
|
388
|
-
While the mapping above uses built-in SDK resolvers, you can extend with custom business logic for API enrichment:
|
|
389
|
-
|
|
390
|
-
```typescript
|
|
391
|
-
const customResolvers = {
|
|
392
|
-
/**
|
|
393
|
-
* Validate and normalize quantity values for API consumption
|
|
394
|
-
*/
|
|
395
|
-
'custom.normalizeQuantity': (qty: any) => {
|
|
396
|
-
const parsed = parseInt(qty) || 0;
|
|
397
|
-
return Math.max(0, parsed); // Ensure non-negative for APIs
|
|
398
|
-
},
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Add human-readable type descriptions for API responses
|
|
402
|
-
*/
|
|
403
|
-
'custom.enrichQuantityType': (type: string, sourceData: any) => {
|
|
404
|
-
const typeDescriptions: Record<string, string> = {
|
|
405
|
-
AVAILABLE: 'Available for Sale',
|
|
406
|
-
RESERVED: 'Reserved by Order',
|
|
407
|
-
EXPECTED: 'Expected Arrival',
|
|
408
|
-
ADJUSTMENT: 'Inventory Adjustment',
|
|
409
|
-
DAMAGED: 'Damaged/Unsellable',
|
|
410
|
-
IN_TRANSIT: 'In Transit to Location',
|
|
411
|
-
};
|
|
412
|
-
return {
|
|
413
|
-
code: type.toUpperCase(),
|
|
414
|
-
description: typeDescriptions[type.toUpperCase()] || type,
|
|
415
|
-
isAvailableForSale: type.toUpperCase() === 'AVAILABLE',
|
|
416
|
-
};
|
|
417
|
-
},
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Calculate days until expected arrival for EXPECTED quantities
|
|
421
|
-
*/
|
|
422
|
-
'custom.calculateExpectedArrival': (expectedOn: string | null) => {
|
|
423
|
-
if (!expectedOn) return null;
|
|
424
|
-
|
|
425
|
-
const expected = new Date(expectedOn);
|
|
426
|
-
const today = new Date();
|
|
427
|
-
const diffMs = expected.getTime() - today.getTime();
|
|
428
|
-
const daysUntil = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
date: expectedOn,
|
|
432
|
-
daysUntil: daysUntil,
|
|
433
|
-
isOverdue: daysUntil < 0,
|
|
434
|
-
isPending: daysUntil >= 0,
|
|
435
|
-
};
|
|
436
|
-
},
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Generate API-friendly status summary
|
|
440
|
-
*/
|
|
441
|
-
'custom.generateStatusSummary': (quantity: any) => {
|
|
442
|
-
const type = (quantity.type || '').toUpperCase();
|
|
443
|
-
const status = (quantity.status || '').toUpperCase();
|
|
444
|
-
const qty = parseInt(quantity.qty) || 0;
|
|
445
|
-
|
|
446
|
-
return {
|
|
447
|
-
quantity: qty,
|
|
448
|
-
type: type,
|
|
449
|
-
status: status,
|
|
450
|
-
isActive: status === 'ACTIVE',
|
|
451
|
-
isAvailable: type === 'AVAILABLE' && status === 'ACTIVE',
|
|
452
|
-
needsAction: type === 'EXPECTED' && !quantity.expectedOn,
|
|
453
|
-
stockLevel: qty === 0 ? 'OUT_OF_STOCK' : qty < 10 ? 'LOW_STOCK' : 'IN_STOCK',
|
|
454
|
-
};
|
|
455
|
-
},
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Format for real-time inventory API responses
|
|
459
|
-
*/
|
|
460
|
-
'custom.formatForRealtimeAPI': (quantity: any) => {
|
|
461
|
-
return {
|
|
462
|
-
location: quantity.locationRef?.trim(),
|
|
463
|
-
sku: quantity.articleRef?.trim(),
|
|
464
|
-
available: quantity.type === 'AVAILABLE' ? parseInt(quantity.qty) || 0 : 0,
|
|
465
|
-
reserved: quantity.type === 'RESERVED' ? parseInt(quantity.qty) || 0 : 0,
|
|
466
|
-
expected: quantity.type === 'EXPECTED' ? parseInt(quantity.qty) || 0 : 0,
|
|
467
|
-
expectedDate: quantity.expectedOn || null,
|
|
468
|
-
updatedOn: quantity.updatedOn,
|
|
469
|
-
isActive: quantity.status === 'ACTIVE',
|
|
470
|
-
};
|
|
471
|
-
},
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
// Use custom resolvers with UniversalMapper
|
|
475
|
-
const mapper = new UniversalMapper(inventoryQuantitiesExportMapping, {
|
|
476
|
-
customResolvers,
|
|
477
|
-
});
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
### JSON-Specific Mapping Considerations
|
|
481
|
-
|
|
482
|
-
**1. camelCase vs snake_case:**
|
|
483
|
-
|
|
484
|
-
```typescript
|
|
485
|
-
// CSV export: snake_case for compatibility
|
|
486
|
-
{ "location_code": "DC01", "last_updated": "..." }
|
|
487
|
-
|
|
488
|
-
// JSON export: camelCase for APIs
|
|
489
|
-
{ "location": "DC01", "updatedOn": "..." }
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
**2. Number Types:**
|
|
493
|
-
|
|
494
|
-
```typescript
|
|
495
|
-
// JSON preserves number types (no quotes)
|
|
496
|
-
{ "quantity": 150 } // ✓ Correct for JSON APIs
|
|
497
|
-
|
|
498
|
-
// CSV requires strings
|
|
499
|
-
"150" // ✓ Correct for CSV
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
**3. Null Handling:**
|
|
503
|
-
|
|
504
|
-
```typescript
|
|
505
|
-
// JSON can use null
|
|
506
|
-
{ "expectedDate": null } // ✓ Valid JSON
|
|
507
|
-
|
|
508
|
-
// CSV uses empty string
|
|
509
|
-
"" // ✓ Valid CSV
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
**4. Metadata Wrapper:**
|
|
513
|
-
|
|
514
|
-
```json
|
|
515
|
-
{
|
|
516
|
-
"metadata": {
|
|
517
|
-
"extractedAt": "2025-01-22T14:30:00.000Z",
|
|
518
|
-
"recordCount": 2,
|
|
519
|
-
"incrementalFrom": "...",
|
|
520
|
-
"incrementalTo": "..."
|
|
521
|
-
},
|
|
522
|
-
"data": [
|
|
523
|
-
/* transformed records */
|
|
524
|
-
]
|
|
525
|
-
}
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
### Available SDK Resolvers
|
|
529
|
-
|
|
530
|
-
The SDK provides these built-in resolvers (no custom code needed):
|
|
531
|
-
|
|
532
|
-
**String Transformations:**
|
|
533
|
-
|
|
534
|
-
- `sdk.trim` - Remove leading/trailing whitespace
|
|
535
|
-
- `sdk.uppercase` - Convert to uppercase
|
|
536
|
-
- `sdk.lowercase` - Convert to lowercase
|
|
537
|
-
- `sdk.toString` - Convert to string
|
|
538
|
-
|
|
539
|
-
**Number Parsing:**
|
|
540
|
-
|
|
541
|
-
- `sdk.parseInt` - Parse as integer (ideal for JSON)
|
|
542
|
-
- `sdk.parseFloat` - Parse as decimal
|
|
543
|
-
- `sdk.number` - Parse as number (auto-detect int/float)
|
|
544
|
-
|
|
545
|
-
**Date Formatting:**
|
|
546
|
-
|
|
547
|
-
- `sdk.formatDate` - ISO 8601 date only (YYYY-MM-DD)
|
|
548
|
-
- `sdk.formatDateShort` - Short date format
|
|
549
|
-
- `sdk.parseDate` - Parse various date formats
|
|
550
|
-
|
|
551
|
-
**Type Conversions:**
|
|
552
|
-
|
|
553
|
-
- `sdk.boolean` - Convert to boolean
|
|
554
|
-
- `sdk.parseJson` - Parse JSON strings
|
|
555
|
-
- `sdk.toJson` - Convert to JSON string
|
|
556
|
-
|
|
557
|
-
**Utilities:**
|
|
558
|
-
|
|
559
|
-
- `sdk.identity` - Return value unchanged
|
|
560
|
-
- `sdk.coalesce` - Return first non-null value
|
|
561
|
-
|
|
562
|
-
See [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) for complete resolver documentation.
|
|
563
|
-
|
|
564
|
-
---
|
|
565
|
-
|
|
566
|
-
## GraphQL Query
|
|
567
|
-
|
|
568
|
-
**Note**: This query is verified against Fluent Commerce schema introspection.
|
|
569
|
-
|
|
570
|
-
```graphql
|
|
571
|
-
query GetInventoryQuantities(
|
|
572
|
-
$retailerId: ID!
|
|
573
|
-
$updatedAfter: DateTime!
|
|
574
|
-
$first: Int!
|
|
575
|
-
$after: String
|
|
576
|
-
) {
|
|
577
|
-
inventoryQuantities(
|
|
578
|
-
retailerId: $retailerId
|
|
579
|
-
updatedOn: { after: $updatedAfter }
|
|
580
|
-
first: $first
|
|
581
|
-
after: $after
|
|
582
|
-
) {
|
|
583
|
-
edges {
|
|
584
|
-
node {
|
|
585
|
-
id
|
|
586
|
-
ref
|
|
587
|
-
locationRef
|
|
588
|
-
articleRef
|
|
589
|
-
qty
|
|
590
|
-
type
|
|
591
|
-
condition
|
|
592
|
-
status
|
|
593
|
-
storageAreaRef
|
|
594
|
-
catalogue {
|
|
595
|
-
ref
|
|
596
|
-
name
|
|
597
|
-
}
|
|
598
|
-
expectedOn
|
|
599
|
-
createdOn
|
|
600
|
-
updatedOn
|
|
601
|
-
}
|
|
602
|
-
cursor
|
|
603
|
-
}
|
|
604
|
-
pageInfo {
|
|
605
|
-
hasNextPage
|
|
606
|
-
# Note: Fluent doesn't return endCursor - cursors are in edges[].cursor
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
---
|
|
613
|
-
|
|
614
|
-
## Versori Workflows Structure
|
|
615
|
-
|
|
616
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
617
|
-
|
|
618
|
-
**Trigger Types:**
|
|
619
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
620
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
621
|
-
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
622
|
-
|
|
623
|
-
**Execution Steps (chained to triggers):**
|
|
624
|
-
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
625
|
-
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
626
|
-
|
|
627
|
-
### Recommended Project Structure
|
|
628
|
-
|
|
629
|
-
This template demonstrates a modular project structure with separate files for better organization and maintainability:
|
|
630
|
-
|
|
631
|
-
```
|
|
632
|
-
inventory-quantities-extraction/
|
|
633
|
-
├── index.ts # Entry point - exports all workflows
|
|
634
|
-
└── src/
|
|
635
|
-
├── workflows/
|
|
636
|
-
│ ├── scheduled/
|
|
637
|
-
│ │ └── daily-inventory-quantities-extraction.ts # Scheduled: Daily extraction
|
|
638
|
-
│ │
|
|
639
|
-
│ └── webhook/
|
|
640
|
-
│ ├── adhoc-inventory-quantities-extraction.ts # Webhook: Manual trigger
|
|
641
|
-
│ └── job-status-check.ts # Webhook: Status query
|
|
642
|
-
│
|
|
643
|
-
├── services/
|
|
644
|
-
│ └── inventory-quantities-extraction.service.ts # Shared orchestration logic (reusable)
|
|
645
|
-
│
|
|
646
|
-
└── config/
|
|
647
|
-
└── inventory-quantities.export.json # Mapping configuration
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
### Entry Point: index.ts
|
|
651
|
-
|
|
652
|
-
**File:** `index.ts`
|
|
653
|
-
|
|
654
|
-
The entry point uses the **MemoryInterpreter pattern** to export all workflows for Versori:
|
|
655
|
-
|
|
656
|
-
```typescript
|
|
657
|
-
/**
|
|
658
|
-
* Entry point - Export all workflows for Versori platform
|
|
659
|
-
*
|
|
660
|
-
* VERSORI MEMORY INTERPRETER PATTERN:
|
|
661
|
-
* This file exports all workflows to be registered with Versori.
|
|
662
|
-
* Each workflow is defined in its own file for better organization.
|
|
663
|
-
*
|
|
664
|
-
* Import and re-export pattern allows Versori to discover and register
|
|
665
|
-
* all workflows without manual configuration.
|
|
666
|
-
*/
|
|
667
|
-
|
|
668
|
-
// Scheduled workflows
|
|
669
|
-
export { inventoryQuantitiesExtractionJson } from './src/workflows/scheduled/daily-inventory-quantities-extraction';
|
|
670
|
-
|
|
671
|
-
// Webhook workflows
|
|
672
|
-
export { inventoryQuantitiesAdHocExtraction } from './src/workflows/webhook/adhoc-inventory-quantities-extraction';
|
|
673
|
-
export { inventoryQuantitiesJobStatus } from './src/workflows/webhook/job-status-check';
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
**Key Points:**
|
|
677
|
-
- ✅ Simple re-export pattern - Versori auto-discovers workflows
|
|
678
|
-
- ✅ Each workflow in separate file for maintainability
|
|
679
|
-
- ✅ Clear naming: `export { workflowName } from './path/to/workflow'`
|
|
680
|
-
- ❌ No configuration needed - Versori reads exports directly
|
|
681
|
-
|
|
682
|
-
---
|
|
683
|
-
|
|
684
|
-
## Example Output
|
|
685
|
-
|
|
686
|
-
The extraction generates JSON files with this structure:
|
|
687
|
-
|
|
688
|
-
```json
|
|
689
|
-
{
|
|
690
|
-
"metadata": {
|
|
691
|
-
"extractedAt": "2025-01-22T14:30:00.000Z",
|
|
692
|
-
"recordCount": 2,
|
|
693
|
-
"incrementalFrom": "2025-01-22T10:00:00.000Z",
|
|
694
|
-
"incrementalTo": "2025-01-22T14:30:00.000Z"
|
|
695
|
-
},
|
|
696
|
-
"data": [
|
|
697
|
-
{
|
|
698
|
-
"ref": "QTY-001",
|
|
699
|
-
"location": "DC01",
|
|
700
|
-
"sku": "SKU-001",
|
|
701
|
-
"quantity": 150,
|
|
702
|
-
"type": "AVAILABLE",
|
|
703
|
-
"condition": "GOOD",
|
|
704
|
-
"status": "ACTIVE",
|
|
705
|
-
"storageArea": "ZONE-A",
|
|
706
|
-
"catalogueRef": "DEFAULT_CATALOGUE",
|
|
707
|
-
"catalogueName": "Default Inventory",
|
|
708
|
-
"expectedDate": null,
|
|
709
|
-
"createdOn": "2025-01-20T10:00:00Z",
|
|
710
|
-
"updatedOn": "2025-01-22T10:30:00Z"
|
|
711
|
-
},
|
|
712
|
-
{
|
|
713
|
-
"ref": "QTY-002",
|
|
714
|
-
"location": "DC02",
|
|
715
|
-
"sku": "SKU-002",
|
|
716
|
-
"quantity": 200,
|
|
717
|
-
"type": "RESERVED",
|
|
718
|
-
"condition": "GOOD",
|
|
719
|
-
"status": "ACTIVE",
|
|
720
|
-
"storageArea": "ZONE-B",
|
|
721
|
-
"catalogueRef": "DEFAULT_CATALOGUE",
|
|
722
|
-
"catalogueName": "Default Inventory",
|
|
723
|
-
"expectedDate": null,
|
|
724
|
-
"createdOn": "2025-01-21T11:00:00Z",
|
|
725
|
-
"updatedOn": "2025-01-22T11:15:00Z"
|
|
726
|
-
}
|
|
727
|
-
]
|
|
728
|
-
}
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
---
|
|
732
|
-
|
|
733
|
-
## Production Safety & Guardrails
|
|
734
|
-
|
|
735
|
-
### Overview
|
|
736
|
-
|
|
737
|
-
Even with **incremental-only** extraction, inventory quantities in JSON format need safeguards:
|
|
738
|
-
|
|
739
|
-
- **JSON parsing memory**: API consumers parse entire JSON before processing
|
|
740
|
-
- **Real-time APIs**: Inventory feeds power order promising, need fast processing
|
|
741
|
-
- **High-frequency updates**: Every 15 minutes accumulates large datasets
|
|
742
|
-
- **Nested structure**: JSON metadata wrapper adds overhead vs CSV
|
|
743
|
-
|
|
744
|
-
### Hard Limits
|
|
745
|
-
|
|
746
|
-
```typescript
|
|
747
|
-
const SAFETY_LIMITS = {
|
|
748
|
-
MAX_RECORDS_PER_RUN: 300000, // 300k quantity records per run
|
|
749
|
-
MAX_RECORDS_PER_FILE: 50000, // 50k per JSON file (lower than CSV)
|
|
750
|
-
MAX_FILE_SIZE_MB: 100, // 100MB per file
|
|
751
|
-
MAX_JSON_SIZE_MB: 200, // Total extraction size
|
|
752
|
-
CHUNK_SIZE: 10000, // Process in chunks
|
|
753
|
-
ESTIMATED_BYTES_PER_RECORD: 400, // JSON with metadata
|
|
754
|
-
};
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
**Why JSON has different limits than CSV?**
|
|
758
|
-
|
|
759
|
-
- JSON includes field names in every record (overhead)
|
|
760
|
-
- JSON metadata wrapper adds size
|
|
761
|
-
- JSON.parse() is memory-intensive
|
|
762
|
-
- API consumers need predictable payload sizes
|
|
763
|
-
|
|
764
|
-
---
|
|
765
|
-
|
|
766
|
-
## Complete Workflow Implementation
|
|
767
|
-
|
|
768
|
-
The code examples below demonstrate the implementation of each workflow component. In your project, these would be organized into separate files following the modular structure shown above.
|
|
769
|
-
|
|
770
|
-
### Workflow 1: Scheduled Extraction (Incremental)
|
|
771
|
-
|
|
772
|
-
**File:** `src/workflows/scheduled/daily-inventory-quantities-extraction.ts`
|
|
773
|
-
|
|
774
|
-
```typescript
|
|
775
|
-
import { schedule, http } from '@versori/run';
|
|
776
|
-
import { Buffer } from 'node:buffer';
|
|
777
|
-
import {
|
|
778
|
-
createClient,
|
|
779
|
-
UniversalMapper,
|
|
780
|
-
S3DataSource,
|
|
781
|
-
JSONBuilder,
|
|
782
|
-
VersoriKVAdapter,
|
|
783
|
-
ExtractionOrchestrator,
|
|
784
|
-
JobTracker,
|
|
785
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
786
|
-
import inventoryQuantitiesExportMapping from './config/inventory-quantities.export.json' with { type: 'json' };
|
|
787
|
-
|
|
788
|
-
// GraphQL query
|
|
789
|
-
const INVENTORY_QUANTITIES_QUERY = `
|
|
790
|
-
query GetInventoryQuantities(
|
|
791
|
-
$retailerId: ID!
|
|
792
|
-
$updatedAfter: DateTime!
|
|
793
|
-
$first: Int!
|
|
794
|
-
$after: String
|
|
795
|
-
) {
|
|
796
|
-
inventoryQuantities(
|
|
797
|
-
retailerId: $retailerId
|
|
798
|
-
updatedOn: { after: $updatedAfter }
|
|
799
|
-
first: $first
|
|
800
|
-
after: $after
|
|
801
|
-
) {
|
|
802
|
-
edges {
|
|
803
|
-
node {
|
|
804
|
-
id
|
|
805
|
-
ref
|
|
806
|
-
locationRef
|
|
807
|
-
articleRef
|
|
808
|
-
qty
|
|
809
|
-
type
|
|
810
|
-
condition
|
|
811
|
-
status
|
|
812
|
-
storageAreaRef
|
|
813
|
-
catalogue {
|
|
814
|
-
ref
|
|
815
|
-
name
|
|
816
|
-
}
|
|
817
|
-
expectedOn
|
|
818
|
-
createdOn
|
|
819
|
-
updatedOn
|
|
820
|
-
}
|
|
821
|
-
cursor
|
|
822
|
-
}
|
|
823
|
-
pageInfo {
|
|
824
|
-
hasNextPage
|
|
825
|
-
# Note: Fluent doesn't return endCursor - cursors are in edges[].cursor
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
`;
|
|
830
|
-
|
|
831
|
-
export const inventoryQuantitiesExtractionJson = schedule(
|
|
832
|
-
'inventory-quantities-extract-json-15min',
|
|
833
|
-
'*/15 * * * *',
|
|
834
|
-
http('extract-inventory-quantities-json', { connection: 'fluent_commerce', validateConnection: true }, async ctx => {
|
|
835
|
-
const { log, activation, openKv } = ctx;
|
|
836
|
-
const executionStartTime = Date.now();
|
|
837
|
-
|
|
838
|
-
// ========================================
|
|
839
|
-
// EXECUTION BOUNDARY: Workflow Start
|
|
840
|
-
// ========================================
|
|
841
|
-
log.info('🚀 [WORKFLOW] Inventory Quantities Extraction - Started', {
|
|
842
|
-
trigger: 'schedule',
|
|
843
|
-
schedule: '*/15 * * * *'
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
const retailerId = activation?.getVariable('retailerId');
|
|
847
|
-
const pageSize = parseInt(activation?.getVariable('pageSize') || '500', 10);
|
|
848
|
-
const maxRecords = parseInt(activation?.getVariable('maxRecords') || '20000', 10);
|
|
849
|
-
const fallbackStartDate =
|
|
850
|
-
activation?.getVariable('fallbackStartDate') || '2024-01-01T00:00:00Z';
|
|
851
|
-
const prettyPrint = activation?.getVariable('prettyPrint') === 'true';
|
|
852
|
-
const overlapBufferSeconds = parseInt(
|
|
853
|
-
activation?.getVariable('overlapBufferSeconds') || '60',
|
|
854
|
-
10
|
|
855
|
-
);
|
|
856
|
-
const OVERLAP_BUFFER_MS = overlapBufferSeconds * 1000;
|
|
857
|
-
|
|
858
|
-
const s3Config = {
|
|
859
|
-
bucket: activation?.getVariable('s3BucketName'),
|
|
860
|
-
region: activation?.getVariable('awsRegion') || 'us-east-1',
|
|
861
|
-
accessKeyId: activation?.getVariable('awsAccessKeyId'),
|
|
862
|
-
secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
|
|
863
|
-
};
|
|
864
|
-
const s3Prefix = activation?.getVariable('s3Prefix') || 'inventory/quantities/';
|
|
865
|
-
|
|
866
|
-
// Validate required variables
|
|
867
|
-
const missing: string[] = [];
|
|
868
|
-
if (!retailerId) missing.push('retailerId');
|
|
869
|
-
if (!s3Config.bucket) missing.push('s3BucketName');
|
|
870
|
-
if (!s3Config.accessKeyId) missing.push('awsAccessKeyId');
|
|
871
|
-
if (!s3Config.secretAccessKey) missing.push('awsSecretAccessKey');
|
|
872
|
-
if (missing.length) {
|
|
873
|
-
log.error('❌ [VALIDATION] Missing required activation variables', { missing });
|
|
874
|
-
return {
|
|
875
|
-
success: false,
|
|
876
|
-
error: `Missing required variables: ${missing.join(', ')}`,
|
|
877
|
-
recommendation: 'Configure these variables in the Activation Variables section of your Versori connector'
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
try {
|
|
882
|
-
// STEP 1/8: Initialize KV state and job tracking
|
|
883
|
-
log.info('⚙️ [STEP 1/8] Initializing KV state and job tracking');
|
|
884
|
-
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
885
|
-
const tracker = new JobTracker(kv, log);
|
|
886
|
-
const stateKey = ['extraction', 'inventory-quantities-json', 'lastRunTime'];
|
|
887
|
-
|
|
888
|
-
// STEP 2/8: Create extraction job
|
|
889
|
-
log.info('⚙️ [STEP 2/8] Creating extraction job');
|
|
890
|
-
const job = await tracker.createJob({
|
|
891
|
-
type: 'extraction',
|
|
892
|
-
entity: 'inventoryQuantities',
|
|
893
|
-
config: {
|
|
894
|
-
extractionMode: 'incremental',
|
|
895
|
-
retailerId,
|
|
896
|
-
s3Bucket: s3Config.bucket,
|
|
897
|
-
s3Prefix,
|
|
898
|
-
},
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
log.info('✅ [JOB] Extraction job created', { jobId: job.id });
|
|
902
|
-
|
|
903
|
-
// STEP 3/8: Load last successful extraction timestamp
|
|
904
|
-
log.info('⚙️ [STEP 3/8] Loading last extraction timestamp');
|
|
905
|
-
const lastRunState = await kv.get(stateKey);
|
|
906
|
-
const rawLastRunTime = lastRunState?.value?.timestamp || fallbackStartDate;
|
|
907
|
-
|
|
908
|
-
// Apply overlap buffer for query (safety window)
|
|
909
|
-
const bufferedLastRunTime = new Date(
|
|
910
|
-
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
|
|
911
|
-
).toISOString();
|
|
912
|
-
|
|
913
|
-
log.info('✅ [STATE] Incremental extraction window configured', {
|
|
914
|
-
jobId: job.id,
|
|
915
|
-
rawLastRunTime,
|
|
916
|
-
bufferedLastRunTime,
|
|
917
|
-
overlapBufferSeconds,
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
// STEP 4/8: Initialize Fluent client and orchestrator
|
|
921
|
-
log.info('⚙️ [STEP 4/8] Initializing Fluent client and orchestrator');
|
|
922
|
-
const client = await createClient(ctx);
|
|
923
|
-
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
924
|
-
|
|
925
|
-
// STEP 5/8: Execute GraphQL query with auto-pagination
|
|
926
|
-
log.info('🔍 [STEP 5/8] Executing GraphQL extraction', {
|
|
927
|
-
query: 'inventoryQuantities',
|
|
928
|
-
pageSize,
|
|
929
|
-
maxRecords,
|
|
930
|
-
dateRange: `from ${bufferedLastRunTime}`,
|
|
931
|
-
retailerId,
|
|
932
|
-
jobId: job.id
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
const extractionStartTime = Date.now();
|
|
936
|
-
const result = await orchestrator.extract({
|
|
937
|
-
query: INVENTORY_QUANTITIES_QUERY,
|
|
938
|
-
resultPath: 'inventoryQuantities.edges.node',
|
|
939
|
-
variables: {
|
|
940
|
-
retailerId,
|
|
941
|
-
updatedAfter: bufferedLastRunTime,
|
|
942
|
-
// SDK adds 'first' automatically based on pageSize
|
|
943
|
-
},
|
|
944
|
-
pageSize,
|
|
945
|
-
maxRecords,
|
|
946
|
-
});
|
|
947
|
-
const extractionDuration = Date.now() - extractionStartTime;
|
|
948
|
-
|
|
949
|
-
const edges = result.data || [];
|
|
950
|
-
|
|
951
|
-
log.info('✅ [EXTRACTION] GraphQL extraction completed', {
|
|
952
|
-
totalRecords: result.stats.totalRecords,
|
|
953
|
-
totalPages: result.stats.totalPages,
|
|
954
|
-
validRecords: result.stats.validRecords ?? edges.length,
|
|
955
|
-
failedValidations: result.stats.failedValidations,
|
|
956
|
-
truncated: result.stats.truncated,
|
|
957
|
-
truncationReason: result.stats.truncationReason,
|
|
958
|
-
durationMs: extractionDuration,
|
|
959
|
-
jobId: job.id
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
if (edges.length === 0) {
|
|
963
|
-
log.info('ℹ️ [RESULT] No new inventory quantity records to extract');
|
|
964
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
965
|
-
await tracker.markCompleted(job.id, {
|
|
966
|
-
recordsExtracted: 0,
|
|
967
|
-
message: 'No new records',
|
|
968
|
-
durationMs: totalDuration
|
|
969
|
-
});
|
|
970
|
-
await kv.set(stateKey, {
|
|
971
|
-
timestamp: new Date().toISOString(),
|
|
972
|
-
recordCount: 0,
|
|
973
|
-
extractedAt: new Date().toISOString(),
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
log.info('✅ [WORKFLOW] Inventory Quantities Extraction - Completed (No Records)', {
|
|
977
|
-
jobId: job.id,
|
|
978
|
-
durationMs: totalDuration
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
return {
|
|
982
|
-
success: true,
|
|
983
|
-
message: 'No new records to extract',
|
|
984
|
-
jobId: job.id,
|
|
985
|
-
rawLastRunTime,
|
|
986
|
-
durationMs: totalDuration
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
log.info('✅ [DATA] Inventory quantity records retrieved', { count: edges.length, jobId: job.id });
|
|
991
|
-
|
|
992
|
-
// STEP 6/8: Transform with UniversalMapper
|
|
993
|
-
log.info('⚙️ [STEP 6/8] Transforming records with UniversalMapper', { recordCount: edges.length });
|
|
994
|
-
const mappingStartTime = Date.now();
|
|
995
|
-
const mapper = new UniversalMapper(inventoryQuantitiesExportMapping);
|
|
996
|
-
const transformedRecords: any[] = [];
|
|
997
|
-
const errors: any[] = [];
|
|
998
|
-
|
|
999
|
-
for (const edge of edges) {
|
|
1000
|
-
const mapped = await mapper.map(edge.node);
|
|
1001
|
-
if (mapped.success) {
|
|
1002
|
-
transformedRecords.push(mapped.data);
|
|
1003
|
-
} else {
|
|
1004
|
-
errors.push({
|
|
1005
|
-
record: edge.node.id,
|
|
1006
|
-
errors: mapped.errors,
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
const mappingDuration = Date.now() - mappingStartTime;
|
|
1012
|
-
|
|
1013
|
-
if (transformedRecords.length === 0) {
|
|
1014
|
-
log.error('❌ [MAPPING] All records failed mapping validation', {
|
|
1015
|
-
totalRecords: edges.length,
|
|
1016
|
-
errorCount: errors.length,
|
|
1017
|
-
jobId: job.id
|
|
1018
|
-
});
|
|
1019
|
-
await tracker.markFailed(job.id, {
|
|
1020
|
-
error: 'All records failed mapping',
|
|
1021
|
-
errors,
|
|
1022
|
-
});
|
|
1023
|
-
return {
|
|
1024
|
-
success: false,
|
|
1025
|
-
error: 'All records failed mapping',
|
|
1026
|
-
jobId: job.id,
|
|
1027
|
-
errors,
|
|
1028
|
-
recommendation: 'Check mapping configuration in config/inventory-quantities.export.json and verify source data format'
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
log.info('✅ [MAPPING] Records transformed successfully', {
|
|
1033
|
-
successful: transformedRecords.length,
|
|
1034
|
-
failed: errors.length,
|
|
1035
|
-
durationMs: mappingDuration,
|
|
1036
|
-
jobId: job.id,
|
|
1037
|
-
});
|
|
1038
|
-
|
|
1039
|
-
// Calculate max updatedOn for next run (without buffer)
|
|
1040
|
-
const maxUpdatedOn = transformedRecords.reduce((max, record) => {
|
|
1041
|
-
const recordTime = new Date(record.updatedOn).getTime();
|
|
1042
|
-
return recordTime > max ? recordTime : max;
|
|
1043
|
-
}, new Date(rawLastRunTime).getTime());
|
|
1044
|
-
|
|
1045
|
-
const newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
1046
|
-
|
|
1047
|
-
// STEP 7/8: Build JSON with metadata and upload to S3
|
|
1048
|
-
log.info('⚙️ [STEP 7/8] Building JSON and uploading to S3');
|
|
1049
|
-
const jsonBuildStartTime = Date.now();
|
|
1050
|
-
const jsonOutput = {
|
|
1051
|
-
metadata: {
|
|
1052
|
-
extractedAt: new Date().toISOString(),
|
|
1053
|
-
recordCount: transformedRecords.length,
|
|
1054
|
-
incrementalFrom: rawLastRunTime,
|
|
1055
|
-
incrementalTo: newTimestamp,
|
|
1056
|
-
jobId: job.id,
|
|
1057
|
-
},
|
|
1058
|
-
data: transformedRecords,
|
|
1059
|
-
};
|
|
1060
|
-
|
|
1061
|
-
// Use JSONBuilder for consistent JSON generation
|
|
1062
|
-
const jsonBuilder = new JSONBuilder({
|
|
1063
|
-
prettyPrint,
|
|
1064
|
-
indent: 2,
|
|
1065
|
-
});
|
|
1066
|
-
const jsonContent = jsonBuilder.build(jsonOutput);
|
|
1067
|
-
const jsonBuildDuration = Date.now() - jsonBuildStartTime;
|
|
1068
|
-
|
|
1069
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1070
|
-
const fileName = `inventory-quantities-${timestamp}.json`;
|
|
1071
|
-
const s3Key = `${s3Prefix}${fileName}`;
|
|
1072
|
-
|
|
1073
|
-
log.info('✅ [JSON] JSON file generated', {
|
|
1074
|
-
fileName,
|
|
1075
|
-
sizeBytes: jsonContent.length,
|
|
1076
|
-
sizeMB: (jsonContent.length / (1024 * 1024)).toFixed(2),
|
|
1077
|
-
recordCount: transformedRecords.length,
|
|
1078
|
-
durationMs: jsonBuildDuration,
|
|
1079
|
-
jobId: job.id,
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
const s3UploadStartTime = Date.now();
|
|
1083
|
-
const s3 = new S3DataSource(
|
|
1084
|
-
{
|
|
1085
|
-
type: 'S3_JSON',
|
|
1086
|
-
connectionId: 's3-inventory-quantities-json-export',
|
|
1087
|
-
name: 'S3 Inventory Quantities JSON Export',
|
|
1088
|
-
s3Config,
|
|
1089
|
-
},
|
|
1090
|
-
log
|
|
1091
|
-
);
|
|
1092
|
-
|
|
1093
|
-
await s3.upload({
|
|
1094
|
-
key: s3Key,
|
|
1095
|
-
body: Buffer.from(jsonContent, 'utf8'),
|
|
1096
|
-
contentType: 'application/json',
|
|
1097
|
-
metadata: {
|
|
1098
|
-
recordCount: String(transformedRecords.length),
|
|
1099
|
-
extractedAt: new Date().toISOString(),
|
|
1100
|
-
incrementalFrom: rawLastRunTime,
|
|
1101
|
-
incrementalTo: newTimestamp,
|
|
1102
|
-
jobId: job.id,
|
|
1103
|
-
},
|
|
1104
|
-
});
|
|
1105
|
-
const s3UploadDuration = Date.now() - s3UploadStartTime;
|
|
1106
|
-
|
|
1107
|
-
log.info('✅ [S3] JSON file uploaded successfully', {
|
|
1108
|
-
s3Key,
|
|
1109
|
-
bucket: s3Config.bucket,
|
|
1110
|
-
durationMs: s3UploadDuration,
|
|
1111
|
-
jobId: job.id
|
|
1112
|
-
});
|
|
1113
|
-
|
|
1114
|
-
// STEP 8/8: Update state and complete job
|
|
1115
|
-
log.info('⚙️ [STEP 8/8] Updating state and completing job');
|
|
1116
|
-
await kv.set(stateKey, {
|
|
1117
|
-
timestamp: newTimestamp, // ← NO buffer applied
|
|
1118
|
-
recordCount: transformedRecords.length,
|
|
1119
|
-
extractedAt: new Date().toISOString(),
|
|
1120
|
-
overlapBufferSeconds,
|
|
1121
|
-
fileName,
|
|
1122
|
-
s3Key,
|
|
1123
|
-
jobId: job.id,
|
|
1124
|
-
errors: errors.length > 0 ? errors : undefined,
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
1128
|
-
await tracker.markCompleted(job.id, {
|
|
1129
|
-
recordsExtracted: transformedRecords.length,
|
|
1130
|
-
recordsFailed: errors.length,
|
|
1131
|
-
fileName,
|
|
1132
|
-
s3Key,
|
|
1133
|
-
newTimestamp,
|
|
1134
|
-
durationMs: totalDuration,
|
|
1135
|
-
performance: {
|
|
1136
|
-
extractionMs: extractionDuration,
|
|
1137
|
-
mappingMs: mappingDuration,
|
|
1138
|
-
jsonBuildMs: jsonBuildDuration,
|
|
1139
|
-
s3UploadMs: s3UploadDuration,
|
|
1140
|
-
totalMs: totalDuration
|
|
1141
|
-
}
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
// ========================================
|
|
1145
|
-
// EXECUTION BOUNDARY: Workflow Success
|
|
1146
|
-
// ========================================
|
|
1147
|
-
log.info('✅ [WORKFLOW] Inventory Quantities Extraction - Completed Successfully', {
|
|
1148
|
-
jobId: job.id,
|
|
1149
|
-
recordsExtracted: transformedRecords.length,
|
|
1150
|
-
recordsFailed: errors.length,
|
|
1151
|
-
fileName,
|
|
1152
|
-
s3Key,
|
|
1153
|
-
durationMs: totalDuration,
|
|
1154
|
-
performance: {
|
|
1155
|
-
extractionMs: extractionDuration,
|
|
1156
|
-
mappingMs: mappingDuration,
|
|
1157
|
-
jsonBuildMs: jsonBuildDuration,
|
|
1158
|
-
s3UploadMs: s3UploadDuration
|
|
1159
|
-
}
|
|
1160
|
-
});
|
|
1161
|
-
|
|
1162
|
-
return {
|
|
1163
|
-
success: true,
|
|
1164
|
-
jobId: job.id,
|
|
1165
|
-
recordsExtracted: transformedRecords.length,
|
|
1166
|
-
recordsFailed: errors.length,
|
|
1167
|
-
fileName,
|
|
1168
|
-
s3Key,
|
|
1169
|
-
rawLastRunTime,
|
|
1170
|
-
newTimestamp,
|
|
1171
|
-
durationMs: totalDuration,
|
|
1172
|
-
performance: {
|
|
1173
|
-
extractionMs: extractionDuration,
|
|
1174
|
-
mappingMs: mappingDuration,
|
|
1175
|
-
jsonBuildMs: jsonBuildDuration,
|
|
1176
|
-
s3UploadMs: s3UploadDuration,
|
|
1177
|
-
totalMs: totalDuration
|
|
1178
|
-
},
|
|
1179
|
-
errors: errors.length > 0 ? errors : undefined,
|
|
1180
|
-
};
|
|
1181
|
-
} catch (error: any) {
|
|
1182
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
1183
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1184
|
-
const errorType = error instanceof Error ? error.constructor.name : 'UnknownError';
|
|
1185
|
-
|
|
1186
|
-
// ========================================
|
|
1187
|
-
// EXECUTION BOUNDARY: Workflow Failure
|
|
1188
|
-
// ========================================
|
|
1189
|
-
log.error('❌ [WORKFLOW] Inventory Quantities Extraction - Failed', {
|
|
1190
|
-
error: errorMessage,
|
|
1191
|
-
errorType,
|
|
1192
|
-
durationMs: totalDuration,
|
|
1193
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
// Determine error recommendation based on error type
|
|
1197
|
-
let recommendation = 'Check logs for details and verify configuration';
|
|
1198
|
-
if (errorMessage.includes('authentication') || errorMessage.includes('credentials')) {
|
|
1199
|
-
recommendation = 'Verify Fluent Commerce and S3 credentials in Activation Variables';
|
|
1200
|
-
} else if (errorMessage.includes('network') || errorMessage.includes('timeout')) {
|
|
1201
|
-
recommendation = 'Check network connectivity and increase timeout settings if needed';
|
|
1202
|
-
} else if (errorMessage.includes('GraphQL') || errorMessage.includes('query')) {
|
|
1203
|
-
recommendation = 'Verify GraphQL query syntax and ensure retailerId has access to inventory data';
|
|
1204
|
-
} else if (errorMessage.includes('S3') || errorMessage.includes('upload')) {
|
|
1205
|
-
recommendation = 'Verify S3 bucket exists, credentials are valid, and bucket permissions allow uploads';
|
|
1206
|
-
} else if (errorMessage.includes('mapping') || errorMessage.includes('transform')) {
|
|
1207
|
-
recommendation = 'Check mapping configuration in config/inventory-quantities.export.json';
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
return {
|
|
1211
|
-
success: false,
|
|
1212
|
-
error: errorMessage,
|
|
1213
|
-
errorType,
|
|
1214
|
-
durationMs: totalDuration,
|
|
1215
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
1216
|
-
recommendation
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
|
-
}));
|
|
1220
|
-
```
|
|
1221
|
-
|
|
1222
|
-
### Workflow 2: Ad Hoc Extraction (HTTP Endpoint)
|
|
1223
|
-
|
|
1224
|
-
**File:** `src/workflows/webhook/adhoc-inventory-quantities-extraction.ts`
|
|
1225
|
-
|
|
1226
|
-
```typescript
|
|
1227
|
-
export const inventoryQuantitiesAdHocExtraction = webhook('inventory-quantities-adhoc-extract', {
|
|
1228
|
-
connection: 'inventory-quantities-adhoc',
|
|
1229
|
-
}).then(http('execute-adhoc-extraction', { connection: 'fluent_commerce', validateConnection: true }, async ctx => {
|
|
1230
|
-
const { log, openKv, activation } = ctx;
|
|
1231
|
-
const executionStartTime = Date.now();
|
|
1232
|
-
|
|
1233
|
-
log.info('🚀 [WEBHOOK] Ad Hoc Extraction - Started', { trigger: 'webhook' });
|
|
1234
|
-
|
|
1235
|
-
const { startDate, endDate, retailerId } = ctx.request.body;
|
|
1236
|
-
|
|
1237
|
-
if (!startDate || !endDate || !retailerId) {
|
|
1238
|
-
log.error('❌ [VALIDATION] Missing required webhook payload fields', {
|
|
1239
|
-
hasStartDate: !!startDate,
|
|
1240
|
-
hasEndDate: !!endDate,
|
|
1241
|
-
hasRetailerId: !!retailerId
|
|
1242
|
-
});
|
|
1243
|
-
return {
|
|
1244
|
-
success: false,
|
|
1245
|
-
error: 'Missing required fields: startDate, endDate, retailerId',
|
|
1246
|
-
recommendation: 'Provide all required fields in the webhook payload: { startDate, endDate, retailerId }'
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
try {
|
|
1251
|
-
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
1252
|
-
const tracker = new JobTracker(kv, log);
|
|
1253
|
-
|
|
1254
|
-
// Create ad hoc job
|
|
1255
|
-
log.info('⚙️ Creating ad hoc extraction job');
|
|
1256
|
-
const job = await tracker.createJob({
|
|
1257
|
-
type: 'extraction',
|
|
1258
|
-
entity: 'inventoryQuantities',
|
|
1259
|
-
config: {
|
|
1260
|
-
extractionMode: 'dateRange',
|
|
1261
|
-
retailerId,
|
|
1262
|
-
startDate,
|
|
1263
|
-
endDate,
|
|
1264
|
-
},
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
log.info('✅ [JOB] Ad hoc extraction job created', { jobId: job.id, startDate, endDate });
|
|
1268
|
-
|
|
1269
|
-
// Execute extraction (similar to scheduled workflow)
|
|
1270
|
-
// ... (extraction logic here - implement full extraction as in scheduled workflow)
|
|
1271
|
-
|
|
1272
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
1273
|
-
log.info('✅ [WEBHOOK] Ad Hoc Extraction - Completed', {
|
|
1274
|
-
jobId: job.id,
|
|
1275
|
-
durationMs: totalDuration
|
|
1276
|
-
});
|
|
1277
|
-
|
|
1278
|
-
return {
|
|
1279
|
-
success: true,
|
|
1280
|
-
jobId: job.id,
|
|
1281
|
-
message: 'Ad hoc extraction started',
|
|
1282
|
-
durationMs: totalDuration
|
|
1283
|
-
};
|
|
1284
|
-
} catch (error: any) {
|
|
1285
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
1286
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1287
|
-
|
|
1288
|
-
log.error('❌ [WEBHOOK] Ad Hoc Extraction - Failed', {
|
|
1289
|
-
error: errorMessage,
|
|
1290
|
-
durationMs: totalDuration
|
|
1291
|
-
});
|
|
1292
|
-
|
|
1293
|
-
return {
|
|
1294
|
-
success: false,
|
|
1295
|
-
error: errorMessage,
|
|
1296
|
-
durationMs: totalDuration,
|
|
1297
|
-
recommendation: 'Check logs for details and verify webhook payload format'
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
);
|
|
1302
|
-
```
|
|
1303
|
-
|
|
1304
|
-
### Workflow 3: Job Status Lookup
|
|
1305
|
-
|
|
1306
|
-
**File:** `src/workflows/webhook/job-status-check.ts`
|
|
1307
|
-
|
|
1308
|
-
```typescript
|
|
1309
|
-
export const inventoryQuantitiesJobStatus = webhook('inventory-quantities-job-status', {
|
|
1310
|
-
connection: 'inventory-quantities-job-status',
|
|
1311
|
-
}).then(
|
|
1312
|
-
http('query-job-status', { validateConnection: true }, async ctx => {
|
|
1313
|
-
const { log, openKv } = ctx;
|
|
1314
|
-
const executionStartTime = Date.now();
|
|
1315
|
-
|
|
1316
|
-
log.info('🚀 [WEBHOOK] Job Status Query - Started', { trigger: 'webhook' });
|
|
1317
|
-
|
|
1318
|
-
const { jobId } = ctx.request.body;
|
|
1319
|
-
|
|
1320
|
-
if (!jobId) {
|
|
1321
|
-
log.error('❌ [VALIDATION] Missing required field: jobId');
|
|
1322
|
-
return {
|
|
1323
|
-
success: false,
|
|
1324
|
-
error: 'Missing required field: jobId',
|
|
1325
|
-
recommendation: 'Provide jobId in webhook payload: { jobId: "your-job-id" }'
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
try {
|
|
1330
|
-
log.info('⚙️ Querying job status', { jobId });
|
|
1331
|
-
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
1332
|
-
const tracker = new JobTracker(kv, log);
|
|
1333
|
-
|
|
1334
|
-
const job = await tracker.getJob(jobId);
|
|
1335
|
-
|
|
1336
|
-
if (!job) {
|
|
1337
|
-
log.warn('⚠️ Job not found', { jobId });
|
|
1338
|
-
return {
|
|
1339
|
-
success: false,
|
|
1340
|
-
error: `Job not found: ${jobId}`,
|
|
1341
|
-
recommendation: 'Verify the jobId is correct and the job was created successfully'
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
1346
|
-
log.info('✅ [WEBHOOK] Job Status Query - Completed', {
|
|
1347
|
-
jobId,
|
|
1348
|
-
status: job.status,
|
|
1349
|
-
durationMs: totalDuration
|
|
1350
|
-
});
|
|
1351
|
-
|
|
1352
|
-
return {
|
|
1353
|
-
success: true,
|
|
1354
|
-
job: {
|
|
1355
|
-
id: job.id,
|
|
1356
|
-
type: job.type,
|
|
1357
|
-
entity: job.entity,
|
|
1358
|
-
status: job.status,
|
|
1359
|
-
createdAt: job.createdAt,
|
|
1360
|
-
completedAt: job.completedAt,
|
|
1361
|
-
result: job.result,
|
|
1362
|
-
error: job.error,
|
|
1363
|
-
},
|
|
1364
|
-
durationMs: totalDuration
|
|
1365
|
-
};
|
|
1366
|
-
} catch (error: any) {
|
|
1367
|
-
const totalDuration = Date.now() - executionStartTime;
|
|
1368
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1369
|
-
|
|
1370
|
-
log.error('❌ [WEBHOOK] Job Status Query - Failed', {
|
|
1371
|
-
error: errorMessage,
|
|
1372
|
-
durationMs: totalDuration
|
|
1373
|
-
});
|
|
1374
|
-
|
|
1375
|
-
return {
|
|
1376
|
-
success: false,
|
|
1377
|
-
error: errorMessage,
|
|
1378
|
-
durationMs: totalDuration,
|
|
1379
|
-
recommendation: 'Check logs for details and verify KV storage is accessible'
|
|
1380
|
-
};
|
|
1381
|
-
}
|
|
1382
|
-
})
|
|
1383
|
-
);
|
|
1384
|
-
```
|
|
1385
|
-
|
|
1386
|
-
---
|
|
1387
|
-
|
|
1388
|
-
## Key Differences from CSV
|
|
1389
|
-
|
|
1390
|
-
1. **JSON Structure with Metadata:**
|
|
1391
|
-
|
|
1392
|
-
```json
|
|
1393
|
-
{
|
|
1394
|
-
"metadata": { ... }, // ← Extraction context
|
|
1395
|
-
"data": [ ... ] // ← Actual records
|
|
1396
|
-
}
|
|
1397
|
-
```
|
|
1398
|
-
|
|
1399
|
-
2. **Pretty Printing Option:**
|
|
1400
|
-
- Set `prettyPrint: true` for human-readable JSON
|
|
1401
|
-
- Set `prettyPrint: false` for compact/production JSON
|
|
1402
|
-
|
|
1403
|
-
3. **Content Type:**
|
|
1404
|
-
- CSV: `text/csv`
|
|
1405
|
-
- JSON: `application/json`
|
|
1406
|
-
|
|
1407
|
-
4. **Field Names:**
|
|
1408
|
-
- CSV: `location_code` (snake_case for compatibility)
|
|
1409
|
-
- JSON: `location` (camelCase for APIs)
|
|
1410
|
-
|
|
1411
|
-
---
|
|
1412
|
-
|
|
1413
|
-
## Use Cases
|
|
1414
|
-
|
|
1415
|
-
**1. REST API Polling:**
|
|
1416
|
-
|
|
1417
|
-
```bash
|
|
1418
|
-
# External system polls S3 for latest file
|
|
1419
|
-
aws s3 ls s3://api-inventory-exports/inventory/quantities/ --recursive | sort | tail -n 1
|
|
1420
|
-
```
|
|
1421
|
-
|
|
1422
|
-
**2. S3 Event Trigger:**
|
|
1423
|
-
|
|
1424
|
-
```typescript
|
|
1425
|
-
// Lambda triggered on S3 PUT event
|
|
1426
|
-
export const handler = async (event: S3Event) => {
|
|
1427
|
-
const key = event.Records[0].s3.object.key;
|
|
1428
|
-
const data = await s3.getObject({ Bucket: bucket, Key: key });
|
|
1429
|
-
const json = JSON.parse(data.Body.toString());
|
|
1430
|
-
await processInventoryQuantities(json.data);
|
|
1431
|
-
};
|
|
1432
|
-
```
|
|
1433
|
-
|
|
1434
|
-
**3. API Gateway Integration:**
|
|
1435
|
-
|
|
1436
|
-
```typescript
|
|
1437
|
-
// Return latest extraction file via API
|
|
1438
|
-
// GET /api/inventory/quantities/latest
|
|
1439
|
-
export async function handler(event: APIGatewayEvent) {
|
|
1440
|
-
const latestFile = await getLatestS3File('inventory/quantities/');
|
|
1441
|
-
const data = await s3.getObject({ Key: latestFile });
|
|
1442
|
-
return {
|
|
1443
|
-
statusCode: 200,
|
|
1444
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1445
|
-
body: data.Body.toString(),
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
```
|
|
1449
|
-
|
|
1450
|
-
---
|
|
1451
|
-
|
|
1452
|
-
## Production Checklist
|
|
1453
|
-
|
|
1454
|
-
- [ ] Set appropriate extraction frequency (15min, hourly, daily)
|
|
1455
|
-
- [ ] Configure `maxRecords` based on expected change volume
|
|
1456
|
-
- [ ] Enable/disable pretty printing based on use case
|
|
1457
|
-
- [ ] Set up S3 lifecycle policy to archive old files
|
|
1458
|
-
- [ ] Document JSON schema for API consumers
|
|
1459
|
-
- [ ] Test with real-time incremental changes
|
|
1460
|
-
- [ ] Verify error handling for partial failures
|
|
1461
|
-
- [ ] Set up CloudFront CDN if serving via HTTP
|
|
1462
|
-
- [ ] Configure CORS if accessed from browser
|
|
1463
|
-
- [ ] Set up job tracking and monitoring
|
|
1464
|
-
- [ ] Test ad hoc extraction workflow
|
|
1465
|
-
- [ ] Validate job status lookup
|
|
1466
|
-
|
|
1467
|
-
---
|
|
1468
|
-
|
|
1469
|
-
**Pattern**: Incremental extraction with overlap buffer and JobTracker integration
|
|
1470
|
-
**Key Learning**: Include metadata for API consumers, use 3-workflow pattern
|
|
1471
|
-
**Use Case**: Real-time inventory quantity sync for external APIs with job tracking
|
|
1472
|
-
**Buffer Pattern**: Query WITH buffer (`updatedOn >= lastRunTime - 60s`), save WITHOUT buffer (`MAX(updatedOn)`)
|
|
1473
|
-
|
|
1474
|
-
---
|
|
1475
|
-
|
|
1476
|
-
### Pattern 8: Backward Pagination (Optional - Advanced)
|
|
1477
|
-
|
|
1478
|
-
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
1479
|
-
|
|
1480
|
-
**When to Use**:
|
|
1481
|
-
|
|
1482
|
-
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
1483
|
-
- ✅ Time-bounded reverse traversal for auditing
|
|
1484
|
-
- ✅ Display newest-first in UI/reports
|
|
1485
|
-
- ❌ **Don't use for standard incremental sync** - use forward pagination (default)
|
|
1486
|
-
|
|
1487
|
-
**GraphQL Query Requirements**:
|
|
1488
|
-
|
|
1489
|
-
Your query must support backward pagination by including `$last` and `$before`:
|
|
1490
|
-
|
|
1491
|
-
```graphql
|
|
1492
|
-
query GetData(
|
|
1493
|
-
$retailerId: ID!
|
|
1494
|
-
$first: Int # For forward pagination
|
|
1495
|
-
$after: String # For forward pagination
|
|
1496
|
-
$last: Int # For backward pagination
|
|
1497
|
-
$before: String # For backward pagination
|
|
1498
|
-
) {
|
|
1499
|
-
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
1500
|
-
edges {
|
|
1501
|
-
cursor # ✅ REQUIRED
|
|
1502
|
-
node {
|
|
1503
|
-
id
|
|
1504
|
-
createdAt
|
|
1505
|
-
# ... other fields
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
pageInfo {
|
|
1509
|
-
hasNextPage # For forward
|
|
1510
|
-
hasPreviousPage # ✅ REQUIRED for backward
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
```
|
|
1515
|
-
|
|
1516
|
-
**Implementation**:
|
|
1517
|
-
|
|
1518
|
-
```typescript
|
|
1519
|
-
// Backward pagination - newest records first
|
|
1520
|
-
const result = await orchestrator.extract({
|
|
1521
|
-
query: YOUR_QUERY,
|
|
1522
|
-
resultPath: 'data.edges.node',
|
|
1523
|
-
variables: {
|
|
1524
|
-
retailerId,
|
|
1525
|
-
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
1526
|
-
// ❌ Don't include last/before - orchestrator injects them
|
|
1527
|
-
},
|
|
1528
|
-
pageSize: 200,
|
|
1529
|
-
direction: 'backward', // ✅ Enable reverse pagination
|
|
1530
|
-
maxRecords: 10000,
|
|
1531
|
-
});
|
|
1532
|
-
|
|
1533
|
-
// Records are returned in reverse chronological order
|
|
1534
|
-
log.info('Newest record', { createdAt: result.data[0].createdAt });
|
|
1535
|
-
log.info('Oldest record', { createdAt: result.data[result.data.length - 1].createdAt });
|
|
1536
|
-
```
|
|
1537
|
-
|
|
1538
|
-
**Key Differences from Forward Pagination**:
|
|
1539
|
-
|
|
1540
|
-
| Aspect | Forward (Default) | Backward |
|
|
1541
|
-
| ---------------------- | -------------------------------- | ----------------------- |
|
|
1542
|
-
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
1543
|
-
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
1544
|
-
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
1545
|
-
| **Cursor Source** | Last edge of page | First edge of page |
|
|
1546
|
-
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
1547
|
-
|
|
1548
|
-
**Important Notes**:
|
|
1549
|
-
|
|
1550
|
-
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
1551
|
-
|
|
1552
|
-
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
1553
|
-
|
|
1554
|
-
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
1555
|
-
|
|
1556
|
-
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
1557
|
-
|
|
1558
|
-
**Example: Extract Latest 1000 Orders**
|
|
1559
|
-
|
|
1560
|
-
```typescript
|
|
1561
|
-
const latestOrders = await orchestrator.extract({
|
|
1562
|
-
query: ORDERS_QUERY,
|
|
1563
|
-
resultPath: 'orders.edges.node',
|
|
1564
|
-
variables: {
|
|
1565
|
-
retailerId,
|
|
1566
|
-
statuses: ['BOOKED', 'ALLOCATED'],
|
|
1567
|
-
},
|
|
1568
|
-
direction: 'backward', // Start from newest
|
|
1569
|
-
maxRecords: 1000, // Stop after 1000 records
|
|
1570
|
-
pageSize: 100, // 100 per page = 10 pages
|
|
1571
|
-
});
|
|
1572
|
-
|
|
1573
|
-
// latestOrders.data[0] is the newest order
|
|
1574
|
-
// latestOrders.data[999] is the 1000th newest order
|
|
1575
|
-
```
|
|
1576
|
-
|
|
1577
|
-
**When to Use Forward vs Backward**:
|
|
1578
|
-
|
|
1579
|
-
```typescript
|
|
1580
|
-
// ✅ Forward (default) - For incremental sync
|
|
1581
|
-
const incrementalData = await orchestrator.extract({
|
|
1582
|
-
query: YOUR_QUERY,
|
|
1583
|
-
resultPath: 'data.edges.node',
|
|
1584
|
-
variables: {
|
|
1585
|
-
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
1586
|
-
},
|
|
1587
|
-
// direction defaults to 'forward'
|
|
1588
|
-
// Processes oldest → newest for proper sequencing
|
|
1589
|
-
});
|
|
1590
|
-
|
|
1591
|
-
// ✅ Backward - For "latest N records" use cases
|
|
1592
|
-
const latestData = await orchestrator.extract({
|
|
1593
|
-
query: YOUR_QUERY,
|
|
1594
|
-
resultPath: 'data.edges.node',
|
|
1595
|
-
direction: 'backward',
|
|
1596
|
-
maxRecords: 100, // Just get latest 100
|
|
1597
|
-
// Gets newest → oldest
|
|
1598
|
-
});
|
|
1599
|
-
```
|
|
1600
|
-
|
|
1601
|
-
**Pagination Variables Reference**:
|
|
1602
|
-
|
|
1603
|
-
| Variable | Forward | Backward | Injected By | Notes |
|
|
1604
|
-
| -------- | ----------- | ----------- | ------------ | ------------------------ |
|
|
1605
|
-
| `first` | ✅ Used | ❌ Not used | Orchestrator | From `pageSize` |
|
|
1606
|
-
| `after` | ✅ Used | ❌ Not used | Orchestrator | From cursor (last edge) |
|
|
1607
|
-
| `last` | ❌ Not used | ✅ Used | Orchestrator | From `pageSize` |
|
|
1608
|
-
| `before` | ❌ Not used | ✅ Used | Orchestrator | From cursor (first edge) |
|
|
1609
|
-
|
|
1610
|
-
**Common Mistakes to Avoid**:
|
|
1611
|
-
|
|
1612
|
-
```typescript
|
|
1613
|
-
// ❌ WRONG - Don't pass pagination variables
|
|
1614
|
-
const result = await orchestrator.extract({
|
|
1615
|
-
variables: {
|
|
1616
|
-
last: 200, // ❌ Orchestrator will override this
|
|
1617
|
-
before: cursor, // ❌ Orchestrator manages cursor
|
|
1618
|
-
},
|
|
1619
|
-
direction: 'backward',
|
|
1620
|
-
});
|
|
1621
|
-
|
|
1622
|
-
// ✅ CORRECT - Let orchestrator inject pagination
|
|
1623
|
-
const result = await orchestrator.extract({
|
|
1624
|
-
variables: {
|
|
1625
|
-
retailerId, // ✅ Your business variables only
|
|
1626
|
-
},
|
|
1627
|
-
pageSize: 200, // ✅ Orchestrator uses this for last/before
|
|
1628
|
-
direction: 'backward',
|
|
1629
|
-
});
|
|
1630
|
-
```
|
|
1631
|
-
|
|
1632
|
-
#### Optional: Reverse Pagination
|
|
1633
|
-
|
|
1634
|
-
- Forward remains default. Reverse requires $last/$before and pageInfo.hasPreviousPage.
|
|
1635
|
-
|
|
1636
|
-
GraphQL:
|
|
1637
|
-
|
|
1638
|
-
```graphql
|
|
1639
|
-
query GetInventoryQuantitiesBackward($retailerId: ID!, $last: Int!, $before: String) {
|
|
1640
|
-
inventoryQuantities(retailerId: $retailerId, last: $last, before: $before) {
|
|
1641
|
-
edges {
|
|
1642
|
-
cursor
|
|
1643
|
-
node {
|
|
1644
|
-
id
|
|
1645
|
-
ref
|
|
1646
|
-
updatedOn
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
pageInfo {
|
|
1650
|
-
hasPreviousPage
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
```
|
|
1655
|
-
|
|
1656
|
-
SDK:
|
|
1657
|
-
|
|
1658
|
-
```typescript
|
|
1659
|
-
await orchestrator.extract({
|
|
1660
|
-
query: INVENTORY_QUANTITIES_BACKWARD_QUERY,
|
|
1661
|
-
resultPath: 'inventoryQuantities.edges.node',
|
|
1662
|
-
variables: { retailerId },
|
|
1663
|
-
pageSize,
|
|
1664
|
-
direction: 'backward',
|
|
1665
|
-
});
|
|
1666
|
-
```
|
|
1667
|
-
|
|
1668
|
-
---
|
|
1669
|
-
|
|
1670
|
-
## Testing Checklist
|
|
1671
|
-
|
|
1672
|
-
**Before production deployment:**
|
|
1673
|
-
|
|
1674
|
-
### 1. Schema Validation
|
|
1675
|
-
|
|
1676
|
-
- [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
|
|
1677
|
-
- [ ] Run `npx fc-connect validate-schema --mapping ./config/inventory-quantities.export.json --schema ./fluent-schema.json`
|
|
1678
|
-
- [ ] Run `npx fc-connect analyze-coverage --mapping ./config/inventory-quantities.export.json --schema ./fluent-schema.json`
|
|
1679
|
-
- [ ] Verify all `source` paths in mapping exist in GraphQL schema
|
|
1680
|
-
- [ ] Verify query structure matches schema (fields, types, filters)
|
|
1681
|
-
|
|
1682
|
-
### 2. Extraction Testing
|
|
1683
|
-
|
|
1684
|
-
- [ ] Test with small dataset first (maxRecords=10)
|
|
1685
|
-
- [ ] Verify ExtractionOrchestrator pagination works correctly
|
|
1686
|
-
- [ ] Test with multiple pages of data (verify cursor handling)
|
|
1687
|
-
- [ ] Verify date range filtering (updatedOn filter)
|
|
1688
|
-
- [ ] Test empty result handling (no records in date range)
|
|
1689
|
-
- [ ] Verify extraction stops at maxRecords limit
|
|
1690
|
-
|
|
1691
|
-
### 3. Mapping Testing
|
|
1692
|
-
|
|
1693
|
-
- [ ] Verify required fields are populated
|
|
1694
|
-
- [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
|
|
1695
|
-
- [ ] Test custom resolvers with edge cases (if any)
|
|
1696
|
-
- [ ] Verify nested field extraction
|
|
1697
|
-
- [ ] Test with null/missing fields
|
|
1698
|
-
- [ ] Verify mapping error collection works
|
|
1699
|
-
|
|
1700
|
-
### 4. JSON Generation Testing
|
|
1701
|
-
|
|
1702
|
-
- [ ] Verify JSON structure matches expected format
|
|
1703
|
-
- [ ] Test JSON validation against schema (if applicable)
|
|
1704
|
-
- [ ] Verify proper nesting and structure
|
|
1705
|
-
- [ ] Test with large datasets (>1000 records)
|
|
1706
|
-
- [ ] Verify UTF-8 encoding
|
|
1707
|
-
- [ ] Test special character escaping
|
|
1708
|
-
|
|
1709
|
-
### 5. S3 Upload Testing
|
|
1710
|
-
|
|
1711
|
-
- [ ] Test S3 connection and authentication
|
|
1712
|
-
- [ ] Verify file upload to correct bucket and path
|
|
1713
|
-
- [ ] Test file naming convention (timestamp format)
|
|
1714
|
-
- [ ] Verify S3 object metadata
|
|
1715
|
-
- [ ] Test upload retry logic (simulate network failure)
|
|
1716
|
-
- [ ] Verify file permissions and ACLs
|
|
1717
|
-
|
|
1718
|
-
### 6. State Management Testing
|
|
1719
|
-
|
|
1720
|
-
- [ ] Verify overlap buffer prevents missed records (60-second default)
|
|
1721
|
-
- [ ] Test state recovery after extraction failure
|
|
1722
|
-
- [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
|
|
1723
|
-
- [ ] Test first run with no previous state (uses fallbackStartDate)
|
|
1724
|
-
- [ ] Verify state update only happens on successful upload
|
|
1725
|
-
- [ ] Test manual date override (doesn't update state)
|
|
1726
|
-
|
|
1727
|
-
### 7. Job Tracking Testing
|
|
1728
|
-
|
|
1729
|
-
- [ ] Test job creation with JobTracker
|
|
1730
|
-
- [ ] Verify job status updates at each stage
|
|
1731
|
-
- [ ] Test job completion with metadata
|
|
1732
|
-
- [ ] Test job failure handling
|
|
1733
|
-
- [ ] Query job status via webhook endpoint
|
|
1734
|
-
- [ ] Verify job status persists in KV store
|
|
1735
|
-
|
|
1736
|
-
### 8. Error Handling Testing
|
|
1737
|
-
|
|
1738
|
-
- [ ] Test with invalid GraphQL query
|
|
1739
|
-
- [ ] Test with mapping errors (invalid field paths)
|
|
1740
|
-
- [ ] Test with S3 connection failures
|
|
1741
|
-
- [ ] Test with authentication failures
|
|
1742
|
-
- [ ] Test with network timeouts
|
|
1743
|
-
- [ ] Verify error logging includes context (jobId, stage, error details)
|
|
1744
|
-
- [ ] Test error threshold logic (if applicable)
|
|
1745
|
-
|
|
1746
|
-
### 9. Staging Environment Testing
|
|
1747
|
-
|
|
1748
|
-
- [ ] Run full extraction in staging environment
|
|
1749
|
-
- [ ] Verify JSON file format with downstream system
|
|
1750
|
-
- [ ] Monitor extraction duration and resource usage
|
|
1751
|
-
- [ ] Test with production-like data volumes
|
|
1752
|
-
- [ ] Verify no performance degradation over time
|
|
1753
|
-
|
|
1754
|
-
### 10. Integration Testing
|
|
1755
|
-
|
|
1756
|
-
- [ ] Test scheduled workflow (cron trigger)
|
|
1757
|
-
- [ ] Test ad hoc webhook trigger
|
|
1758
|
-
- [ ] Test job status query webhook
|
|
1759
|
-
- [ ] Verify activation variables are read correctly
|
|
1760
|
-
- [ ] Test with different extraction modes (incremental, date range)
|
|
1761
|
-
- [ ] End-to-end test: trigger → extract → transform → upload → verify file
|
|
1762
|
-
|
|
1763
|
-
---
|
|
1764
|
-
## Monitoring & Alerting
|
|
1765
|
-
|
|
1766
|
-
### Success Response Example
|
|
1767
|
-
|
|
1768
|
-
```json
|
|
1769
|
-
{
|
|
1770
|
-
"success": true,
|
|
1771
|
-
"jobId": "SCHEDULED_IQ_20251102_140000_abc123",
|
|
1772
|
-
"recordsExtracted": 1523,
|
|
1773
|
-
"fileName": "inventory-quantities-2025-11-02T14-00-00-000Z.json",
|
|
1774
|
-
"s3Path": "s3://bucket/inventory-quantities/inventory-quantities-2025-11-02T14-00-00-000Z.json",
|
|
1775
|
-
"metrics": {
|
|
1776
|
-
"extractionDurationMs": 12543,
|
|
1777
|
-
"totalPages": 8,
|
|
1778
|
-
"pageSize": 200,
|
|
1779
|
-
"mappingErrors": 0,
|
|
1780
|
-
"fileSizeBytes": 524288,
|
|
1781
|
-
"uploadDurationMs": 1234
|
|
1782
|
-
},
|
|
1783
|
-
"timestamps": {
|
|
1784
|
-
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
1785
|
-
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
1786
|
-
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
1787
|
-
},
|
|
1788
|
-
"state": {
|
|
1789
|
-
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
1790
|
-
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
1791
|
-
"stateUpdated": true,
|
|
1792
|
-
"overlapBufferSeconds": 60
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
```
|
|
1796
|
-
|
|
1797
|
-
### Error Response Example
|
|
1798
|
-
|
|
1799
|
-
```json
|
|
1800
|
-
{
|
|
1801
|
-
"success": false,
|
|
1802
|
-
"jobId": "ADHOC_IQ_20251102_140500_xyz789",
|
|
1803
|
-
"error": "S3 upload failed: Connection timeout",
|
|
1804
|
-
"errorCategory": "NETWORK",
|
|
1805
|
-
"recordsExtracted": 0,
|
|
1806
|
-
"stage": "s3_upload",
|
|
1807
|
-
"details": {
|
|
1808
|
-
"message": "Failed to upload file after 3 retry attempts",
|
|
1809
|
-
"retryAttempts": 3,
|
|
1810
|
-
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
1811
|
-
},
|
|
1812
|
-
"state": {
|
|
1813
|
-
"stateUpdated": false,
|
|
1814
|
-
"willRetryNextRun": true,
|
|
1815
|
-
"note": "State not advanced - next extraction will retry same time window"
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
```
|
|
1819
|
-
|
|
1820
|
-
### Key Metrics to Track
|
|
1821
|
-
|
|
1822
|
-
```typescript
|
|
1823
|
-
const METRICS = {
|
|
1824
|
-
// Extraction Performance
|
|
1825
|
-
extractionDurationMs: Date.now() - extractionStart,
|
|
1826
|
-
recordCount: records.length,
|
|
1827
|
-
pageCount: extractionResult.stats.totalPages,
|
|
1828
|
-
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
1829
|
-
|
|
1830
|
-
// Transformation Performance
|
|
1831
|
-
transformedCount: transformedRecords.length,
|
|
1832
|
-
failedCount: mappingErrors.length,
|
|
1833
|
-
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
1834
|
-
|
|
1835
|
-
// File Generation
|
|
1836
|
-
fileSizeMB: (jsonContent.length / (1024 * 1024)).toFixed(2),
|
|
1837
|
-
|
|
1838
|
-
// Upload Performance
|
|
1839
|
-
uploadDurationMs: uploadEnd - uploadStart,
|
|
1840
|
-
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
1841
|
-
|
|
1842
|
-
// State Management
|
|
1843
|
-
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
1844
|
-
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
1845
|
-
};
|
|
1846
|
-
|
|
1847
|
-
log.info('Extraction metrics', metrics);
|
|
1848
|
-
```
|
|
1849
|
-
|
|
1850
|
-
### Alert Thresholds
|
|
1851
|
-
|
|
1852
|
-
```typescript
|
|
1853
|
-
const ALERT_THRESHOLDS = {
|
|
1854
|
-
// Duration Alerts
|
|
1855
|
-
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
1856
|
-
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
1857
|
-
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
1858
|
-
|
|
1859
|
-
// Error Rate Alerts
|
|
1860
|
-
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
1861
|
-
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
1862
|
-
|
|
1863
|
-
// Volume Alerts
|
|
1864
|
-
MAX_RECORDS_PER_RUN: 100000,
|
|
1865
|
-
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
1866
|
-
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
1867
|
-
|
|
1868
|
-
// State Alerts
|
|
1869
|
-
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
1870
|
-
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
1871
|
-
};
|
|
1872
|
-
|
|
1873
|
-
// Check thresholds
|
|
1874
|
-
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
1875
|
-
log.warn('Extraction duration exceeded threshold', {
|
|
1876
|
-
duration: metrics.extractionDurationMs,
|
|
1877
|
-
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
1878
|
-
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
1879
|
-
});
|
|
1880
|
-
}
|
|
1881
|
-
```
|
|
1882
|
-
|
|
1883
|
-
### Monitoring Dashboard Queries
|
|
1884
|
-
|
|
1885
|
-
**Versori Platform Logs Query:**
|
|
1886
|
-
|
|
1887
|
-
```
|
|
1888
|
-
# Successful extractions
|
|
1889
|
-
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
1890
|
-
|
|
1891
|
-
# Failed extractions
|
|
1892
|
-
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
1893
|
-
|
|
1894
|
-
# Performance issues
|
|
1895
|
-
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
1896
|
-
|
|
1897
|
-
# High error rates
|
|
1898
|
-
errorRate:>5
|
|
1899
|
-
|
|
1900
|
-
# State management issues
|
|
1901
|
-
stateUpdated:false AND success:true
|
|
1902
|
-
```
|
|
1903
|
-
|
|
1904
|
-
### Common Issues and Solutions
|
|
1905
|
-
|
|
1906
|
-
**Issue**: "Extraction timeout after 10 minutes"
|
|
1907
|
-
|
|
1908
|
-
- **Cause**: Too many records in single extraction
|
|
1909
|
-
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
1910
|
-
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
1911
|
-
|
|
1912
|
-
**Issue**: "Mapping errors for 50% of records"
|
|
1913
|
-
|
|
1914
|
-
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
1915
|
-
- **Fix**: Run schema validation, update mapping config paths
|
|
1916
|
-
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
1917
|
-
|
|
1918
|
-
**Issue**: "S3 connection timeout"
|
|
1919
|
-
|
|
1920
|
-
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
1921
|
-
- **Fix**: Check S3 credentials, verify network connectivity
|
|
1922
|
-
- **Prevention**: Implement connection health checks, monitor connection status
|
|
1923
|
-
|
|
1924
|
-
**Issue**: "State not updating after successful extraction"
|
|
1925
|
-
|
|
1926
|
-
- **Cause**: KV write failure or intentional retry logic
|
|
1927
|
-
- **Fix**: Check KV logs, verify state update code executed
|
|
1928
|
-
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
1929
|
-
|
|
1930
|
-
**Issue**: "First run exceeds record limits"
|
|
1931
|
-
|
|
1932
|
-
- **Cause**: No previous timestamp, fetches all historical records
|
|
1933
|
-
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
1934
|
-
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
1935
|
-
|
|
1936
|
-
**Issue**: "Excessive duplicate records in output"
|
|
1937
|
-
|
|
1938
|
-
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
1939
|
-
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
1940
|
-
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
1941
|
-
|
|
1942
|
-
---
|
|
1943
|
-
|
|
1944
|
-
## Troubleshooting Quick Reference
|
|
1945
|
-
|
|
1946
|
-
| Error Message | Likely Cause | Solution |
|
|
1947
|
-
|--------------|--------------|----------|
|
|
1948
|
-
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
1949
|
-
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
1950
|
-
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
1951
|
-
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
1952
|
-
| "S3 authentication failed" | Invalid credentials | Verify S3 credentials in activation variables |
|
|
1953
|
-
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
1954
|
-
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
1955
|
-
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
1956
|
-
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
1957
|
-
| "JSON generation failed" | Format-specific error | Check JSON generation logic, validate output |
|
|
1958
|
-
|
|
1959
|
-
---
|
|
1
|
+
---
|
|
2
|
+
template_id: tpl-extract-inventory-quantities-graphql-to-s3-json
|
|
3
|
+
canonical_filename: template-extraction-inventory-quantities-to-s3-json.md
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
sdk_version: ^0.1.39
|
|
6
|
+
runtime: versori
|
|
7
|
+
direction: extraction
|
|
8
|
+
source: fluent-graphql
|
|
9
|
+
destination: s3-json
|
|
10
|
+
entity: inventory-quantities
|
|
11
|
+
format: json
|
|
12
|
+
logging: versori
|
|
13
|
+
status: stable
|
|
14
|
+
features:
|
|
15
|
+
- memory-management
|
|
16
|
+
- enhanced-logging
|
|
17
|
+
- pagination-progress
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Template: Extraction - Inventory Quantities GraphQL to S3 JSON
|
|
21
|
+
|
|
22
|
+
**Template Version:** 2.0.0
|
|
23
|
+
**SDK Version:** @fluentcommerce/fc-connect-sdk@^0.1.39
|
|
24
|
+
**Last Updated:** 2025-01-24
|
|
25
|
+
**Deployment Target:** Versori Platform
|
|
26
|
+
|
|
27
|
+
**🆕 Version 2.0.0 Enhancements:**
|
|
28
|
+
- ✅ **Memory Management** - Clear large result sets after processing batches
|
|
29
|
+
- ✅ **Enhanced Logging** - Pagination progress tracking with emoji indicators (📊, 📥, ✅)
|
|
30
|
+
- ✅ **Pagination Progress** - Real-time page-by-page progress logging with metrics
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📚 STEP 1: Load These Docs (Human Checklist)
|
|
35
|
+
|
|
36
|
+
1. REQUIRED (load all)
|
|
37
|
+
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
38
|
+
- [ ] fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
39
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
40
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
41
|
+
- [ ] fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
42
|
+
- [ ] fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
43
|
+
|
|
44
|
+
Copy-paste list (open these):
|
|
45
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/api-reference/
|
|
46
|
+
fc-connect-sdk/docs/02-CORE-GUIDES/mapping/
|
|
47
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/data-sources/
|
|
48
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/parsers/
|
|
49
|
+
fc-connect-sdk/docs/03-PATTERN-GUIDES/extraction/
|
|
50
|
+
fc-connect-sdk/docs/04-REFERENCE/platforms/versori/
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 📋 STEP 2: Tell Your AI (Prompt)
|
|
55
|
+
|
|
56
|
+
Copy/paste the standardized prompt from `docs/template-loading-matrix.md#prompts`.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 💻 STEP 3: Implementation (Verified Imports)
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { Buffer } from 'node:buffer';
|
|
64
|
+
import {
|
|
65
|
+
createClient,
|
|
66
|
+
UniversalMapper,
|
|
67
|
+
S3DataSource,
|
|
68
|
+
JSONBuilder,
|
|
69
|
+
VersoriKVAdapter,
|
|
70
|
+
ExtractionOrchestrator,
|
|
71
|
+
JobTracker,
|
|
72
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
These are the only SDK imports required for this template. Keep type-only imports out of code samples. Prefer the orchestrator-based flow shown below for pagination and stats.
|
|
76
|
+
|
|
77
|
+
Note on ad hoc runs: Use `fromDate`/`toDate` in webhook payloads. Default `updateState` is `false` for ad hoc; set to `true` only when you want the run to advance the saved timestamp used by scheduled runs.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# Versori Scheduled: Inventory Quantities Extraction to S3 JSON (Detailed Records)
|
|
82
|
+
|
|
83
|
+
**FC Connect SDK Use Case Guide**
|
|
84
|
+
|
|
85
|
+
> SDK: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
86
|
+
> Version: ^0.1.39
|
|
87
|
+
|
|
88
|
+
Context: Scheduled Versori workflow that extracts inventory quantities (detailed quantity records) from Fluent Commerce via GraphQL query with **incremental timestamp tracking**, transforms with `UniversalMapper`, and writes JSON files to S3 for API consumption and audit trail systems.
|
|
89
|
+
|
|
90
|
+
**Pattern**: EXTRACTION (Fluent → S3 JSON)
|
|
91
|
+
**Complexity**: Medium | Runtime: Versori Platform (Scheduled)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## ⚠️ IMPORTANT: Sample Code for SDK Demonstration Only
|
|
96
|
+
|
|
97
|
+
> **🔴 PRODUCTION RECOMMENDATION**
|
|
98
|
+
>
|
|
99
|
+
> This guide demonstrates FC Connect SDK capabilities for **extraction and mapping workflows**. This is a **Versori sample connector** showing how to build inventory quantity extraction workflows using the SDK.
|
|
100
|
+
>
|
|
101
|
+
> **✅ FOR PRODUCTION IMPLEMENTATIONS:**
|
|
102
|
+
>
|
|
103
|
+
> - **ONLY use INCREMENTAL mode with scheduled runs** (e.g., every 15 minutes for real-time)
|
|
104
|
+
> - Incremental mode is safe, efficient, and production-ready
|
|
105
|
+
> - Uses overlap buffer to prevent missed records
|
|
106
|
+
> - Natural rate limiting via timestamps
|
|
107
|
+
> - JSON format ideal for API consumption
|
|
108
|
+
>
|
|
109
|
+
> **🎯 RECOMMENDED SCHEDULE:**
|
|
110
|
+
>
|
|
111
|
+
> - **Every 15 minutes** for real-time inventory APIs
|
|
112
|
+
> - **Hourly** for standard inventory feeds
|
|
113
|
+
> - **Daily** for analytics/reporting systems
|
|
114
|
+
>
|
|
115
|
+
> **⚠️ NOTE:** This sample shows incremental mode implementation. For your production Versori connector, adapt this pattern with proper error handling, monitoring, and testing for your specific use case.
|
|
116
|
+
>
|
|
117
|
+
> **This is a reference implementation showing HOW to use the SDK - adapt for your needs.**
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## What You'll Build
|
|
122
|
+
|
|
123
|
+
- **Three workflows**: Scheduled extraction, ad hoc extraction, job status lookup
|
|
124
|
+
- **Incremental extraction** using `updatedOn > lastRunTime` filter
|
|
125
|
+
- **State management** with VersoriKVAdapter to track last successful run
|
|
126
|
+
- **JobTracker integration** for KV-backed job tracking
|
|
127
|
+
- GraphQL query with auto-pagination
|
|
128
|
+
- UniversalMapper transformation for export schema
|
|
129
|
+
- JSON file generation with proper structure
|
|
130
|
+
- S3 upload to target bucket
|
|
131
|
+
- **Failure recovery** - maintains last successful timestamp on errors
|
|
132
|
+
|
|
133
|
+
## Business Use Case
|
|
134
|
+
|
|
135
|
+
**Real-time inventory API for external systems:**
|
|
136
|
+
|
|
137
|
+
- Extract inventory quantity changes every 15 minutes
|
|
138
|
+
- Export as JSON for REST API consumption
|
|
139
|
+
- Support for webhooks/polling by downstream systems
|
|
140
|
+
- Lightweight incremental updates
|
|
141
|
+
- Detailed audit trail with quantity types (AVAILABLE, RESERVED, EXPECTED, etc.)
|
|
142
|
+
- Feed to order management and ecommerce platforms
|
|
143
|
+
|
|
144
|
+
## Inventory Quantities Explained
|
|
145
|
+
|
|
146
|
+
**InventoryQuantity** = Detailed quantity record with type breakdown
|
|
147
|
+
|
|
148
|
+
- Individual quantity records by type (AVAILABLE, RESERVED, EXPECTED, etc.)
|
|
149
|
+
- SKU + Location + Type combination
|
|
150
|
+
- Retailer-defined types for specific tracking needs
|
|
151
|
+
- Used for: Audit trails, detailed inventory tracking, compliance
|
|
152
|
+
|
|
153
|
+
**vs InventoryPosition** = Physical on-hand calculation
|
|
154
|
+
|
|
155
|
+
- Aggregated stock in warehouse
|
|
156
|
+
- Used for: Stock reporting
|
|
157
|
+
|
|
158
|
+
**vs VirtualPosition** = ATP (Available To Promise) calculation
|
|
159
|
+
|
|
160
|
+
- Calculated quantity available for orders
|
|
161
|
+
- Used for: Order promising
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## SDK Methods Used
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { Buffer } from 'node:buffer';
|
|
169
|
+
import {
|
|
170
|
+
createClient,
|
|
171
|
+
UniversalMapper,
|
|
172
|
+
S3DataSource,
|
|
173
|
+
JSONBuilder,
|
|
174
|
+
VersoriKVAdapter,
|
|
175
|
+
ExtractionOrchestrator,
|
|
176
|
+
JobTracker,
|
|
177
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
178
|
+
|
|
179
|
+
// STEP 1: Create client
|
|
180
|
+
const client = await createClient(ctx);
|
|
181
|
+
|
|
182
|
+
// STEP 2: Setup job tracking
|
|
183
|
+
const kv = new VersoriKVAdapter(ctx.openKv(':project:'));
|
|
184
|
+
const tracker = new JobTracker(kv, log);
|
|
185
|
+
|
|
186
|
+
// STEP 3: Create extraction job
|
|
187
|
+
const job = await tracker.createJob({
|
|
188
|
+
type: 'extraction',
|
|
189
|
+
entity: 'inventoryQuantities',
|
|
190
|
+
config: { extractionMode: 'incremental' },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// STEP 4: Initialize orchestrator
|
|
194
|
+
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
195
|
+
|
|
196
|
+
// STEP 5: Execute extraction with auto-pagination
|
|
197
|
+
const result = await orchestrator.extract({
|
|
198
|
+
query: INVENTORY_QUANTITIES_QUERY,
|
|
199
|
+
resultPath: 'inventoryQuantities.edges.node',
|
|
200
|
+
variables: { retailerId, updatedAfter },
|
|
201
|
+
maxRecords: 20000,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// STEP 6: Transform with UniversalMapper
|
|
205
|
+
const mapper = new UniversalMapper(exportMapping);
|
|
206
|
+
const transformed = result.data.map(r => mapper.map(r));
|
|
207
|
+
|
|
208
|
+
// STEP 7: Build JSON and upload to S3
|
|
209
|
+
const jsonBuilder = new JSONBuilder({
|
|
210
|
+
prettyPrint: true,
|
|
211
|
+
indent: 2,
|
|
212
|
+
});
|
|
213
|
+
const jsonOutput = {
|
|
214
|
+
metadata: { extractedAt: new Date().toISOString(), recordCount: transformed.length },
|
|
215
|
+
data: transformed,
|
|
216
|
+
};
|
|
217
|
+
const jsonContent = jsonBuilder.build(jsonOutput);
|
|
218
|
+
const s3 = new S3DataSource(config, log);
|
|
219
|
+
await s3.upload({
|
|
220
|
+
key: s3Key,
|
|
221
|
+
body: Buffer.from(jsonContent, 'utf8'),
|
|
222
|
+
contentType: 'application/json'
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// STEP 8: Update job status
|
|
226
|
+
await tracker.markCompleted(job.id, { recordsExtracted: transformed.length, s3Key });
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Activation Variables
|
|
232
|
+
|
|
233
|
+
Configure these variables in your Versori connector's Activation Variables section:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"retailerId": "your-retailer-id",
|
|
238
|
+
"s3BucketName": "api-inventory-exports",
|
|
239
|
+
"awsAccessKeyId": "AKIAXXXXXXXXXXXX",
|
|
240
|
+
"awsSecretAccessKey": "********",
|
|
241
|
+
"awsRegion": "us-east-1",
|
|
242
|
+
"s3Prefix": "inventory/quantities/",
|
|
243
|
+
"pageSize": 500,
|
|
244
|
+
"maxRecords": 20000,
|
|
245
|
+
"fallbackStartDate": "2024-01-01T00:00:00Z",
|
|
246
|
+
"overlapBufferSeconds": "60",
|
|
247
|
+
"prettyPrint": "true"
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Variable Descriptions:**
|
|
252
|
+
|
|
253
|
+
| Variable | Required | Description | Default | Example |
|
|
254
|
+
|----------|----------|-------------|---------|---------|
|
|
255
|
+
| `retailerId` | ✅ Yes | Fluent Commerce retailer ID for GraphQL queries | - | `"ACME_CORP"` |
|
|
256
|
+
| `s3BucketName` | ✅ Yes | Target S3 bucket for JSON exports | - | `"api-inventory-exports"` |
|
|
257
|
+
| `awsAccessKeyId` | ✅ Yes | AWS access key ID for S3 uploads | - | `"AKIAIOSFODNN7EXAMPLE"` |
|
|
258
|
+
| `awsSecretAccessKey` | ✅ Yes | AWS secret access key for S3 uploads | - | `"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"` |
|
|
259
|
+
| `awsRegion` | No | AWS region for S3 bucket | `"us-east-1"` | `"us-west-2"` |
|
|
260
|
+
| `s3Prefix` | No | S3 key prefix for file organization | `"inventory/quantities/"` | `"exports/inventory/"` |
|
|
261
|
+
| `pageSize` | No | GraphQL pagination page size | `500` | `200` |
|
|
262
|
+
| `maxRecords` | No | Maximum records per extraction run | `20000` | `50000` |
|
|
263
|
+
| `fallbackStartDate` | No | Initial timestamp for first run | `"2024-01-01T00:00:00Z"` | `"2025-01-01T00:00:00Z"` |
|
|
264
|
+
| `overlapBufferSeconds` | No | Safety buffer to prevent missed records (seconds) | `60` | `120` |
|
|
265
|
+
| `prettyPrint` | No | Pretty-print JSON output (`"true"` or `"false"`) | `"true"` | `"false"` |
|
|
266
|
+
|
|
267
|
+
**🔒 Security Notes:**
|
|
268
|
+
- Store credentials securely in Versori's encrypted variable storage
|
|
269
|
+
- Never commit credentials to source control
|
|
270
|
+
- Rotate AWS keys regularly
|
|
271
|
+
- Use IAM roles with minimal required permissions
|
|
272
|
+
|
|
273
|
+
**⚙️ Performance Tuning:**
|
|
274
|
+
- **pageSize**: Lower values (100-200) for slower networks, higher values (500-1000) for faster connections
|
|
275
|
+
- **maxRecords**: Adjust based on expected change volume - 20K is safe for 15-minute incremental runs
|
|
276
|
+
- **overlapBufferSeconds**: Increase if you see missed records, decrease if seeing too many duplicates
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Export Mapping Configuration
|
|
281
|
+
|
|
282
|
+
Create file: `./config/inventory-quantities.export.json`
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"name": "inventory-quantities.export",
|
|
287
|
+
"version": "1.0.0",
|
|
288
|
+
"description": "Fluent Inventory Quantities → JSON API Export Mapping",
|
|
289
|
+
"fields": {
|
|
290
|
+
"ref": { "source": "ref", "required": true, "resolver": "sdk.trim" },
|
|
291
|
+
"location": { "source": "locationRef", "required": true, "resolver": "sdk.trim" },
|
|
292
|
+
"sku": { "source": "articleRef", "required": true, "resolver": "sdk.trim" },
|
|
293
|
+
"quantity": { "source": "qty", "required": true, "resolver": "sdk.parseInt" },
|
|
294
|
+
"type": { "source": "type", "required": true, "resolver": "sdk.uppercase" },
|
|
295
|
+
"condition": { "source": "condition", "required": false, "resolver": "sdk.uppercase" },
|
|
296
|
+
"status": { "source": "status", "required": true, "resolver": "sdk.uppercase" },
|
|
297
|
+
"storageArea": { "source": "storageAreaRef", "required": false, "resolver": "sdk.trim" },
|
|
298
|
+
"catalogueRef": { "source": "catalogue.ref", "required": false, "resolver": "sdk.trim" },
|
|
299
|
+
"catalogueName": { "source": "catalogue.name", "required": false, "resolver": "sdk.trim" },
|
|
300
|
+
"expectedDate": { "source": "expectedOn", "required": false, "resolver": "sdk.formatDate" },
|
|
301
|
+
"createdOn": { "source": "createdOn", "required": true, "resolver": "sdk.toString" },
|
|
302
|
+
"updatedOn": { "source": "updatedOn", "required": true, "resolver": "sdk.toString" }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Mapping & Resolvers Explained
|
|
310
|
+
|
|
311
|
+
This section explains how the SDK transforms raw GraphQL data into your JSON export format using **UniversalMapper** and **SDK resolvers**.
|
|
312
|
+
|
|
313
|
+
### SDK Resolvers Used
|
|
314
|
+
|
|
315
|
+
| Field | Resolver | Why? | Example Transformation |
|
|
316
|
+
| --------------- | ---------------- | --------------------------------------------- | ----------------------------------------------------------- |
|
|
317
|
+
| `ref` | `sdk.trim` | Clean quantity record references | `" QTY-001 "` → `"QTY-001"` |
|
|
318
|
+
| `location` | `sdk.trim` | Clean location references | `" DC01 "` → `"DC01"` |
|
|
319
|
+
| `sku` | `sdk.trim` | Clean SKU references | `" SKU-001 "` → `"SKU-001"` |
|
|
320
|
+
| `quantity` | `sdk.parseInt` | Parse quantity as integer for JSON APIs | `"150"` → `150` |
|
|
321
|
+
| `type` | `sdk.uppercase` | Normalize type codes for consistency | `"available"` → `"AVAILABLE"` |
|
|
322
|
+
| `condition` | `sdk.uppercase` | Normalize condition codes | `"good"` → `"GOOD"` |
|
|
323
|
+
| `status` | `sdk.uppercase` | Normalize status codes | `"active"` → `"ACTIVE"` |
|
|
324
|
+
| `storageArea` | `sdk.trim` | Clean storage area references | `" ZONE-A "` → `"ZONE-A"` |
|
|
325
|
+
| `catalogueRef` | `sdk.trim` | Clean catalogue references from nested object | `" DEFAULT_CATALOGUE "` → `"DEFAULT_CATALOGUE"` |
|
|
326
|
+
| `catalogueName` | `sdk.trim` | Clean catalogue names from nested object | `" Default Inventory "` → `"Default Inventory"` |
|
|
327
|
+
| `expectedDate` | `sdk.formatDate` | Format dates for JSON APIs | `"2025-01-30T00:00:00.000Z"` → `"2025-01-30"` |
|
|
328
|
+
| `createdOn` | `sdk.toString` | Preserve ISO 8601 timestamp string | `"2025-01-22T10:00:00.000Z"` → `"2025-01-22T10:00:00.000Z"` |
|
|
329
|
+
| `updatedOn` | `sdk.toString` | Preserve ISO 8601 timestamp for tracking | `"2025-01-22T10:30:00.000Z"` → `"2025-01-22T10:30:00.000Z"` |
|
|
330
|
+
|
|
331
|
+
### Transformation Flow
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
// 1. GraphQL Response (raw data from Fluent Commerce)
|
|
335
|
+
const rawQuantity = {
|
|
336
|
+
ref: ' QTY-001 ',
|
|
337
|
+
locationRef: ' DC01 ',
|
|
338
|
+
articleRef: ' SKU-001 ',
|
|
339
|
+
qty: '150',
|
|
340
|
+
type: 'available',
|
|
341
|
+
condition: 'good',
|
|
342
|
+
status: 'active',
|
|
343
|
+
storageAreaRef: ' ZONE-A ',
|
|
344
|
+
catalogue: {
|
|
345
|
+
ref: ' DEFAULT_CATALOGUE ',
|
|
346
|
+
name: ' Default Inventory ',
|
|
347
|
+
},
|
|
348
|
+
expectedOn: '2025-01-25T00:00:00.000Z',
|
|
349
|
+
createdOn: '2025-01-20T10:00:00.000Z',
|
|
350
|
+
updatedOn: '2025-01-22T10:30:00.000Z',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// 2. UniversalMapper applies SDK resolvers
|
|
354
|
+
const mapper = new UniversalMapper(inventoryQuantitiesExportMapping);
|
|
355
|
+
const result = await mapper.map(rawQuantity);
|
|
356
|
+
|
|
357
|
+
// 3. Transformed Output (JSON-friendly camelCase)
|
|
358
|
+
const transformedQuantity = {
|
|
359
|
+
ref: 'QTY-001',
|
|
360
|
+
location: 'DC01',
|
|
361
|
+
sku: 'SKU-001',
|
|
362
|
+
quantity: 150, // Parsed as number for JSON
|
|
363
|
+
type: 'AVAILABLE',
|
|
364
|
+
condition: 'GOOD',
|
|
365
|
+
status: 'ACTIVE',
|
|
366
|
+
storageArea: 'ZONE-A',
|
|
367
|
+
catalogueRef: 'DEFAULT_CATALOGUE',
|
|
368
|
+
catalogueName: 'Default Inventory',
|
|
369
|
+
expectedDate: '2025-01-25',
|
|
370
|
+
createdOn: '2025-01-20T10:00:00.000Z',
|
|
371
|
+
updatedOn: '2025-01-22T10:30:00.000Z', // ISO 8601 for APIs
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// 4. Final JSON Structure with Metadata
|
|
375
|
+
const jsonOutput = {
|
|
376
|
+
metadata: {
|
|
377
|
+
extractedAt: '2025-01-22T14:30:00.000Z',
|
|
378
|
+
recordCount: 1,
|
|
379
|
+
incrementalFrom: '2025-01-22T10:00:00.000Z',
|
|
380
|
+
incrementalTo: '2025-01-22T14:30:00.000Z',
|
|
381
|
+
},
|
|
382
|
+
data: [transformedQuantity],
|
|
383
|
+
};
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Custom Resolvers for Inventory Quantity-Specific Logic
|
|
387
|
+
|
|
388
|
+
While the mapping above uses built-in SDK resolvers, you can extend with custom business logic for API enrichment:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
const customResolvers = {
|
|
392
|
+
/**
|
|
393
|
+
* Validate and normalize quantity values for API consumption
|
|
394
|
+
*/
|
|
395
|
+
'custom.normalizeQuantity': (qty: any) => {
|
|
396
|
+
const parsed = parseInt(qty) || 0;
|
|
397
|
+
return Math.max(0, parsed); // Ensure non-negative for APIs
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Add human-readable type descriptions for API responses
|
|
402
|
+
*/
|
|
403
|
+
'custom.enrichQuantityType': (type: string, sourceData: any) => {
|
|
404
|
+
const typeDescriptions: Record<string, string> = {
|
|
405
|
+
AVAILABLE: 'Available for Sale',
|
|
406
|
+
RESERVED: 'Reserved by Order',
|
|
407
|
+
EXPECTED: 'Expected Arrival',
|
|
408
|
+
ADJUSTMENT: 'Inventory Adjustment',
|
|
409
|
+
DAMAGED: 'Damaged/Unsellable',
|
|
410
|
+
IN_TRANSIT: 'In Transit to Location',
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
code: type.toUpperCase(),
|
|
414
|
+
description: typeDescriptions[type.toUpperCase()] || type,
|
|
415
|
+
isAvailableForSale: type.toUpperCase() === 'AVAILABLE',
|
|
416
|
+
};
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Calculate days until expected arrival for EXPECTED quantities
|
|
421
|
+
*/
|
|
422
|
+
'custom.calculateExpectedArrival': (expectedOn: string | null) => {
|
|
423
|
+
if (!expectedOn) return null;
|
|
424
|
+
|
|
425
|
+
const expected = new Date(expectedOn);
|
|
426
|
+
const today = new Date();
|
|
427
|
+
const diffMs = expected.getTime() - today.getTime();
|
|
428
|
+
const daysUntil = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
date: expectedOn,
|
|
432
|
+
daysUntil: daysUntil,
|
|
433
|
+
isOverdue: daysUntil < 0,
|
|
434
|
+
isPending: daysUntil >= 0,
|
|
435
|
+
};
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Generate API-friendly status summary
|
|
440
|
+
*/
|
|
441
|
+
'custom.generateStatusSummary': (quantity: any) => {
|
|
442
|
+
const type = (quantity.type || '').toUpperCase();
|
|
443
|
+
const status = (quantity.status || '').toUpperCase();
|
|
444
|
+
const qty = parseInt(quantity.qty) || 0;
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
quantity: qty,
|
|
448
|
+
type: type,
|
|
449
|
+
status: status,
|
|
450
|
+
isActive: status === 'ACTIVE',
|
|
451
|
+
isAvailable: type === 'AVAILABLE' && status === 'ACTIVE',
|
|
452
|
+
needsAction: type === 'EXPECTED' && !quantity.expectedOn,
|
|
453
|
+
stockLevel: qty === 0 ? 'OUT_OF_STOCK' : qty < 10 ? 'LOW_STOCK' : 'IN_STOCK',
|
|
454
|
+
};
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Format for real-time inventory API responses
|
|
459
|
+
*/
|
|
460
|
+
'custom.formatForRealtimeAPI': (quantity: any) => {
|
|
461
|
+
return {
|
|
462
|
+
location: quantity.locationRef?.trim(),
|
|
463
|
+
sku: quantity.articleRef?.trim(),
|
|
464
|
+
available: quantity.type === 'AVAILABLE' ? parseInt(quantity.qty) || 0 : 0,
|
|
465
|
+
reserved: quantity.type === 'RESERVED' ? parseInt(quantity.qty) || 0 : 0,
|
|
466
|
+
expected: quantity.type === 'EXPECTED' ? parseInt(quantity.qty) || 0 : 0,
|
|
467
|
+
expectedDate: quantity.expectedOn || null,
|
|
468
|
+
updatedOn: quantity.updatedOn,
|
|
469
|
+
isActive: quantity.status === 'ACTIVE',
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// Use custom resolvers with UniversalMapper
|
|
475
|
+
const mapper = new UniversalMapper(inventoryQuantitiesExportMapping, {
|
|
476
|
+
customResolvers,
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### JSON-Specific Mapping Considerations
|
|
481
|
+
|
|
482
|
+
**1. camelCase vs snake_case:**
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// CSV export: snake_case for compatibility
|
|
486
|
+
{ "location_code": "DC01", "last_updated": "..." }
|
|
487
|
+
|
|
488
|
+
// JSON export: camelCase for APIs
|
|
489
|
+
{ "location": "DC01", "updatedOn": "..." }
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**2. Number Types:**
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// JSON preserves number types (no quotes)
|
|
496
|
+
{ "quantity": 150 } // ✓ Correct for JSON APIs
|
|
497
|
+
|
|
498
|
+
// CSV requires strings
|
|
499
|
+
"150" // ✓ Correct for CSV
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**3. Null Handling:**
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
// JSON can use null
|
|
506
|
+
{ "expectedDate": null } // ✓ Valid JSON
|
|
507
|
+
|
|
508
|
+
// CSV uses empty string
|
|
509
|
+
"" // ✓ Valid CSV
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**4. Metadata Wrapper:**
|
|
513
|
+
|
|
514
|
+
```json
|
|
515
|
+
{
|
|
516
|
+
"metadata": {
|
|
517
|
+
"extractedAt": "2025-01-22T14:30:00.000Z",
|
|
518
|
+
"recordCount": 2,
|
|
519
|
+
"incrementalFrom": "...",
|
|
520
|
+
"incrementalTo": "..."
|
|
521
|
+
},
|
|
522
|
+
"data": [
|
|
523
|
+
/* transformed records */
|
|
524
|
+
]
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Available SDK Resolvers
|
|
529
|
+
|
|
530
|
+
The SDK provides these built-in resolvers (no custom code needed):
|
|
531
|
+
|
|
532
|
+
**String Transformations:**
|
|
533
|
+
|
|
534
|
+
- `sdk.trim` - Remove leading/trailing whitespace
|
|
535
|
+
- `sdk.uppercase` - Convert to uppercase
|
|
536
|
+
- `sdk.lowercase` - Convert to lowercase
|
|
537
|
+
- `sdk.toString` - Convert to string
|
|
538
|
+
|
|
539
|
+
**Number Parsing:**
|
|
540
|
+
|
|
541
|
+
- `sdk.parseInt` - Parse as integer (ideal for JSON)
|
|
542
|
+
- `sdk.parseFloat` - Parse as decimal
|
|
543
|
+
- `sdk.number` - Parse as number (auto-detect int/float)
|
|
544
|
+
|
|
545
|
+
**Date Formatting:**
|
|
546
|
+
|
|
547
|
+
- `sdk.formatDate` - ISO 8601 date only (YYYY-MM-DD)
|
|
548
|
+
- `sdk.formatDateShort` - Short date format
|
|
549
|
+
- `sdk.parseDate` - Parse various date formats
|
|
550
|
+
|
|
551
|
+
**Type Conversions:**
|
|
552
|
+
|
|
553
|
+
- `sdk.boolean` - Convert to boolean
|
|
554
|
+
- `sdk.parseJson` - Parse JSON strings
|
|
555
|
+
- `sdk.toJson` - Convert to JSON string
|
|
556
|
+
|
|
557
|
+
**Utilities:**
|
|
558
|
+
|
|
559
|
+
- `sdk.identity` - Return value unchanged
|
|
560
|
+
- `sdk.coalesce` - Return first non-null value
|
|
561
|
+
|
|
562
|
+
See [Universal Mapping Guide](../../../../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) for complete resolver documentation.
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## GraphQL Query
|
|
567
|
+
|
|
568
|
+
**Note**: This query is verified against Fluent Commerce schema introspection.
|
|
569
|
+
|
|
570
|
+
```graphql
|
|
571
|
+
query GetInventoryQuantities(
|
|
572
|
+
$retailerId: ID!
|
|
573
|
+
$updatedAfter: DateTime!
|
|
574
|
+
$first: Int!
|
|
575
|
+
$after: String
|
|
576
|
+
) {
|
|
577
|
+
inventoryQuantities(
|
|
578
|
+
retailerId: $retailerId
|
|
579
|
+
updatedOn: { after: $updatedAfter }
|
|
580
|
+
first: $first
|
|
581
|
+
after: $after
|
|
582
|
+
) {
|
|
583
|
+
edges {
|
|
584
|
+
node {
|
|
585
|
+
id
|
|
586
|
+
ref
|
|
587
|
+
locationRef
|
|
588
|
+
articleRef
|
|
589
|
+
qty
|
|
590
|
+
type
|
|
591
|
+
condition
|
|
592
|
+
status
|
|
593
|
+
storageAreaRef
|
|
594
|
+
catalogue {
|
|
595
|
+
ref
|
|
596
|
+
name
|
|
597
|
+
}
|
|
598
|
+
expectedOn
|
|
599
|
+
createdOn
|
|
600
|
+
updatedOn
|
|
601
|
+
}
|
|
602
|
+
cursor
|
|
603
|
+
}
|
|
604
|
+
pageInfo {
|
|
605
|
+
hasNextPage
|
|
606
|
+
# Note: Fluent doesn't return endCursor - cursors are in edges[].cursor
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Versori Workflows Structure
|
|
615
|
+
|
|
616
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
617
|
+
|
|
618
|
+
**Trigger Types:**
|
|
619
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
620
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
621
|
+
- **`workflow()`** → Durable workflows (advanced, rarely used)
|
|
622
|
+
|
|
623
|
+
**Execution Steps (chained to triggers):**
|
|
624
|
+
- **`http()`** → External API calls (chained from schedule/webhook)
|
|
625
|
+
- **`fn()`** → Internal processing (chained from schedule/webhook)
|
|
626
|
+
|
|
627
|
+
### Recommended Project Structure
|
|
628
|
+
|
|
629
|
+
This template demonstrates a modular project structure with separate files for better organization and maintainability:
|
|
630
|
+
|
|
631
|
+
```
|
|
632
|
+
inventory-quantities-extraction/
|
|
633
|
+
├── index.ts # Entry point - exports all workflows
|
|
634
|
+
└── src/
|
|
635
|
+
├── workflows/
|
|
636
|
+
│ ├── scheduled/
|
|
637
|
+
│ │ └── daily-inventory-quantities-extraction.ts # Scheduled: Daily extraction
|
|
638
|
+
│ │
|
|
639
|
+
│ └── webhook/
|
|
640
|
+
│ ├── adhoc-inventory-quantities-extraction.ts # Webhook: Manual trigger
|
|
641
|
+
│ └── job-status-check.ts # Webhook: Status query
|
|
642
|
+
│
|
|
643
|
+
├── services/
|
|
644
|
+
│ └── inventory-quantities-extraction.service.ts # Shared orchestration logic (reusable)
|
|
645
|
+
│
|
|
646
|
+
└── config/
|
|
647
|
+
└── inventory-quantities.export.json # Mapping configuration
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Entry Point: index.ts
|
|
651
|
+
|
|
652
|
+
**File:** `index.ts`
|
|
653
|
+
|
|
654
|
+
The entry point uses the **MemoryInterpreter pattern** to export all workflows for Versori:
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
/**
|
|
658
|
+
* Entry point - Export all workflows for Versori platform
|
|
659
|
+
*
|
|
660
|
+
* VERSORI MEMORY INTERPRETER PATTERN:
|
|
661
|
+
* This file exports all workflows to be registered with Versori.
|
|
662
|
+
* Each workflow is defined in its own file for better organization.
|
|
663
|
+
*
|
|
664
|
+
* Import and re-export pattern allows Versori to discover and register
|
|
665
|
+
* all workflows without manual configuration.
|
|
666
|
+
*/
|
|
667
|
+
|
|
668
|
+
// Scheduled workflows
|
|
669
|
+
export { inventoryQuantitiesExtractionJson } from './src/workflows/scheduled/daily-inventory-quantities-extraction';
|
|
670
|
+
|
|
671
|
+
// Webhook workflows
|
|
672
|
+
export { inventoryQuantitiesAdHocExtraction } from './src/workflows/webhook/adhoc-inventory-quantities-extraction';
|
|
673
|
+
export { inventoryQuantitiesJobStatus } from './src/workflows/webhook/job-status-check';
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
**Key Points:**
|
|
677
|
+
- ✅ Simple re-export pattern - Versori auto-discovers workflows
|
|
678
|
+
- ✅ Each workflow in separate file for maintainability
|
|
679
|
+
- ✅ Clear naming: `export { workflowName } from './path/to/workflow'`
|
|
680
|
+
- ❌ No configuration needed - Versori reads exports directly
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## Example Output
|
|
685
|
+
|
|
686
|
+
The extraction generates JSON files with this structure:
|
|
687
|
+
|
|
688
|
+
```json
|
|
689
|
+
{
|
|
690
|
+
"metadata": {
|
|
691
|
+
"extractedAt": "2025-01-22T14:30:00.000Z",
|
|
692
|
+
"recordCount": 2,
|
|
693
|
+
"incrementalFrom": "2025-01-22T10:00:00.000Z",
|
|
694
|
+
"incrementalTo": "2025-01-22T14:30:00.000Z"
|
|
695
|
+
},
|
|
696
|
+
"data": [
|
|
697
|
+
{
|
|
698
|
+
"ref": "QTY-001",
|
|
699
|
+
"location": "DC01",
|
|
700
|
+
"sku": "SKU-001",
|
|
701
|
+
"quantity": 150,
|
|
702
|
+
"type": "AVAILABLE",
|
|
703
|
+
"condition": "GOOD",
|
|
704
|
+
"status": "ACTIVE",
|
|
705
|
+
"storageArea": "ZONE-A",
|
|
706
|
+
"catalogueRef": "DEFAULT_CATALOGUE",
|
|
707
|
+
"catalogueName": "Default Inventory",
|
|
708
|
+
"expectedDate": null,
|
|
709
|
+
"createdOn": "2025-01-20T10:00:00Z",
|
|
710
|
+
"updatedOn": "2025-01-22T10:30:00Z"
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
"ref": "QTY-002",
|
|
714
|
+
"location": "DC02",
|
|
715
|
+
"sku": "SKU-002",
|
|
716
|
+
"quantity": 200,
|
|
717
|
+
"type": "RESERVED",
|
|
718
|
+
"condition": "GOOD",
|
|
719
|
+
"status": "ACTIVE",
|
|
720
|
+
"storageArea": "ZONE-B",
|
|
721
|
+
"catalogueRef": "DEFAULT_CATALOGUE",
|
|
722
|
+
"catalogueName": "Default Inventory",
|
|
723
|
+
"expectedDate": null,
|
|
724
|
+
"createdOn": "2025-01-21T11:00:00Z",
|
|
725
|
+
"updatedOn": "2025-01-22T11:15:00Z"
|
|
726
|
+
}
|
|
727
|
+
]
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## Production Safety & Guardrails
|
|
734
|
+
|
|
735
|
+
### Overview
|
|
736
|
+
|
|
737
|
+
Even with **incremental-only** extraction, inventory quantities in JSON format need safeguards:
|
|
738
|
+
|
|
739
|
+
- **JSON parsing memory**: API consumers parse entire JSON before processing
|
|
740
|
+
- **Real-time APIs**: Inventory feeds power order promising, need fast processing
|
|
741
|
+
- **High-frequency updates**: Every 15 minutes accumulates large datasets
|
|
742
|
+
- **Nested structure**: JSON metadata wrapper adds overhead vs CSV
|
|
743
|
+
|
|
744
|
+
### Hard Limits
|
|
745
|
+
|
|
746
|
+
```typescript
|
|
747
|
+
const SAFETY_LIMITS = {
|
|
748
|
+
MAX_RECORDS_PER_RUN: 300000, // 300k quantity records per run
|
|
749
|
+
MAX_RECORDS_PER_FILE: 50000, // 50k per JSON file (lower than CSV)
|
|
750
|
+
MAX_FILE_SIZE_MB: 100, // 100MB per file
|
|
751
|
+
MAX_JSON_SIZE_MB: 200, // Total extraction size
|
|
752
|
+
CHUNK_SIZE: 10000, // Process in chunks
|
|
753
|
+
ESTIMATED_BYTES_PER_RECORD: 400, // JSON with metadata
|
|
754
|
+
};
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**Why JSON has different limits than CSV?**
|
|
758
|
+
|
|
759
|
+
- JSON includes field names in every record (overhead)
|
|
760
|
+
- JSON metadata wrapper adds size
|
|
761
|
+
- JSON.parse() is memory-intensive
|
|
762
|
+
- API consumers need predictable payload sizes
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
## Complete Workflow Implementation
|
|
767
|
+
|
|
768
|
+
The code examples below demonstrate the implementation of each workflow component. In your project, these would be organized into separate files following the modular structure shown above.
|
|
769
|
+
|
|
770
|
+
### Workflow 1: Scheduled Extraction (Incremental)
|
|
771
|
+
|
|
772
|
+
**File:** `src/workflows/scheduled/daily-inventory-quantities-extraction.ts`
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
import { schedule, http } from '@versori/run';
|
|
776
|
+
import { Buffer } from 'node:buffer';
|
|
777
|
+
import {
|
|
778
|
+
createClient,
|
|
779
|
+
UniversalMapper,
|
|
780
|
+
S3DataSource,
|
|
781
|
+
JSONBuilder,
|
|
782
|
+
VersoriKVAdapter,
|
|
783
|
+
ExtractionOrchestrator,
|
|
784
|
+
JobTracker,
|
|
785
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
786
|
+
import inventoryQuantitiesExportMapping from './config/inventory-quantities.export.json' with { type: 'json' };
|
|
787
|
+
|
|
788
|
+
// GraphQL query
|
|
789
|
+
const INVENTORY_QUANTITIES_QUERY = `
|
|
790
|
+
query GetInventoryQuantities(
|
|
791
|
+
$retailerId: ID!
|
|
792
|
+
$updatedAfter: DateTime!
|
|
793
|
+
$first: Int!
|
|
794
|
+
$after: String
|
|
795
|
+
) {
|
|
796
|
+
inventoryQuantities(
|
|
797
|
+
retailerId: $retailerId
|
|
798
|
+
updatedOn: { after: $updatedAfter }
|
|
799
|
+
first: $first
|
|
800
|
+
after: $after
|
|
801
|
+
) {
|
|
802
|
+
edges {
|
|
803
|
+
node {
|
|
804
|
+
id
|
|
805
|
+
ref
|
|
806
|
+
locationRef
|
|
807
|
+
articleRef
|
|
808
|
+
qty
|
|
809
|
+
type
|
|
810
|
+
condition
|
|
811
|
+
status
|
|
812
|
+
storageAreaRef
|
|
813
|
+
catalogue {
|
|
814
|
+
ref
|
|
815
|
+
name
|
|
816
|
+
}
|
|
817
|
+
expectedOn
|
|
818
|
+
createdOn
|
|
819
|
+
updatedOn
|
|
820
|
+
}
|
|
821
|
+
cursor
|
|
822
|
+
}
|
|
823
|
+
pageInfo {
|
|
824
|
+
hasNextPage
|
|
825
|
+
# Note: Fluent doesn't return endCursor - cursors are in edges[].cursor
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
`;
|
|
830
|
+
|
|
831
|
+
export const inventoryQuantitiesExtractionJson = schedule(
|
|
832
|
+
'inventory-quantities-extract-json-15min',
|
|
833
|
+
'*/15 * * * *',
|
|
834
|
+
http('extract-inventory-quantities-json', { connection: 'fluent_commerce', validateConnection: true }, async ctx => {
|
|
835
|
+
const { log, activation, openKv } = ctx;
|
|
836
|
+
const executionStartTime = Date.now();
|
|
837
|
+
|
|
838
|
+
// ========================================
|
|
839
|
+
// EXECUTION BOUNDARY: Workflow Start
|
|
840
|
+
// ========================================
|
|
841
|
+
log.info('🚀 [WORKFLOW] Inventory Quantities Extraction - Started', {
|
|
842
|
+
trigger: 'schedule',
|
|
843
|
+
schedule: '*/15 * * * *'
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
const retailerId = activation?.getVariable('retailerId');
|
|
847
|
+
const pageSize = parseInt(activation?.getVariable('pageSize') || '500', 10);
|
|
848
|
+
const maxRecords = parseInt(activation?.getVariable('maxRecords') || '20000', 10);
|
|
849
|
+
const fallbackStartDate =
|
|
850
|
+
activation?.getVariable('fallbackStartDate') || '2024-01-01T00:00:00Z';
|
|
851
|
+
const prettyPrint = activation?.getVariable('prettyPrint') === 'true';
|
|
852
|
+
const overlapBufferSeconds = parseInt(
|
|
853
|
+
activation?.getVariable('overlapBufferSeconds') || '60',
|
|
854
|
+
10
|
|
855
|
+
);
|
|
856
|
+
const OVERLAP_BUFFER_MS = overlapBufferSeconds * 1000;
|
|
857
|
+
|
|
858
|
+
const s3Config = {
|
|
859
|
+
bucket: activation?.getVariable('s3BucketName'),
|
|
860
|
+
region: activation?.getVariable('awsRegion') || 'us-east-1',
|
|
861
|
+
accessKeyId: activation?.getVariable('awsAccessKeyId'),
|
|
862
|
+
secretAccessKey: activation?.getVariable('awsSecretAccessKey'),
|
|
863
|
+
};
|
|
864
|
+
const s3Prefix = activation?.getVariable('s3Prefix') || 'inventory/quantities/';
|
|
865
|
+
|
|
866
|
+
// Validate required variables
|
|
867
|
+
const missing: string[] = [];
|
|
868
|
+
if (!retailerId) missing.push('retailerId');
|
|
869
|
+
if (!s3Config.bucket) missing.push('s3BucketName');
|
|
870
|
+
if (!s3Config.accessKeyId) missing.push('awsAccessKeyId');
|
|
871
|
+
if (!s3Config.secretAccessKey) missing.push('awsSecretAccessKey');
|
|
872
|
+
if (missing.length) {
|
|
873
|
+
log.error('❌ [VALIDATION] Missing required activation variables', { missing });
|
|
874
|
+
return {
|
|
875
|
+
success: false,
|
|
876
|
+
error: `Missing required variables: ${missing.join(', ')}`,
|
|
877
|
+
recommendation: 'Configure these variables in the Activation Variables section of your Versori connector'
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
try {
|
|
882
|
+
// STEP 1/8: Initialize KV state and job tracking
|
|
883
|
+
log.info('⚙️ [STEP 1/8] Initializing KV state and job tracking');
|
|
884
|
+
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
885
|
+
const tracker = new JobTracker(kv, log);
|
|
886
|
+
const stateKey = ['extraction', 'inventory-quantities-json', 'lastRunTime'];
|
|
887
|
+
|
|
888
|
+
// STEP 2/8: Create extraction job
|
|
889
|
+
log.info('⚙️ [STEP 2/8] Creating extraction job');
|
|
890
|
+
const job = await tracker.createJob({
|
|
891
|
+
type: 'extraction',
|
|
892
|
+
entity: 'inventoryQuantities',
|
|
893
|
+
config: {
|
|
894
|
+
extractionMode: 'incremental',
|
|
895
|
+
retailerId,
|
|
896
|
+
s3Bucket: s3Config.bucket,
|
|
897
|
+
s3Prefix,
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
log.info('✅ [JOB] Extraction job created', { jobId: job.id });
|
|
902
|
+
|
|
903
|
+
// STEP 3/8: Load last successful extraction timestamp
|
|
904
|
+
log.info('⚙️ [STEP 3/8] Loading last extraction timestamp');
|
|
905
|
+
const lastRunState = await kv.get(stateKey);
|
|
906
|
+
const rawLastRunTime = lastRunState?.value?.timestamp || fallbackStartDate;
|
|
907
|
+
|
|
908
|
+
// Apply overlap buffer for query (safety window)
|
|
909
|
+
const bufferedLastRunTime = new Date(
|
|
910
|
+
new Date(rawLastRunTime).getTime() - OVERLAP_BUFFER_MS
|
|
911
|
+
).toISOString();
|
|
912
|
+
|
|
913
|
+
log.info('✅ [STATE] Incremental extraction window configured', {
|
|
914
|
+
jobId: job.id,
|
|
915
|
+
rawLastRunTime,
|
|
916
|
+
bufferedLastRunTime,
|
|
917
|
+
overlapBufferSeconds,
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// STEP 4/8: Initialize Fluent client and orchestrator
|
|
921
|
+
log.info('⚙️ [STEP 4/8] Initializing Fluent client and orchestrator');
|
|
922
|
+
const client = await createClient(ctx);
|
|
923
|
+
const orchestrator = new ExtractionOrchestrator(client, log);
|
|
924
|
+
|
|
925
|
+
// STEP 5/8: Execute GraphQL query with auto-pagination
|
|
926
|
+
log.info('🔍 [STEP 5/8] Executing GraphQL extraction', {
|
|
927
|
+
query: 'inventoryQuantities',
|
|
928
|
+
pageSize,
|
|
929
|
+
maxRecords,
|
|
930
|
+
dateRange: `from ${bufferedLastRunTime}`,
|
|
931
|
+
retailerId,
|
|
932
|
+
jobId: job.id
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
const extractionStartTime = Date.now();
|
|
936
|
+
const result = await orchestrator.extract({
|
|
937
|
+
query: INVENTORY_QUANTITIES_QUERY,
|
|
938
|
+
resultPath: 'inventoryQuantities.edges.node',
|
|
939
|
+
variables: {
|
|
940
|
+
retailerId,
|
|
941
|
+
updatedAfter: bufferedLastRunTime,
|
|
942
|
+
// SDK adds 'first' automatically based on pageSize
|
|
943
|
+
},
|
|
944
|
+
pageSize,
|
|
945
|
+
maxRecords,
|
|
946
|
+
});
|
|
947
|
+
const extractionDuration = Date.now() - extractionStartTime;
|
|
948
|
+
|
|
949
|
+
const edges = result.data || [];
|
|
950
|
+
|
|
951
|
+
log.info('✅ [EXTRACTION] GraphQL extraction completed', {
|
|
952
|
+
totalRecords: result.stats.totalRecords,
|
|
953
|
+
totalPages: result.stats.totalPages,
|
|
954
|
+
validRecords: result.stats.validRecords ?? edges.length,
|
|
955
|
+
failedValidations: result.stats.failedValidations,
|
|
956
|
+
truncated: result.stats.truncated,
|
|
957
|
+
truncationReason: result.stats.truncationReason,
|
|
958
|
+
durationMs: extractionDuration,
|
|
959
|
+
jobId: job.id
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
if (edges.length === 0) {
|
|
963
|
+
log.info('ℹ️ [RESULT] No new inventory quantity records to extract');
|
|
964
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
965
|
+
await tracker.markCompleted(job.id, {
|
|
966
|
+
recordsExtracted: 0,
|
|
967
|
+
message: 'No new records',
|
|
968
|
+
durationMs: totalDuration
|
|
969
|
+
});
|
|
970
|
+
await kv.set(stateKey, {
|
|
971
|
+
timestamp: new Date().toISOString(),
|
|
972
|
+
recordCount: 0,
|
|
973
|
+
extractedAt: new Date().toISOString(),
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
log.info('✅ [WORKFLOW] Inventory Quantities Extraction - Completed (No Records)', {
|
|
977
|
+
jobId: job.id,
|
|
978
|
+
durationMs: totalDuration
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
return {
|
|
982
|
+
success: true,
|
|
983
|
+
message: 'No new records to extract',
|
|
984
|
+
jobId: job.id,
|
|
985
|
+
rawLastRunTime,
|
|
986
|
+
durationMs: totalDuration
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
log.info('✅ [DATA] Inventory quantity records retrieved', { count: edges.length, jobId: job.id });
|
|
991
|
+
|
|
992
|
+
// STEP 6/8: Transform with UniversalMapper
|
|
993
|
+
log.info('⚙️ [STEP 6/8] Transforming records with UniversalMapper', { recordCount: edges.length });
|
|
994
|
+
const mappingStartTime = Date.now();
|
|
995
|
+
const mapper = new UniversalMapper(inventoryQuantitiesExportMapping);
|
|
996
|
+
const transformedRecords: any[] = [];
|
|
997
|
+
const errors: any[] = [];
|
|
998
|
+
|
|
999
|
+
for (const edge of edges) {
|
|
1000
|
+
const mapped = await mapper.map(edge.node);
|
|
1001
|
+
if (mapped.success) {
|
|
1002
|
+
transformedRecords.push(mapped.data);
|
|
1003
|
+
} else {
|
|
1004
|
+
errors.push({
|
|
1005
|
+
record: edge.node.id,
|
|
1006
|
+
errors: mapped.errors,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const mappingDuration = Date.now() - mappingStartTime;
|
|
1012
|
+
|
|
1013
|
+
if (transformedRecords.length === 0) {
|
|
1014
|
+
log.error('❌ [MAPPING] All records failed mapping validation', {
|
|
1015
|
+
totalRecords: edges.length,
|
|
1016
|
+
errorCount: errors.length,
|
|
1017
|
+
jobId: job.id
|
|
1018
|
+
});
|
|
1019
|
+
await tracker.markFailed(job.id, {
|
|
1020
|
+
error: 'All records failed mapping',
|
|
1021
|
+
errors,
|
|
1022
|
+
});
|
|
1023
|
+
return {
|
|
1024
|
+
success: false,
|
|
1025
|
+
error: 'All records failed mapping',
|
|
1026
|
+
jobId: job.id,
|
|
1027
|
+
errors,
|
|
1028
|
+
recommendation: 'Check mapping configuration in config/inventory-quantities.export.json and verify source data format'
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
log.info('✅ [MAPPING] Records transformed successfully', {
|
|
1033
|
+
successful: transformedRecords.length,
|
|
1034
|
+
failed: errors.length,
|
|
1035
|
+
durationMs: mappingDuration,
|
|
1036
|
+
jobId: job.id,
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
// Calculate max updatedOn for next run (without buffer)
|
|
1040
|
+
const maxUpdatedOn = transformedRecords.reduce((max, record) => {
|
|
1041
|
+
const recordTime = new Date(record.updatedOn).getTime();
|
|
1042
|
+
return recordTime > max ? recordTime : max;
|
|
1043
|
+
}, new Date(rawLastRunTime).getTime());
|
|
1044
|
+
|
|
1045
|
+
const newTimestamp = new Date(maxUpdatedOn).toISOString();
|
|
1046
|
+
|
|
1047
|
+
// STEP 7/8: Build JSON with metadata and upload to S3
|
|
1048
|
+
log.info('⚙️ [STEP 7/8] Building JSON and uploading to S3');
|
|
1049
|
+
const jsonBuildStartTime = Date.now();
|
|
1050
|
+
const jsonOutput = {
|
|
1051
|
+
metadata: {
|
|
1052
|
+
extractedAt: new Date().toISOString(),
|
|
1053
|
+
recordCount: transformedRecords.length,
|
|
1054
|
+
incrementalFrom: rawLastRunTime,
|
|
1055
|
+
incrementalTo: newTimestamp,
|
|
1056
|
+
jobId: job.id,
|
|
1057
|
+
},
|
|
1058
|
+
data: transformedRecords,
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// Use JSONBuilder for consistent JSON generation
|
|
1062
|
+
const jsonBuilder = new JSONBuilder({
|
|
1063
|
+
prettyPrint,
|
|
1064
|
+
indent: 2,
|
|
1065
|
+
});
|
|
1066
|
+
const jsonContent = jsonBuilder.build(jsonOutput);
|
|
1067
|
+
const jsonBuildDuration = Date.now() - jsonBuildStartTime;
|
|
1068
|
+
|
|
1069
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1070
|
+
const fileName = `inventory-quantities-${timestamp}.json`;
|
|
1071
|
+
const s3Key = `${s3Prefix}${fileName}`;
|
|
1072
|
+
|
|
1073
|
+
log.info('✅ [JSON] JSON file generated', {
|
|
1074
|
+
fileName,
|
|
1075
|
+
sizeBytes: jsonContent.length,
|
|
1076
|
+
sizeMB: (jsonContent.length / (1024 * 1024)).toFixed(2),
|
|
1077
|
+
recordCount: transformedRecords.length,
|
|
1078
|
+
durationMs: jsonBuildDuration,
|
|
1079
|
+
jobId: job.id,
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
const s3UploadStartTime = Date.now();
|
|
1083
|
+
const s3 = new S3DataSource(
|
|
1084
|
+
{
|
|
1085
|
+
type: 'S3_JSON',
|
|
1086
|
+
connectionId: 's3-inventory-quantities-json-export',
|
|
1087
|
+
name: 'S3 Inventory Quantities JSON Export',
|
|
1088
|
+
s3Config,
|
|
1089
|
+
},
|
|
1090
|
+
log
|
|
1091
|
+
);
|
|
1092
|
+
|
|
1093
|
+
await s3.upload({
|
|
1094
|
+
key: s3Key,
|
|
1095
|
+
body: Buffer.from(jsonContent, 'utf8'),
|
|
1096
|
+
contentType: 'application/json',
|
|
1097
|
+
metadata: {
|
|
1098
|
+
recordCount: String(transformedRecords.length),
|
|
1099
|
+
extractedAt: new Date().toISOString(),
|
|
1100
|
+
incrementalFrom: rawLastRunTime,
|
|
1101
|
+
incrementalTo: newTimestamp,
|
|
1102
|
+
jobId: job.id,
|
|
1103
|
+
},
|
|
1104
|
+
});
|
|
1105
|
+
const s3UploadDuration = Date.now() - s3UploadStartTime;
|
|
1106
|
+
|
|
1107
|
+
log.info('✅ [S3] JSON file uploaded successfully', {
|
|
1108
|
+
s3Key,
|
|
1109
|
+
bucket: s3Config.bucket,
|
|
1110
|
+
durationMs: s3UploadDuration,
|
|
1111
|
+
jobId: job.id
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// STEP 8/8: Update state and complete job
|
|
1115
|
+
log.info('⚙️ [STEP 8/8] Updating state and completing job');
|
|
1116
|
+
await kv.set(stateKey, {
|
|
1117
|
+
timestamp: newTimestamp, // ← NO buffer applied
|
|
1118
|
+
recordCount: transformedRecords.length,
|
|
1119
|
+
extractedAt: new Date().toISOString(),
|
|
1120
|
+
overlapBufferSeconds,
|
|
1121
|
+
fileName,
|
|
1122
|
+
s3Key,
|
|
1123
|
+
jobId: job.id,
|
|
1124
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
1128
|
+
await tracker.markCompleted(job.id, {
|
|
1129
|
+
recordsExtracted: transformedRecords.length,
|
|
1130
|
+
recordsFailed: errors.length,
|
|
1131
|
+
fileName,
|
|
1132
|
+
s3Key,
|
|
1133
|
+
newTimestamp,
|
|
1134
|
+
durationMs: totalDuration,
|
|
1135
|
+
performance: {
|
|
1136
|
+
extractionMs: extractionDuration,
|
|
1137
|
+
mappingMs: mappingDuration,
|
|
1138
|
+
jsonBuildMs: jsonBuildDuration,
|
|
1139
|
+
s3UploadMs: s3UploadDuration,
|
|
1140
|
+
totalMs: totalDuration
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// ========================================
|
|
1145
|
+
// EXECUTION BOUNDARY: Workflow Success
|
|
1146
|
+
// ========================================
|
|
1147
|
+
log.info('✅ [WORKFLOW] Inventory Quantities Extraction - Completed Successfully', {
|
|
1148
|
+
jobId: job.id,
|
|
1149
|
+
recordsExtracted: transformedRecords.length,
|
|
1150
|
+
recordsFailed: errors.length,
|
|
1151
|
+
fileName,
|
|
1152
|
+
s3Key,
|
|
1153
|
+
durationMs: totalDuration,
|
|
1154
|
+
performance: {
|
|
1155
|
+
extractionMs: extractionDuration,
|
|
1156
|
+
mappingMs: mappingDuration,
|
|
1157
|
+
jsonBuildMs: jsonBuildDuration,
|
|
1158
|
+
s3UploadMs: s3UploadDuration
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
return {
|
|
1163
|
+
success: true,
|
|
1164
|
+
jobId: job.id,
|
|
1165
|
+
recordsExtracted: transformedRecords.length,
|
|
1166
|
+
recordsFailed: errors.length,
|
|
1167
|
+
fileName,
|
|
1168
|
+
s3Key,
|
|
1169
|
+
rawLastRunTime,
|
|
1170
|
+
newTimestamp,
|
|
1171
|
+
durationMs: totalDuration,
|
|
1172
|
+
performance: {
|
|
1173
|
+
extractionMs: extractionDuration,
|
|
1174
|
+
mappingMs: mappingDuration,
|
|
1175
|
+
jsonBuildMs: jsonBuildDuration,
|
|
1176
|
+
s3UploadMs: s3UploadDuration,
|
|
1177
|
+
totalMs: totalDuration
|
|
1178
|
+
},
|
|
1179
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
1180
|
+
};
|
|
1181
|
+
} catch (error: any) {
|
|
1182
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
1183
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1184
|
+
const errorType = error instanceof Error ? error.constructor.name : 'UnknownError';
|
|
1185
|
+
|
|
1186
|
+
// ========================================
|
|
1187
|
+
// EXECUTION BOUNDARY: Workflow Failure
|
|
1188
|
+
// ========================================
|
|
1189
|
+
log.error('❌ [WORKFLOW] Inventory Quantities Extraction - Failed', {
|
|
1190
|
+
error: errorMessage,
|
|
1191
|
+
errorType,
|
|
1192
|
+
durationMs: totalDuration,
|
|
1193
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// Determine error recommendation based on error type
|
|
1197
|
+
let recommendation = 'Check logs for details and verify configuration';
|
|
1198
|
+
if (errorMessage.includes('authentication') || errorMessage.includes('credentials')) {
|
|
1199
|
+
recommendation = 'Verify Fluent Commerce and S3 credentials in Activation Variables';
|
|
1200
|
+
} else if (errorMessage.includes('network') || errorMessage.includes('timeout')) {
|
|
1201
|
+
recommendation = 'Check network connectivity and increase timeout settings if needed';
|
|
1202
|
+
} else if (errorMessage.includes('GraphQL') || errorMessage.includes('query')) {
|
|
1203
|
+
recommendation = 'Verify GraphQL query syntax and ensure retailerId has access to inventory data';
|
|
1204
|
+
} else if (errorMessage.includes('S3') || errorMessage.includes('upload')) {
|
|
1205
|
+
recommendation = 'Verify S3 bucket exists, credentials are valid, and bucket permissions allow uploads';
|
|
1206
|
+
} else if (errorMessage.includes('mapping') || errorMessage.includes('transform')) {
|
|
1207
|
+
recommendation = 'Check mapping configuration in config/inventory-quantities.export.json';
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
return {
|
|
1211
|
+
success: false,
|
|
1212
|
+
error: errorMessage,
|
|
1213
|
+
errorType,
|
|
1214
|
+
durationMs: totalDuration,
|
|
1215
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1216
|
+
recommendation
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
}));
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
### Workflow 2: Ad Hoc Extraction (HTTP Endpoint)
|
|
1223
|
+
|
|
1224
|
+
**File:** `src/workflows/webhook/adhoc-inventory-quantities-extraction.ts`
|
|
1225
|
+
|
|
1226
|
+
```typescript
|
|
1227
|
+
export const inventoryQuantitiesAdHocExtraction = webhook('inventory-quantities-adhoc-extract', {
|
|
1228
|
+
connection: 'inventory-quantities-adhoc',
|
|
1229
|
+
}).then(http('execute-adhoc-extraction', { connection: 'fluent_commerce', validateConnection: true }, async ctx => {
|
|
1230
|
+
const { log, openKv, activation } = ctx;
|
|
1231
|
+
const executionStartTime = Date.now();
|
|
1232
|
+
|
|
1233
|
+
log.info('🚀 [WEBHOOK] Ad Hoc Extraction - Started', { trigger: 'webhook' });
|
|
1234
|
+
|
|
1235
|
+
const { startDate, endDate, retailerId } = ctx.request.body;
|
|
1236
|
+
|
|
1237
|
+
if (!startDate || !endDate || !retailerId) {
|
|
1238
|
+
log.error('❌ [VALIDATION] Missing required webhook payload fields', {
|
|
1239
|
+
hasStartDate: !!startDate,
|
|
1240
|
+
hasEndDate: !!endDate,
|
|
1241
|
+
hasRetailerId: !!retailerId
|
|
1242
|
+
});
|
|
1243
|
+
return {
|
|
1244
|
+
success: false,
|
|
1245
|
+
error: 'Missing required fields: startDate, endDate, retailerId',
|
|
1246
|
+
recommendation: 'Provide all required fields in the webhook payload: { startDate, endDate, retailerId }'
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
try {
|
|
1251
|
+
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
1252
|
+
const tracker = new JobTracker(kv, log);
|
|
1253
|
+
|
|
1254
|
+
// Create ad hoc job
|
|
1255
|
+
log.info('⚙️ Creating ad hoc extraction job');
|
|
1256
|
+
const job = await tracker.createJob({
|
|
1257
|
+
type: 'extraction',
|
|
1258
|
+
entity: 'inventoryQuantities',
|
|
1259
|
+
config: {
|
|
1260
|
+
extractionMode: 'dateRange',
|
|
1261
|
+
retailerId,
|
|
1262
|
+
startDate,
|
|
1263
|
+
endDate,
|
|
1264
|
+
},
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
log.info('✅ [JOB] Ad hoc extraction job created', { jobId: job.id, startDate, endDate });
|
|
1268
|
+
|
|
1269
|
+
// Execute extraction (similar to scheduled workflow)
|
|
1270
|
+
// ... (extraction logic here - implement full extraction as in scheduled workflow)
|
|
1271
|
+
|
|
1272
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
1273
|
+
log.info('✅ [WEBHOOK] Ad Hoc Extraction - Completed', {
|
|
1274
|
+
jobId: job.id,
|
|
1275
|
+
durationMs: totalDuration
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
return {
|
|
1279
|
+
success: true,
|
|
1280
|
+
jobId: job.id,
|
|
1281
|
+
message: 'Ad hoc extraction started',
|
|
1282
|
+
durationMs: totalDuration
|
|
1283
|
+
};
|
|
1284
|
+
} catch (error: any) {
|
|
1285
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
1286
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1287
|
+
|
|
1288
|
+
log.error('❌ [WEBHOOK] Ad Hoc Extraction - Failed', {
|
|
1289
|
+
error: errorMessage,
|
|
1290
|
+
durationMs: totalDuration
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
return {
|
|
1294
|
+
success: false,
|
|
1295
|
+
error: errorMessage,
|
|
1296
|
+
durationMs: totalDuration,
|
|
1297
|
+
recommendation: 'Check logs for details and verify webhook payload format'
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
);
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
### Workflow 3: Job Status Lookup
|
|
1305
|
+
|
|
1306
|
+
**File:** `src/workflows/webhook/job-status-check.ts`
|
|
1307
|
+
|
|
1308
|
+
```typescript
|
|
1309
|
+
export const inventoryQuantitiesJobStatus = webhook('inventory-quantities-job-status', {
|
|
1310
|
+
connection: 'inventory-quantities-job-status',
|
|
1311
|
+
}).then(
|
|
1312
|
+
http('query-job-status', { validateConnection: true }, async ctx => {
|
|
1313
|
+
const { log, openKv } = ctx;
|
|
1314
|
+
const executionStartTime = Date.now();
|
|
1315
|
+
|
|
1316
|
+
log.info('🚀 [WEBHOOK] Job Status Query - Started', { trigger: 'webhook' });
|
|
1317
|
+
|
|
1318
|
+
const { jobId } = ctx.request.body;
|
|
1319
|
+
|
|
1320
|
+
if (!jobId) {
|
|
1321
|
+
log.error('❌ [VALIDATION] Missing required field: jobId');
|
|
1322
|
+
return {
|
|
1323
|
+
success: false,
|
|
1324
|
+
error: 'Missing required field: jobId',
|
|
1325
|
+
recommendation: 'Provide jobId in webhook payload: { jobId: "your-job-id" }'
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
try {
|
|
1330
|
+
log.info('⚙️ Querying job status', { jobId });
|
|
1331
|
+
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
1332
|
+
const tracker = new JobTracker(kv, log);
|
|
1333
|
+
|
|
1334
|
+
const job = await tracker.getJob(jobId);
|
|
1335
|
+
|
|
1336
|
+
if (!job) {
|
|
1337
|
+
log.warn('⚠️ Job not found', { jobId });
|
|
1338
|
+
return {
|
|
1339
|
+
success: false,
|
|
1340
|
+
error: `Job not found: ${jobId}`,
|
|
1341
|
+
recommendation: 'Verify the jobId is correct and the job was created successfully'
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
1346
|
+
log.info('✅ [WEBHOOK] Job Status Query - Completed', {
|
|
1347
|
+
jobId,
|
|
1348
|
+
status: job.status,
|
|
1349
|
+
durationMs: totalDuration
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
return {
|
|
1353
|
+
success: true,
|
|
1354
|
+
job: {
|
|
1355
|
+
id: job.id,
|
|
1356
|
+
type: job.type,
|
|
1357
|
+
entity: job.entity,
|
|
1358
|
+
status: job.status,
|
|
1359
|
+
createdAt: job.createdAt,
|
|
1360
|
+
completedAt: job.completedAt,
|
|
1361
|
+
result: job.result,
|
|
1362
|
+
error: job.error,
|
|
1363
|
+
},
|
|
1364
|
+
durationMs: totalDuration
|
|
1365
|
+
};
|
|
1366
|
+
} catch (error: any) {
|
|
1367
|
+
const totalDuration = Date.now() - executionStartTime;
|
|
1368
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1369
|
+
|
|
1370
|
+
log.error('❌ [WEBHOOK] Job Status Query - Failed', {
|
|
1371
|
+
error: errorMessage,
|
|
1372
|
+
durationMs: totalDuration
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
return {
|
|
1376
|
+
success: false,
|
|
1377
|
+
error: errorMessage,
|
|
1378
|
+
durationMs: totalDuration,
|
|
1379
|
+
recommendation: 'Check logs for details and verify KV storage is accessible'
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
})
|
|
1383
|
+
);
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
---
|
|
1387
|
+
|
|
1388
|
+
## Key Differences from CSV
|
|
1389
|
+
|
|
1390
|
+
1. **JSON Structure with Metadata:**
|
|
1391
|
+
|
|
1392
|
+
```json
|
|
1393
|
+
{
|
|
1394
|
+
"metadata": { ... }, // ← Extraction context
|
|
1395
|
+
"data": [ ... ] // ← Actual records
|
|
1396
|
+
}
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
2. **Pretty Printing Option:**
|
|
1400
|
+
- Set `prettyPrint: true` for human-readable JSON
|
|
1401
|
+
- Set `prettyPrint: false` for compact/production JSON
|
|
1402
|
+
|
|
1403
|
+
3. **Content Type:**
|
|
1404
|
+
- CSV: `text/csv`
|
|
1405
|
+
- JSON: `application/json`
|
|
1406
|
+
|
|
1407
|
+
4. **Field Names:**
|
|
1408
|
+
- CSV: `location_code` (snake_case for compatibility)
|
|
1409
|
+
- JSON: `location` (camelCase for APIs)
|
|
1410
|
+
|
|
1411
|
+
---
|
|
1412
|
+
|
|
1413
|
+
## Use Cases
|
|
1414
|
+
|
|
1415
|
+
**1. REST API Polling:**
|
|
1416
|
+
|
|
1417
|
+
```bash
|
|
1418
|
+
# External system polls S3 for latest file
|
|
1419
|
+
aws s3 ls s3://api-inventory-exports/inventory/quantities/ --recursive | sort | tail -n 1
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
**2. S3 Event Trigger:**
|
|
1423
|
+
|
|
1424
|
+
```typescript
|
|
1425
|
+
// Lambda triggered on S3 PUT event
|
|
1426
|
+
export const handler = async (event: S3Event) => {
|
|
1427
|
+
const key = event.Records[0].s3.object.key;
|
|
1428
|
+
const data = await s3.getObject({ Bucket: bucket, Key: key });
|
|
1429
|
+
const json = JSON.parse(data.Body.toString());
|
|
1430
|
+
await processInventoryQuantities(json.data);
|
|
1431
|
+
};
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
**3. API Gateway Integration:**
|
|
1435
|
+
|
|
1436
|
+
```typescript
|
|
1437
|
+
// Return latest extraction file via API
|
|
1438
|
+
// GET /api/inventory/quantities/latest
|
|
1439
|
+
export async function handler(event: APIGatewayEvent) {
|
|
1440
|
+
const latestFile = await getLatestS3File('inventory/quantities/');
|
|
1441
|
+
const data = await s3.getObject({ Key: latestFile });
|
|
1442
|
+
return {
|
|
1443
|
+
statusCode: 200,
|
|
1444
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1445
|
+
body: data.Body.toString(),
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
---
|
|
1451
|
+
|
|
1452
|
+
## Production Checklist
|
|
1453
|
+
|
|
1454
|
+
- [ ] Set appropriate extraction frequency (15min, hourly, daily)
|
|
1455
|
+
- [ ] Configure `maxRecords` based on expected change volume
|
|
1456
|
+
- [ ] Enable/disable pretty printing based on use case
|
|
1457
|
+
- [ ] Set up S3 lifecycle policy to archive old files
|
|
1458
|
+
- [ ] Document JSON schema for API consumers
|
|
1459
|
+
- [ ] Test with real-time incremental changes
|
|
1460
|
+
- [ ] Verify error handling for partial failures
|
|
1461
|
+
- [ ] Set up CloudFront CDN if serving via HTTP
|
|
1462
|
+
- [ ] Configure CORS if accessed from browser
|
|
1463
|
+
- [ ] Set up job tracking and monitoring
|
|
1464
|
+
- [ ] Test ad hoc extraction workflow
|
|
1465
|
+
- [ ] Validate job status lookup
|
|
1466
|
+
|
|
1467
|
+
---
|
|
1468
|
+
|
|
1469
|
+
**Pattern**: Incremental extraction with overlap buffer and JobTracker integration
|
|
1470
|
+
**Key Learning**: Include metadata for API consumers, use 3-workflow pattern
|
|
1471
|
+
**Use Case**: Real-time inventory quantity sync for external APIs with job tracking
|
|
1472
|
+
**Buffer Pattern**: Query WITH buffer (`updatedOn >= lastRunTime - 60s`), save WITHOUT buffer (`MAX(updatedOn)`)
|
|
1473
|
+
|
|
1474
|
+
---
|
|
1475
|
+
|
|
1476
|
+
### Pattern 8: Backward Pagination (Optional - Advanced)
|
|
1477
|
+
|
|
1478
|
+
**Use Case**: Extract data in reverse chronological order (newest to oldest) instead of oldest to newest.
|
|
1479
|
+
|
|
1480
|
+
**When to Use**:
|
|
1481
|
+
|
|
1482
|
+
- ✅ Need most recent records first (e.g., latest orders, recent inventory updates)
|
|
1483
|
+
- ✅ Time-bounded reverse traversal for auditing
|
|
1484
|
+
- ✅ Display newest-first in UI/reports
|
|
1485
|
+
- ❌ **Don't use for standard incremental sync** - use forward pagination (default)
|
|
1486
|
+
|
|
1487
|
+
**GraphQL Query Requirements**:
|
|
1488
|
+
|
|
1489
|
+
Your query must support backward pagination by including `$last` and `$before`:
|
|
1490
|
+
|
|
1491
|
+
```graphql
|
|
1492
|
+
query GetData(
|
|
1493
|
+
$retailerId: ID!
|
|
1494
|
+
$first: Int # For forward pagination
|
|
1495
|
+
$after: String # For forward pagination
|
|
1496
|
+
$last: Int # For backward pagination
|
|
1497
|
+
$before: String # For backward pagination
|
|
1498
|
+
) {
|
|
1499
|
+
data(retailerId: $retailerId, first: $first, after: $after, last: $last, before: $before) {
|
|
1500
|
+
edges {
|
|
1501
|
+
cursor # ✅ REQUIRED
|
|
1502
|
+
node {
|
|
1503
|
+
id
|
|
1504
|
+
createdAt
|
|
1505
|
+
# ... other fields
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
pageInfo {
|
|
1509
|
+
hasNextPage # For forward
|
|
1510
|
+
hasPreviousPage # ✅ REQUIRED for backward
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
**Implementation**:
|
|
1517
|
+
|
|
1518
|
+
```typescript
|
|
1519
|
+
// Backward pagination - newest records first
|
|
1520
|
+
const result = await orchestrator.extract({
|
|
1521
|
+
query: YOUR_QUERY,
|
|
1522
|
+
resultPath: 'data.edges.node',
|
|
1523
|
+
variables: {
|
|
1524
|
+
retailerId,
|
|
1525
|
+
dateRangeFilter: { from: bufferedLastRunTime, to: effectiveEndTime },
|
|
1526
|
+
// ❌ Don't include last/before - orchestrator injects them
|
|
1527
|
+
},
|
|
1528
|
+
pageSize: 200,
|
|
1529
|
+
direction: 'backward', // ✅ Enable reverse pagination
|
|
1530
|
+
maxRecords: 10000,
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
// Records are returned in reverse chronological order
|
|
1534
|
+
log.info('Newest record', { createdAt: result.data[0].createdAt });
|
|
1535
|
+
log.info('Oldest record', { createdAt: result.data[result.data.length - 1].createdAt });
|
|
1536
|
+
```
|
|
1537
|
+
|
|
1538
|
+
**Key Differences from Forward Pagination**:
|
|
1539
|
+
|
|
1540
|
+
| Aspect | Forward (Default) | Backward |
|
|
1541
|
+
| ---------------------- | -------------------------------- | ----------------------- |
|
|
1542
|
+
| **Direction** | `direction: 'forward'` (default) | `direction: 'backward'` |
|
|
1543
|
+
| **Variables Injected** | `first`, `after` | `last`, `before` |
|
|
1544
|
+
| **PageInfo Field** | `hasNextPage` | `hasPreviousPage` |
|
|
1545
|
+
| **Cursor Source** | Last edge of page | First edge of page |
|
|
1546
|
+
| **Record Order** | Oldest → Newest | Newest → Oldest |
|
|
1547
|
+
|
|
1548
|
+
**Important Notes**:
|
|
1549
|
+
|
|
1550
|
+
1. **Orchestrator injects variables**: Don't pass `last` or `before` in your variables object - the orchestrator injects them based on `pageSize` and cursor tracking.
|
|
1551
|
+
|
|
1552
|
+
2. **Query signature**: Your GraphQL query must declare `$last` and `$before` parameters even if you don't pass them explicitly.
|
|
1553
|
+
|
|
1554
|
+
3. **PageInfo requirement**: Response must include `pageInfo.hasPreviousPage` or the orchestrator will throw an error.
|
|
1555
|
+
|
|
1556
|
+
4. **Cursor requirement**: Each edge must include `cursor` field for pagination to work.
|
|
1557
|
+
|
|
1558
|
+
**Example: Extract Latest 1000 Orders**
|
|
1559
|
+
|
|
1560
|
+
```typescript
|
|
1561
|
+
const latestOrders = await orchestrator.extract({
|
|
1562
|
+
query: ORDERS_QUERY,
|
|
1563
|
+
resultPath: 'orders.edges.node',
|
|
1564
|
+
variables: {
|
|
1565
|
+
retailerId,
|
|
1566
|
+
statuses: ['BOOKED', 'ALLOCATED'],
|
|
1567
|
+
},
|
|
1568
|
+
direction: 'backward', // Start from newest
|
|
1569
|
+
maxRecords: 1000, // Stop after 1000 records
|
|
1570
|
+
pageSize: 100, // 100 per page = 10 pages
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
// latestOrders.data[0] is the newest order
|
|
1574
|
+
// latestOrders.data[999] is the 1000th newest order
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
**When to Use Forward vs Backward**:
|
|
1578
|
+
|
|
1579
|
+
```typescript
|
|
1580
|
+
// ✅ Forward (default) - For incremental sync
|
|
1581
|
+
const incrementalData = await orchestrator.extract({
|
|
1582
|
+
query: YOUR_QUERY,
|
|
1583
|
+
resultPath: 'data.edges.node',
|
|
1584
|
+
variables: {
|
|
1585
|
+
dateRangeFilter: { from: lastSyncTime, to: now },
|
|
1586
|
+
},
|
|
1587
|
+
// direction defaults to 'forward'
|
|
1588
|
+
// Processes oldest → newest for proper sequencing
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
// ✅ Backward - For "latest N records" use cases
|
|
1592
|
+
const latestData = await orchestrator.extract({
|
|
1593
|
+
query: YOUR_QUERY,
|
|
1594
|
+
resultPath: 'data.edges.node',
|
|
1595
|
+
direction: 'backward',
|
|
1596
|
+
maxRecords: 100, // Just get latest 100
|
|
1597
|
+
// Gets newest → oldest
|
|
1598
|
+
});
|
|
1599
|
+
```
|
|
1600
|
+
|
|
1601
|
+
**Pagination Variables Reference**:
|
|
1602
|
+
|
|
1603
|
+
| Variable | Forward | Backward | Injected By | Notes |
|
|
1604
|
+
| -------- | ----------- | ----------- | ------------ | ------------------------ |
|
|
1605
|
+
| `first` | ✅ Used | ❌ Not used | Orchestrator | From `pageSize` |
|
|
1606
|
+
| `after` | ✅ Used | ❌ Not used | Orchestrator | From cursor (last edge) |
|
|
1607
|
+
| `last` | ❌ Not used | ✅ Used | Orchestrator | From `pageSize` |
|
|
1608
|
+
| `before` | ❌ Not used | ✅ Used | Orchestrator | From cursor (first edge) |
|
|
1609
|
+
|
|
1610
|
+
**Common Mistakes to Avoid**:
|
|
1611
|
+
|
|
1612
|
+
```typescript
|
|
1613
|
+
// ❌ WRONG - Don't pass pagination variables
|
|
1614
|
+
const result = await orchestrator.extract({
|
|
1615
|
+
variables: {
|
|
1616
|
+
last: 200, // ❌ Orchestrator will override this
|
|
1617
|
+
before: cursor, // ❌ Orchestrator manages cursor
|
|
1618
|
+
},
|
|
1619
|
+
direction: 'backward',
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
// ✅ CORRECT - Let orchestrator inject pagination
|
|
1623
|
+
const result = await orchestrator.extract({
|
|
1624
|
+
variables: {
|
|
1625
|
+
retailerId, // ✅ Your business variables only
|
|
1626
|
+
},
|
|
1627
|
+
pageSize: 200, // ✅ Orchestrator uses this for last/before
|
|
1628
|
+
direction: 'backward',
|
|
1629
|
+
});
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
#### Optional: Reverse Pagination
|
|
1633
|
+
|
|
1634
|
+
- Forward remains default. Reverse requires $last/$before and pageInfo.hasPreviousPage.
|
|
1635
|
+
|
|
1636
|
+
GraphQL:
|
|
1637
|
+
|
|
1638
|
+
```graphql
|
|
1639
|
+
query GetInventoryQuantitiesBackward($retailerId: ID!, $last: Int!, $before: String) {
|
|
1640
|
+
inventoryQuantities(retailerId: $retailerId, last: $last, before: $before) {
|
|
1641
|
+
edges {
|
|
1642
|
+
cursor
|
|
1643
|
+
node {
|
|
1644
|
+
id
|
|
1645
|
+
ref
|
|
1646
|
+
updatedOn
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
pageInfo {
|
|
1650
|
+
hasPreviousPage
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
```
|
|
1655
|
+
|
|
1656
|
+
SDK:
|
|
1657
|
+
|
|
1658
|
+
```typescript
|
|
1659
|
+
await orchestrator.extract({
|
|
1660
|
+
query: INVENTORY_QUANTITIES_BACKWARD_QUERY,
|
|
1661
|
+
resultPath: 'inventoryQuantities.edges.node',
|
|
1662
|
+
variables: { retailerId },
|
|
1663
|
+
pageSize,
|
|
1664
|
+
direction: 'backward',
|
|
1665
|
+
});
|
|
1666
|
+
```
|
|
1667
|
+
|
|
1668
|
+
---
|
|
1669
|
+
|
|
1670
|
+
## Testing Checklist
|
|
1671
|
+
|
|
1672
|
+
**Before production deployment:**
|
|
1673
|
+
|
|
1674
|
+
### 1. Schema Validation
|
|
1675
|
+
|
|
1676
|
+
- [ ] Run `npx fc-connect introspect-schema --url <your-graphql-url>`
|
|
1677
|
+
- [ ] Run `npx fc-connect validate-schema --mapping ./config/inventory-quantities.export.json --schema ./fluent-schema.json`
|
|
1678
|
+
- [ ] Run `npx fc-connect analyze-coverage --mapping ./config/inventory-quantities.export.json --schema ./fluent-schema.json`
|
|
1679
|
+
- [ ] Verify all `source` paths in mapping exist in GraphQL schema
|
|
1680
|
+
- [ ] Verify query structure matches schema (fields, types, filters)
|
|
1681
|
+
|
|
1682
|
+
### 2. Extraction Testing
|
|
1683
|
+
|
|
1684
|
+
- [ ] Test with small dataset first (maxRecords=10)
|
|
1685
|
+
- [ ] Verify ExtractionOrchestrator pagination works correctly
|
|
1686
|
+
- [ ] Test with multiple pages of data (verify cursor handling)
|
|
1687
|
+
- [ ] Verify date range filtering (updatedOn filter)
|
|
1688
|
+
- [ ] Test empty result handling (no records in date range)
|
|
1689
|
+
- [ ] Verify extraction stops at maxRecords limit
|
|
1690
|
+
|
|
1691
|
+
### 3. Mapping Testing
|
|
1692
|
+
|
|
1693
|
+
- [ ] Verify required fields are populated
|
|
1694
|
+
- [ ] Verify SDK resolvers work correctly (sdk.trim, sdk.parseInt, sdk.formatDate, etc.)
|
|
1695
|
+
- [ ] Test custom resolvers with edge cases (if any)
|
|
1696
|
+
- [ ] Verify nested field extraction
|
|
1697
|
+
- [ ] Test with null/missing fields
|
|
1698
|
+
- [ ] Verify mapping error collection works
|
|
1699
|
+
|
|
1700
|
+
### 4. JSON Generation Testing
|
|
1701
|
+
|
|
1702
|
+
- [ ] Verify JSON structure matches expected format
|
|
1703
|
+
- [ ] Test JSON validation against schema (if applicable)
|
|
1704
|
+
- [ ] Verify proper nesting and structure
|
|
1705
|
+
- [ ] Test with large datasets (>1000 records)
|
|
1706
|
+
- [ ] Verify UTF-8 encoding
|
|
1707
|
+
- [ ] Test special character escaping
|
|
1708
|
+
|
|
1709
|
+
### 5. S3 Upload Testing
|
|
1710
|
+
|
|
1711
|
+
- [ ] Test S3 connection and authentication
|
|
1712
|
+
- [ ] Verify file upload to correct bucket and path
|
|
1713
|
+
- [ ] Test file naming convention (timestamp format)
|
|
1714
|
+
- [ ] Verify S3 object metadata
|
|
1715
|
+
- [ ] Test upload retry logic (simulate network failure)
|
|
1716
|
+
- [ ] Verify file permissions and ACLs
|
|
1717
|
+
|
|
1718
|
+
### 6. State Management Testing
|
|
1719
|
+
|
|
1720
|
+
- [ ] Verify overlap buffer prevents missed records (60-second default)
|
|
1721
|
+
- [ ] Test state recovery after extraction failure
|
|
1722
|
+
- [ ] Verify timestamp saved WITHOUT buffer (MAX(updatedOn))
|
|
1723
|
+
- [ ] Test first run with no previous state (uses fallbackStartDate)
|
|
1724
|
+
- [ ] Verify state update only happens on successful upload
|
|
1725
|
+
- [ ] Test manual date override (doesn't update state)
|
|
1726
|
+
|
|
1727
|
+
### 7. Job Tracking Testing
|
|
1728
|
+
|
|
1729
|
+
- [ ] Test job creation with JobTracker
|
|
1730
|
+
- [ ] Verify job status updates at each stage
|
|
1731
|
+
- [ ] Test job completion with metadata
|
|
1732
|
+
- [ ] Test job failure handling
|
|
1733
|
+
- [ ] Query job status via webhook endpoint
|
|
1734
|
+
- [ ] Verify job status persists in KV store
|
|
1735
|
+
|
|
1736
|
+
### 8. Error Handling Testing
|
|
1737
|
+
|
|
1738
|
+
- [ ] Test with invalid GraphQL query
|
|
1739
|
+
- [ ] Test with mapping errors (invalid field paths)
|
|
1740
|
+
- [ ] Test with S3 connection failures
|
|
1741
|
+
- [ ] Test with authentication failures
|
|
1742
|
+
- [ ] Test with network timeouts
|
|
1743
|
+
- [ ] Verify error logging includes context (jobId, stage, error details)
|
|
1744
|
+
- [ ] Test error threshold logic (if applicable)
|
|
1745
|
+
|
|
1746
|
+
### 9. Staging Environment Testing
|
|
1747
|
+
|
|
1748
|
+
- [ ] Run full extraction in staging environment
|
|
1749
|
+
- [ ] Verify JSON file format with downstream system
|
|
1750
|
+
- [ ] Monitor extraction duration and resource usage
|
|
1751
|
+
- [ ] Test with production-like data volumes
|
|
1752
|
+
- [ ] Verify no performance degradation over time
|
|
1753
|
+
|
|
1754
|
+
### 10. Integration Testing
|
|
1755
|
+
|
|
1756
|
+
- [ ] Test scheduled workflow (cron trigger)
|
|
1757
|
+
- [ ] Test ad hoc webhook trigger
|
|
1758
|
+
- [ ] Test job status query webhook
|
|
1759
|
+
- [ ] Verify activation variables are read correctly
|
|
1760
|
+
- [ ] Test with different extraction modes (incremental, date range)
|
|
1761
|
+
- [ ] End-to-end test: trigger → extract → transform → upload → verify file
|
|
1762
|
+
|
|
1763
|
+
---
|
|
1764
|
+
## Monitoring & Alerting
|
|
1765
|
+
|
|
1766
|
+
### Success Response Example
|
|
1767
|
+
|
|
1768
|
+
```json
|
|
1769
|
+
{
|
|
1770
|
+
"success": true,
|
|
1771
|
+
"jobId": "SCHEDULED_IQ_20251102_140000_abc123",
|
|
1772
|
+
"recordsExtracted": 1523,
|
|
1773
|
+
"fileName": "inventory-quantities-2025-11-02T14-00-00-000Z.json",
|
|
1774
|
+
"s3Path": "s3://bucket/inventory-quantities/inventory-quantities-2025-11-02T14-00-00-000Z.json",
|
|
1775
|
+
"metrics": {
|
|
1776
|
+
"extractionDurationMs": 12543,
|
|
1777
|
+
"totalPages": 8,
|
|
1778
|
+
"pageSize": 200,
|
|
1779
|
+
"mappingErrors": 0,
|
|
1780
|
+
"fileSizeBytes": 524288,
|
|
1781
|
+
"uploadDurationMs": 1234
|
|
1782
|
+
},
|
|
1783
|
+
"timestamps": {
|
|
1784
|
+
"extractionStart": "2025-11-02T14:00:00.000Z",
|
|
1785
|
+
"extractionEnd": "2025-11-02T14:00:12.543Z",
|
|
1786
|
+
"uploadComplete": "2025-11-02T14:00:13.777Z"
|
|
1787
|
+
},
|
|
1788
|
+
"state": {
|
|
1789
|
+
"previousTimestamp": "2025-11-02T13:00:00.000Z",
|
|
1790
|
+
"newTimestamp": "2025-11-02T13:59:58.123Z",
|
|
1791
|
+
"stateUpdated": true,
|
|
1792
|
+
"overlapBufferSeconds": 60
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
```
|
|
1796
|
+
|
|
1797
|
+
### Error Response Example
|
|
1798
|
+
|
|
1799
|
+
```json
|
|
1800
|
+
{
|
|
1801
|
+
"success": false,
|
|
1802
|
+
"jobId": "ADHOC_IQ_20251102_140500_xyz789",
|
|
1803
|
+
"error": "S3 upload failed: Connection timeout",
|
|
1804
|
+
"errorCategory": "NETWORK",
|
|
1805
|
+
"recordsExtracted": 0,
|
|
1806
|
+
"stage": "s3_upload",
|
|
1807
|
+
"details": {
|
|
1808
|
+
"message": "Failed to upload file after 3 retry attempts",
|
|
1809
|
+
"retryAttempts": 3,
|
|
1810
|
+
"lastError": "ETIMEDOUT: Connection timed out after 30000ms"
|
|
1811
|
+
},
|
|
1812
|
+
"state": {
|
|
1813
|
+
"stateUpdated": false,
|
|
1814
|
+
"willRetryNextRun": true,
|
|
1815
|
+
"note": "State not advanced - next extraction will retry same time window"
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
### Key Metrics to Track
|
|
1821
|
+
|
|
1822
|
+
```typescript
|
|
1823
|
+
const METRICS = {
|
|
1824
|
+
// Extraction Performance
|
|
1825
|
+
extractionDurationMs: Date.now() - extractionStart,
|
|
1826
|
+
recordCount: records.length,
|
|
1827
|
+
pageCount: extractionResult.stats.totalPages,
|
|
1828
|
+
avgRecordsPerPage: records.length / extractionResult.stats.totalPages,
|
|
1829
|
+
|
|
1830
|
+
// Transformation Performance
|
|
1831
|
+
transformedCount: transformedRecords.length,
|
|
1832
|
+
failedCount: mappingErrors.length,
|
|
1833
|
+
errorRate: ((mappingErrors.length / records.length) * 100).toFixed(2) + '%',
|
|
1834
|
+
|
|
1835
|
+
// File Generation
|
|
1836
|
+
fileSizeMB: (jsonContent.length / (1024 * 1024)).toFixed(2),
|
|
1837
|
+
|
|
1838
|
+
// Upload Performance
|
|
1839
|
+
uploadDurationMs: uploadEnd - uploadStart,
|
|
1840
|
+
uploadSpeedMBps: (fileSizeMB / (uploadDurationMs / 1000)).toFixed(2),
|
|
1841
|
+
|
|
1842
|
+
// State Management
|
|
1843
|
+
timeSinceLastRun: Date.now() - new Date(lastTimestamp).getTime(),
|
|
1844
|
+
recordsPerMinute: (records.length / (extractionDurationMs / 60000)).toFixed(0),
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
log.info('Extraction metrics', metrics);
|
|
1848
|
+
```
|
|
1849
|
+
|
|
1850
|
+
### Alert Thresholds
|
|
1851
|
+
|
|
1852
|
+
```typescript
|
|
1853
|
+
const ALERT_THRESHOLDS = {
|
|
1854
|
+
// Duration Alerts
|
|
1855
|
+
EXTRACTION_DURATION_MS: 5 * 60 * 1000, // 5 minutes
|
|
1856
|
+
UPLOAD_DURATION_MS: 2 * 60 * 1000, // 2 minutes
|
|
1857
|
+
TOTAL_DURATION_MS: 10 * 60 * 1000, // 10 minutes
|
|
1858
|
+
|
|
1859
|
+
// Error Rate Alerts
|
|
1860
|
+
MAX_ERROR_RATE: 0.05, // 5% mapping errors
|
|
1861
|
+
MAX_VALIDATION_FAILURES: 0.02, // 2% validation failures
|
|
1862
|
+
|
|
1863
|
+
// Volume Alerts
|
|
1864
|
+
MAX_RECORDS_PER_RUN: 100000,
|
|
1865
|
+
MIN_RECORDS_WARNING: 0, // Alert if no records found
|
|
1866
|
+
MAX_FILE_SIZE_MB: 150, // 150MB
|
|
1867
|
+
|
|
1868
|
+
// State Alerts
|
|
1869
|
+
MAX_TIME_SINCE_LAST_RUN_HOURS: 25, // Alert if >25 hours (should run hourly)
|
|
1870
|
+
MAX_OVERLAP_BUFFER_SECONDS: 300, // Alert if buffer >5 minutes
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
// Check thresholds
|
|
1874
|
+
if (metrics.extractionDurationMs > ALERT_THRESHOLDS.EXTRACTION_DURATION_MS) {
|
|
1875
|
+
log.warn('Extraction duration exceeded threshold', {
|
|
1876
|
+
duration: metrics.extractionDurationMs,
|
|
1877
|
+
threshold: ALERT_THRESHOLDS.EXTRACTION_DURATION_MS,
|
|
1878
|
+
recommendation: 'Consider reducing maxRecords or increasing extraction frequency'
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
```
|
|
1882
|
+
|
|
1883
|
+
### Monitoring Dashboard Queries
|
|
1884
|
+
|
|
1885
|
+
**Versori Platform Logs Query:**
|
|
1886
|
+
|
|
1887
|
+
```
|
|
1888
|
+
# Successful extractions
|
|
1889
|
+
log_level:info AND message:"Extraction complete" AND jobId:*
|
|
1890
|
+
|
|
1891
|
+
# Failed extractions
|
|
1892
|
+
log_level:error AND message:"Extraction workflow failed" AND jobId:*
|
|
1893
|
+
|
|
1894
|
+
# Performance issues
|
|
1895
|
+
extractionDurationMs:>300000 OR uploadDurationMs:>120000
|
|
1896
|
+
|
|
1897
|
+
# High error rates
|
|
1898
|
+
errorRate:>5
|
|
1899
|
+
|
|
1900
|
+
# State management issues
|
|
1901
|
+
stateUpdated:false AND success:true
|
|
1902
|
+
```
|
|
1903
|
+
|
|
1904
|
+
### Common Issues and Solutions
|
|
1905
|
+
|
|
1906
|
+
**Issue**: "Extraction timeout after 10 minutes"
|
|
1907
|
+
|
|
1908
|
+
- **Cause**: Too many records in single extraction
|
|
1909
|
+
- **Fix**: Reduce maxRecords, increase extraction frequency, or optimize query filters
|
|
1910
|
+
- **Prevention**: Monitor recordCount trends, set appropriate maxRecords
|
|
1911
|
+
|
|
1912
|
+
**Issue**: "Mapping errors for 50% of records"
|
|
1913
|
+
|
|
1914
|
+
- **Cause**: Schema mismatch between GraphQL response and mapping config
|
|
1915
|
+
- **Fix**: Run schema validation, update mapping config paths
|
|
1916
|
+
- **Prevention**: Use `npx fc-connect validate-schema` before deployment
|
|
1917
|
+
|
|
1918
|
+
**Issue**: "S3 connection timeout"
|
|
1919
|
+
|
|
1920
|
+
- **Cause**: Network issues, firewall, or connection pool exhaustion
|
|
1921
|
+
- **Fix**: Check S3 credentials, verify network connectivity
|
|
1922
|
+
- **Prevention**: Implement connection health checks, monitor connection status
|
|
1923
|
+
|
|
1924
|
+
**Issue**: "State not updating after successful extraction"
|
|
1925
|
+
|
|
1926
|
+
- **Cause**: KV write failure or intentional retry logic
|
|
1927
|
+
- **Fix**: Check KV logs, verify state update code executed
|
|
1928
|
+
- **Prevention**: Add KV write verification, log state updates explicitly
|
|
1929
|
+
|
|
1930
|
+
**Issue**: "First run exceeds record limits"
|
|
1931
|
+
|
|
1932
|
+
- **Cause**: No previous timestamp, fetches all historical records
|
|
1933
|
+
- **Fix**: Set fallbackStartDate close to current date, apply additional filters
|
|
1934
|
+
- **Prevention**: Use appropriate fallbackStartDate for initial runs
|
|
1935
|
+
|
|
1936
|
+
**Issue**: "Excessive duplicate records in output"
|
|
1937
|
+
|
|
1938
|
+
- **Cause**: Overlap buffer (expected) or timestamp not saved correctly
|
|
1939
|
+
- **Fix**: Verify newTimestamp saved WITHOUT buffer, check state persistence
|
|
1940
|
+
- **Prevention**: Monitor duplicate rates, verify state update logic
|
|
1941
|
+
|
|
1942
|
+
---
|
|
1943
|
+
|
|
1944
|
+
## Troubleshooting Quick Reference
|
|
1945
|
+
|
|
1946
|
+
| Error Message | Likely Cause | Solution |
|
|
1947
|
+
|--------------|--------------|----------|
|
|
1948
|
+
| "Failed to create Fluent Commerce client" | Authentication failure | Check OAuth2 credentials, verify connection config |
|
|
1949
|
+
| "GraphQL query validation error" | Invalid query syntax | Validate query against schema with introspection tool |
|
|
1950
|
+
| "Pagination cursor invalid" | Stale cursor or query change | Reset extraction, verify cursor handling in query |
|
|
1951
|
+
| "Mapping failed: field not found" | Schema mismatch | Run schema validation, update mapping paths |
|
|
1952
|
+
| "S3 authentication failed" | Invalid credentials | Verify S3 credentials in activation variables |
|
|
1953
|
+
| "Connection pool exhausted" | Too many concurrent requests | Reduce concurrency, increase pool size |
|
|
1954
|
+
| "KV operation failed" | Versori KV issue | Check Versori platform status, retry operation |
|
|
1955
|
+
| "Job status not found" | Invalid jobId or expired | Verify jobId format, check KV retention policy |
|
|
1956
|
+
| "Memory limit exceeded" | Dataset too large | Reduce maxRecords, enable streaming mode |
|
|
1957
|
+
| "JSON generation failed" | Format-specific error | Check JSON generation logic, validate output |
|
|
1958
|
+
|
|
1959
|
+
---
|