@fluentcommerce/fc-connect-sdk 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -2
- package/README.md +39 -0
- package/dist/cjs/auth/index.d.ts +3 -0
- package/dist/cjs/auth/index.js +13 -0
- package/dist/cjs/auth/profile-loader.d.ts +18 -0
- package/dist/cjs/auth/profile-loader.js +208 -0
- package/dist/cjs/client-factory.d.ts +4 -0
- package/dist/cjs/client-factory.js +10 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/auth/index.d.ts +3 -0
- package/dist/esm/auth/index.js +2 -0
- package/dist/esm/auth/profile-loader.d.ts +18 -0
- package/dist/esm/auth/profile-loader.js +169 -0
- package/dist/esm/client-factory.d.ts +4 -0
- package/dist/esm/client-factory.js +9 -0
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/auth/index.d.ts +3 -0
- package/dist/types/auth/profile-loader.d.ts +18 -0
- package/dist/types/client-factory.d.ts +4 -0
- package/dist/types/index.d.ts +3 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -482
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
|
@@ -1,1140 +1,1140 @@
|
|
|
1
|
-
# Standalone: Fluent GraphQL → S3 Parquet Export
|
|
2
|
-
|
|
3
|
-
**FC Connect SDK Use Case Guide**
|
|
4
|
-
|
|
5
|
-
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
6
|
-
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
7
|
-
|
|
8
|
-
**Context**: Node.js script that extracts inventory/orders from Fluent Commerce via GraphQL and exports to S3 as Parquet files
|
|
9
|
-
|
|
10
|
-
**Complexity**: Medium
|
|
11
|
-
|
|
12
|
-
**Runtime**: Node.js ≥18 / Deno
|
|
13
|
-
|
|
14
|
-
**Estimated Lines**: ~500 lines
|
|
15
|
-
|
|
16
|
-
## What You'll Build
|
|
17
|
-
|
|
18
|
-
- Standalone Node.js/Deno extraction script
|
|
19
|
-
- OAuth2 authentication with Fluent Commerce
|
|
20
|
-
- GraphQL query execution with auto-pagination
|
|
21
|
-
- Data transformation with UniversalMapper
|
|
22
|
-
- Parquet file generation
|
|
23
|
-
- S3 upload with metadata
|
|
24
|
-
- Incremental extraction (last updated timestamp tracking)
|
|
25
|
-
- Error handling and performance metrics
|
|
26
|
-
- Scheduled execution support
|
|
27
|
-
|
|
28
|
-
## SDK Methods Used
|
|
29
|
-
|
|
30
|
-
- `createClient({ config: { baseUrl, clientId, clientSecret, retailerId } })` - OAuth2 client creation
|
|
31
|
-
- `client.graphql({ query, variables, pagination })` - Execute GraphQL query with auto-pagination
|
|
32
|
-
- `UniversalMapper(extractionConfig)` - Transform data for export format
|
|
33
|
-
- `ParquetParserService.writeParquetContent(data, options)` - Generate Parquet buffer
|
|
34
|
-
- `S3DataSource.uploadFile(key, buffer, options)` - Upload to S3
|
|
35
|
-
- `StateService` - Track incremental extraction state
|
|
36
|
-
|
|
37
|
-
## Complete Working Code
|
|
38
|
-
|
|
39
|
-
### 1. Project Setup
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
# Initialize project
|
|
43
|
-
mkdir fluent-graphql-export
|
|
44
|
-
cd fluent-graphql-export
|
|
45
|
-
npm init -y
|
|
46
|
-
|
|
47
|
-
# Install dependencies
|
|
48
|
-
npm install @fluentcommerce/fc-connect-sdk
|
|
49
|
-
npm install dotenv
|
|
50
|
-
|
|
51
|
-
# Create directory structure
|
|
52
|
-
mkdir -p src config logs state
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### 2. Environment Configuration
|
|
56
|
-
|
|
57
|
-
Create `.env`:
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
# Fluent Commerce API
|
|
61
|
-
FLUENT_BASE_URL=https://your-account.api.fluentretail.com
|
|
62
|
-
FLUENT_CLIENT_ID=your-oauth-client-id
|
|
63
|
-
FLUENT_CLIENT_SECRET=your-oauth-client-secret
|
|
64
|
-
FLUENT_RETAILER_ID=your-retailer-id
|
|
65
|
-
|
|
66
|
-
# AWS S3 (Target)
|
|
67
|
-
TARGET_AWS_ACCESS_KEY_ID=your-aws-access-key
|
|
68
|
-
TARGET_AWS_SECRET_ACCESS_KEY=your-aws-secret-key
|
|
69
|
-
TARGET_AWS_REGION=us-east-1
|
|
70
|
-
TARGET_S3_BUCKET=fluent-exports
|
|
71
|
-
|
|
72
|
-
# Export Configuration
|
|
73
|
-
EXPORT_ENTITY_TYPE=inventory # or 'orders', 'products'
|
|
74
|
-
EXPORT_PAGE_SIZE=100
|
|
75
|
-
EXPORT_MAX_RECORDS=10000
|
|
76
|
-
EXPORT_INCREMENTAL=true
|
|
77
|
-
STATE_FILE=./state/last-export.json
|
|
78
|
-
LOG_LEVEL=info
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### 3. Extraction Configuration
|
|
82
|
-
|
|
83
|
-
Create `config/extraction-mappings.json`:
|
|
84
|
-
|
|
85
|
-
```json
|
|
86
|
-
{
|
|
87
|
-
"inventory": {
|
|
88
|
-
"fields": {
|
|
89
|
-
"sku": { "source": "productRef", "resolver": "sdk.uppercase" },
|
|
90
|
-
"location_code": { "source": "locationRef", "resolver": "sdk.toString" },
|
|
91
|
-
"quantity": { "source": "qty", "resolver": "sdk.parseInt" },
|
|
92
|
-
"available_qty": { "source": "availableQty", "resolver": "sdk.parseInt" },
|
|
93
|
-
"reserved_qty": { "source": "reservedQty", "resolver": "sdk.parseInt" },
|
|
94
|
-
"status": { "source": "status", "resolver": "sdk.lowercase" },
|
|
95
|
-
"last_updated": { "source": "updatedOn", "resolver": "sdk.formatDate" },
|
|
96
|
-
"extracted_at": {
|
|
97
|
-
"source": null,
|
|
98
|
-
"resolver": "custom.timestamp",
|
|
99
|
-
"defaultValue": "{{ now }}"
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
"orders": {
|
|
104
|
-
"fields": {
|
|
105
|
-
"order_id": { "source": "ref", "resolver": "sdk.toString" },
|
|
106
|
-
"order_status": { "source": "status", "resolver": "sdk.lowercase" },
|
|
107
|
-
"total_amount": { "source": "totalPrice", "resolver": "sdk.parseFloat" },
|
|
108
|
-
"tax_amount": { "source": "totalTaxPrice", "resolver": "sdk.parseFloat" },
|
|
109
|
-
"customer_email": { "source": "customer.email", "resolver": "sdk.toString" },
|
|
110
|
-
"customer_name": {
|
|
111
|
-
"source": null,
|
|
112
|
-
"resolver": "custom.fullName"
|
|
113
|
-
},
|
|
114
|
-
"created_date": { "source": "createdOn", "resolver": "sdk.formatDate" },
|
|
115
|
-
"item_count": {
|
|
116
|
-
"source": "items.edges",
|
|
117
|
-
"resolver": "custom.arrayLength"
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### 4. GraphQL Queries
|
|
125
|
-
|
|
126
|
-
Create `config/queries.json`:
|
|
127
|
-
|
|
128
|
-
```json
|
|
129
|
-
{
|
|
130
|
-
"inventory": {
|
|
131
|
-
"query": "query GetInventory($retailerId: ID!, $first: Int!, $after: String, $updatedFrom: DateTime) {\n inventoryPositions(\n retailerId: $retailerId\n first: $first\n after: $after\n updatedOn: { from: $updatedFrom }\n ) {\n edges {\n node {\n id\n ref\n productRef\n locationRef\n qty\n availableQty\n reservedQty\n status\n createdOn\n updatedOn\n }\n cursor\n }\n pageInfo {\n hasNextPage\n }\n }\n}",
|
|
132
|
-
"connectionPath": "inventoryPositions"
|
|
133
|
-
},
|
|
134
|
-
"orders": {
|
|
135
|
-
"query": "query GetOrders($retailerId: ID!, $first: Int!, $after: String, $createdFrom: DateTime) {\n orders(\n retailerId: $retailerId\n first: $first\n after: $after\n createdOn: { from: $createdFrom }\n ) {\n edges {\n node {\n id\n ref\n type\n status\n createdOn\n totalPrice\n totalTaxPrice\n customer {\n ref\n email\n firstName\n lastName\n }\n items {\n edges {\n node {\n ref\n productRef\n quantity\n price\n }\n }\n }\n }\n cursor\n }\n pageInfo {\n hasNextPage\n }\n }\n}",
|
|
136
|
-
"connectionPath": "orders"
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### 5. Main Extraction Script
|
|
142
|
-
|
|
143
|
-
Create `src/export-to-s3.ts`:
|
|
144
|
-
|
|
145
|
-
> **⚠️ RUNTIME COMPATIBILITY NOTE:**
|
|
146
|
-
> This template includes `import { Buffer } from 'node:buffer';` for Deno compatibility.
|
|
147
|
-
> While Buffer is globally available in Node.js, including this import ensures the code works
|
|
148
|
-
> across all runtimes (Node.js, Deno, Versori) without modification.
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
import * as dotenv from 'dotenv';
|
|
152
|
-
import * as fs from 'fs/promises';
|
|
153
|
-
import * as path from 'path';
|
|
154
|
-
import { Buffer } from 'node:buffer'; // Required for Deno compatibility
|
|
155
|
-
import {
|
|
156
|
-
createClient,
|
|
157
|
-
FluentClient,
|
|
158
|
-
UniversalMapper,
|
|
159
|
-
ParquetParserService,
|
|
160
|
-
S3DataSource,
|
|
161
|
-
createConsoleLogger,
|
|
162
|
-
toStructuredLogger
|
|
163
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
164
|
-
|
|
165
|
-
// Load environment variables
|
|
166
|
-
dotenv.config();
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* State management for incremental extraction
|
|
170
|
-
*/
|
|
171
|
-
interface ExportState {
|
|
172
|
-
lastExportTimestamp: string;
|
|
173
|
-
lastEntityType: string;
|
|
174
|
-
totalRecordsExported: number;
|
|
175
|
-
lastExportDuration: number;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
class StateManager {
|
|
179
|
-
constructor(private stateFilePath: string) {}
|
|
180
|
-
|
|
181
|
-
async loadState(): Promise<ExportState | null> {
|
|
182
|
-
try {
|
|
183
|
-
const data = await fs.readFile(this.stateFilePath, 'utf-8');
|
|
184
|
-
return JSON.parse(data);
|
|
185
|
-
} catch {
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async saveState(state: ExportState): Promise<void> {
|
|
191
|
-
const dir = path.dirname(this.stateFilePath);
|
|
192
|
-
await fs.mkdir(dir, { recursive: true });
|
|
193
|
-
await fs.writeFile(this.stateFilePath, JSON.stringify(state, null, 2));
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Export metrics tracking
|
|
199
|
-
*/
|
|
200
|
-
interface ExportMetrics {
|
|
201
|
-
startTime: number;
|
|
202
|
-
endTime: number;
|
|
203
|
-
totalRecords: number;
|
|
204
|
-
totalPages: number;
|
|
205
|
-
queryTimeMs: number;
|
|
206
|
-
transformTimeMs: number;
|
|
207
|
-
parquetWriteTimeMs: number;
|
|
208
|
-
s3UploadTimeMs: number;
|
|
209
|
-
fileSizeBytes: number;
|
|
210
|
-
recordsPerSecond: number;
|
|
211
|
-
errors: string[];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* GraphQL to S3 Parquet Exporter
|
|
216
|
-
*/
|
|
217
|
-
class GraphQLParquetExporter {
|
|
218
|
-
private client: FluentClient;
|
|
219
|
-
private logger: LoggingService;
|
|
220
|
-
private stateManager: StateManager;
|
|
221
|
-
private mapper: UniversalMapper;
|
|
222
|
-
private parquetParser: ParquetParserService;
|
|
223
|
-
private s3: S3DataSource;
|
|
224
|
-
private queries: Record<string, any>;
|
|
225
|
-
private mappings: Record<string, any>;
|
|
226
|
-
|
|
227
|
-
constructor() {
|
|
228
|
-
this.logger = toStructuredLogger(createConsoleLogger(), {
|
|
229
|
-
logLevel: process.env.LOG_LEVEL || 'info'
|
|
230
|
-
});
|
|
231
|
-
this.stateManager = new StateManager(process.env.STATE_FILE || './state/last-export.json');
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async initialize(): Promise<void> {
|
|
235
|
-
this.logger.info('Initializing GraphQL Parquet Exporter');
|
|
236
|
-
|
|
237
|
-
// Create Fluent client with OAuth2
|
|
238
|
-
this.client = await createClient({
|
|
239
|
-
config: {
|
|
240
|
-
baseUrl: process.env.FLUENT_BASE_URL!,
|
|
241
|
-
clientId: process.env.FLUENT_CLIENT_ID!,
|
|
242
|
-
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
|
|
243
|
-
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
244
|
-
},
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Load queries configuration
|
|
248
|
-
this.queries = JSON.parse(await fs.readFile('config/queries.json', 'utf-8'));
|
|
249
|
-
|
|
250
|
-
// Load extraction mappings
|
|
251
|
-
this.mappings = JSON.parse(await fs.readFile('config/extraction-mappings.json', 'utf-8'));
|
|
252
|
-
|
|
253
|
-
// Initialize Parquet parser
|
|
254
|
-
this.parquetParser = new ParquetParserService(this.logger, {
|
|
255
|
-
batchSize: 5000,
|
|
256
|
-
enableStreaming: false,
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Initialize S3 data source for target
|
|
260
|
-
this.s3 = new S3DataSource(
|
|
261
|
-
{
|
|
262
|
-
type: 'S3_CSV',
|
|
263
|
-
connectionId: 'target-s3',
|
|
264
|
-
name: 'Export Target S3',
|
|
265
|
-
s3Config: {
|
|
266
|
-
bucket: process.env.TARGET_S3_BUCKET!,
|
|
267
|
-
region: process.env.TARGET_AWS_REGION!,
|
|
268
|
-
accessKeyId: process.env.TARGET_AWS_ACCESS_KEY_ID!,
|
|
269
|
-
secretAccessKey: process.env.TARGET_AWS_SECRET_ACCESS_KEY!,
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
this.logger
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
this.logger.info('Initialization complete');
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Execute extraction workflow
|
|
280
|
-
*/
|
|
281
|
-
async export(
|
|
282
|
-
entityType: string = process.env.EXPORT_ENTITY_TYPE || 'inventory'
|
|
283
|
-
): Promise<ExportMetrics> {
|
|
284
|
-
const metrics: ExportMetrics = {
|
|
285
|
-
startTime: Date.now(),
|
|
286
|
-
endTime: 0,
|
|
287
|
-
totalRecords: 0,
|
|
288
|
-
totalPages: 0,
|
|
289
|
-
queryTimeMs: 0,
|
|
290
|
-
transformTimeMs: 0,
|
|
291
|
-
parquetWriteTimeMs: 0,
|
|
292
|
-
s3UploadTimeMs: 0,
|
|
293
|
-
fileSizeBytes: 0,
|
|
294
|
-
recordsPerSecond: 0,
|
|
295
|
-
errors: [],
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
try {
|
|
299
|
-
this.logger.info(`Starting export for entity type: ${entityType}`);
|
|
300
|
-
|
|
301
|
-
// STEP 1: Determine incremental extraction timestamp
|
|
302
|
-
const fromTimestamp = await this.getIncrementalTimestamp(entityType);
|
|
303
|
-
|
|
304
|
-
// STEP 2: Execute GraphQL query with auto-pagination
|
|
305
|
-
const queryStart = Date.now();
|
|
306
|
-
const rawData = await this.executeQuery(entityType, fromTimestamp, metrics);
|
|
307
|
-
metrics.queryTimeMs = Date.now() - queryStart;
|
|
308
|
-
metrics.totalRecords = rawData.length;
|
|
309
|
-
this.logger.info(`Extracted ${rawData.length} records in ${metrics.queryTimeMs}ms`);
|
|
310
|
-
|
|
311
|
-
if (rawData.length === 0) {
|
|
312
|
-
this.logger.info('No new records to export');
|
|
313
|
-
metrics.endTime = Date.now();
|
|
314
|
-
return metrics;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// STEP 3: Transform data for export
|
|
318
|
-
const transformStart = Date.now();
|
|
319
|
-
const transformedData = await this.transformData(entityType, rawData);
|
|
320
|
-
metrics.transformTimeMs = Date.now() - transformStart;
|
|
321
|
-
this.logger.info(
|
|
322
|
-
`Transformed ${transformedData.length} records in ${metrics.transformTimeMs}ms`
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
// STEP 4: Generate Parquet file
|
|
326
|
-
const parquetStart = Date.now();
|
|
327
|
-
const parquetBuffer = await this.generateParquet(transformedData);
|
|
328
|
-
metrics.parquetWriteTimeMs = Date.now() - parquetStart;
|
|
329
|
-
metrics.fileSizeBytes = parquetBuffer.length;
|
|
330
|
-
this.logger.info(
|
|
331
|
-
`Generated Parquet file: ${parquetBuffer.length} bytes in ${metrics.parquetWriteTimeMs}ms`
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
// STEP 5: Upload to S3
|
|
335
|
-
const uploadStart = Date.now();
|
|
336
|
-
const s3Key = await this.uploadToS3(entityType, parquetBuffer, metrics);
|
|
337
|
-
metrics.s3UploadTimeMs = Date.now() - uploadStart;
|
|
338
|
-
this.logger.info(`Uploaded to S3: ${s3Key} in ${metrics.s3UploadTimeMs}ms`);
|
|
339
|
-
|
|
340
|
-
// STEP 6: Update state
|
|
341
|
-
await this.updateState(entityType, metrics);
|
|
342
|
-
|
|
343
|
-
metrics.endTime = Date.now();
|
|
344
|
-
metrics.recordsPerSecond = Math.round(
|
|
345
|
-
(metrics.totalRecords / (metrics.endTime - metrics.startTime)) * 1000
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
this.logger.info('Export completed successfully', {
|
|
349
|
-
entityType,
|
|
350
|
-
totalRecords: metrics.totalRecords,
|
|
351
|
-
totalPages: metrics.totalPages,
|
|
352
|
-
duration: metrics.endTime - metrics.startTime,
|
|
353
|
-
recordsPerSecond: metrics.recordsPerSecond,
|
|
354
|
-
s3Key,
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
return metrics;
|
|
358
|
-
} catch (error) {
|
|
359
|
-
metrics.errors.push((error as Error).message);
|
|
360
|
-
this.logger.error('Export failed', error as Error, { entityType });
|
|
361
|
-
throw error;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Get incremental extraction timestamp
|
|
367
|
-
*/
|
|
368
|
-
private async getIncrementalTimestamp(entityType: string): Promise<string | undefined> {
|
|
369
|
-
if (process.env.EXPORT_INCREMENTAL !== 'true') {
|
|
370
|
-
this.logger.info('Incremental extraction disabled, performing full export');
|
|
371
|
-
return undefined;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const state = await this.stateManager.loadState();
|
|
375
|
-
if (!state || state.lastEntityType !== entityType) {
|
|
376
|
-
this.logger.info('No previous state found, performing full export');
|
|
377
|
-
return undefined;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
this.logger.info(`Incremental extraction from: ${state.lastExportTimestamp}`);
|
|
381
|
-
return state.lastExportTimestamp;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Execute GraphQL query with auto-pagination
|
|
386
|
-
*/
|
|
387
|
-
private async executeQuery(
|
|
388
|
-
entityType: string,
|
|
389
|
-
fromTimestamp: string | undefined,
|
|
390
|
-
metrics: ExportMetrics
|
|
391
|
-
): Promise<any[]> {
|
|
392
|
-
const queryConfig = this.queries[entityType];
|
|
393
|
-
if (!queryConfig) {
|
|
394
|
-
throw new Error(`No query configuration found for entity type: ${entityType}`);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const variables: any = {
|
|
398
|
-
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
399
|
-
first: parseInt(process.env.EXPORT_PAGE_SIZE || '100'),
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
// Add incremental filter if timestamp provided
|
|
403
|
-
if (fromTimestamp) {
|
|
404
|
-
if (entityType === 'inventory') {
|
|
405
|
-
variables.updatedFrom = fromTimestamp;
|
|
406
|
-
} else if (entityType === 'orders') {
|
|
407
|
-
variables.createdFrom = fromTimestamp;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Execute query with auto-pagination
|
|
412
|
-
const result = await this.client.graphql({
|
|
413
|
-
query: queryConfig.query,
|
|
414
|
-
variables,
|
|
415
|
-
pagination: {
|
|
416
|
-
maxRecords: parseInt(process.env.EXPORT_MAX_RECORDS || '10000'),
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// Extract records from connection
|
|
421
|
-
const connectionPath = queryConfig.connectionPath;
|
|
422
|
-
const connection = result.data[connectionPath];
|
|
423
|
-
if (!connection?.edges) {
|
|
424
|
-
return [];
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return connection.edges.map((edge: any) => edge.node);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Transform data using UniversalMapper
|
|
432
|
-
*/
|
|
433
|
-
private async transformData(entityType: string, rawData: any[]): Promise<any[]> {
|
|
434
|
-
const mappingConfig = this.mappings[entityType];
|
|
435
|
-
if (!mappingConfig) {
|
|
436
|
-
throw new Error(`No mapping configuration found for entity type: ${entityType}`);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Initialize mapper with custom resolvers
|
|
440
|
-
this.mapper = new UniversalMapper(mappingConfig, {
|
|
441
|
-
customResolvers: {
|
|
442
|
-
'custom.timestamp': () => new Date().toISOString(),
|
|
443
|
-
'custom.fullName': (value: any, sourceData: any) => {
|
|
444
|
-
const firstName = this.getNestedValue(sourceData, 'customer.firstName') || '';
|
|
445
|
-
const lastName = this.getNestedValue(sourceData, 'customer.lastName') || '';
|
|
446
|
-
return `${firstName} ${lastName}`.trim();
|
|
447
|
-
},
|
|
448
|
-
'custom.arrayLength': (value: any) => {
|
|
449
|
-
return Array.isArray(value) ? value.length : 0;
|
|
450
|
-
},
|
|
451
|
-
},
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
const transformed = [];
|
|
455
|
-
for (const record of rawData) {
|
|
456
|
-
const result = await this.mapper.map(record);
|
|
457
|
-
if (!result.success) {
|
|
458
|
-
this.logger.warn('Mapping errors encountered', {
|
|
459
|
-
record: record.id || record.ref,
|
|
460
|
-
errors: result.errors,
|
|
461
|
-
});
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
transformed.push(result.data);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return transformed;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Generate Parquet file buffer
|
|
472
|
-
*/
|
|
473
|
-
private async generateParquet(data: any[]): Promise<Buffer> {
|
|
474
|
-
return await this.s3.writeParquetContent(data, {
|
|
475
|
-
compression: 'SNAPPY',
|
|
476
|
-
rowGroupSize: 10000,
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Upload Parquet file to S3
|
|
482
|
-
*/
|
|
483
|
-
private async uploadToS3(
|
|
484
|
-
entityType: string,
|
|
485
|
-
buffer: Buffer,
|
|
486
|
-
metrics: ExportMetrics
|
|
487
|
-
): Promise<string> {
|
|
488
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
489
|
-
const key = `exports/${entityType}/${new Date().toISOString().split('T')[0]}/${entityType}_${timestamp}.parquet`;
|
|
490
|
-
|
|
491
|
-
await this.s3.uploadFile(key, buffer, {
|
|
492
|
-
contentType: 'application/octet-stream',
|
|
493
|
-
metadata: {
|
|
494
|
-
'entity-type': entityType,
|
|
495
|
-
'record-count': metrics.totalRecords.toString(),
|
|
496
|
-
'export-timestamp': new Date().toISOString(),
|
|
497
|
-
incremental: process.env.EXPORT_INCREMENTAL || 'false',
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
return key;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Update export state
|
|
506
|
-
*/
|
|
507
|
-
private async updateState(entityType: string, metrics: ExportMetrics): Promise<void> {
|
|
508
|
-
const state: ExportState = {
|
|
509
|
-
lastExportTimestamp: new Date().toISOString(),
|
|
510
|
-
lastEntityType: entityType,
|
|
511
|
-
totalRecordsExported: metrics.totalRecords,
|
|
512
|
-
lastExportDuration: metrics.endTime - metrics.startTime,
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
await this.stateManager.saveState(state);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Get nested value from object
|
|
520
|
-
*/
|
|
521
|
-
private getNestedValue(obj: any, path: string): any {
|
|
522
|
-
return path.split('.').reduce((current, key) => {
|
|
523
|
-
return current?.[key];
|
|
524
|
-
}, obj);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Export metrics summary
|
|
529
|
-
*/
|
|
530
|
-
printMetrics(metrics: ExportMetrics): void {
|
|
531
|
-
const duration = metrics.endTime - metrics.startTime;
|
|
532
|
-
|
|
533
|
-
console.log('\n=== Export Metrics ===');
|
|
534
|
-
console.log(`Total Duration: ${duration}ms`);
|
|
535
|
-
console.log(`Total Records: ${metrics.totalRecords}`);
|
|
536
|
-
console.log(`Total Pages: ${metrics.totalPages}`);
|
|
537
|
-
console.log(`Records/Second: ${metrics.recordsPerSecond}`);
|
|
538
|
-
console.log(`\nBreakdown:`);
|
|
539
|
-
console.log(
|
|
540
|
-
` Query Time: ${metrics.queryTimeMs}ms (${Math.round((metrics.queryTimeMs / duration) * 100)}%)`
|
|
541
|
-
);
|
|
542
|
-
console.log(
|
|
543
|
-
` Transform Time: ${metrics.transformTimeMs}ms (${Math.round((metrics.transformTimeMs / duration) * 100)}%)`
|
|
544
|
-
);
|
|
545
|
-
console.log(
|
|
546
|
-
` Parquet Write: ${metrics.parquetWriteTimeMs}ms (${Math.round((metrics.parquetWriteTimeMs / duration) * 100)}%)`
|
|
547
|
-
);
|
|
548
|
-
console.log(
|
|
549
|
-
` S3 Upload: ${metrics.s3UploadTimeMs}ms (${Math.round((metrics.s3UploadTimeMs / duration) * 100)}%)`
|
|
550
|
-
);
|
|
551
|
-
console.log(`\nFile Size: ${Math.round(metrics.fileSizeBytes / 1024)} KB`);
|
|
552
|
-
|
|
553
|
-
if (metrics.errors.length > 0) {
|
|
554
|
-
console.log(`\nErrors: ${metrics.errors.length}`);
|
|
555
|
-
metrics.errors.forEach((error, idx) => {
|
|
556
|
-
console.log(` ${idx + 1}. ${error}`);
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
console.log('====================\n');
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Main execution function
|
|
566
|
-
*/
|
|
567
|
-
async function main() {
|
|
568
|
-
const exporter = new GraphQLParquetExporter();
|
|
569
|
-
|
|
570
|
-
try {
|
|
571
|
-
await exporter.initialize();
|
|
572
|
-
|
|
573
|
-
const entityType = process.argv[2] || process.env.EXPORT_ENTITY_TYPE || 'inventory';
|
|
574
|
-
const metrics = await exporter.export(entityType);
|
|
575
|
-
|
|
576
|
-
exporter.printMetrics(metrics);
|
|
577
|
-
process.exit(0);
|
|
578
|
-
} catch (error) {
|
|
579
|
-
console.error('Export failed:', error);
|
|
580
|
-
process.exit(1);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Run if executed directly
|
|
585
|
-
if (require.main === module) {
|
|
586
|
-
main();
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
export { GraphQLParquetExporter };
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
## Key Patterns Explained
|
|
593
|
-
|
|
594
|
-
### Pattern 1: GraphQL Query with Pagination Variables
|
|
595
|
-
|
|
596
|
-
```typescript
|
|
597
|
-
// Define query with pagination variables
|
|
598
|
-
const query = `
|
|
599
|
-
query GetInventory($first: Int!, $after: String, $updatedFrom: DateTime) {
|
|
600
|
-
inventoryPositions(
|
|
601
|
-
first: $first
|
|
602
|
-
after: $after
|
|
603
|
-
updatedOn: { from: $updatedFrom }
|
|
604
|
-
) {
|
|
605
|
-
edges {
|
|
606
|
-
node { id ref productRef qty }
|
|
607
|
-
cursor
|
|
608
|
-
}
|
|
609
|
-
pageInfo { hasNextPage }
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
`;
|
|
613
|
-
|
|
614
|
-
// SDK auto-detects $first and $after for auto-pagination
|
|
615
|
-
const result = await client.graphql({
|
|
616
|
-
query,
|
|
617
|
-
variables: { first: 100, updatedFrom: '2024-01-01T00:00:00Z' },
|
|
618
|
-
pagination: { maxRecords: 10000 },
|
|
619
|
-
});
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
**Why This Works:**
|
|
623
|
-
|
|
624
|
-
- SDK detects `$first` and `$after` variables
|
|
625
|
-
- Automatically fetches all pages until `hasNextPage = false`
|
|
626
|
-
- Merges results into single response
|
|
627
|
-
- Progress callbacks track pagination status
|
|
628
|
-
|
|
629
|
-
### Pattern 2: Auto-Pagination Loop
|
|
630
|
-
|
|
631
|
-
```typescript
|
|
632
|
-
// Manual pagination (OLD WAY)
|
|
633
|
-
let allRecords = [];
|
|
634
|
-
let hasMore = true;
|
|
635
|
-
let cursor = null;
|
|
636
|
-
|
|
637
|
-
while (hasMore) {
|
|
638
|
-
const result = await client.graphql({
|
|
639
|
-
query,
|
|
640
|
-
variables: { first: 100, after: cursor },
|
|
641
|
-
});
|
|
642
|
-
allRecords.push(...result.data.inventoryPositions.edges);
|
|
643
|
-
hasMore = result.data.inventoryPositions.pageInfo.hasNextPage;
|
|
644
|
-
cursor = result.data.inventoryPositions.edges[edges.length - 1]?.cursor;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Auto-pagination (NEW WAY)
|
|
648
|
-
const result = await client.graphql({
|
|
649
|
-
query,
|
|
650
|
-
variables: { first: 100 },
|
|
651
|
-
pagination: {
|
|
652
|
-
maxRecords: 10000,
|
|
653
|
-
},
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
const allRecords = result.data.inventoryPositions.edges;
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
**Benefits:**
|
|
660
|
-
|
|
661
|
-
- 90% less code
|
|
662
|
-
- Built-in safety limits
|
|
663
|
-
- Progress tracking
|
|
664
|
-
- Error handling
|
|
665
|
-
- Deduplication
|
|
666
|
-
|
|
667
|
-
### Pattern 3: Data Transformation for Export
|
|
668
|
-
|
|
669
|
-
```typescript
|
|
670
|
-
// Configure extraction mapping
|
|
671
|
-
const mappingConfig = {
|
|
672
|
-
fields: {
|
|
673
|
-
// Direct field mapping
|
|
674
|
-
sku: { source: 'productRef', resolver: 'sdk.uppercase' },
|
|
675
|
-
|
|
676
|
-
// Nested field access
|
|
677
|
-
customer_email: { source: 'customer.email', resolver: 'sdk.toString' },
|
|
678
|
-
|
|
679
|
-
// Type conversion
|
|
680
|
-
quantity: { source: 'qty', resolver: 'sdk.parseInt' },
|
|
681
|
-
|
|
682
|
-
// Date formatting
|
|
683
|
-
last_updated: { source: 'updatedOn', resolver: 'sdk.formatDate' },
|
|
684
|
-
|
|
685
|
-
// Custom resolver
|
|
686
|
-
full_name: {
|
|
687
|
-
source: null,
|
|
688
|
-
resolver: 'custom.fullName',
|
|
689
|
-
},
|
|
690
|
-
},
|
|
691
|
-
};
|
|
692
|
-
|
|
693
|
-
// Custom resolvers
|
|
694
|
-
const mapper = new UniversalMapper(mappingConfig, {
|
|
695
|
-
customResolvers: {
|
|
696
|
-
'custom.fullName': (value, sourceData, config, helpers) => {
|
|
697
|
-
const firstName = sourceData.customer?.firstName || '';
|
|
698
|
-
const lastName = sourceData.customer?.lastName || '';
|
|
699
|
-
return `${firstName} ${lastName}`.trim();
|
|
700
|
-
},
|
|
701
|
-
},
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
// Transform
|
|
705
|
-
const result = await mapper.map(record);
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
### Pattern 4: Parquet File Generation
|
|
709
|
-
|
|
710
|
-
```typescript
|
|
711
|
-
// Use S3DataSource's built-in Parquet writer
|
|
712
|
-
const parquetBuffer = await s3.writeParquetContent(transformedData, {
|
|
713
|
-
compression: 'SNAPPY', // Fast compression
|
|
714
|
-
rowGroupSize: 10000, // Optimize for analytics
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
// Alternative: Use ParquetParserService directly
|
|
718
|
-
const parser = new ParquetParserService(logger);
|
|
719
|
-
const buffer = await parser.write(transformedData, 'output.parquet', {
|
|
720
|
-
schema: {
|
|
721
|
-
sku: { type: 'UTF8' },
|
|
722
|
-
quantity: { type: 'INT64' },
|
|
723
|
-
last_updated: { type: 'TIMESTAMP_MILLIS' },
|
|
724
|
-
},
|
|
725
|
-
});
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
**Compression Options:**
|
|
729
|
-
|
|
730
|
-
- `UNCOMPRESSED` - Fastest write, largest file
|
|
731
|
-
- `SNAPPY` - Good balance (recommended)
|
|
732
|
-
- `GZIP` - Best compression, slower
|
|
733
|
-
- `BROTLI` - Best compression ratio
|
|
734
|
-
|
|
735
|
-
### Pattern 5: Incremental Extraction
|
|
736
|
-
|
|
737
|
-
```typescript
|
|
738
|
-
// State management
|
|
739
|
-
interface ExportState {
|
|
740
|
-
lastExportTimestamp: string;
|
|
741
|
-
lastEntityType: string;
|
|
742
|
-
totalRecordsExported: number;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Load previous state
|
|
746
|
-
const state = await loadState();
|
|
747
|
-
const fromTimestamp = state?.lastExportTimestamp;
|
|
748
|
-
|
|
749
|
-
// Query with incremental filter
|
|
750
|
-
const result = await client.graphql({
|
|
751
|
-
query: `
|
|
752
|
-
query GetInventory($first: Int!, $after: String, $updatedFrom: DateTime) {
|
|
753
|
-
inventoryPositions(
|
|
754
|
-
first: $first
|
|
755
|
-
after: $after
|
|
756
|
-
updatedOn: { from: $updatedFrom } # Incremental filter
|
|
757
|
-
) {
|
|
758
|
-
edges { node { id ref qty updatedOn } }
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
`,
|
|
762
|
-
variables: {
|
|
763
|
-
first: 100,
|
|
764
|
-
updatedFrom: fromTimestamp, // Only records updated since last export
|
|
765
|
-
},
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
// Save new state
|
|
769
|
-
await saveState({
|
|
770
|
-
lastExportTimestamp: new Date().toISOString(),
|
|
771
|
-
lastEntityType: entityType,
|
|
772
|
-
totalRecordsExported: result.data.inventoryPositions.edges.length,
|
|
773
|
-
});
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
## Deployment Options
|
|
777
|
-
|
|
778
|
-
### Local Execution
|
|
779
|
-
|
|
780
|
-
```bash
|
|
781
|
-
# Run once
|
|
782
|
-
npm run export
|
|
783
|
-
|
|
784
|
-
# Export specific entity
|
|
785
|
-
npm run export orders
|
|
786
|
-
|
|
787
|
-
# With custom config
|
|
788
|
-
EXPORT_ENTITY_TYPE=products EXPORT_MAX_RECORDS=50000 npm run export
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
### Scheduled Export (Daily)
|
|
792
|
-
|
|
793
|
-
**Option 1: Cron (Linux/Mac)**
|
|
794
|
-
|
|
795
|
-
```bash
|
|
796
|
-
# Edit crontab
|
|
797
|
-
crontab -e
|
|
798
|
-
|
|
799
|
-
# Add daily export at 2 AM
|
|
800
|
-
0 2 * * * cd /path/to/fluent-graphql-export && npm run export >> logs/export.log 2>&1
|
|
801
|
-
```
|
|
802
|
-
|
|
803
|
-
**Option 2: Windows Task Scheduler**
|
|
804
|
-
|
|
805
|
-
```powershell
|
|
806
|
-
# Create scheduled task
|
|
807
|
-
$action = New-ScheduledTaskAction -Execute "node" -Argument "src/export-to-s3.ts"
|
|
808
|
-
$trigger = New-ScheduledTaskTrigger -Daily -At 2am
|
|
809
|
-
Register-ScheduledTask -TaskName "FluentExport" -Action $action -Trigger $trigger
|
|
810
|
-
```
|
|
811
|
-
|
|
812
|
-
**Option 3: Node-cron (Cross-platform)**
|
|
813
|
-
|
|
814
|
-
Create `src/scheduler.ts`:
|
|
815
|
-
|
|
816
|
-
```typescript
|
|
817
|
-
import cron from 'node-cron';
|
|
818
|
-
import { GraphQLParquetExporter } from './export-to-s3';
|
|
819
|
-
|
|
820
|
-
const exporter = new GraphQLParquetExporter();
|
|
821
|
-
|
|
822
|
-
// Run daily at 2 AM
|
|
823
|
-
cron.schedule('0 2 * * *', async () => {
|
|
824
|
-
console.log('Starting scheduled export...');
|
|
825
|
-
try {
|
|
826
|
-
await exporter.initialize();
|
|
827
|
-
await exporter.export('inventory');
|
|
828
|
-
console.log('Scheduled export completed');
|
|
829
|
-
} catch (error) {
|
|
830
|
-
console.error('Scheduled export failed:', error);
|
|
831
|
-
}
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
console.log('Scheduler started');
|
|
835
|
-
```
|
|
836
|
-
|
|
837
|
-
### AWS Lambda Deployment
|
|
838
|
-
|
|
839
|
-
Create `lambda/handler.ts`:
|
|
840
|
-
|
|
841
|
-
```typescript
|
|
842
|
-
import { GraphQLParquetExporter } from '../src/export-to-s3';
|
|
843
|
-
|
|
844
|
-
export const handler = async (event: any) => {
|
|
845
|
-
const exporter = new GraphQLParquetExporter();
|
|
846
|
-
|
|
847
|
-
try {
|
|
848
|
-
await exporter.initialize();
|
|
849
|
-
const entityType = event.entityType || 'inventory';
|
|
850
|
-
const metrics = await exporter.export(entityType);
|
|
851
|
-
|
|
852
|
-
return {
|
|
853
|
-
statusCode: 200,
|
|
854
|
-
body: JSON.stringify({
|
|
855
|
-
message: 'Export completed successfully',
|
|
856
|
-
metrics,
|
|
857
|
-
}),
|
|
858
|
-
};
|
|
859
|
-
} catch (error) {
|
|
860
|
-
return {
|
|
861
|
-
statusCode: 500,
|
|
862
|
-
body: JSON.stringify({
|
|
863
|
-
message: 'Export failed',
|
|
864
|
-
error: (error as Error).message,
|
|
865
|
-
}),
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
};
|
|
869
|
-
```
|
|
870
|
-
|
|
871
|
-
**EventBridge Schedule:**
|
|
872
|
-
|
|
873
|
-
```json
|
|
874
|
-
{
|
|
875
|
-
"scheduleExpression": "cron(0 2 * * ? *)",
|
|
876
|
-
"target": {
|
|
877
|
-
"arn": "arn:aws:lambda:us-east-1:123456789:function:fluent-export",
|
|
878
|
-
"input": "{\"entityType\": \"inventory\"}"
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
```
|
|
882
|
-
|
|
883
|
-
## Testing
|
|
884
|
-
|
|
885
|
-
### Test Extraction Locally
|
|
886
|
-
|
|
887
|
-
```bash
|
|
888
|
-
# Test with small dataset
|
|
889
|
-
EXPORT_MAX_RECORDS=10 EXPORT_INCREMENTAL=false npm run export
|
|
890
|
-
|
|
891
|
-
# Test incremental extraction
|
|
892
|
-
EXPORT_INCREMENTAL=true npm run export
|
|
893
|
-
|
|
894
|
-
# Verify S3 upload
|
|
895
|
-
aws s3 ls s3://fluent-exports/exports/inventory/
|
|
896
|
-
|
|
897
|
-
# Download and validate Parquet
|
|
898
|
-
aws s3 cp s3://fluent-exports/exports/inventory/latest.parquet test.parquet
|
|
899
|
-
python -c "import pandas as pd; print(pd.read_parquet('test.parquet'))"
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
### Validation Script
|
|
903
|
-
|
|
904
|
-
Create `scripts/validate-export.sh`:
|
|
905
|
-
|
|
906
|
-
```bash
|
|
907
|
-
#!/bin/bash
|
|
908
|
-
echo "Validating export..."
|
|
909
|
-
|
|
910
|
-
# Check state file
|
|
911
|
-
if [ -f "./state/last-export.json" ]; then
|
|
912
|
-
echo "✓ State file exists"
|
|
913
|
-
cat ./state/last-export.json | jq .
|
|
914
|
-
else
|
|
915
|
-
echo "✗ State file missing"
|
|
916
|
-
exit 1
|
|
917
|
-
fi
|
|
918
|
-
|
|
919
|
-
# Check S3 upload
|
|
920
|
-
LATEST_FILE=$(aws s3 ls s3://fluent-exports/exports/inventory/ --recursive | sort | tail -n 1 | awk '{print $4}')
|
|
921
|
-
if [ -z "$LATEST_FILE" ]; then
|
|
922
|
-
echo "✗ No files found in S3"
|
|
923
|
-
exit 1
|
|
924
|
-
fi
|
|
925
|
-
echo "✓ Latest export: $LATEST_FILE"
|
|
926
|
-
|
|
927
|
-
# Get file size
|
|
928
|
-
FILE_SIZE=$(aws s3 ls s3://fluent-exports/$LATEST_FILE | awk '{print $3}')
|
|
929
|
-
echo "✓ File size: $FILE_SIZE bytes"
|
|
930
|
-
|
|
931
|
-
# Check record count
|
|
932
|
-
RECORD_COUNT=$(cat ./state/last-export.json | jq -r .totalRecordsExported)
|
|
933
|
-
echo "✓ Records exported: $RECORD_COUNT"
|
|
934
|
-
|
|
935
|
-
echo "Validation complete!"
|
|
936
|
-
```
|
|
937
|
-
|
|
938
|
-
## Common Issues
|
|
939
|
-
|
|
940
|
-
### Issue 1: Auto-Pagination Not Triggering
|
|
941
|
-
|
|
942
|
-
**Symptoms:**
|
|
943
|
-
|
|
944
|
-
- Only first page of results returned
|
|
945
|
-
- `autoPagination` metadata missing
|
|
946
|
-
|
|
947
|
-
**Cause:**
|
|
948
|
-
|
|
949
|
-
- Query missing `$first` or `$after` variables
|
|
950
|
-
- Variables not passed in query execution
|
|
951
|
-
|
|
952
|
-
**Solution:**
|
|
953
|
-
|
|
954
|
-
```typescript
|
|
955
|
-
// ✗ Wrong - no pagination variables
|
|
956
|
-
const query = `query { inventoryPositions(first: 100) { edges { node { id } } } }`;
|
|
957
|
-
|
|
958
|
-
// ✓ Correct - pagination variables defined
|
|
959
|
-
const query = `query GetInventory($first: Int!, $after: String) {
|
|
960
|
-
inventoryPositions(first: $first, after: $after) {
|
|
961
|
-
edges { node { id } }
|
|
962
|
-
pageInfo { hasNextPage }
|
|
963
|
-
}
|
|
964
|
-
}`;
|
|
965
|
-
|
|
966
|
-
const result = await client.graphql({
|
|
967
|
-
query,
|
|
968
|
-
variables: { first: 100 }, // SDK adds $after automatically
|
|
969
|
-
});
|
|
970
|
-
```
|
|
971
|
-
|
|
972
|
-
### Issue 2: Parquet File Too Large
|
|
973
|
-
|
|
974
|
-
**Symptoms:**
|
|
975
|
-
|
|
976
|
-
- Out of memory errors
|
|
977
|
-
- Slow S3 upload
|
|
978
|
-
- Lambda timeout
|
|
979
|
-
|
|
980
|
-
**Solution:**
|
|
981
|
-
|
|
982
|
-
```typescript
|
|
983
|
-
// Use compression
|
|
984
|
-
const buffer = await s3.writeParquetContent(data, {
|
|
985
|
-
compression: 'SNAPPY', // or 'GZIP' for better compression
|
|
986
|
-
rowGroupSize: 5000, // Smaller row groups
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
// Or split into multiple files
|
|
990
|
-
const BATCH_SIZE = 10000;
|
|
991
|
-
for (let i = 0; i < data.length; i += BATCH_SIZE) {
|
|
992
|
-
const batch = data.slice(i, i + BATCH_SIZE);
|
|
993
|
-
const buffer = await s3.writeParquetContent(batch);
|
|
994
|
-
await s3.uploadFile(`exports/part-${i}.parquet`, buffer);
|
|
995
|
-
}
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
### Issue 3: Incremental Extraction Missing Records
|
|
999
|
-
|
|
1000
|
-
**Symptoms:**
|
|
1001
|
-
|
|
1002
|
-
- Records not appearing in export
|
|
1003
|
-
- Duplicate records in subsequent exports
|
|
1004
|
-
|
|
1005
|
-
**Cause:**
|
|
1006
|
-
|
|
1007
|
-
- Incorrect timestamp filtering
|
|
1008
|
-
- Clock skew between systems
|
|
1009
|
-
|
|
1010
|
-
**Solution:**
|
|
1011
|
-
|
|
1012
|
-
```typescript
|
|
1013
|
-
// Add buffer to timestamp (5 minutes)
|
|
1014
|
-
const lastTimestamp = new Date(state.lastExportTimestamp);
|
|
1015
|
-
const fromTimestamp = new Date(lastTimestamp.getTime() - 5 * 60 * 1000).toISOString();
|
|
1016
|
-
|
|
1017
|
-
// Use overlap detection
|
|
1018
|
-
const seenRecords = new Set(previousExport.map(r => r.id));
|
|
1019
|
-
const newRecords = currentExport.filter(r => !seenRecords.has(r.id));
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
### Issue 4: GraphQL Query Timeout
|
|
1023
|
-
|
|
1024
|
-
**Symptoms:**
|
|
1025
|
-
|
|
1026
|
-
- Query fails after long wait
|
|
1027
|
-
- Partial results returned
|
|
1028
|
-
|
|
1029
|
-
**Solution:**
|
|
1030
|
-
|
|
1031
|
-
```typescript
|
|
1032
|
-
// Use pagination safety limits
|
|
1033
|
-
const result = await client.graphql({
|
|
1034
|
-
query,
|
|
1035
|
-
variables: { first: 100 },
|
|
1036
|
-
pagination: {
|
|
1037
|
-
maxPages: 100, // Stop after 100 pages
|
|
1038
|
-
maxRecords: 10000, // Stop after 10k records
|
|
1039
|
-
timeoutMs: 300000, // Stop after 5 minutes
|
|
1040
|
-
},
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
// Check for truncation
|
|
1044
|
-
if (result.extensions.autoPagination.truncated) {
|
|
1045
|
-
console.log('Results truncated:', result.extensions.autoPagination.truncationReason);
|
|
1046
|
-
// Handle partial export...
|
|
1047
|
-
}
|
|
1048
|
-
```
|
|
1049
|
-
|
|
1050
|
-
### Issue 5: UniversalMapper Transform Failures
|
|
1051
|
-
|
|
1052
|
-
**Symptoms:**
|
|
1053
|
-
|
|
1054
|
-
- Some records missing from export
|
|
1055
|
-
- Warning logs about mapping errors
|
|
1056
|
-
|
|
1057
|
-
**Solution:**
|
|
1058
|
-
|
|
1059
|
-
```typescript
|
|
1060
|
-
// Add error handling to mapper
|
|
1061
|
-
const results = [];
|
|
1062
|
-
for (const record of rawData) {
|
|
1063
|
-
const result = await mapper.map(record);
|
|
1064
|
-
if (!result.success) {
|
|
1065
|
-
logger.warn('Mapping failed', {
|
|
1066
|
-
record: record.id,
|
|
1067
|
-
errors: result.errors,
|
|
1068
|
-
});
|
|
1069
|
-
// Include partial data or skip
|
|
1070
|
-
if (result.data) {
|
|
1071
|
-
results.push({ ...result.data, _errors: result.errors });
|
|
1072
|
-
}
|
|
1073
|
-
continue;
|
|
1074
|
-
}
|
|
1075
|
-
results.push(result.data);
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// Or use default values
|
|
1079
|
-
const mappingConfig = {
|
|
1080
|
-
fields: {
|
|
1081
|
-
qty: {
|
|
1082
|
-
source: 'qty',
|
|
1083
|
-
resolver: 'sdk.parseInt',
|
|
1084
|
-
defaultValue: 0, // Use 0 if parsing fails
|
|
1085
|
-
},
|
|
1086
|
-
},
|
|
1087
|
-
};
|
|
1088
|
-
```
|
|
1089
|
-
|
|
1090
|
-
## Related Guides
|
|
1091
|
-
|
|
1092
|
-
- [Auto-Pagination Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Deep dive into automatic pagination
|
|
1093
|
-
- [Universal Mapping Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Field mapping and transformations
|
|
1094
|
-
- [Extraction Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - General extraction patterns
|
|
1095
|
-
- [Connector Scenarios Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - More complete examples
|
|
1096
|
-
- [Standalone CSV Ingestion](./s3-csv-batch-api.md) - Ingestion counterpart
|
|
1097
|
-
- [SFTP to Batch API](../../02-CORE-GUIDES/ingestion/ingestion-readme.md) - SFTP-based ingestion (see Versori templates)
|
|
1098
|
-
|
|
1099
|
-
## Performance Optimization Tips
|
|
1100
|
-
|
|
1101
|
-
1. **Batch Size Tuning:**
|
|
1102
|
-
- Small records: 200-500 per page
|
|
1103
|
-
- Large records: 50-100 per page
|
|
1104
|
-
- Monitor query time vs network overhead
|
|
1105
|
-
|
|
1106
|
-
2. **Parquet Optimization:**
|
|
1107
|
-
- Use SNAPPY compression for balance
|
|
1108
|
-
- Adjust rowGroupSize based on query patterns
|
|
1109
|
-
- Use columnar encoding for analytics
|
|
1110
|
-
|
|
1111
|
-
3. **Incremental Extraction:**
|
|
1112
|
-
- Always use timestamp filtering when possible
|
|
1113
|
-
- Add overlap window to prevent gaps
|
|
1114
|
-
- Track state per entity type
|
|
1115
|
-
|
|
1116
|
-
4. **Memory Management:**
|
|
1117
|
-
- Process large exports in batches
|
|
1118
|
-
- Clear data after each batch upload
|
|
1119
|
-
- Use streaming for very large datasets
|
|
1120
|
-
|
|
1121
|
-
5. **Error Recovery:**
|
|
1122
|
-
- Implement retry logic with exponential backoff
|
|
1123
|
-
- Save checkpoints for long-running exports
|
|
1124
|
-
- Keep partial results for debugging
|
|
1125
|
-
|
|
1126
|
-
## Production Checklist
|
|
1127
|
-
|
|
1128
|
-
- [ ] Environment variables configured
|
|
1129
|
-
- [ ] S3 bucket created with proper permissions
|
|
1130
|
-
- [ ] OAuth2 credentials tested
|
|
1131
|
-
- [ ] GraphQL queries validated
|
|
1132
|
-
- [ ] Extraction mappings tested with sample data
|
|
1133
|
-
- [ ] Incremental extraction state directory created
|
|
1134
|
-
- [ ] Logging configured (file + console)
|
|
1135
|
-
- [ ] Error alerting configured
|
|
1136
|
-
- [ ] Scheduled execution tested
|
|
1137
|
-
- [ ] S3 export verified in analytics tools
|
|
1138
|
-
- [ ] Monitoring dashboards created
|
|
1139
|
-
- [ ] Backup strategy for state files
|
|
1140
|
-
- [ ] Documentation for operators
|
|
1
|
+
# Standalone: Fluent GraphQL → S3 Parquet Export
|
|
2
|
+
|
|
3
|
+
**FC Connect SDK Use Case Guide**
|
|
4
|
+
|
|
5
|
+
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
6
|
+
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
7
|
+
|
|
8
|
+
**Context**: Node.js script that extracts inventory/orders from Fluent Commerce via GraphQL and exports to S3 as Parquet files
|
|
9
|
+
|
|
10
|
+
**Complexity**: Medium
|
|
11
|
+
|
|
12
|
+
**Runtime**: Node.js ≥18 / Deno
|
|
13
|
+
|
|
14
|
+
**Estimated Lines**: ~500 lines
|
|
15
|
+
|
|
16
|
+
## What You'll Build
|
|
17
|
+
|
|
18
|
+
- Standalone Node.js/Deno extraction script
|
|
19
|
+
- OAuth2 authentication with Fluent Commerce
|
|
20
|
+
- GraphQL query execution with auto-pagination
|
|
21
|
+
- Data transformation with UniversalMapper
|
|
22
|
+
- Parquet file generation
|
|
23
|
+
- S3 upload with metadata
|
|
24
|
+
- Incremental extraction (last updated timestamp tracking)
|
|
25
|
+
- Error handling and performance metrics
|
|
26
|
+
- Scheduled execution support
|
|
27
|
+
|
|
28
|
+
## SDK Methods Used
|
|
29
|
+
|
|
30
|
+
- `createClient({ config: { baseUrl, clientId, clientSecret, retailerId } })` - OAuth2 client creation
|
|
31
|
+
- `client.graphql({ query, variables, pagination })` - Execute GraphQL query with auto-pagination
|
|
32
|
+
- `UniversalMapper(extractionConfig)` - Transform data for export format
|
|
33
|
+
- `ParquetParserService.writeParquetContent(data, options)` - Generate Parquet buffer
|
|
34
|
+
- `S3DataSource.uploadFile(key, buffer, options)` - Upload to S3
|
|
35
|
+
- `StateService` - Track incremental extraction state
|
|
36
|
+
|
|
37
|
+
## Complete Working Code
|
|
38
|
+
|
|
39
|
+
### 1. Project Setup
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Initialize project
|
|
43
|
+
mkdir fluent-graphql-export
|
|
44
|
+
cd fluent-graphql-export
|
|
45
|
+
npm init -y
|
|
46
|
+
|
|
47
|
+
# Install dependencies
|
|
48
|
+
npm install @fluentcommerce/fc-connect-sdk
|
|
49
|
+
npm install dotenv
|
|
50
|
+
|
|
51
|
+
# Create directory structure
|
|
52
|
+
mkdir -p src config logs state
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Environment Configuration
|
|
56
|
+
|
|
57
|
+
Create `.env`:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Fluent Commerce API
|
|
61
|
+
FLUENT_BASE_URL=https://your-account.api.fluentretail.com
|
|
62
|
+
FLUENT_CLIENT_ID=your-oauth-client-id
|
|
63
|
+
FLUENT_CLIENT_SECRET=your-oauth-client-secret
|
|
64
|
+
FLUENT_RETAILER_ID=your-retailer-id
|
|
65
|
+
|
|
66
|
+
# AWS S3 (Target)
|
|
67
|
+
TARGET_AWS_ACCESS_KEY_ID=your-aws-access-key
|
|
68
|
+
TARGET_AWS_SECRET_ACCESS_KEY=your-aws-secret-key
|
|
69
|
+
TARGET_AWS_REGION=us-east-1
|
|
70
|
+
TARGET_S3_BUCKET=fluent-exports
|
|
71
|
+
|
|
72
|
+
# Export Configuration
|
|
73
|
+
EXPORT_ENTITY_TYPE=inventory # or 'orders', 'products'
|
|
74
|
+
EXPORT_PAGE_SIZE=100
|
|
75
|
+
EXPORT_MAX_RECORDS=10000
|
|
76
|
+
EXPORT_INCREMENTAL=true
|
|
77
|
+
STATE_FILE=./state/last-export.json
|
|
78
|
+
LOG_LEVEL=info
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. Extraction Configuration
|
|
82
|
+
|
|
83
|
+
Create `config/extraction-mappings.json`:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"inventory": {
|
|
88
|
+
"fields": {
|
|
89
|
+
"sku": { "source": "productRef", "resolver": "sdk.uppercase" },
|
|
90
|
+
"location_code": { "source": "locationRef", "resolver": "sdk.toString" },
|
|
91
|
+
"quantity": { "source": "qty", "resolver": "sdk.parseInt" },
|
|
92
|
+
"available_qty": { "source": "availableQty", "resolver": "sdk.parseInt" },
|
|
93
|
+
"reserved_qty": { "source": "reservedQty", "resolver": "sdk.parseInt" },
|
|
94
|
+
"status": { "source": "status", "resolver": "sdk.lowercase" },
|
|
95
|
+
"last_updated": { "source": "updatedOn", "resolver": "sdk.formatDate" },
|
|
96
|
+
"extracted_at": {
|
|
97
|
+
"source": null,
|
|
98
|
+
"resolver": "custom.timestamp",
|
|
99
|
+
"defaultValue": "{{ now }}"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"orders": {
|
|
104
|
+
"fields": {
|
|
105
|
+
"order_id": { "source": "ref", "resolver": "sdk.toString" },
|
|
106
|
+
"order_status": { "source": "status", "resolver": "sdk.lowercase" },
|
|
107
|
+
"total_amount": { "source": "totalPrice", "resolver": "sdk.parseFloat" },
|
|
108
|
+
"tax_amount": { "source": "totalTaxPrice", "resolver": "sdk.parseFloat" },
|
|
109
|
+
"customer_email": { "source": "customer.email", "resolver": "sdk.toString" },
|
|
110
|
+
"customer_name": {
|
|
111
|
+
"source": null,
|
|
112
|
+
"resolver": "custom.fullName"
|
|
113
|
+
},
|
|
114
|
+
"created_date": { "source": "createdOn", "resolver": "sdk.formatDate" },
|
|
115
|
+
"item_count": {
|
|
116
|
+
"source": "items.edges",
|
|
117
|
+
"resolver": "custom.arrayLength"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 4. GraphQL Queries
|
|
125
|
+
|
|
126
|
+
Create `config/queries.json`:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"inventory": {
|
|
131
|
+
"query": "query GetInventory($retailerId: ID!, $first: Int!, $after: String, $updatedFrom: DateTime) {\n inventoryPositions(\n retailerId: $retailerId\n first: $first\n after: $after\n updatedOn: { from: $updatedFrom }\n ) {\n edges {\n node {\n id\n ref\n productRef\n locationRef\n qty\n availableQty\n reservedQty\n status\n createdOn\n updatedOn\n }\n cursor\n }\n pageInfo {\n hasNextPage\n }\n }\n}",
|
|
132
|
+
"connectionPath": "inventoryPositions"
|
|
133
|
+
},
|
|
134
|
+
"orders": {
|
|
135
|
+
"query": "query GetOrders($retailerId: ID!, $first: Int!, $after: String, $createdFrom: DateTime) {\n orders(\n retailerId: $retailerId\n first: $first\n after: $after\n createdOn: { from: $createdFrom }\n ) {\n edges {\n node {\n id\n ref\n type\n status\n createdOn\n totalPrice\n totalTaxPrice\n customer {\n ref\n email\n firstName\n lastName\n }\n items {\n edges {\n node {\n ref\n productRef\n quantity\n price\n }\n }\n }\n }\n cursor\n }\n pageInfo {\n hasNextPage\n }\n }\n}",
|
|
136
|
+
"connectionPath": "orders"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 5. Main Extraction Script
|
|
142
|
+
|
|
143
|
+
Create `src/export-to-s3.ts`:
|
|
144
|
+
|
|
145
|
+
> **⚠️ RUNTIME COMPATIBILITY NOTE:**
|
|
146
|
+
> This template includes `import { Buffer } from 'node:buffer';` for Deno compatibility.
|
|
147
|
+
> While Buffer is globally available in Node.js, including this import ensures the code works
|
|
148
|
+
> across all runtimes (Node.js, Deno, Versori) without modification.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import * as dotenv from 'dotenv';
|
|
152
|
+
import * as fs from 'fs/promises';
|
|
153
|
+
import * as path from 'path';
|
|
154
|
+
import { Buffer } from 'node:buffer'; // Required for Deno compatibility
|
|
155
|
+
import {
|
|
156
|
+
createClient,
|
|
157
|
+
FluentClient,
|
|
158
|
+
UniversalMapper,
|
|
159
|
+
ParquetParserService,
|
|
160
|
+
S3DataSource,
|
|
161
|
+
createConsoleLogger,
|
|
162
|
+
toStructuredLogger
|
|
163
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
164
|
+
|
|
165
|
+
// Load environment variables
|
|
166
|
+
dotenv.config();
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* State management for incremental extraction
|
|
170
|
+
*/
|
|
171
|
+
interface ExportState {
|
|
172
|
+
lastExportTimestamp: string;
|
|
173
|
+
lastEntityType: string;
|
|
174
|
+
totalRecordsExported: number;
|
|
175
|
+
lastExportDuration: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
class StateManager {
|
|
179
|
+
constructor(private stateFilePath: string) {}
|
|
180
|
+
|
|
181
|
+
async loadState(): Promise<ExportState | null> {
|
|
182
|
+
try {
|
|
183
|
+
const data = await fs.readFile(this.stateFilePath, 'utf-8');
|
|
184
|
+
return JSON.parse(data);
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async saveState(state: ExportState): Promise<void> {
|
|
191
|
+
const dir = path.dirname(this.stateFilePath);
|
|
192
|
+
await fs.mkdir(dir, { recursive: true });
|
|
193
|
+
await fs.writeFile(this.stateFilePath, JSON.stringify(state, null, 2));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Export metrics tracking
|
|
199
|
+
*/
|
|
200
|
+
interface ExportMetrics {
|
|
201
|
+
startTime: number;
|
|
202
|
+
endTime: number;
|
|
203
|
+
totalRecords: number;
|
|
204
|
+
totalPages: number;
|
|
205
|
+
queryTimeMs: number;
|
|
206
|
+
transformTimeMs: number;
|
|
207
|
+
parquetWriteTimeMs: number;
|
|
208
|
+
s3UploadTimeMs: number;
|
|
209
|
+
fileSizeBytes: number;
|
|
210
|
+
recordsPerSecond: number;
|
|
211
|
+
errors: string[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* GraphQL to S3 Parquet Exporter
|
|
216
|
+
*/
|
|
217
|
+
class GraphQLParquetExporter {
|
|
218
|
+
private client: FluentClient;
|
|
219
|
+
private logger: LoggingService;
|
|
220
|
+
private stateManager: StateManager;
|
|
221
|
+
private mapper: UniversalMapper;
|
|
222
|
+
private parquetParser: ParquetParserService;
|
|
223
|
+
private s3: S3DataSource;
|
|
224
|
+
private queries: Record<string, any>;
|
|
225
|
+
private mappings: Record<string, any>;
|
|
226
|
+
|
|
227
|
+
constructor() {
|
|
228
|
+
this.logger = toStructuredLogger(createConsoleLogger(), {
|
|
229
|
+
logLevel: process.env.LOG_LEVEL || 'info'
|
|
230
|
+
});
|
|
231
|
+
this.stateManager = new StateManager(process.env.STATE_FILE || './state/last-export.json');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async initialize(): Promise<void> {
|
|
235
|
+
this.logger.info('Initializing GraphQL Parquet Exporter');
|
|
236
|
+
|
|
237
|
+
// Create Fluent client with OAuth2
|
|
238
|
+
this.client = await createClient({
|
|
239
|
+
config: {
|
|
240
|
+
baseUrl: process.env.FLUENT_BASE_URL!,
|
|
241
|
+
clientId: process.env.FLUENT_CLIENT_ID!,
|
|
242
|
+
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
|
|
243
|
+
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Load queries configuration
|
|
248
|
+
this.queries = JSON.parse(await fs.readFile('config/queries.json', 'utf-8'));
|
|
249
|
+
|
|
250
|
+
// Load extraction mappings
|
|
251
|
+
this.mappings = JSON.parse(await fs.readFile('config/extraction-mappings.json', 'utf-8'));
|
|
252
|
+
|
|
253
|
+
// Initialize Parquet parser
|
|
254
|
+
this.parquetParser = new ParquetParserService(this.logger, {
|
|
255
|
+
batchSize: 5000,
|
|
256
|
+
enableStreaming: false,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Initialize S3 data source for target
|
|
260
|
+
this.s3 = new S3DataSource(
|
|
261
|
+
{
|
|
262
|
+
type: 'S3_CSV',
|
|
263
|
+
connectionId: 'target-s3',
|
|
264
|
+
name: 'Export Target S3',
|
|
265
|
+
s3Config: {
|
|
266
|
+
bucket: process.env.TARGET_S3_BUCKET!,
|
|
267
|
+
region: process.env.TARGET_AWS_REGION!,
|
|
268
|
+
accessKeyId: process.env.TARGET_AWS_ACCESS_KEY_ID!,
|
|
269
|
+
secretAccessKey: process.env.TARGET_AWS_SECRET_ACCESS_KEY!,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
this.logger
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
this.logger.info('Initialization complete');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Execute extraction workflow
|
|
280
|
+
*/
|
|
281
|
+
async export(
|
|
282
|
+
entityType: string = process.env.EXPORT_ENTITY_TYPE || 'inventory'
|
|
283
|
+
): Promise<ExportMetrics> {
|
|
284
|
+
const metrics: ExportMetrics = {
|
|
285
|
+
startTime: Date.now(),
|
|
286
|
+
endTime: 0,
|
|
287
|
+
totalRecords: 0,
|
|
288
|
+
totalPages: 0,
|
|
289
|
+
queryTimeMs: 0,
|
|
290
|
+
transformTimeMs: 0,
|
|
291
|
+
parquetWriteTimeMs: 0,
|
|
292
|
+
s3UploadTimeMs: 0,
|
|
293
|
+
fileSizeBytes: 0,
|
|
294
|
+
recordsPerSecond: 0,
|
|
295
|
+
errors: [],
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
this.logger.info(`Starting export for entity type: ${entityType}`);
|
|
300
|
+
|
|
301
|
+
// STEP 1: Determine incremental extraction timestamp
|
|
302
|
+
const fromTimestamp = await this.getIncrementalTimestamp(entityType);
|
|
303
|
+
|
|
304
|
+
// STEP 2: Execute GraphQL query with auto-pagination
|
|
305
|
+
const queryStart = Date.now();
|
|
306
|
+
const rawData = await this.executeQuery(entityType, fromTimestamp, metrics);
|
|
307
|
+
metrics.queryTimeMs = Date.now() - queryStart;
|
|
308
|
+
metrics.totalRecords = rawData.length;
|
|
309
|
+
this.logger.info(`Extracted ${rawData.length} records in ${metrics.queryTimeMs}ms`);
|
|
310
|
+
|
|
311
|
+
if (rawData.length === 0) {
|
|
312
|
+
this.logger.info('No new records to export');
|
|
313
|
+
metrics.endTime = Date.now();
|
|
314
|
+
return metrics;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// STEP 3: Transform data for export
|
|
318
|
+
const transformStart = Date.now();
|
|
319
|
+
const transformedData = await this.transformData(entityType, rawData);
|
|
320
|
+
metrics.transformTimeMs = Date.now() - transformStart;
|
|
321
|
+
this.logger.info(
|
|
322
|
+
`Transformed ${transformedData.length} records in ${metrics.transformTimeMs}ms`
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// STEP 4: Generate Parquet file
|
|
326
|
+
const parquetStart = Date.now();
|
|
327
|
+
const parquetBuffer = await this.generateParquet(transformedData);
|
|
328
|
+
metrics.parquetWriteTimeMs = Date.now() - parquetStart;
|
|
329
|
+
metrics.fileSizeBytes = parquetBuffer.length;
|
|
330
|
+
this.logger.info(
|
|
331
|
+
`Generated Parquet file: ${parquetBuffer.length} bytes in ${metrics.parquetWriteTimeMs}ms`
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// STEP 5: Upload to S3
|
|
335
|
+
const uploadStart = Date.now();
|
|
336
|
+
const s3Key = await this.uploadToS3(entityType, parquetBuffer, metrics);
|
|
337
|
+
metrics.s3UploadTimeMs = Date.now() - uploadStart;
|
|
338
|
+
this.logger.info(`Uploaded to S3: ${s3Key} in ${metrics.s3UploadTimeMs}ms`);
|
|
339
|
+
|
|
340
|
+
// STEP 6: Update state
|
|
341
|
+
await this.updateState(entityType, metrics);
|
|
342
|
+
|
|
343
|
+
metrics.endTime = Date.now();
|
|
344
|
+
metrics.recordsPerSecond = Math.round(
|
|
345
|
+
(metrics.totalRecords / (metrics.endTime - metrics.startTime)) * 1000
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
this.logger.info('Export completed successfully', {
|
|
349
|
+
entityType,
|
|
350
|
+
totalRecords: metrics.totalRecords,
|
|
351
|
+
totalPages: metrics.totalPages,
|
|
352
|
+
duration: metrics.endTime - metrics.startTime,
|
|
353
|
+
recordsPerSecond: metrics.recordsPerSecond,
|
|
354
|
+
s3Key,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return metrics;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
metrics.errors.push((error as Error).message);
|
|
360
|
+
this.logger.error('Export failed', error as Error, { entityType });
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get incremental extraction timestamp
|
|
367
|
+
*/
|
|
368
|
+
private async getIncrementalTimestamp(entityType: string): Promise<string | undefined> {
|
|
369
|
+
if (process.env.EXPORT_INCREMENTAL !== 'true') {
|
|
370
|
+
this.logger.info('Incremental extraction disabled, performing full export');
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const state = await this.stateManager.loadState();
|
|
375
|
+
if (!state || state.lastEntityType !== entityType) {
|
|
376
|
+
this.logger.info('No previous state found, performing full export');
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.logger.info(`Incremental extraction from: ${state.lastExportTimestamp}`);
|
|
381
|
+
return state.lastExportTimestamp;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Execute GraphQL query with auto-pagination
|
|
386
|
+
*/
|
|
387
|
+
private async executeQuery(
|
|
388
|
+
entityType: string,
|
|
389
|
+
fromTimestamp: string | undefined,
|
|
390
|
+
metrics: ExportMetrics
|
|
391
|
+
): Promise<any[]> {
|
|
392
|
+
const queryConfig = this.queries[entityType];
|
|
393
|
+
if (!queryConfig) {
|
|
394
|
+
throw new Error(`No query configuration found for entity type: ${entityType}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const variables: any = {
|
|
398
|
+
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
399
|
+
first: parseInt(process.env.EXPORT_PAGE_SIZE || '100'),
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Add incremental filter if timestamp provided
|
|
403
|
+
if (fromTimestamp) {
|
|
404
|
+
if (entityType === 'inventory') {
|
|
405
|
+
variables.updatedFrom = fromTimestamp;
|
|
406
|
+
} else if (entityType === 'orders') {
|
|
407
|
+
variables.createdFrom = fromTimestamp;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Execute query with auto-pagination
|
|
412
|
+
const result = await this.client.graphql({
|
|
413
|
+
query: queryConfig.query,
|
|
414
|
+
variables,
|
|
415
|
+
pagination: {
|
|
416
|
+
maxRecords: parseInt(process.env.EXPORT_MAX_RECORDS || '10000'),
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Extract records from connection
|
|
421
|
+
const connectionPath = queryConfig.connectionPath;
|
|
422
|
+
const connection = result.data[connectionPath];
|
|
423
|
+
if (!connection?.edges) {
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return connection.edges.map((edge: any) => edge.node);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Transform data using UniversalMapper
|
|
432
|
+
*/
|
|
433
|
+
private async transformData(entityType: string, rawData: any[]): Promise<any[]> {
|
|
434
|
+
const mappingConfig = this.mappings[entityType];
|
|
435
|
+
if (!mappingConfig) {
|
|
436
|
+
throw new Error(`No mapping configuration found for entity type: ${entityType}`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Initialize mapper with custom resolvers
|
|
440
|
+
this.mapper = new UniversalMapper(mappingConfig, {
|
|
441
|
+
customResolvers: {
|
|
442
|
+
'custom.timestamp': () => new Date().toISOString(),
|
|
443
|
+
'custom.fullName': (value: any, sourceData: any) => {
|
|
444
|
+
const firstName = this.getNestedValue(sourceData, 'customer.firstName') || '';
|
|
445
|
+
const lastName = this.getNestedValue(sourceData, 'customer.lastName') || '';
|
|
446
|
+
return `${firstName} ${lastName}`.trim();
|
|
447
|
+
},
|
|
448
|
+
'custom.arrayLength': (value: any) => {
|
|
449
|
+
return Array.isArray(value) ? value.length : 0;
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const transformed = [];
|
|
455
|
+
for (const record of rawData) {
|
|
456
|
+
const result = await this.mapper.map(record);
|
|
457
|
+
if (!result.success) {
|
|
458
|
+
this.logger.warn('Mapping errors encountered', {
|
|
459
|
+
record: record.id || record.ref,
|
|
460
|
+
errors: result.errors,
|
|
461
|
+
});
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
transformed.push(result.data);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return transformed;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Generate Parquet file buffer
|
|
472
|
+
*/
|
|
473
|
+
private async generateParquet(data: any[]): Promise<Buffer> {
|
|
474
|
+
return await this.s3.writeParquetContent(data, {
|
|
475
|
+
compression: 'SNAPPY',
|
|
476
|
+
rowGroupSize: 10000,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Upload Parquet file to S3
|
|
482
|
+
*/
|
|
483
|
+
private async uploadToS3(
|
|
484
|
+
entityType: string,
|
|
485
|
+
buffer: Buffer,
|
|
486
|
+
metrics: ExportMetrics
|
|
487
|
+
): Promise<string> {
|
|
488
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
489
|
+
const key = `exports/${entityType}/${new Date().toISOString().split('T')[0]}/${entityType}_${timestamp}.parquet`;
|
|
490
|
+
|
|
491
|
+
await this.s3.uploadFile(key, buffer, {
|
|
492
|
+
contentType: 'application/octet-stream',
|
|
493
|
+
metadata: {
|
|
494
|
+
'entity-type': entityType,
|
|
495
|
+
'record-count': metrics.totalRecords.toString(),
|
|
496
|
+
'export-timestamp': new Date().toISOString(),
|
|
497
|
+
incremental: process.env.EXPORT_INCREMENTAL || 'false',
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return key;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Update export state
|
|
506
|
+
*/
|
|
507
|
+
private async updateState(entityType: string, metrics: ExportMetrics): Promise<void> {
|
|
508
|
+
const state: ExportState = {
|
|
509
|
+
lastExportTimestamp: new Date().toISOString(),
|
|
510
|
+
lastEntityType: entityType,
|
|
511
|
+
totalRecordsExported: metrics.totalRecords,
|
|
512
|
+
lastExportDuration: metrics.endTime - metrics.startTime,
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
await this.stateManager.saveState(state);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Get nested value from object
|
|
520
|
+
*/
|
|
521
|
+
private getNestedValue(obj: any, path: string): any {
|
|
522
|
+
return path.split('.').reduce((current, key) => {
|
|
523
|
+
return current?.[key];
|
|
524
|
+
}, obj);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Export metrics summary
|
|
529
|
+
*/
|
|
530
|
+
printMetrics(metrics: ExportMetrics): void {
|
|
531
|
+
const duration = metrics.endTime - metrics.startTime;
|
|
532
|
+
|
|
533
|
+
console.log('\n=== Export Metrics ===');
|
|
534
|
+
console.log(`Total Duration: ${duration}ms`);
|
|
535
|
+
console.log(`Total Records: ${metrics.totalRecords}`);
|
|
536
|
+
console.log(`Total Pages: ${metrics.totalPages}`);
|
|
537
|
+
console.log(`Records/Second: ${metrics.recordsPerSecond}`);
|
|
538
|
+
console.log(`\nBreakdown:`);
|
|
539
|
+
console.log(
|
|
540
|
+
` Query Time: ${metrics.queryTimeMs}ms (${Math.round((metrics.queryTimeMs / duration) * 100)}%)`
|
|
541
|
+
);
|
|
542
|
+
console.log(
|
|
543
|
+
` Transform Time: ${metrics.transformTimeMs}ms (${Math.round((metrics.transformTimeMs / duration) * 100)}%)`
|
|
544
|
+
);
|
|
545
|
+
console.log(
|
|
546
|
+
` Parquet Write: ${metrics.parquetWriteTimeMs}ms (${Math.round((metrics.parquetWriteTimeMs / duration) * 100)}%)`
|
|
547
|
+
);
|
|
548
|
+
console.log(
|
|
549
|
+
` S3 Upload: ${metrics.s3UploadTimeMs}ms (${Math.round((metrics.s3UploadTimeMs / duration) * 100)}%)`
|
|
550
|
+
);
|
|
551
|
+
console.log(`\nFile Size: ${Math.round(metrics.fileSizeBytes / 1024)} KB`);
|
|
552
|
+
|
|
553
|
+
if (metrics.errors.length > 0) {
|
|
554
|
+
console.log(`\nErrors: ${metrics.errors.length}`);
|
|
555
|
+
metrics.errors.forEach((error, idx) => {
|
|
556
|
+
console.log(` ${idx + 1}. ${error}`);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
console.log('====================\n');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Main execution function
|
|
566
|
+
*/
|
|
567
|
+
async function main() {
|
|
568
|
+
const exporter = new GraphQLParquetExporter();
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
await exporter.initialize();
|
|
572
|
+
|
|
573
|
+
const entityType = process.argv[2] || process.env.EXPORT_ENTITY_TYPE || 'inventory';
|
|
574
|
+
const metrics = await exporter.export(entityType);
|
|
575
|
+
|
|
576
|
+
exporter.printMetrics(metrics);
|
|
577
|
+
process.exit(0);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
console.error('Export failed:', error);
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Run if executed directly
|
|
585
|
+
if (require.main === module) {
|
|
586
|
+
main();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export { GraphQLParquetExporter };
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
## Key Patterns Explained
|
|
593
|
+
|
|
594
|
+
### Pattern 1: GraphQL Query with Pagination Variables
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
// Define query with pagination variables
|
|
598
|
+
const query = `
|
|
599
|
+
query GetInventory($first: Int!, $after: String, $updatedFrom: DateTime) {
|
|
600
|
+
inventoryPositions(
|
|
601
|
+
first: $first
|
|
602
|
+
after: $after
|
|
603
|
+
updatedOn: { from: $updatedFrom }
|
|
604
|
+
) {
|
|
605
|
+
edges {
|
|
606
|
+
node { id ref productRef qty }
|
|
607
|
+
cursor
|
|
608
|
+
}
|
|
609
|
+
pageInfo { hasNextPage }
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
`;
|
|
613
|
+
|
|
614
|
+
// SDK auto-detects $first and $after for auto-pagination
|
|
615
|
+
const result = await client.graphql({
|
|
616
|
+
query,
|
|
617
|
+
variables: { first: 100, updatedFrom: '2024-01-01T00:00:00Z' },
|
|
618
|
+
pagination: { maxRecords: 10000 },
|
|
619
|
+
});
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**Why This Works:**
|
|
623
|
+
|
|
624
|
+
- SDK detects `$first` and `$after` variables
|
|
625
|
+
- Automatically fetches all pages until `hasNextPage = false`
|
|
626
|
+
- Merges results into single response
|
|
627
|
+
- Progress callbacks track pagination status
|
|
628
|
+
|
|
629
|
+
### Pattern 2: Auto-Pagination Loop
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
// Manual pagination (OLD WAY)
|
|
633
|
+
let allRecords = [];
|
|
634
|
+
let hasMore = true;
|
|
635
|
+
let cursor = null;
|
|
636
|
+
|
|
637
|
+
while (hasMore) {
|
|
638
|
+
const result = await client.graphql({
|
|
639
|
+
query,
|
|
640
|
+
variables: { first: 100, after: cursor },
|
|
641
|
+
});
|
|
642
|
+
allRecords.push(...result.data.inventoryPositions.edges);
|
|
643
|
+
hasMore = result.data.inventoryPositions.pageInfo.hasNextPage;
|
|
644
|
+
cursor = result.data.inventoryPositions.edges[edges.length - 1]?.cursor;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Auto-pagination (NEW WAY)
|
|
648
|
+
const result = await client.graphql({
|
|
649
|
+
query,
|
|
650
|
+
variables: { first: 100 },
|
|
651
|
+
pagination: {
|
|
652
|
+
maxRecords: 10000,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const allRecords = result.data.inventoryPositions.edges;
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
**Benefits:**
|
|
660
|
+
|
|
661
|
+
- 90% less code
|
|
662
|
+
- Built-in safety limits
|
|
663
|
+
- Progress tracking
|
|
664
|
+
- Error handling
|
|
665
|
+
- Deduplication
|
|
666
|
+
|
|
667
|
+
### Pattern 3: Data Transformation for Export
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// Configure extraction mapping
|
|
671
|
+
const mappingConfig = {
|
|
672
|
+
fields: {
|
|
673
|
+
// Direct field mapping
|
|
674
|
+
sku: { source: 'productRef', resolver: 'sdk.uppercase' },
|
|
675
|
+
|
|
676
|
+
// Nested field access
|
|
677
|
+
customer_email: { source: 'customer.email', resolver: 'sdk.toString' },
|
|
678
|
+
|
|
679
|
+
// Type conversion
|
|
680
|
+
quantity: { source: 'qty', resolver: 'sdk.parseInt' },
|
|
681
|
+
|
|
682
|
+
// Date formatting
|
|
683
|
+
last_updated: { source: 'updatedOn', resolver: 'sdk.formatDate' },
|
|
684
|
+
|
|
685
|
+
// Custom resolver
|
|
686
|
+
full_name: {
|
|
687
|
+
source: null,
|
|
688
|
+
resolver: 'custom.fullName',
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// Custom resolvers
|
|
694
|
+
const mapper = new UniversalMapper(mappingConfig, {
|
|
695
|
+
customResolvers: {
|
|
696
|
+
'custom.fullName': (value, sourceData, config, helpers) => {
|
|
697
|
+
const firstName = sourceData.customer?.firstName || '';
|
|
698
|
+
const lastName = sourceData.customer?.lastName || '';
|
|
699
|
+
return `${firstName} ${lastName}`.trim();
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Transform
|
|
705
|
+
const result = await mapper.map(record);
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Pattern 4: Parquet File Generation
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
// Use S3DataSource's built-in Parquet writer
|
|
712
|
+
const parquetBuffer = await s3.writeParquetContent(transformedData, {
|
|
713
|
+
compression: 'SNAPPY', // Fast compression
|
|
714
|
+
rowGroupSize: 10000, // Optimize for analytics
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Alternative: Use ParquetParserService directly
|
|
718
|
+
const parser = new ParquetParserService(logger);
|
|
719
|
+
const buffer = await parser.write(transformedData, 'output.parquet', {
|
|
720
|
+
schema: {
|
|
721
|
+
sku: { type: 'UTF8' },
|
|
722
|
+
quantity: { type: 'INT64' },
|
|
723
|
+
last_updated: { type: 'TIMESTAMP_MILLIS' },
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
**Compression Options:**
|
|
729
|
+
|
|
730
|
+
- `UNCOMPRESSED` - Fastest write, largest file
|
|
731
|
+
- `SNAPPY` - Good balance (recommended)
|
|
732
|
+
- `GZIP` - Best compression, slower
|
|
733
|
+
- `BROTLI` - Best compression ratio
|
|
734
|
+
|
|
735
|
+
### Pattern 5: Incremental Extraction
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
// State management
|
|
739
|
+
interface ExportState {
|
|
740
|
+
lastExportTimestamp: string;
|
|
741
|
+
lastEntityType: string;
|
|
742
|
+
totalRecordsExported: number;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Load previous state
|
|
746
|
+
const state = await loadState();
|
|
747
|
+
const fromTimestamp = state?.lastExportTimestamp;
|
|
748
|
+
|
|
749
|
+
// Query with incremental filter
|
|
750
|
+
const result = await client.graphql({
|
|
751
|
+
query: `
|
|
752
|
+
query GetInventory($first: Int!, $after: String, $updatedFrom: DateTime) {
|
|
753
|
+
inventoryPositions(
|
|
754
|
+
first: $first
|
|
755
|
+
after: $after
|
|
756
|
+
updatedOn: { from: $updatedFrom } # Incremental filter
|
|
757
|
+
) {
|
|
758
|
+
edges { node { id ref qty updatedOn } }
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
`,
|
|
762
|
+
variables: {
|
|
763
|
+
first: 100,
|
|
764
|
+
updatedFrom: fromTimestamp, // Only records updated since last export
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// Save new state
|
|
769
|
+
await saveState({
|
|
770
|
+
lastExportTimestamp: new Date().toISOString(),
|
|
771
|
+
lastEntityType: entityType,
|
|
772
|
+
totalRecordsExported: result.data.inventoryPositions.edges.length,
|
|
773
|
+
});
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
## Deployment Options
|
|
777
|
+
|
|
778
|
+
### Local Execution
|
|
779
|
+
|
|
780
|
+
```bash
|
|
781
|
+
# Run once
|
|
782
|
+
npm run export
|
|
783
|
+
|
|
784
|
+
# Export specific entity
|
|
785
|
+
npm run export orders
|
|
786
|
+
|
|
787
|
+
# With custom config
|
|
788
|
+
EXPORT_ENTITY_TYPE=products EXPORT_MAX_RECORDS=50000 npm run export
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
### Scheduled Export (Daily)
|
|
792
|
+
|
|
793
|
+
**Option 1: Cron (Linux/Mac)**
|
|
794
|
+
|
|
795
|
+
```bash
|
|
796
|
+
# Edit crontab
|
|
797
|
+
crontab -e
|
|
798
|
+
|
|
799
|
+
# Add daily export at 2 AM
|
|
800
|
+
0 2 * * * cd /path/to/fluent-graphql-export && npm run export >> logs/export.log 2>&1
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
**Option 2: Windows Task Scheduler**
|
|
804
|
+
|
|
805
|
+
```powershell
|
|
806
|
+
# Create scheduled task
|
|
807
|
+
$action = New-ScheduledTaskAction -Execute "node" -Argument "src/export-to-s3.ts"
|
|
808
|
+
$trigger = New-ScheduledTaskTrigger -Daily -At 2am
|
|
809
|
+
Register-ScheduledTask -TaskName "FluentExport" -Action $action -Trigger $trigger
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**Option 3: Node-cron (Cross-platform)**
|
|
813
|
+
|
|
814
|
+
Create `src/scheduler.ts`:
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
import cron from 'node-cron';
|
|
818
|
+
import { GraphQLParquetExporter } from './export-to-s3';
|
|
819
|
+
|
|
820
|
+
const exporter = new GraphQLParquetExporter();
|
|
821
|
+
|
|
822
|
+
// Run daily at 2 AM
|
|
823
|
+
cron.schedule('0 2 * * *', async () => {
|
|
824
|
+
console.log('Starting scheduled export...');
|
|
825
|
+
try {
|
|
826
|
+
await exporter.initialize();
|
|
827
|
+
await exporter.export('inventory');
|
|
828
|
+
console.log('Scheduled export completed');
|
|
829
|
+
} catch (error) {
|
|
830
|
+
console.error('Scheduled export failed:', error);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
console.log('Scheduler started');
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### AWS Lambda Deployment
|
|
838
|
+
|
|
839
|
+
Create `lambda/handler.ts`:
|
|
840
|
+
|
|
841
|
+
```typescript
|
|
842
|
+
import { GraphQLParquetExporter } from '../src/export-to-s3';
|
|
843
|
+
|
|
844
|
+
export const handler = async (event: any) => {
|
|
845
|
+
const exporter = new GraphQLParquetExporter();
|
|
846
|
+
|
|
847
|
+
try {
|
|
848
|
+
await exporter.initialize();
|
|
849
|
+
const entityType = event.entityType || 'inventory';
|
|
850
|
+
const metrics = await exporter.export(entityType);
|
|
851
|
+
|
|
852
|
+
return {
|
|
853
|
+
statusCode: 200,
|
|
854
|
+
body: JSON.stringify({
|
|
855
|
+
message: 'Export completed successfully',
|
|
856
|
+
metrics,
|
|
857
|
+
}),
|
|
858
|
+
};
|
|
859
|
+
} catch (error) {
|
|
860
|
+
return {
|
|
861
|
+
statusCode: 500,
|
|
862
|
+
body: JSON.stringify({
|
|
863
|
+
message: 'Export failed',
|
|
864
|
+
error: (error as Error).message,
|
|
865
|
+
}),
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**EventBridge Schedule:**
|
|
872
|
+
|
|
873
|
+
```json
|
|
874
|
+
{
|
|
875
|
+
"scheduleExpression": "cron(0 2 * * ? *)",
|
|
876
|
+
"target": {
|
|
877
|
+
"arn": "arn:aws:lambda:us-east-1:123456789:function:fluent-export",
|
|
878
|
+
"input": "{\"entityType\": \"inventory\"}"
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
## Testing
|
|
884
|
+
|
|
885
|
+
### Test Extraction Locally
|
|
886
|
+
|
|
887
|
+
```bash
|
|
888
|
+
# Test with small dataset
|
|
889
|
+
EXPORT_MAX_RECORDS=10 EXPORT_INCREMENTAL=false npm run export
|
|
890
|
+
|
|
891
|
+
# Test incremental extraction
|
|
892
|
+
EXPORT_INCREMENTAL=true npm run export
|
|
893
|
+
|
|
894
|
+
# Verify S3 upload
|
|
895
|
+
aws s3 ls s3://fluent-exports/exports/inventory/
|
|
896
|
+
|
|
897
|
+
# Download and validate Parquet
|
|
898
|
+
aws s3 cp s3://fluent-exports/exports/inventory/latest.parquet test.parquet
|
|
899
|
+
python -c "import pandas as pd; print(pd.read_parquet('test.parquet'))"
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### Validation Script
|
|
903
|
+
|
|
904
|
+
Create `scripts/validate-export.sh`:
|
|
905
|
+
|
|
906
|
+
```bash
|
|
907
|
+
#!/bin/bash
|
|
908
|
+
echo "Validating export..."
|
|
909
|
+
|
|
910
|
+
# Check state file
|
|
911
|
+
if [ -f "./state/last-export.json" ]; then
|
|
912
|
+
echo "✓ State file exists"
|
|
913
|
+
cat ./state/last-export.json | jq .
|
|
914
|
+
else
|
|
915
|
+
echo "✗ State file missing"
|
|
916
|
+
exit 1
|
|
917
|
+
fi
|
|
918
|
+
|
|
919
|
+
# Check S3 upload
|
|
920
|
+
LATEST_FILE=$(aws s3 ls s3://fluent-exports/exports/inventory/ --recursive | sort | tail -n 1 | awk '{print $4}')
|
|
921
|
+
if [ -z "$LATEST_FILE" ]; then
|
|
922
|
+
echo "✗ No files found in S3"
|
|
923
|
+
exit 1
|
|
924
|
+
fi
|
|
925
|
+
echo "✓ Latest export: $LATEST_FILE"
|
|
926
|
+
|
|
927
|
+
# Get file size
|
|
928
|
+
FILE_SIZE=$(aws s3 ls s3://fluent-exports/$LATEST_FILE | awk '{print $3}')
|
|
929
|
+
echo "✓ File size: $FILE_SIZE bytes"
|
|
930
|
+
|
|
931
|
+
# Check record count
|
|
932
|
+
RECORD_COUNT=$(cat ./state/last-export.json | jq -r .totalRecordsExported)
|
|
933
|
+
echo "✓ Records exported: $RECORD_COUNT"
|
|
934
|
+
|
|
935
|
+
echo "Validation complete!"
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
## Common Issues
|
|
939
|
+
|
|
940
|
+
### Issue 1: Auto-Pagination Not Triggering
|
|
941
|
+
|
|
942
|
+
**Symptoms:**
|
|
943
|
+
|
|
944
|
+
- Only first page of results returned
|
|
945
|
+
- `autoPagination` metadata missing
|
|
946
|
+
|
|
947
|
+
**Cause:**
|
|
948
|
+
|
|
949
|
+
- Query missing `$first` or `$after` variables
|
|
950
|
+
- Variables not passed in query execution
|
|
951
|
+
|
|
952
|
+
**Solution:**
|
|
953
|
+
|
|
954
|
+
```typescript
|
|
955
|
+
// ✗ Wrong - no pagination variables
|
|
956
|
+
const query = `query { inventoryPositions(first: 100) { edges { node { id } } } }`;
|
|
957
|
+
|
|
958
|
+
// ✓ Correct - pagination variables defined
|
|
959
|
+
const query = `query GetInventory($first: Int!, $after: String) {
|
|
960
|
+
inventoryPositions(first: $first, after: $after) {
|
|
961
|
+
edges { node { id } }
|
|
962
|
+
pageInfo { hasNextPage }
|
|
963
|
+
}
|
|
964
|
+
}`;
|
|
965
|
+
|
|
966
|
+
const result = await client.graphql({
|
|
967
|
+
query,
|
|
968
|
+
variables: { first: 100 }, // SDK adds $after automatically
|
|
969
|
+
});
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### Issue 2: Parquet File Too Large
|
|
973
|
+
|
|
974
|
+
**Symptoms:**
|
|
975
|
+
|
|
976
|
+
- Out of memory errors
|
|
977
|
+
- Slow S3 upload
|
|
978
|
+
- Lambda timeout
|
|
979
|
+
|
|
980
|
+
**Solution:**
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
// Use compression
|
|
984
|
+
const buffer = await s3.writeParquetContent(data, {
|
|
985
|
+
compression: 'SNAPPY', // or 'GZIP' for better compression
|
|
986
|
+
rowGroupSize: 5000, // Smaller row groups
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// Or split into multiple files
|
|
990
|
+
const BATCH_SIZE = 10000;
|
|
991
|
+
for (let i = 0; i < data.length; i += BATCH_SIZE) {
|
|
992
|
+
const batch = data.slice(i, i + BATCH_SIZE);
|
|
993
|
+
const buffer = await s3.writeParquetContent(batch);
|
|
994
|
+
await s3.uploadFile(`exports/part-${i}.parquet`, buffer);
|
|
995
|
+
}
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
### Issue 3: Incremental Extraction Missing Records
|
|
999
|
+
|
|
1000
|
+
**Symptoms:**
|
|
1001
|
+
|
|
1002
|
+
- Records not appearing in export
|
|
1003
|
+
- Duplicate records in subsequent exports
|
|
1004
|
+
|
|
1005
|
+
**Cause:**
|
|
1006
|
+
|
|
1007
|
+
- Incorrect timestamp filtering
|
|
1008
|
+
- Clock skew between systems
|
|
1009
|
+
|
|
1010
|
+
**Solution:**
|
|
1011
|
+
|
|
1012
|
+
```typescript
|
|
1013
|
+
// Add buffer to timestamp (5 minutes)
|
|
1014
|
+
const lastTimestamp = new Date(state.lastExportTimestamp);
|
|
1015
|
+
const fromTimestamp = new Date(lastTimestamp.getTime() - 5 * 60 * 1000).toISOString();
|
|
1016
|
+
|
|
1017
|
+
// Use overlap detection
|
|
1018
|
+
const seenRecords = new Set(previousExport.map(r => r.id));
|
|
1019
|
+
const newRecords = currentExport.filter(r => !seenRecords.has(r.id));
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
### Issue 4: GraphQL Query Timeout
|
|
1023
|
+
|
|
1024
|
+
**Symptoms:**
|
|
1025
|
+
|
|
1026
|
+
- Query fails after long wait
|
|
1027
|
+
- Partial results returned
|
|
1028
|
+
|
|
1029
|
+
**Solution:**
|
|
1030
|
+
|
|
1031
|
+
```typescript
|
|
1032
|
+
// Use pagination safety limits
|
|
1033
|
+
const result = await client.graphql({
|
|
1034
|
+
query,
|
|
1035
|
+
variables: { first: 100 },
|
|
1036
|
+
pagination: {
|
|
1037
|
+
maxPages: 100, // Stop after 100 pages
|
|
1038
|
+
maxRecords: 10000, // Stop after 10k records
|
|
1039
|
+
timeoutMs: 300000, // Stop after 5 minutes
|
|
1040
|
+
},
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// Check for truncation
|
|
1044
|
+
if (result.extensions.autoPagination.truncated) {
|
|
1045
|
+
console.log('Results truncated:', result.extensions.autoPagination.truncationReason);
|
|
1046
|
+
// Handle partial export...
|
|
1047
|
+
}
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
### Issue 5: UniversalMapper Transform Failures
|
|
1051
|
+
|
|
1052
|
+
**Symptoms:**
|
|
1053
|
+
|
|
1054
|
+
- Some records missing from export
|
|
1055
|
+
- Warning logs about mapping errors
|
|
1056
|
+
|
|
1057
|
+
**Solution:**
|
|
1058
|
+
|
|
1059
|
+
```typescript
|
|
1060
|
+
// Add error handling to mapper
|
|
1061
|
+
const results = [];
|
|
1062
|
+
for (const record of rawData) {
|
|
1063
|
+
const result = await mapper.map(record);
|
|
1064
|
+
if (!result.success) {
|
|
1065
|
+
logger.warn('Mapping failed', {
|
|
1066
|
+
record: record.id,
|
|
1067
|
+
errors: result.errors,
|
|
1068
|
+
});
|
|
1069
|
+
// Include partial data or skip
|
|
1070
|
+
if (result.data) {
|
|
1071
|
+
results.push({ ...result.data, _errors: result.errors });
|
|
1072
|
+
}
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
results.push(result.data);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Or use default values
|
|
1079
|
+
const mappingConfig = {
|
|
1080
|
+
fields: {
|
|
1081
|
+
qty: {
|
|
1082
|
+
source: 'qty',
|
|
1083
|
+
resolver: 'sdk.parseInt',
|
|
1084
|
+
defaultValue: 0, // Use 0 if parsing fails
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
};
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
## Related Guides
|
|
1091
|
+
|
|
1092
|
+
- [Auto-Pagination Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Deep dive into automatic pagination
|
|
1093
|
+
- [Universal Mapping Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - Field mapping and transformations
|
|
1094
|
+
- [Extraction Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - General extraction patterns
|
|
1095
|
+
- [Connector Scenarios Guide](../../02-CORE-GUIDES/advanced-services/advanced-services-readme.md) - More complete examples
|
|
1096
|
+
- [Standalone CSV Ingestion](./s3-csv-batch-api.md) - Ingestion counterpart
|
|
1097
|
+
- [SFTP to Batch API](../../02-CORE-GUIDES/ingestion/ingestion-readme.md) - SFTP-based ingestion (see Versori templates)
|
|
1098
|
+
|
|
1099
|
+
## Performance Optimization Tips
|
|
1100
|
+
|
|
1101
|
+
1. **Batch Size Tuning:**
|
|
1102
|
+
- Small records: 200-500 per page
|
|
1103
|
+
- Large records: 50-100 per page
|
|
1104
|
+
- Monitor query time vs network overhead
|
|
1105
|
+
|
|
1106
|
+
2. **Parquet Optimization:**
|
|
1107
|
+
- Use SNAPPY compression for balance
|
|
1108
|
+
- Adjust rowGroupSize based on query patterns
|
|
1109
|
+
- Use columnar encoding for analytics
|
|
1110
|
+
|
|
1111
|
+
3. **Incremental Extraction:**
|
|
1112
|
+
- Always use timestamp filtering when possible
|
|
1113
|
+
- Add overlap window to prevent gaps
|
|
1114
|
+
- Track state per entity type
|
|
1115
|
+
|
|
1116
|
+
4. **Memory Management:**
|
|
1117
|
+
- Process large exports in batches
|
|
1118
|
+
- Clear data after each batch upload
|
|
1119
|
+
- Use streaming for very large datasets
|
|
1120
|
+
|
|
1121
|
+
5. **Error Recovery:**
|
|
1122
|
+
- Implement retry logic with exponential backoff
|
|
1123
|
+
- Save checkpoints for long-running exports
|
|
1124
|
+
- Keep partial results for debugging
|
|
1125
|
+
|
|
1126
|
+
## Production Checklist
|
|
1127
|
+
|
|
1128
|
+
- [ ] Environment variables configured
|
|
1129
|
+
- [ ] S3 bucket created with proper permissions
|
|
1130
|
+
- [ ] OAuth2 credentials tested
|
|
1131
|
+
- [ ] GraphQL queries validated
|
|
1132
|
+
- [ ] Extraction mappings tested with sample data
|
|
1133
|
+
- [ ] Incremental extraction state directory created
|
|
1134
|
+
- [ ] Logging configured (file + console)
|
|
1135
|
+
- [ ] Error alerting configured
|
|
1136
|
+
- [ ] Scheduled execution tested
|
|
1137
|
+
- [ ] S3 export verified in analytics tools
|
|
1138
|
+
- [ ] Monitoring dashboards created
|
|
1139
|
+
- [ ] Backup strategy for state files
|
|
1140
|
+
- [ ] Documentation for operators
|