@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md
CHANGED
|
@@ -1,1108 +1,1108 @@
|
|
|
1
|
-
# Module 3: Delta Sync (Change Detection)
|
|
2
|
-
|
|
3
|
-
> **Learning Objective:** Implement efficient delta synchronization to process only changed records, reducing processing time and API calls by 90%+.
|
|
4
|
-
>
|
|
5
|
-
> **Level:** Advanced
|
|
6
|
-
|
|
7
|
-
## Table of Contents
|
|
8
|
-
|
|
9
|
-
1. [What is Delta Sync?](#what-is-delta-sync)
|
|
10
|
-
2. [Why Delta Sync Matters](#why-delta-sync-matters)
|
|
11
|
-
3. [Change Detection Strategies](#change-detection-strategies)
|
|
12
|
-
4. [SDK State Management](#sdk-state-management)
|
|
13
|
-
5. [Pattern 1: Timestamp-Based Detection](#pattern-1-timestamp-based-detection)
|
|
14
|
-
6. [Pattern 2: Hash-Based Detection](#pattern-2-hash-based-detection)
|
|
15
|
-
7. [Pattern 3: Source System Change Tracking](#pattern-3-source-system-change-tracking)
|
|
16
|
-
8. [State Storage Options](#state-storage-options)
|
|
17
|
-
9. [Complete Delta Sync Implementation](#complete-delta-sync-implementation)
|
|
18
|
-
10. [Performance Optimization](#performance-optimization)
|
|
19
|
-
11. [Next Steps](#next-steps)
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## What is Delta Sync?
|
|
24
|
-
|
|
25
|
-
**Delta sync** (delta synchronization) means processing only the records that have **changed** since the last sync, rather than reprocessing the entire dataset.
|
|
26
|
-
|
|
27
|
-
### Full Sync vs Delta Sync
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
FULL SYNC (every run):
|
|
31
|
-
Day 1: Process 50,000 records → 10 minutes
|
|
32
|
-
Day 2: Process 50,000 records → 10 minutes (99% duplicates!)
|
|
33
|
-
Day 3: Process 50,000 records → 10 minutes (99% duplicates!)
|
|
34
|
-
|
|
35
|
-
DELTA SYNC (only changes):
|
|
36
|
-
Day 1: Process 50,000 records → 10 minutes (initial load)
|
|
37
|
-
Day 2: Process 50 changed records → 10 seconds
|
|
38
|
-
Day 3: Process 120 changed records → 15 seconds
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Visual Comparison
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
FULL SYNC:
|
|
45
|
-
┌──────────────────────────────────────┐
|
|
46
|
-
│ Source: 50,000 inventory records │
|
|
47
|
-
└──────────────┬───────────────────────┘
|
|
48
|
-
│
|
|
49
|
-
│ Process ALL 50K records every time
|
|
50
|
-
▼
|
|
51
|
-
┌──────────────────────────────────────┐
|
|
52
|
-
│ Fluent Batch API: 500 batches │
|
|
53
|
-
│ Processing Time: 10 minutes │
|
|
54
|
-
│ API Calls: 500 │
|
|
55
|
-
└──────────────────────────────────────┘
|
|
56
|
-
|
|
57
|
-
DELTA SYNC:
|
|
58
|
-
┌──────────────────────────────────────┐
|
|
59
|
-
│ Source: 50,000 inventory records │
|
|
60
|
-
│ Changed: 50 records (0.1%) │
|
|
61
|
-
└──────────────┬───────────────────────┘
|
|
62
|
-
│
|
|
63
|
-
│ Process ONLY 50 changed records
|
|
64
|
-
▼
|
|
65
|
-
┌──────────────────────────────────────┐
|
|
66
|
-
│ Fluent Batch API: 1 batch │
|
|
67
|
-
│ Processing Time: 10 seconds │
|
|
68
|
-
│ API Calls: 1 │
|
|
69
|
-
└──────────────────────────────────────┘
|
|
70
|
-
|
|
71
|
-
Performance Improvement: 99% reduction in processing time
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## Why Delta Sync Matters
|
|
77
|
-
|
|
78
|
-
### Benefits
|
|
79
|
-
|
|
80
|
-
| Benefit | Impact | Example |
|
|
81
|
-
|---------|--------|---------|
|
|
82
|
-
| **Faster Processing** | 90-99% time reduction | 10 min → 10 sec |
|
|
83
|
-
| **Lower API Costs** | Fewer API calls | 500 calls → 1 call |
|
|
84
|
-
| **Reduced Load** | Less database/network strain | Minimal impact on source system |
|
|
85
|
-
| **Real-Time Capable** | Frequent syncs (hourly/15min) | Near real-time updates |
|
|
86
|
-
| **Error Recovery** | Smaller failure blast radius | 50 records vs 50K records |
|
|
87
|
-
|
|
88
|
-
### When Delta Sync is Critical
|
|
89
|
-
|
|
90
|
-
| Scenario | Why? | Change Rate |
|
|
91
|
-
|----------|------|-------------|
|
|
92
|
-
| **High-Frequency Sync** | Hourly/15-minute syncs | < 1% change rate |
|
|
93
|
-
| **Large Datasets** | 100K+ records | Any change rate |
|
|
94
|
-
| **Real-Time Requirements** | Must sync frequently | < 5% change rate |
|
|
95
|
-
| **API Rate Limits** | Limited API quota | Any change rate |
|
|
96
|
-
| **Cost Optimization** | Pay-per-API-call pricing | Any change rate |
|
|
97
|
-
|
|
98
|
-
### Cost Example
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
Scenario: 100,000 inventory records, 0.5% daily change rate
|
|
102
|
-
|
|
103
|
-
FULL SYNC (daily):
|
|
104
|
-
- Records processed: 100,000
|
|
105
|
-
- Batches: 1,000
|
|
106
|
-
- API calls: 1,000
|
|
107
|
-
- Monthly API calls: 30,000
|
|
108
|
-
- Processing time: 20 minutes/day
|
|
109
|
-
|
|
110
|
-
DELTA SYNC (daily):
|
|
111
|
-
- Records processed: 500 (0.5% of 100K)
|
|
112
|
-
- Batches: 5
|
|
113
|
-
- API calls: 5
|
|
114
|
-
- Monthly API calls: 150
|
|
115
|
-
- Processing time: 30 seconds/day
|
|
116
|
-
|
|
117
|
-
Savings:
|
|
118
|
-
- API calls: 99.5% reduction
|
|
119
|
-
- Processing time: 97.5% reduction
|
|
120
|
-
- Infrastructure cost: 95%+ reduction
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
---
|
|
124
|
-
|
|
125
|
-
## Change Detection Strategies
|
|
126
|
-
|
|
127
|
-
### Strategy Comparison
|
|
128
|
-
|
|
129
|
-
| Strategy | Pros | Cons | Best For |
|
|
130
|
-
|----------|------|------|----------|
|
|
131
|
-
| **Timestamp** | Simple, reliable | Requires lastModified field | Most systems |
|
|
132
|
-
| **Hash** | Detects any change | CPU overhead for hashing | Systems without timestamps |
|
|
133
|
-
| **Source System** | Most accurate | Requires source system support | Modern APIs |
|
|
134
|
-
| **Hybrid** | Best accuracy | More complex | Production systems |
|
|
135
|
-
|
|
136
|
-
### Decision Matrix
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
Does source have lastModified/updatedAt field?
|
|
140
|
-
├── YES → Use Timestamp-Based Detection
|
|
141
|
-
│ └── Does source support "changes since" query?
|
|
142
|
-
│ ├── YES → Use Source System Change Tracking (best)
|
|
143
|
-
│ └── NO → Use Timestamp Comparison
|
|
144
|
-
│
|
|
145
|
-
└── NO → Use Hash-Based Detection
|
|
146
|
-
└── Can you add lastModified to source?
|
|
147
|
-
├── YES → Add timestamp, use Timestamp-Based
|
|
148
|
-
└── NO → Hash-Based (required)
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## SDK State Management
|
|
154
|
-
|
|
155
|
-
### StateService Overview
|
|
156
|
-
|
|
157
|
-
The SDK provides `StateService` for tracking processed files and records:
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
161
|
-
|
|
162
|
-
// Versori platform (KV storage)
|
|
163
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
164
|
-
const stateService = new StateService(logger);
|
|
165
|
-
|
|
166
|
-
// Standalone (file-based storage)
|
|
167
|
-
import { FileKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
168
|
-
const kvAdapter = new FileKVAdapter('./state');
|
|
169
|
-
const stateService = new StateService(logger);
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### StateService Methods
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
// Check if file was processed
|
|
176
|
-
const processed = await stateService.isFileProcessed('file-key');
|
|
177
|
-
|
|
178
|
-
// Mark file as processed
|
|
179
|
-
await stateService.markFileProcessed('file-key', {
|
|
180
|
-
processedAt: new Date(),
|
|
181
|
-
recordCount: 5000,
|
|
182
|
-
jobId: 'job-123'
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Get file processing metadata
|
|
186
|
-
const metadata = await stateService.getFileMetadata('file-key');
|
|
187
|
-
|
|
188
|
-
// Store custom state
|
|
189
|
-
await stateService.setState('last-sync-timestamp', Date.now());
|
|
190
|
-
|
|
191
|
-
// Retrieve custom state
|
|
192
|
-
const lastSync = await stateService.getState('last-sync-timestamp');
|
|
193
|
-
|
|
194
|
-
// Clear state (for testing/reset)
|
|
195
|
-
await stateService.clearFileState('file-key');
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### KV Storage Adapters
|
|
199
|
-
|
|
200
|
-
**VersoriKVAdapter** (for Versori platform):
|
|
201
|
-
```typescript
|
|
202
|
-
import { VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
203
|
-
|
|
204
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
**FileKVAdapter** (for standalone Node.js/Deno):
|
|
208
|
-
```typescript
|
|
209
|
-
import { FileKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
210
|
-
|
|
211
|
-
const kvAdapter = new FileKVAdapter('./state', {
|
|
212
|
-
encoding: 'json',
|
|
213
|
-
pretty: true
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
## Pattern 1: Timestamp-Based Detection
|
|
220
|
-
|
|
221
|
-
### Concept
|
|
222
|
-
|
|
223
|
-
Compare `lastModified` timestamp from source with last sync timestamp.
|
|
224
|
-
|
|
225
|
-
### How It Works
|
|
226
|
-
|
|
227
|
-
```
|
|
228
|
-
Step 1: Get last sync timestamp from state
|
|
229
|
-
└── lastSync = 2025-01-15T10:00:00Z
|
|
230
|
-
|
|
231
|
-
Step 2: Query source for records modified after lastSync
|
|
232
|
-
└── SELECT * FROM inventory WHERE lastModified > '2025-01-15T10:00:00Z'
|
|
233
|
-
|
|
234
|
-
Step 3: Process only those changed records
|
|
235
|
-
└── 50 records (instead of 50,000)
|
|
236
|
-
|
|
237
|
-
Step 4: Update last sync timestamp
|
|
238
|
-
└── lastSync = 2025-01-15T14:00:00Z (current time)
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Implementation
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
/**
|
|
245
|
-
* Timestamp-Based Delta Sync
|
|
246
|
-
*
|
|
247
|
-
* Requirements:
|
|
248
|
-
* - Source data has lastModified/updatedAt field
|
|
249
|
-
* - Timestamps are reliable and monotonic
|
|
250
|
-
*/
|
|
251
|
-
|
|
252
|
-
import { createClient, S3DataSource, CSVParserService, UniversalMapper, StateService, FileKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
253
|
-
|
|
254
|
-
async function deltaSync() {
|
|
255
|
-
// Initialize state service
|
|
256
|
-
const kvAdapter = new FileKVAdapter('./state');
|
|
257
|
-
const stateService = new StateService(logger);
|
|
258
|
-
|
|
259
|
-
// Get last sync timestamp
|
|
260
|
-
let lastSyncTime = await stateService.getState('last-sync-timestamp');
|
|
261
|
-
|
|
262
|
-
if (!lastSyncTime) {
|
|
263
|
-
console.log('First run - performing full sync');
|
|
264
|
-
lastSyncTime = new Date(0).toISOString(); // Epoch (all records)
|
|
265
|
-
} else {
|
|
266
|
-
console.log(`Last sync: ${lastSyncTime}`);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Download CSV
|
|
270
|
-
const s3 = new S3DataSource({
|
|
271
|
-
type: 'S3_CSV',
|
|
272
|
-
connectionId: 'my-s3',
|
|
273
|
-
name: 'My S3 Source',
|
|
274
|
-
s3Config: {
|
|
275
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
276
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
277
|
-
region: process.env.AWS_REGION,
|
|
278
|
-
bucket: process.env.AWS_BUCKET
|
|
279
|
-
}
|
|
280
|
-
}, console);
|
|
281
|
-
|
|
282
|
-
const csvContent = await s3.downloadFile('inventory/current.csv');
|
|
283
|
-
|
|
284
|
-
// Parse CSV
|
|
285
|
-
const parser = new CSVParserService({ headers: true });
|
|
286
|
-
const allRecords = await parser.parse(csvContent);
|
|
287
|
-
|
|
288
|
-
console.log(`Total records in source: ${allRecords.length}`);
|
|
289
|
-
|
|
290
|
-
// Filter changed records
|
|
291
|
-
const changedRecords = allRecords.filter(record => {
|
|
292
|
-
const recordTimestamp = new Date(record.lastModified).toISOString();
|
|
293
|
-
return recordTimestamp > lastSyncTime;
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
console.log(`Changed records: ${changedRecords.length} (${((changedRecords.length / allRecords.length) * 100).toFixed(2)}%)`);
|
|
297
|
-
|
|
298
|
-
if (changedRecords.length === 0) {
|
|
299
|
-
console.log('No changes detected - skipping sync');
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Map records
|
|
304
|
-
const mapper = new UniversalMapper({
|
|
305
|
-
fields: {
|
|
306
|
-
ref: { source: 'sku', resolver: 'custom.buildRef' },
|
|
307
|
-
type: { value: 'INVENTORY' },
|
|
308
|
-
productRef: { source: 'sku', required: true },
|
|
309
|
-
locationRef: { source: 'location', required: true },
|
|
310
|
-
onHand: { source: 'qty', resolver: 'sdk.parseInt' }
|
|
311
|
-
}
|
|
312
|
-
}, {
|
|
313
|
-
customResolvers: {
|
|
314
|
-
'custom.buildRef': (v, d) => `${d.sku}-${d.location}`
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
const mappedRecords = [];
|
|
319
|
-
for (const record of changedRecords) {
|
|
320
|
-
const result = await mapper.map(record);
|
|
321
|
-
if (result.success) {
|
|
322
|
-
mappedRecords.push(result.data);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Create client and job
|
|
327
|
-
const client = await createClient({
|
|
328
|
-
config: {
|
|
329
|
-
baseUrl: process.env.FLUENT_BASE_URL,
|
|
330
|
-
clientId: process.env.FLUENT_CLIENT_ID,
|
|
331
|
-
clientSecret: process.env.FLUENT_CLIENT_SECRET,
|
|
332
|
-
retailerId: process.env.FLUENT_RETAILER_ID
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
const job = await client.createJob({
|
|
337
|
-
name: `Delta Sync - ${new Date().toISOString()}`,
|
|
338
|
-
retailerId: '2'
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
// Send batches
|
|
342
|
-
const BATCH_SIZE = 100;
|
|
343
|
-
for (let i = 0; i < mappedRecords.length; i += BATCH_SIZE) {
|
|
344
|
-
const batch = mappedRecords.slice(i, i + BATCH_SIZE);
|
|
345
|
-
await client.sendBatch(job.id, { entities: batch });
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Poll completion
|
|
349
|
-
let status = await client.getJobStatus(job.id);
|
|
350
|
-
while (status.status === 'PENDING' || status.status === 'PROCESSING') {
|
|
351
|
-
await new Promise(r => setTimeout(r, 30000));
|
|
352
|
-
status = await client.getJobStatus(job.id);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (status.status === 'COMPLETED') {
|
|
356
|
-
// Update last sync timestamp
|
|
357
|
-
const currentTime = new Date().toISOString();
|
|
358
|
-
await stateService.setState('last-sync-timestamp', currentTime);
|
|
359
|
-
|
|
360
|
-
console.log(`✓ Delta sync completed - ${mappedRecords.length} records processed`);
|
|
361
|
-
console.log(`Next sync will process changes after ${currentTime}`);
|
|
362
|
-
} else {
|
|
363
|
-
throw new Error(`Job failed: ${status.status}`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
deltaSync().catch(console.error);
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
### Timestamp Format Handling
|
|
371
|
-
|
|
372
|
-
```typescript
|
|
373
|
-
/**
|
|
374
|
-
* Normalize various timestamp formats
|
|
375
|
-
*/
|
|
376
|
-
function normalizeTimestamp(timestamp: string | number | Date): string {
|
|
377
|
-
// Handle different input types
|
|
378
|
-
if (timestamp instanceof Date) {
|
|
379
|
-
return timestamp.toISOString();
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (typeof timestamp === 'number') {
|
|
383
|
-
return new Date(timestamp).toISOString();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Parse string timestamp
|
|
387
|
-
const date = new Date(timestamp);
|
|
388
|
-
|
|
389
|
-
if (isNaN(date.getTime())) {
|
|
390
|
-
throw new Error(`Invalid timestamp: ${timestamp}`);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return date.toISOString();
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Usage
|
|
397
|
-
const recordTimestamp = normalizeTimestamp(record.lastModified);
|
|
398
|
-
const isChanged = recordTimestamp > lastSyncTime;
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
---
|
|
402
|
-
|
|
403
|
-
## Pattern 2: Hash-Based Detection
|
|
404
|
-
|
|
405
|
-
### Concept
|
|
406
|
-
|
|
407
|
-
Calculate hash of record content and compare with previous hash.
|
|
408
|
-
|
|
409
|
-
### How It Works
|
|
410
|
-
|
|
411
|
-
```
|
|
412
|
-
Step 1: Load previous hashes from state
|
|
413
|
-
└── { "SKU001-WH01": "a1b2c3d4", "SKU002-WH01": "e5f6g7h8" }
|
|
414
|
-
|
|
415
|
-
Step 2: For each record, calculate current hash
|
|
416
|
-
└── currentHash = hash(sku + location + qty + ...)
|
|
417
|
-
|
|
418
|
-
Step 3: Compare with previous hash
|
|
419
|
-
└── If hash changed → process record
|
|
420
|
-
└── If hash same → skip record
|
|
421
|
-
|
|
422
|
-
Step 4: Store current hashes for next run
|
|
423
|
-
└── { "SKU001-WH01": "a1b2c3d4", "SKU002-WH01": "x9y8z7w6" }
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
### Implementation
|
|
427
|
-
|
|
428
|
-
```typescript
|
|
429
|
-
/**
|
|
430
|
-
* Hash-Based Delta Sync
|
|
431
|
-
*
|
|
432
|
-
* Use when:
|
|
433
|
-
* - Source data has NO lastModified field
|
|
434
|
-
* - Need to detect ANY field changes
|
|
435
|
-
*/
|
|
436
|
-
|
|
437
|
-
import crypto from 'crypto';
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Calculate hash of record
|
|
441
|
-
*/
|
|
442
|
-
function calculateRecordHash(record: any, fields: string[]): string {
|
|
443
|
-
// Extract relevant fields in consistent order
|
|
444
|
-
const values = fields.map(field => String(record[field] || ''));
|
|
445
|
-
|
|
446
|
-
// Join and hash
|
|
447
|
-
const combined = values.join('|');
|
|
448
|
-
return crypto.createHash('md5').update(combined).digest('hex');
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
async function hashBasedDeltaSync() {
|
|
452
|
-
const kvAdapter = new FileKVAdapter('./state');
|
|
453
|
-
const stateService = new StateService(logger);
|
|
454
|
-
|
|
455
|
-
// Load previous hashes
|
|
456
|
-
const previousHashes = await stateService.getState('record-hashes') || {};
|
|
457
|
-
|
|
458
|
-
console.log(`Loaded ${Object.keys(previousHashes).length} previous hashes`);
|
|
459
|
-
|
|
460
|
-
// Download and parse
|
|
461
|
-
const s3 = new S3DataSource({ /* config */ }, console);
|
|
462
|
-
const csvContent = await s3.downloadFile('inventory/current.csv');
|
|
463
|
-
|
|
464
|
-
const parser = new CSVParserService({ headers: true });
|
|
465
|
-
const allRecords = await parser.parse(csvContent);
|
|
466
|
-
|
|
467
|
-
console.log(`Total records: ${allRecords.length}`);
|
|
468
|
-
|
|
469
|
-
// Detect changes
|
|
470
|
-
const changedRecords = [];
|
|
471
|
-
const currentHashes: Record<string, string> = {};
|
|
472
|
-
|
|
473
|
-
// Fields to include in hash (all fields that matter)
|
|
474
|
-
const hashFields = ['sku', 'location', 'qty', 'status'];
|
|
475
|
-
|
|
476
|
-
for (const record of allRecords) {
|
|
477
|
-
const recordKey = `${record.sku}-${record.location}`;
|
|
478
|
-
const currentHash = calculateRecordHash(record, hashFields);
|
|
479
|
-
|
|
480
|
-
currentHashes[recordKey] = currentHash;
|
|
481
|
-
|
|
482
|
-
const previousHash = previousHashes[recordKey];
|
|
483
|
-
|
|
484
|
-
if (!previousHash) {
|
|
485
|
-
// New record
|
|
486
|
-
console.log(`New record: ${recordKey}`);
|
|
487
|
-
changedRecords.push(record);
|
|
488
|
-
} else if (currentHash !== previousHash) {
|
|
489
|
-
// Changed record
|
|
490
|
-
console.log(`Changed record: ${recordKey} (hash: ${previousHash} → ${currentHash})`);
|
|
491
|
-
changedRecords.push(record);
|
|
492
|
-
}
|
|
493
|
-
// else: unchanged, skip
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
console.log(`Changed records: ${changedRecords.length} (${((changedRecords.length / allRecords.length) * 100).toFixed(2)}%)`);
|
|
497
|
-
|
|
498
|
-
if (changedRecords.length === 0) {
|
|
499
|
-
console.log('No changes detected - skipping sync');
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Process changed records (same as timestamp-based)
|
|
504
|
-
const mapper = new UniversalMapper({ /* config */ });
|
|
505
|
-
const mappedRecords = [];
|
|
506
|
-
|
|
507
|
-
for (const record of changedRecords) {
|
|
508
|
-
const result = await mapper.map(record);
|
|
509
|
-
if (result.success) {
|
|
510
|
-
mappedRecords.push(result.data);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Send to Batch API
|
|
515
|
-
const client = await createClient({ /* config */ });
|
|
516
|
-
const job = await client.createJob({ name: 'Hash-Based Delta Sync', retailerId: '2' });
|
|
517
|
-
|
|
518
|
-
const BATCH_SIZE = 100;
|
|
519
|
-
for (let i = 0; i < mappedRecords.length; i += BATCH_SIZE) {
|
|
520
|
-
await client.sendBatch(job.id, {
|
|
521
|
-
entities: mappedRecords.slice(i, i + BATCH_SIZE)
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Poll completion
|
|
526
|
-
let status = await client.getJobStatus(job.id);
|
|
527
|
-
while (status.status === 'PENDING' || status.status === 'PROCESSING') {
|
|
528
|
-
await new Promise(r => setTimeout(r, 30000));
|
|
529
|
-
status = await client.getJobStatus(job.id);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (status.status === 'COMPLETED') {
|
|
533
|
-
// Store current hashes for next run
|
|
534
|
-
await stateService.setState('record-hashes', currentHashes);
|
|
535
|
-
|
|
536
|
-
console.log(`✓ Delta sync completed - ${mappedRecords.length} records processed`);
|
|
537
|
-
console.log(`Stored ${Object.keys(currentHashes).length} hashes for next run`);
|
|
538
|
-
} else {
|
|
539
|
-
throw new Error(`Job failed: ${status.status}`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
hashBasedDeltaSync().catch(console.error);
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
### Hash Performance Optimization
|
|
547
|
-
|
|
548
|
-
```typescript
|
|
549
|
-
/**
|
|
550
|
-
* Fast hash using Node.js crypto (faster than MD5)
|
|
551
|
-
*/
|
|
552
|
-
function fastHash(data: string): string {
|
|
553
|
-
return crypto.createHash('sha1').update(data).digest('hex');
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Calculate hash for large datasets efficiently
|
|
558
|
-
*/
|
|
559
|
-
async function calculateHashesBatch(
|
|
560
|
-
records: any[],
|
|
561
|
-
fields: string[],
|
|
562
|
-
batchSize = 1000
|
|
563
|
-
): Promise<Map<string, string>> {
|
|
564
|
-
const hashes = new Map<string, string>();
|
|
565
|
-
|
|
566
|
-
for (let i = 0; i < records.length; i += batchSize) {
|
|
567
|
-
const batch = records.slice(i, i + batchSize);
|
|
568
|
-
|
|
569
|
-
for (const record of batch) {
|
|
570
|
-
const key = `${record.sku}-${record.location}`;
|
|
571
|
-
const values = fields.map(f => String(record[f] || ''));
|
|
572
|
-
const hash = fastHash(values.join('|'));
|
|
573
|
-
hashes.set(key, hash);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
if (i % 10000 === 0) {
|
|
577
|
-
console.log(`Processed ${i} records...`);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
return hashes;
|
|
582
|
-
}
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
---
|
|
586
|
-
|
|
587
|
-
## Pattern 3: Source System Change Tracking
|
|
588
|
-
|
|
589
|
-
### Concept
|
|
590
|
-
|
|
591
|
-
Use source system's native change tracking (change logs, CDC, delta exports).
|
|
592
|
-
|
|
593
|
-
### How It Works
|
|
594
|
-
|
|
595
|
-
```
|
|
596
|
-
Source System with Change Tracking:
|
|
597
|
-
┌─────────────────────────────────┐
|
|
598
|
-
│ Inventory Table │
|
|
599
|
-
│ - Changes logged automatically │
|
|
600
|
-
│ - Delta export API available │
|
|
601
|
-
└──────────────┬──────────────────┘
|
|
602
|
-
│
|
|
603
|
-
│ Query: GET /inventory/changes?since=2025-01-15T10:00:00Z
|
|
604
|
-
▼
|
|
605
|
-
┌─────────────────────────────────┐
|
|
606
|
-
│ API Response: Only changed │
|
|
607
|
-
│ - 50 records (not 50,000) │
|
|
608
|
-
│ - Source filtered, not client │
|
|
609
|
-
└─────────────────────────────────┘
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
### Implementation (API-Based)
|
|
613
|
-
|
|
614
|
-
```typescript
|
|
615
|
-
/**
|
|
616
|
-
* Source System Change Tracking
|
|
617
|
-
*
|
|
618
|
-
* Use when:
|
|
619
|
-
* - Source system provides change tracking API
|
|
620
|
-
* - Most efficient - source does the filtering
|
|
621
|
-
*/
|
|
622
|
-
|
|
623
|
-
interface SourceSystemConfig {
|
|
624
|
-
apiUrl: string;
|
|
625
|
-
apiKey: string;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Fetch only changed records from source system
|
|
630
|
-
*/
|
|
631
|
-
async function fetchChangedRecords(
|
|
632
|
-
config: SourceSystemConfig,
|
|
633
|
-
lastSyncTime: string
|
|
634
|
-
): Promise<any[]> {
|
|
635
|
-
const response = await fetch(
|
|
636
|
-
`${config.apiUrl}/inventory/changes?since=${encodeURIComponent(lastSyncTime)}`,
|
|
637
|
-
{
|
|
638
|
-
headers: {
|
|
639
|
-
'Authorization': `Bearer ${config.apiKey}`,
|
|
640
|
-
'Content-Type': 'application/json'
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
if (!response.ok) {
|
|
646
|
-
throw new Error(`Source API error: ${response.statusText}`);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
const data = await response.json();
|
|
650
|
-
return data.changes;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
async function sourceSystemDeltaSync() {
|
|
654
|
-
const kvAdapter = new FileKVAdapter('./state');
|
|
655
|
-
const stateService = new StateService(logger);
|
|
656
|
-
|
|
657
|
-
// Get last sync timestamp
|
|
658
|
-
let lastSyncTime = await stateService.getState('last-sync-timestamp');
|
|
659
|
-
|
|
660
|
-
if (!lastSyncTime) {
|
|
661
|
-
lastSyncTime = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); // Last 24 hours
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
console.log(`Fetching changes since ${lastSyncTime}`);
|
|
665
|
-
|
|
666
|
-
// Fetch only changed records from source
|
|
667
|
-
const changedRecords = await fetchChangedRecords({
|
|
668
|
-
apiUrl: process.env.SOURCE_API_URL!,
|
|
669
|
-
apiKey: process.env.SOURCE_API_KEY!
|
|
670
|
-
}, lastSyncTime);
|
|
671
|
-
|
|
672
|
-
console.log(`Source returned ${changedRecords.length} changed records`);
|
|
673
|
-
|
|
674
|
-
if (changedRecords.length === 0) {
|
|
675
|
-
console.log('No changes detected');
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Process changed records (same as other patterns)
|
|
680
|
-
const mapper = new UniversalMapper({ /* config */ });
|
|
681
|
-
const mappedRecords = [];
|
|
682
|
-
|
|
683
|
-
for (const record of changedRecords) {
|
|
684
|
-
const result = await mapper.map(record);
|
|
685
|
-
if (result.success) {
|
|
686
|
-
mappedRecords.push(result.data);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Send to Batch API
|
|
691
|
-
const client = await createClient({ /* config */ });
|
|
692
|
-
const job = await client.createJob({
|
|
693
|
-
name: `Source Delta Sync - ${new Date().toISOString()}`,
|
|
694
|
-
retailerId: '2'
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
const BATCH_SIZE = 100;
|
|
698
|
-
for (let i = 0; i < mappedRecords.length; i += BATCH_SIZE) {
|
|
699
|
-
await client.sendBatch(job.id, {
|
|
700
|
-
entities: mappedRecords.slice(i, i + BATCH_SIZE)
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// Poll completion
|
|
705
|
-
let status = await client.getJobStatus(job.id);
|
|
706
|
-
while (status.status === 'PENDING' || status.status === 'PROCESSING') {
|
|
707
|
-
await new Promise(r => setTimeout(r, 30000));
|
|
708
|
-
status = await client.getJobStatus(job.id);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (status.status === 'COMPLETED') {
|
|
712
|
-
// Update last sync timestamp
|
|
713
|
-
await stateService.setState('last-sync-timestamp', new Date().toISOString());
|
|
714
|
-
console.log(`✓ Delta sync completed - ${mappedRecords.length} records processed`);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
```
|
|
718
|
-
|
|
719
|
-
### Implementation (File-Based Delta)
|
|
720
|
-
|
|
721
|
-
```typescript
|
|
722
|
-
/**
|
|
723
|
-
* Source generates delta files (only changed records)
|
|
724
|
-
*/
|
|
725
|
-
|
|
726
|
-
async function fileDeltaSync() {
|
|
727
|
-
const s3 = new S3DataSource({ /* config */ }, console);
|
|
728
|
-
|
|
729
|
-
// Source system uploads delta files to specific prefix
|
|
730
|
-
const deltaFiles = await s3.listFiles('inventory/deltas/');
|
|
731
|
-
|
|
732
|
-
console.log(`Found ${deltaFiles.length} delta files to process`);
|
|
733
|
-
|
|
734
|
-
for (const file of deltaFiles) {
|
|
735
|
-
// Download delta file (contains only changes)
|
|
736
|
-
const csvContent = await s3.downloadFile(file.key);
|
|
737
|
-
|
|
738
|
-
const parser = new CSVParserService({ headers: true });
|
|
739
|
-
const changedRecords = await parser.parse(csvContent);
|
|
740
|
-
|
|
741
|
-
console.log(`Processing ${changedRecords.length} changes from ${file.key}`);
|
|
742
|
-
|
|
743
|
-
// Process records (same as other patterns)
|
|
744
|
-
// ...
|
|
745
|
-
|
|
746
|
-
// Archive after processing
|
|
747
|
-
await s3.moveFile(file.key, file.key.replace('deltas/', 'deltas/processed/'));
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
---
|
|
753
|
-
|
|
754
|
-
## State Storage Options
|
|
755
|
-
|
|
756
|
-
### Option 1: File-Based (Standalone)
|
|
757
|
-
|
|
758
|
-
**Best for**: Node.js/Deno standalone scripts
|
|
759
|
-
|
|
760
|
-
```typescript
|
|
761
|
-
import { FileKVAdapter, StateService } from '@fluentcommerce/fc-connect-sdk';
|
|
762
|
-
|
|
763
|
-
const kvAdapter = new FileKVAdapter('./state', {
|
|
764
|
-
encoding: 'json',
|
|
765
|
-
pretty: true
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
const stateService = new StateService(logger);
|
|
769
|
-
|
|
770
|
-
// State stored in: ./state/last-sync-timestamp.json
|
|
771
|
-
await stateService.setState('last-sync-timestamp', '2025-01-15T10:00:00Z');
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
**File structure**:
|
|
775
|
-
```
|
|
776
|
-
./state/
|
|
777
|
-
├── last-sync-timestamp.json
|
|
778
|
-
├── record-hashes.json
|
|
779
|
-
└── processed-files.json
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
### Option 2: Versori KV Storage
|
|
783
|
-
|
|
784
|
-
**Best for**: Versori platform workflows
|
|
785
|
-
|
|
786
|
-
```typescript
|
|
787
|
-
import { VersoriKVAdapter, StateService } from '@fluentcommerce/fc-connect-sdk';
|
|
788
|
-
|
|
789
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
790
|
-
const stateService = new StateService(logger);
|
|
791
|
-
|
|
792
|
-
// State stored in Versori KV storage (persistent across workflow runs)
|
|
793
|
-
await stateService.setState('last-sync-timestamp', '2025-01-15T10:00:00Z');
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
### Option 3: S3 State Storage (Custom)
|
|
797
|
-
|
|
798
|
-
**Best for**: Distributed processing, shared state
|
|
799
|
-
|
|
800
|
-
```typescript
|
|
801
|
-
/**
|
|
802
|
-
* Custom S3-based state storage
|
|
803
|
-
*/
|
|
804
|
-
class S3StateStore {
|
|
805
|
-
constructor(
|
|
806
|
-
private s3: S3DataSource,
|
|
807
|
-
private statePrefix: string = 'state/'
|
|
808
|
-
) {}
|
|
809
|
-
|
|
810
|
-
async getState(key: string): Promise<any> {
|
|
811
|
-
try {
|
|
812
|
-
const content = await this.s3.downloadFile(`${this.statePrefix}${key}.json`);
|
|
813
|
-
return JSON.parse(content);
|
|
814
|
-
} catch (error) {
|
|
815
|
-
return null; // State doesn't exist
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
async setState(key: string, value: any): Promise<void> {
|
|
820
|
-
await this.s3.uploadFile(
|
|
821
|
-
`${this.statePrefix}${key}.json`,
|
|
822
|
-
JSON.stringify(value, null, 2)
|
|
823
|
-
);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// Usage
|
|
828
|
-
const stateStore = new S3StateStore(s3, 'inventory-sync/state/');
|
|
829
|
-
const lastSync = await stateStore.getState('last-sync-timestamp');
|
|
830
|
-
await stateStore.setState('last-sync-timestamp', new Date().toISOString());
|
|
831
|
-
```
|
|
832
|
-
|
|
833
|
-
---
|
|
834
|
-
|
|
835
|
-
## Complete Delta Sync Implementation
|
|
836
|
-
|
|
837
|
-
### Production-Ready Pattern (Versori Scheduled)
|
|
838
|
-
|
|
839
|
-
```typescript
|
|
840
|
-
/**
|
|
841
|
-
* Complete Delta Sync - Versori Scheduled Workflow
|
|
842
|
-
*
|
|
843
|
-
* Features:
|
|
844
|
-
* - Timestamp-based change detection
|
|
845
|
-
* - State management with Versori KV
|
|
846
|
-
* - Batch API processing
|
|
847
|
-
* - Error handling and logging
|
|
848
|
-
* - File archival
|
|
849
|
-
*/
|
|
850
|
-
|
|
851
|
-
import { createClient, S3DataSource, CSVParserService, UniversalMapper, StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
852
|
-
|
|
853
|
-
export default async function scheduledDeltaSync(activation: any, log: any, connections: any) {
|
|
854
|
-
try {
|
|
855
|
-
log.info('Starting delta sync workflow');
|
|
856
|
-
|
|
857
|
-
// Initialize state service
|
|
858
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
859
|
-
const stateService = new StateService(logger);
|
|
860
|
-
|
|
861
|
-
// Get last sync timestamp
|
|
862
|
-
let lastSyncTime = await stateService.getState('last-sync-timestamp');
|
|
863
|
-
|
|
864
|
-
if (!lastSyncTime) {
|
|
865
|
-
log.info('First run - performing full sync');
|
|
866
|
-
lastSyncTime = new Date(0).toISOString();
|
|
867
|
-
} else {
|
|
868
|
-
log.info('Last sync timestamp', { lastSyncTime });
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// Create S3 data source
|
|
872
|
-
const s3 = new S3DataSource({
|
|
873
|
-
connection: connections.aws_s3
|
|
874
|
-
}, log);
|
|
875
|
-
|
|
876
|
-
// Download current inventory file
|
|
877
|
-
const csvContent = await s3.downloadFile('inventory/current/inventory.csv');
|
|
878
|
-
|
|
879
|
-
// Parse CSV
|
|
880
|
-
const parser = new CSVParserService({ headers: true, skipEmptyLines: true });
|
|
881
|
-
const allRecords = await parser.parse(csvContent);
|
|
882
|
-
|
|
883
|
-
log.info('Parsed CSV', { totalRecords: allRecords.length });
|
|
884
|
-
|
|
885
|
-
// Filter changed records
|
|
886
|
-
const changedRecords = allRecords.filter(record => {
|
|
887
|
-
if (!record.lastModified) {
|
|
888
|
-
log.warn('Record missing lastModified', { sku: record.sku });
|
|
889
|
-
return true; // Include if no timestamp (safer)
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
const recordTimestamp = new Date(record.lastModified).toISOString();
|
|
893
|
-
return recordTimestamp > lastSyncTime;
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
log.info('Change detection complete', {
|
|
897
|
-
totalRecords: allRecords.length,
|
|
898
|
-
changedRecords: changedRecords.length,
|
|
899
|
-
changePercentage: ((changedRecords.length / allRecords.length) * 100).toFixed(2) + '%'
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
if (changedRecords.length === 0) {
|
|
903
|
-
log.info('No changes detected - skipping sync');
|
|
904
|
-
return { status: 200, body: { message: 'No changes', totalRecords: allRecords.length } };
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// Map records
|
|
908
|
-
const mapper = new UniversalMapper({
|
|
909
|
-
fields: {
|
|
910
|
-
ref: { source: 'sku', resolver: 'custom.buildRef' },
|
|
911
|
-
type: { value: 'INVENTORY' },
|
|
912
|
-
productRef: { source: 'sku', required: true },
|
|
913
|
-
locationRef: { source: 'location', required: true },
|
|
914
|
-
onHand: { source: 'qty', resolver: 'sdk.parseInt' },
|
|
915
|
-
status: { source: 'status', resolver: 'sdk.uppercase' }
|
|
916
|
-
}
|
|
917
|
-
}, {
|
|
918
|
-
customResolvers: {
|
|
919
|
-
'custom.buildRef': (value, data) => `${data.sku}-${data.location}`
|
|
920
|
-
}
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
const mappedRecords = [];
|
|
924
|
-
const mappingErrors = [];
|
|
925
|
-
|
|
926
|
-
for (const record of changedRecords) {
|
|
927
|
-
const result = await mapper.map(record);
|
|
928
|
-
if (result.success) {
|
|
929
|
-
mappedRecords.push(result.data);
|
|
930
|
-
} else {
|
|
931
|
-
mappingErrors.push({ record, errors: result.errors });
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
if (mappingErrors.length > 0) {
|
|
936
|
-
log.warn('Mapping errors encountered', { errorCount: mappingErrors.length });
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
log.info('Mapping complete', { mappedRecords: mappedRecords.length });
|
|
940
|
-
|
|
941
|
-
// Create Fluent client
|
|
942
|
-
const client = await createClient({
|
|
943
|
-
connection: connections.fluent_commerce,
|
|
944
|
-
logger: log
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
// Create Batch job
|
|
948
|
-
const job = await client.createJob({
|
|
949
|
-
name: `Delta Sync - ${new Date().toISOString()}`,
|
|
950
|
-
retailerId: '2'
|
|
951
|
-
});
|
|
952
|
-
|
|
953
|
-
log.info('Created Batch job', { jobId: job.id });
|
|
954
|
-
|
|
955
|
-
// Send batches
|
|
956
|
-
const BATCH_SIZE = 100;
|
|
957
|
-
let batchCount = 0;
|
|
958
|
-
|
|
959
|
-
for (let i = 0; i < mappedRecords.length; i += BATCH_SIZE) {
|
|
960
|
-
const batch = mappedRecords.slice(i, i + BATCH_SIZE);
|
|
961
|
-
|
|
962
|
-
await client.sendBatch(job.id, { entities: batch });
|
|
963
|
-
batchCount++;
|
|
964
|
-
|
|
965
|
-
log.info('Sent batch', { batchNumber: batchCount, recordCount: batch.length });
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Poll status
|
|
969
|
-
let status = await client.getJobStatus(job.id);
|
|
970
|
-
let pollCount = 0;
|
|
971
|
-
|
|
972
|
-
while (status.status === 'PENDING' || status.status === 'PROCESSING') {
|
|
973
|
-
pollCount++;
|
|
974
|
-
|
|
975
|
-
log.info('Polling job status', {
|
|
976
|
-
status: status.status,
|
|
977
|
-
completedBatches: status.completedBatches,
|
|
978
|
-
totalBatches: status.totalBatches,
|
|
979
|
-
pollCount
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
await new Promise(resolve => setTimeout(resolve, 30000)); // 30 seconds
|
|
983
|
-
|
|
984
|
-
status = await client.getJobStatus(job.id);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
if (status.status === 'COMPLETED') {
|
|
988
|
-
// Update last sync timestamp
|
|
989
|
-
const currentTime = new Date().toISOString();
|
|
990
|
-
await stateService.setState('last-sync-timestamp', currentTime);
|
|
991
|
-
|
|
992
|
-
log.info('Delta sync completed successfully', {
|
|
993
|
-
recordsProcessed: mappedRecords.length,
|
|
994
|
-
errorCount: status.errorSummary?.totalErrors || 0,
|
|
995
|
-
nextSyncAfter: currentTime
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
return {
|
|
999
|
-
status: 200,
|
|
1000
|
-
body: {
|
|
1001
|
-
success: true,
|
|
1002
|
-
totalRecords: allRecords.length,
|
|
1003
|
-
changedRecords: changedRecords.length,
|
|
1004
|
-
processedRecords: mappedRecords.length,
|
|
1005
|
-
jobId: job.id,
|
|
1006
|
-
nextSyncAfter: currentTime
|
|
1007
|
-
}
|
|
1008
|
-
};
|
|
1009
|
-
} else {
|
|
1010
|
-
throw new Error(`Job failed with status: ${status.status}`);
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
} catch (error: any) {
|
|
1014
|
-
log.error('Delta sync failed', error);
|
|
1015
|
-
|
|
1016
|
-
return {
|
|
1017
|
-
status: 500,
|
|
1018
|
-
body: {
|
|
1019
|
-
success: false,
|
|
1020
|
-
error: error.message
|
|
1021
|
-
}
|
|
1022
|
-
};
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
```
|
|
1026
|
-
|
|
1027
|
-
---
|
|
1028
|
-
|
|
1029
|
-
## Performance Optimization
|
|
1030
|
-
|
|
1031
|
-
### Memory-Efficient Hash Calculation
|
|
1032
|
-
|
|
1033
|
-
```typescript
|
|
1034
|
-
/**
|
|
1035
|
-
* Stream-based hash calculation for large datasets
|
|
1036
|
-
*/
|
|
1037
|
-
async function streamHashCalculation(
|
|
1038
|
-
csvStream: ReadableStream,
|
|
1039
|
-
hashFields: string[]
|
|
1040
|
-
): Promise<Map<string, { hash: string; record: any }>> {
|
|
1041
|
-
const parser = new CSVParserService({ headers: true });
|
|
1042
|
-
const hashes = new Map();
|
|
1043
|
-
|
|
1044
|
-
for await (const record of parser.streamParse(csvStream)) {
|
|
1045
|
-
const key = `${record.sku}-${record.location}`;
|
|
1046
|
-
const values = hashFields.map(f => String(record[f] || ''));
|
|
1047
|
-
const hash = fastHash(values.join('|'));
|
|
1048
|
-
|
|
1049
|
-
hashes.set(key, { hash, record });
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
return hashes;
|
|
1053
|
-
}
|
|
1054
|
-
```
|
|
1055
|
-
|
|
1056
|
-
### Incremental State Updates
|
|
1057
|
-
|
|
1058
|
-
```typescript
|
|
1059
|
-
/**
|
|
1060
|
-
* Update state incrementally instead of all at once
|
|
1061
|
-
*/
|
|
1062
|
-
async function updateStateIncremental(
|
|
1063
|
-
stateService: StateService,
|
|
1064
|
-
records: any[],
|
|
1065
|
-
batchSize = 1000
|
|
1066
|
-
) {
|
|
1067
|
-
const hashes: Record<string, string> = {};
|
|
1068
|
-
|
|
1069
|
-
for (let i = 0; i < records.length; i += batchSize) {
|
|
1070
|
-
const batch = records.slice(i, i + batchSize);
|
|
1071
|
-
|
|
1072
|
-
for (const record of batch) {
|
|
1073
|
-
const key = `${record.sku}-${record.location}`;
|
|
1074
|
-
hashes[key] = calculateRecordHash(record, ['sku', 'location', 'qty']);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// Update state every batch (more resilient to failures)
|
|
1078
|
-
await stateService.setState('record-hashes', hashes);
|
|
1079
|
-
|
|
1080
|
-
console.log(`Updated state for ${i + batch.length} records`);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
```
|
|
1084
|
-
|
|
1085
|
-
---
|
|
1086
|
-
|
|
1087
|
-
## Next Steps
|
|
1088
|
-
|
|
1089
|
-
Now that you understand delta sync, you're ready to explore webhook patterns for event-driven architectures!
|
|
1090
|
-
|
|
1091
|
-
**Continue to:** [Module 4: Webhook Patterns →](./integration-patterns-04-webhook-patterns.md)
|
|
1092
|
-
|
|
1093
|
-
Or explore:
|
|
1094
|
-
- [Module 5: Error Handling](./integration-patterns-05-error-handling.md) - Resilience strategies
|
|
1095
|
-
- [Complete Example: Delta Sync](../examples/delta-sync.ts)
|
|
1096
|
-
- [Complete Example: Versori Delta Sync](../../../01-TEMPLATES/versori/workflows/readme.md) - See delta sync patterns
|
|
1097
|
-
|
|
1098
|
-
---
|
|
1099
|
-
|
|
1100
|
-
## Additional Resources
|
|
1101
|
-
|
|
1102
|
-
- [StateService API Reference](../../../02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md#state-management-service)
|
|
1103
|
-
- [File Tracking Patterns](../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md)
|
|
1104
|
-
- [Performance Optimization Guide](../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md)
|
|
1105
|
-
|
|
1106
|
-
---
|
|
1107
|
-
|
|
1108
|
-
[← Back to Index](../integration-patterns-readme.md) | [Previous: Batch Processing →](./integration-patterns-02-batch-processing.md) | [Next: Webhook Patterns →](./integration-patterns-04-webhook-patterns.md)
|
|
1
|
+
# Module 3: Delta Sync (Change Detection)
|
|
2
|
+
|
|
3
|
+
> **Learning Objective:** Implement efficient delta synchronization to process only changed records, reducing processing time and API calls by 90%+.
|
|
4
|
+
>
|
|
5
|
+
> **Level:** Advanced
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [What is Delta Sync?](#what-is-delta-sync)
|
|
10
|
+
2. [Why Delta Sync Matters](#why-delta-sync-matters)
|
|
11
|
+
3. [Change Detection Strategies](#change-detection-strategies)
|
|
12
|
+
4. [SDK State Management](#sdk-state-management)
|
|
13
|
+
5. [Pattern 1: Timestamp-Based Detection](#pattern-1-timestamp-based-detection)
|
|
14
|
+
6. [Pattern 2: Hash-Based Detection](#pattern-2-hash-based-detection)
|
|
15
|
+
7. [Pattern 3: Source System Change Tracking](#pattern-3-source-system-change-tracking)
|
|
16
|
+
8. [State Storage Options](#state-storage-options)
|
|
17
|
+
9. [Complete Delta Sync Implementation](#complete-delta-sync-implementation)
|
|
18
|
+
10. [Performance Optimization](#performance-optimization)
|
|
19
|
+
11. [Next Steps](#next-steps)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## What is Delta Sync?
|
|
24
|
+
|
|
25
|
+
**Delta sync** (delta synchronization) means processing only the records that have **changed** since the last sync, rather than reprocessing the entire dataset.
|
|
26
|
+
|
|
27
|
+
### Full Sync vs Delta Sync
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
FULL SYNC (every run):
|
|
31
|
+
Day 1: Process 50,000 records → 10 minutes
|
|
32
|
+
Day 2: Process 50,000 records → 10 minutes (99% duplicates!)
|
|
33
|
+
Day 3: Process 50,000 records → 10 minutes (99% duplicates!)
|
|
34
|
+
|
|
35
|
+
DELTA SYNC (only changes):
|
|
36
|
+
Day 1: Process 50,000 records → 10 minutes (initial load)
|
|
37
|
+
Day 2: Process 50 changed records → 10 seconds
|
|
38
|
+
Day 3: Process 120 changed records → 15 seconds
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Visual Comparison
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
FULL SYNC:
|
|
45
|
+
┌──────────────────────────────────────┐
|
|
46
|
+
│ Source: 50,000 inventory records │
|
|
47
|
+
└──────────────┬───────────────────────┘
|
|
48
|
+
│
|
|
49
|
+
│ Process ALL 50K records every time
|
|
50
|
+
▼
|
|
51
|
+
┌──────────────────────────────────────┐
|
|
52
|
+
│ Fluent Batch API: 500 batches │
|
|
53
|
+
│ Processing Time: 10 minutes │
|
|
54
|
+
│ API Calls: 500 │
|
|
55
|
+
└──────────────────────────────────────┘
|
|
56
|
+
|
|
57
|
+
DELTA SYNC:
|
|
58
|
+
┌──────────────────────────────────────┐
|
|
59
|
+
│ Source: 50,000 inventory records │
|
|
60
|
+
│ Changed: 50 records (0.1%) │
|
|
61
|
+
└──────────────┬───────────────────────┘
|
|
62
|
+
│
|
|
63
|
+
│ Process ONLY 50 changed records
|
|
64
|
+
▼
|
|
65
|
+
┌──────────────────────────────────────┐
|
|
66
|
+
│ Fluent Batch API: 1 batch │
|
|
67
|
+
│ Processing Time: 10 seconds │
|
|
68
|
+
│ API Calls: 1 │
|
|
69
|
+
└──────────────────────────────────────┘
|
|
70
|
+
|
|
71
|
+
Performance Improvement: 99% reduction in processing time
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Why Delta Sync Matters
|
|
77
|
+
|
|
78
|
+
### Benefits
|
|
79
|
+
|
|
80
|
+
| Benefit | Impact | Example |
|
|
81
|
+
|---------|--------|---------|
|
|
82
|
+
| **Faster Processing** | 90-99% time reduction | 10 min → 10 sec |
|
|
83
|
+
| **Lower API Costs** | Fewer API calls | 500 calls → 1 call |
|
|
84
|
+
| **Reduced Load** | Less database/network strain | Minimal impact on source system |
|
|
85
|
+
| **Real-Time Capable** | Frequent syncs (hourly/15min) | Near real-time updates |
|
|
86
|
+
| **Error Recovery** | Smaller failure blast radius | 50 records vs 50K records |
|
|
87
|
+
|
|
88
|
+
### When Delta Sync is Critical
|
|
89
|
+
|
|
90
|
+
| Scenario | Why? | Change Rate |
|
|
91
|
+
|----------|------|-------------|
|
|
92
|
+
| **High-Frequency Sync** | Hourly/15-minute syncs | < 1% change rate |
|
|
93
|
+
| **Large Datasets** | 100K+ records | Any change rate |
|
|
94
|
+
| **Real-Time Requirements** | Must sync frequently | < 5% change rate |
|
|
95
|
+
| **API Rate Limits** | Limited API quota | Any change rate |
|
|
96
|
+
| **Cost Optimization** | Pay-per-API-call pricing | Any change rate |
|
|
97
|
+
|
|
98
|
+
### Cost Example
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Scenario: 100,000 inventory records, 0.5% daily change rate
|
|
102
|
+
|
|
103
|
+
FULL SYNC (daily):
|
|
104
|
+
- Records processed: 100,000
|
|
105
|
+
- Batches: 1,000
|
|
106
|
+
- API calls: 1,000
|
|
107
|
+
- Monthly API calls: 30,000
|
|
108
|
+
- Processing time: 20 minutes/day
|
|
109
|
+
|
|
110
|
+
DELTA SYNC (daily):
|
|
111
|
+
- Records processed: 500 (0.5% of 100K)
|
|
112
|
+
- Batches: 5
|
|
113
|
+
- API calls: 5
|
|
114
|
+
- Monthly API calls: 150
|
|
115
|
+
- Processing time: 30 seconds/day
|
|
116
|
+
|
|
117
|
+
Savings:
|
|
118
|
+
- API calls: 99.5% reduction
|
|
119
|
+
- Processing time: 97.5% reduction
|
|
120
|
+
- Infrastructure cost: 95%+ reduction
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Change Detection Strategies
|
|
126
|
+
|
|
127
|
+
### Strategy Comparison
|
|
128
|
+
|
|
129
|
+
| Strategy | Pros | Cons | Best For |
|
|
130
|
+
|----------|------|------|----------|
|
|
131
|
+
| **Timestamp** | Simple, reliable | Requires lastModified field | Most systems |
|
|
132
|
+
| **Hash** | Detects any change | CPU overhead for hashing | Systems without timestamps |
|
|
133
|
+
| **Source System** | Most accurate | Requires source system support | Modern APIs |
|
|
134
|
+
| **Hybrid** | Best accuracy | More complex | Production systems |
|
|
135
|
+
|
|
136
|
+
### Decision Matrix
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Does source have lastModified/updatedAt field?
|
|
140
|
+
├── YES → Use Timestamp-Based Detection
|
|
141
|
+
│ └── Does source support "changes since" query?
|
|
142
|
+
│ ├── YES → Use Source System Change Tracking (best)
|
|
143
|
+
│ └── NO → Use Timestamp Comparison
|
|
144
|
+
│
|
|
145
|
+
└── NO → Use Hash-Based Detection
|
|
146
|
+
└── Can you add lastModified to source?
|
|
147
|
+
├── YES → Add timestamp, use Timestamp-Based
|
|
148
|
+
└── NO → Hash-Based (required)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## SDK State Management
|
|
154
|
+
|
|
155
|
+
### StateService Overview
|
|
156
|
+
|
|
157
|
+
The SDK provides `StateService` for tracking processed files and records:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
161
|
+
|
|
162
|
+
// Versori platform (KV storage)
|
|
163
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
164
|
+
const stateService = new StateService(logger);
|
|
165
|
+
|
|
166
|
+
// Standalone (file-based storage)
|
|
167
|
+
import { FileKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
168
|
+
const kvAdapter = new FileKVAdapter('./state');
|
|
169
|
+
const stateService = new StateService(logger);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### StateService Methods
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Check if file was processed
|
|
176
|
+
const processed = await stateService.isFileProcessed('file-key');
|
|
177
|
+
|
|
178
|
+
// Mark file as processed
|
|
179
|
+
await stateService.markFileProcessed('file-key', {
|
|
180
|
+
processedAt: new Date(),
|
|
181
|
+
recordCount: 5000,
|
|
182
|
+
jobId: 'job-123'
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Get file processing metadata
|
|
186
|
+
const metadata = await stateService.getFileMetadata('file-key');
|
|
187
|
+
|
|
188
|
+
// Store custom state
|
|
189
|
+
await stateService.setState('last-sync-timestamp', Date.now());
|
|
190
|
+
|
|
191
|
+
// Retrieve custom state
|
|
192
|
+
const lastSync = await stateService.getState('last-sync-timestamp');
|
|
193
|
+
|
|
194
|
+
// Clear state (for testing/reset)
|
|
195
|
+
await stateService.clearFileState('file-key');
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### KV Storage Adapters
|
|
199
|
+
|
|
200
|
+
**VersoriKVAdapter** (for Versori platform):
|
|
201
|
+
```typescript
|
|
202
|
+
import { VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
203
|
+
|
|
204
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**FileKVAdapter** (for standalone Node.js/Deno):
|
|
208
|
+
```typescript
|
|
209
|
+
import { FileKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
210
|
+
|
|
211
|
+
const kvAdapter = new FileKVAdapter('./state', {
|
|
212
|
+
encoding: 'json',
|
|
213
|
+
pretty: true
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Pattern 1: Timestamp-Based Detection
|
|
220
|
+
|
|
221
|
+
### Concept
|
|
222
|
+
|
|
223
|
+
Compare `lastModified` timestamp from source with last sync timestamp.
|
|
224
|
+
|
|
225
|
+
### How It Works
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
Step 1: Get last sync timestamp from state
|
|
229
|
+
└── lastSync = 2025-01-15T10:00:00Z
|
|
230
|
+
|
|
231
|
+
Step 2: Query source for records modified after lastSync
|
|
232
|
+
└── SELECT * FROM inventory WHERE lastModified > '2025-01-15T10:00:00Z'
|
|
233
|
+
|
|
234
|
+
Step 3: Process only those changed records
|
|
235
|
+
└── 50 records (instead of 50,000)
|
|
236
|
+
|
|
237
|
+
Step 4: Update last sync timestamp
|
|
238
|
+
└── lastSync = 2025-01-15T14:00:00Z (current time)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Implementation
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
/**
|
|
245
|
+
* Timestamp-Based Delta Sync
|
|
246
|
+
*
|
|
247
|
+
* Requirements:
|
|
248
|
+
* - Source data has lastModified/updatedAt field
|
|
249
|
+
* - Timestamps are reliable and monotonic
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
import { createClient, S3DataSource, CSVParserService, UniversalMapper, StateService, FileKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
253
|
+
|
|
254
|
+
async function deltaSync() {
|
|
255
|
+
// Initialize state service
|
|
256
|
+
const kvAdapter = new FileKVAdapter('./state');
|
|
257
|
+
const stateService = new StateService(logger);
|
|
258
|
+
|
|
259
|
+
// Get last sync timestamp
|
|
260
|
+
let lastSyncTime = await stateService.getState('last-sync-timestamp');
|
|
261
|
+
|
|
262
|
+
if (!lastSyncTime) {
|
|
263
|
+
console.log('First run - performing full sync');
|
|
264
|
+
lastSyncTime = new Date(0).toISOString(); // Epoch (all records)
|
|
265
|
+
} else {
|
|
266
|
+
console.log(`Last sync: ${lastSyncTime}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Download CSV
|
|
270
|
+
const s3 = new S3DataSource({
|
|
271
|
+
type: 'S3_CSV',
|
|
272
|
+
connectionId: 'my-s3',
|
|
273
|
+
name: 'My S3 Source',
|
|
274
|
+
s3Config: {
|
|
275
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
276
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
277
|
+
region: process.env.AWS_REGION,
|
|
278
|
+
bucket: process.env.AWS_BUCKET
|
|
279
|
+
}
|
|
280
|
+
}, console);
|
|
281
|
+
|
|
282
|
+
const csvContent = await s3.downloadFile('inventory/current.csv');
|
|
283
|
+
|
|
284
|
+
// Parse CSV
|
|
285
|
+
const parser = new CSVParserService({ headers: true });
|
|
286
|
+
const allRecords = await parser.parse(csvContent);
|
|
287
|
+
|
|
288
|
+
console.log(`Total records in source: ${allRecords.length}`);
|
|
289
|
+
|
|
290
|
+
// Filter changed records
|
|
291
|
+
const changedRecords = allRecords.filter(record => {
|
|
292
|
+
const recordTimestamp = new Date(record.lastModified).toISOString();
|
|
293
|
+
return recordTimestamp > lastSyncTime;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
console.log(`Changed records: ${changedRecords.length} (${((changedRecords.length / allRecords.length) * 100).toFixed(2)}%)`);
|
|
297
|
+
|
|
298
|
+
if (changedRecords.length === 0) {
|
|
299
|
+
console.log('No changes detected - skipping sync');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Map records
|
|
304
|
+
const mapper = new UniversalMapper({
|
|
305
|
+
fields: {
|
|
306
|
+
ref: { source: 'sku', resolver: 'custom.buildRef' },
|
|
307
|
+
type: { value: 'INVENTORY' },
|
|
308
|
+
productRef: { source: 'sku', required: true },
|
|
309
|
+
locationRef: { source: 'location', required: true },
|
|
310
|
+
onHand: { source: 'qty', resolver: 'sdk.parseInt' }
|
|
311
|
+
}
|
|
312
|
+
}, {
|
|
313
|
+
customResolvers: {
|
|
314
|
+
'custom.buildRef': (v, d) => `${d.sku}-${d.location}`
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const mappedRecords = [];
|
|
319
|
+
for (const record of changedRecords) {
|
|
320
|
+
const result = await mapper.map(record);
|
|
321
|
+
if (result.success) {
|
|
322
|
+
mappedRecords.push(result.data);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Create client and job
|
|
327
|
+
const client = await createClient({
|
|
328
|
+
config: {
|
|
329
|
+
baseUrl: process.env.FLUENT_BASE_URL,
|
|
330
|
+
clientId: process.env.FLUENT_CLIENT_ID,
|
|
331
|
+
clientSecret: process.env.FLUENT_CLIENT_SECRET,
|
|
332
|
+
retailerId: process.env.FLUENT_RETAILER_ID
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const job = await client.createJob({
|
|
337
|
+
name: `Delta Sync - ${new Date().toISOString()}`,
|
|
338
|
+
retailerId: '2'
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Send batches
|
|
342
|
+
const BATCH_SIZE = 100;
|
|
343
|
+
for (let i = 0; i < mappedRecords.length; i += BATCH_SIZE) {
|
|
344
|
+
const batch = mappedRecords.slice(i, i + BATCH_SIZE);
|
|
345
|
+
await client.sendBatch(job.id, { entities: batch });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Poll completion
|
|
349
|
+
let status = await client.getJobStatus(job.id);
|
|
350
|
+
while (status.status === 'PENDING' || status.status === 'PROCESSING') {
|
|
351
|
+
await new Promise(r => setTimeout(r, 30000));
|
|
352
|
+
status = await client.getJobStatus(job.id);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (status.status === 'COMPLETED') {
|
|
356
|
+
// Update last sync timestamp
|
|
357
|
+
const currentTime = new Date().toISOString();
|
|
358
|
+
await stateService.setState('last-sync-timestamp', currentTime);
|
|
359
|
+
|
|
360
|
+
console.log(`✓ Delta sync completed - ${mappedRecords.length} records processed`);
|
|
361
|
+
console.log(`Next sync will process changes after ${currentTime}`);
|
|
362
|
+
} else {
|
|
363
|
+
throw new Error(`Job failed: ${status.status}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
deltaSync().catch(console.error);
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Timestamp Format Handling
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
/**
|
|
374
|
+
* Normalize various timestamp formats
|
|
375
|
+
*/
|
|
376
|
+
function normalizeTimestamp(timestamp: string | number | Date): string {
|
|
377
|
+
// Handle different input types
|
|
378
|
+
if (timestamp instanceof Date) {
|
|
379
|
+
return timestamp.toISOString();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (typeof timestamp === 'number') {
|
|
383
|
+
return new Date(timestamp).toISOString();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Parse string timestamp
|
|
387
|
+
const date = new Date(timestamp);
|
|
388
|
+
|
|
389
|
+
if (isNaN(date.getTime())) {
|
|
390
|
+
throw new Error(`Invalid timestamp: ${timestamp}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return date.toISOString();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Usage
|
|
397
|
+
const recordTimestamp = normalizeTimestamp(record.lastModified);
|
|
398
|
+
const isChanged = recordTimestamp > lastSyncTime;
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Pattern 2: Hash-Based Detection
|
|
404
|
+
|
|
405
|
+
### Concept
|
|
406
|
+
|
|
407
|
+
Calculate hash of record content and compare with previous hash.
|
|
408
|
+
|
|
409
|
+
### How It Works
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
Step 1: Load previous hashes from state
|
|
413
|
+
└── { "SKU001-WH01": "a1b2c3d4", "SKU002-WH01": "e5f6g7h8" }
|
|
414
|
+
|
|
415
|
+
Step 2: For each record, calculate current hash
|
|
416
|
+
└── currentHash = hash(sku + location + qty + ...)
|
|
417
|
+
|
|
418
|
+
Step 3: Compare with previous hash
|
|
419
|
+
└── If hash changed → process record
|
|
420
|
+
└── If hash same → skip record
|
|
421
|
+
|
|
422
|
+
Step 4: Store current hashes for next run
|
|
423
|
+
└── { "SKU001-WH01": "a1b2c3d4", "SKU002-WH01": "x9y8z7w6" }
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Implementation
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
/**
|
|
430
|
+
* Hash-Based Delta Sync
|
|
431
|
+
*
|
|
432
|
+
* Use when:
|
|
433
|
+
* - Source data has NO lastModified field
|
|
434
|
+
* - Need to detect ANY field changes
|
|
435
|
+
*/
|
|
436
|
+
|
|
437
|
+
import crypto from 'crypto';
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Calculate hash of record
|
|
441
|
+
*/
|
|
442
|
+
function calculateRecordHash(record: any, fields: string[]): string {
|
|
443
|
+
// Extract relevant fields in consistent order
|
|
444
|
+
const values = fields.map(field => String(record[field] || ''));
|
|
445
|
+
|
|
446
|
+
// Join and hash
|
|
447
|
+
const combined = values.join('|');
|
|
448
|
+
return crypto.createHash('md5').update(combined).digest('hex');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function hashBasedDeltaSync() {
|
|
452
|
+
const kvAdapter = new FileKVAdapter('./state');
|
|
453
|
+
const stateService = new StateService(logger);
|
|
454
|
+
|
|
455
|
+
// Load previous hashes
|
|
456
|
+
const previousHashes = await stateService.getState('record-hashes') || {};
|
|
457
|
+
|
|
458
|
+
console.log(`Loaded ${Object.keys(previousHashes).length} previous hashes`);
|
|
459
|
+
|
|
460
|
+
// Download and parse
|
|
461
|
+
const s3 = new S3DataSource({ /* config */ }, console);
|
|
462
|
+
const csvContent = await s3.downloadFile('inventory/current.csv');
|
|
463
|
+
|
|
464
|
+
const parser = new CSVParserService({ headers: true });
|
|
465
|
+
const allRecords = await parser.parse(csvContent);
|
|
466
|
+
|
|
467
|
+
console.log(`Total records: ${allRecords.length}`);
|
|
468
|
+
|
|
469
|
+
// Detect changes
|
|
470
|
+
const changedRecords = [];
|
|
471
|
+
const currentHashes: Record<string, string> = {};
|
|
472
|
+
|
|
473
|
+
// Fields to include in hash (all fields that matter)
|
|
474
|
+
const hashFields = ['sku', 'location', 'qty', 'status'];
|
|
475
|
+
|
|
476
|
+
for (const record of allRecords) {
|
|
477
|
+
const recordKey = `${record.sku}-${record.location}`;
|
|
478
|
+
const currentHash = calculateRecordHash(record, hashFields);
|
|
479
|
+
|
|
480
|
+
currentHashes[recordKey] = currentHash;
|
|
481
|
+
|
|
482
|
+
const previousHash = previousHashes[recordKey];
|
|
483
|
+
|
|
484
|
+
if (!previousHash) {
|
|
485
|
+
// New record
|
|
486
|
+
console.log(`New record: ${recordKey}`);
|
|
487
|
+
changedRecords.push(record);
|
|
488
|
+
} else if (currentHash !== previousHash) {
|
|
489
|
+
// Changed record
|
|
490
|
+
console.log(`Changed record: ${recordKey} (hash: ${previousHash} → ${currentHash})`);
|
|
491
|
+
changedRecords.push(record);
|
|
492
|
+
}
|
|
493
|
+
// else: unchanged, skip
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
console.log(`Changed records: ${changedRecords.length} (${((changedRecords.length / allRecords.length) * 100).toFixed(2)}%)`);
|
|
497
|
+
|
|
498
|
+
if (changedRecords.length === 0) {
|
|
499
|
+
console.log('No changes detected - skipping sync');
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Process changed records (same as timestamp-based)
|
|
504
|
+
const mapper = new UniversalMapper({ /* config */ });
|
|
505
|
+
const mappedRecords = [];
|
|
506
|
+
|
|
507
|
+
for (const record of changedRecords) {
|
|
508
|
+
const result = await mapper.map(record);
|
|
509
|
+
if (result.success) {
|
|
510
|
+
mappedRecords.push(result.data);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Send to Batch API
|
|
515
|
+
const client = await createClient({ /* config */ });
|
|
516
|
+
const job = await client.createJob({ name: 'Hash-Based Delta Sync', retailerId: '2' });
|
|
517
|
+
|
|
518
|
+
const BATCH_SIZE = 100;
|
|
519
|
+
for (let i = 0; i < mappedRecords.length; i += BATCH_SIZE) {
|
|
520
|
+
await client.sendBatch(job.id, {
|
|
521
|
+
entities: mappedRecords.slice(i, i + BATCH_SIZE)
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Poll completion
|
|
526
|
+
let status = await client.getJobStatus(job.id);
|
|
527
|
+
while (status.status === 'PENDING' || status.status === 'PROCESSING') {
|
|
528
|
+
await new Promise(r => setTimeout(r, 30000));
|
|
529
|
+
status = await client.getJobStatus(job.id);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (status.status === 'COMPLETED') {
|
|
533
|
+
// Store current hashes for next run
|
|
534
|
+
await stateService.setState('record-hashes', currentHashes);
|
|
535
|
+
|
|
536
|
+
console.log(`✓ Delta sync completed - ${mappedRecords.length} records processed`);
|
|
537
|
+
console.log(`Stored ${Object.keys(currentHashes).length} hashes for next run`);
|
|
538
|
+
} else {
|
|
539
|
+
throw new Error(`Job failed: ${status.status}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
hashBasedDeltaSync().catch(console.error);
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Hash Performance Optimization
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
/**
|
|
550
|
+
* Fast hash using Node.js crypto (faster than MD5)
|
|
551
|
+
*/
|
|
552
|
+
function fastHash(data: string): string {
|
|
553
|
+
return crypto.createHash('sha1').update(data).digest('hex');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Calculate hash for large datasets efficiently
|
|
558
|
+
*/
|
|
559
|
+
async function calculateHashesBatch(
|
|
560
|
+
records: any[],
|
|
561
|
+
fields: string[],
|
|
562
|
+
batchSize = 1000
|
|
563
|
+
): Promise<Map<string, string>> {
|
|
564
|
+
const hashes = new Map<string, string>();
|
|
565
|
+
|
|
566
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
567
|
+
const batch = records.slice(i, i + batchSize);
|
|
568
|
+
|
|
569
|
+
for (const record of batch) {
|
|
570
|
+
const key = `${record.sku}-${record.location}`;
|
|
571
|
+
const values = fields.map(f => String(record[f] || ''));
|
|
572
|
+
const hash = fastHash(values.join('|'));
|
|
573
|
+
hashes.set(key, hash);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (i % 10000 === 0) {
|
|
577
|
+
console.log(`Processed ${i} records...`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return hashes;
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Pattern 3: Source System Change Tracking
|
|
588
|
+
|
|
589
|
+
### Concept
|
|
590
|
+
|
|
591
|
+
Use source system's native change tracking (change logs, CDC, delta exports).
|
|
592
|
+
|
|
593
|
+
### How It Works
|
|
594
|
+
|
|
595
|
+
```
|
|
596
|
+
Source System with Change Tracking:
|
|
597
|
+
┌─────────────────────────────────┐
|
|
598
|
+
│ Inventory Table │
|
|
599
|
+
│ - Changes logged automatically │
|
|
600
|
+
│ - Delta export API available │
|
|
601
|
+
└──────────────┬──────────────────┘
|
|
602
|
+
│
|
|
603
|
+
│ Query: GET /inventory/changes?since=2025-01-15T10:00:00Z
|
|
604
|
+
▼
|
|
605
|
+
┌─────────────────────────────────┐
|
|
606
|
+
│ API Response: Only changed │
|
|
607
|
+
│ - 50 records (not 50,000) │
|
|
608
|
+
│ - Source filtered, not client │
|
|
609
|
+
└─────────────────────────────────┘
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Implementation (API-Based)
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
/**
|
|
616
|
+
* Source System Change Tracking
|
|
617
|
+
*
|
|
618
|
+
* Use when:
|
|
619
|
+
* - Source system provides change tracking API
|
|
620
|
+
* - Most efficient - source does the filtering
|
|
621
|
+
*/
|
|
622
|
+
|
|
623
|
+
interface SourceSystemConfig {
|
|
624
|
+
apiUrl: string;
|
|
625
|
+
apiKey: string;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Fetch only changed records from source system
|
|
630
|
+
*/
|
|
631
|
+
async function fetchChangedRecords(
|
|
632
|
+
config: SourceSystemConfig,
|
|
633
|
+
lastSyncTime: string
|
|
634
|
+
): Promise<any[]> {
|
|
635
|
+
const response = await fetch(
|
|
636
|
+
`${config.apiUrl}/inventory/changes?since=${encodeURIComponent(lastSyncTime)}`,
|
|
637
|
+
{
|
|
638
|
+
headers: {
|
|
639
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
640
|
+
'Content-Type': 'application/json'
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
if (!response.ok) {
|
|
646
|
+
throw new Error(`Source API error: ${response.statusText}`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const data = await response.json();
|
|
650
|
+
return data.changes;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function sourceSystemDeltaSync() {
|
|
654
|
+
const kvAdapter = new FileKVAdapter('./state');
|
|
655
|
+
const stateService = new StateService(logger);
|
|
656
|
+
|
|
657
|
+
// Get last sync timestamp
|
|
658
|
+
let lastSyncTime = await stateService.getState('last-sync-timestamp');
|
|
659
|
+
|
|
660
|
+
if (!lastSyncTime) {
|
|
661
|
+
lastSyncTime = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); // Last 24 hours
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
console.log(`Fetching changes since ${lastSyncTime}`);
|
|
665
|
+
|
|
666
|
+
// Fetch only changed records from source
|
|
667
|
+
const changedRecords = await fetchChangedRecords({
|
|
668
|
+
apiUrl: process.env.SOURCE_API_URL!,
|
|
669
|
+
apiKey: process.env.SOURCE_API_KEY!
|
|
670
|
+
}, lastSyncTime);
|
|
671
|
+
|
|
672
|
+
console.log(`Source returned ${changedRecords.length} changed records`);
|
|
673
|
+
|
|
674
|
+
if (changedRecords.length === 0) {
|
|
675
|
+
console.log('No changes detected');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Process changed records (same as other patterns)
|
|
680
|
+
const mapper = new UniversalMapper({ /* config */ });
|
|
681
|
+
const mappedRecords = [];
|
|
682
|
+
|
|
683
|
+
for (const record of changedRecords) {
|
|
684
|
+
const result = await mapper.map(record);
|
|
685
|
+
if (result.success) {
|
|
686
|
+
mappedRecords.push(result.data);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Send to Batch API
|
|
691
|
+
const client = await createClient({ /* config */ });
|
|
692
|
+
const job = await client.createJob({
|
|
693
|
+
name: `Source Delta Sync - ${new Date().toISOString()}`,
|
|
694
|
+
retailerId: '2'
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const BATCH_SIZE = 100;
|
|
698
|
+
for (let i = 0; i < mappedRecords.length; i += BATCH_SIZE) {
|
|
699
|
+
await client.sendBatch(job.id, {
|
|
700
|
+
entities: mappedRecords.slice(i, i + BATCH_SIZE)
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Poll completion
|
|
705
|
+
let status = await client.getJobStatus(job.id);
|
|
706
|
+
while (status.status === 'PENDING' || status.status === 'PROCESSING') {
|
|
707
|
+
await new Promise(r => setTimeout(r, 30000));
|
|
708
|
+
status = await client.getJobStatus(job.id);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (status.status === 'COMPLETED') {
|
|
712
|
+
// Update last sync timestamp
|
|
713
|
+
await stateService.setState('last-sync-timestamp', new Date().toISOString());
|
|
714
|
+
console.log(`✓ Delta sync completed - ${mappedRecords.length} records processed`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Implementation (File-Based Delta)
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
/**
|
|
723
|
+
* Source generates delta files (only changed records)
|
|
724
|
+
*/
|
|
725
|
+
|
|
726
|
+
async function fileDeltaSync() {
|
|
727
|
+
const s3 = new S3DataSource({ /* config */ }, console);
|
|
728
|
+
|
|
729
|
+
// Source system uploads delta files to specific prefix
|
|
730
|
+
const deltaFiles = await s3.listFiles('inventory/deltas/');
|
|
731
|
+
|
|
732
|
+
console.log(`Found ${deltaFiles.length} delta files to process`);
|
|
733
|
+
|
|
734
|
+
for (const file of deltaFiles) {
|
|
735
|
+
// Download delta file (contains only changes)
|
|
736
|
+
const csvContent = await s3.downloadFile(file.key);
|
|
737
|
+
|
|
738
|
+
const parser = new CSVParserService({ headers: true });
|
|
739
|
+
const changedRecords = await parser.parse(csvContent);
|
|
740
|
+
|
|
741
|
+
console.log(`Processing ${changedRecords.length} changes from ${file.key}`);
|
|
742
|
+
|
|
743
|
+
// Process records (same as other patterns)
|
|
744
|
+
// ...
|
|
745
|
+
|
|
746
|
+
// Archive after processing
|
|
747
|
+
await s3.moveFile(file.key, file.key.replace('deltas/', 'deltas/processed/'));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
## State Storage Options
|
|
755
|
+
|
|
756
|
+
### Option 1: File-Based (Standalone)
|
|
757
|
+
|
|
758
|
+
**Best for**: Node.js/Deno standalone scripts
|
|
759
|
+
|
|
760
|
+
```typescript
|
|
761
|
+
import { FileKVAdapter, StateService } from '@fluentcommerce/fc-connect-sdk';
|
|
762
|
+
|
|
763
|
+
const kvAdapter = new FileKVAdapter('./state', {
|
|
764
|
+
encoding: 'json',
|
|
765
|
+
pretty: true
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const stateService = new StateService(logger);
|
|
769
|
+
|
|
770
|
+
// State stored in: ./state/last-sync-timestamp.json
|
|
771
|
+
await stateService.setState('last-sync-timestamp', '2025-01-15T10:00:00Z');
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
**File structure**:
|
|
775
|
+
```
|
|
776
|
+
./state/
|
|
777
|
+
├── last-sync-timestamp.json
|
|
778
|
+
├── record-hashes.json
|
|
779
|
+
└── processed-files.json
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### Option 2: Versori KV Storage
|
|
783
|
+
|
|
784
|
+
**Best for**: Versori platform workflows
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
import { VersoriKVAdapter, StateService } from '@fluentcommerce/fc-connect-sdk';
|
|
788
|
+
|
|
789
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
790
|
+
const stateService = new StateService(logger);
|
|
791
|
+
|
|
792
|
+
// State stored in Versori KV storage (persistent across workflow runs)
|
|
793
|
+
await stateService.setState('last-sync-timestamp', '2025-01-15T10:00:00Z');
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### Option 3: S3 State Storage (Custom)
|
|
797
|
+
|
|
798
|
+
**Best for**: Distributed processing, shared state
|
|
799
|
+
|
|
800
|
+
```typescript
|
|
801
|
+
/**
|
|
802
|
+
* Custom S3-based state storage
|
|
803
|
+
*/
|
|
804
|
+
class S3StateStore {
|
|
805
|
+
constructor(
|
|
806
|
+
private s3: S3DataSource,
|
|
807
|
+
private statePrefix: string = 'state/'
|
|
808
|
+
) {}
|
|
809
|
+
|
|
810
|
+
async getState(key: string): Promise<any> {
|
|
811
|
+
try {
|
|
812
|
+
const content = await this.s3.downloadFile(`${this.statePrefix}${key}.json`);
|
|
813
|
+
return JSON.parse(content);
|
|
814
|
+
} catch (error) {
|
|
815
|
+
return null; // State doesn't exist
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async setState(key: string, value: any): Promise<void> {
|
|
820
|
+
await this.s3.uploadFile(
|
|
821
|
+
`${this.statePrefix}${key}.json`,
|
|
822
|
+
JSON.stringify(value, null, 2)
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Usage
|
|
828
|
+
const stateStore = new S3StateStore(s3, 'inventory-sync/state/');
|
|
829
|
+
const lastSync = await stateStore.getState('last-sync-timestamp');
|
|
830
|
+
await stateStore.setState('last-sync-timestamp', new Date().toISOString());
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
---
|
|
834
|
+
|
|
835
|
+
## Complete Delta Sync Implementation
|
|
836
|
+
|
|
837
|
+
### Production-Ready Pattern (Versori Scheduled)
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
/**
|
|
841
|
+
* Complete Delta Sync - Versori Scheduled Workflow
|
|
842
|
+
*
|
|
843
|
+
* Features:
|
|
844
|
+
* - Timestamp-based change detection
|
|
845
|
+
* - State management with Versori KV
|
|
846
|
+
* - Batch API processing
|
|
847
|
+
* - Error handling and logging
|
|
848
|
+
* - File archival
|
|
849
|
+
*/
|
|
850
|
+
|
|
851
|
+
import { createClient, S3DataSource, CSVParserService, UniversalMapper, StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
852
|
+
|
|
853
|
+
export default async function scheduledDeltaSync(activation: any, log: any, connections: any) {
|
|
854
|
+
try {
|
|
855
|
+
log.info('Starting delta sync workflow');
|
|
856
|
+
|
|
857
|
+
// Initialize state service
|
|
858
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
859
|
+
const stateService = new StateService(logger);
|
|
860
|
+
|
|
861
|
+
// Get last sync timestamp
|
|
862
|
+
let lastSyncTime = await stateService.getState('last-sync-timestamp');
|
|
863
|
+
|
|
864
|
+
if (!lastSyncTime) {
|
|
865
|
+
log.info('First run - performing full sync');
|
|
866
|
+
lastSyncTime = new Date(0).toISOString();
|
|
867
|
+
} else {
|
|
868
|
+
log.info('Last sync timestamp', { lastSyncTime });
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Create S3 data source
|
|
872
|
+
const s3 = new S3DataSource({
|
|
873
|
+
connection: connections.aws_s3
|
|
874
|
+
}, log);
|
|
875
|
+
|
|
876
|
+
// Download current inventory file
|
|
877
|
+
const csvContent = await s3.downloadFile('inventory/current/inventory.csv');
|
|
878
|
+
|
|
879
|
+
// Parse CSV
|
|
880
|
+
const parser = new CSVParserService({ headers: true, skipEmptyLines: true });
|
|
881
|
+
const allRecords = await parser.parse(csvContent);
|
|
882
|
+
|
|
883
|
+
log.info('Parsed CSV', { totalRecords: allRecords.length });
|
|
884
|
+
|
|
885
|
+
// Filter changed records
|
|
886
|
+
const changedRecords = allRecords.filter(record => {
|
|
887
|
+
if (!record.lastModified) {
|
|
888
|
+
log.warn('Record missing lastModified', { sku: record.sku });
|
|
889
|
+
return true; // Include if no timestamp (safer)
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const recordTimestamp = new Date(record.lastModified).toISOString();
|
|
893
|
+
return recordTimestamp > lastSyncTime;
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
log.info('Change detection complete', {
|
|
897
|
+
totalRecords: allRecords.length,
|
|
898
|
+
changedRecords: changedRecords.length,
|
|
899
|
+
changePercentage: ((changedRecords.length / allRecords.length) * 100).toFixed(2) + '%'
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
if (changedRecords.length === 0) {
|
|
903
|
+
log.info('No changes detected - skipping sync');
|
|
904
|
+
return { status: 200, body: { message: 'No changes', totalRecords: allRecords.length } };
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Map records
|
|
908
|
+
const mapper = new UniversalMapper({
|
|
909
|
+
fields: {
|
|
910
|
+
ref: { source: 'sku', resolver: 'custom.buildRef' },
|
|
911
|
+
type: { value: 'INVENTORY' },
|
|
912
|
+
productRef: { source: 'sku', required: true },
|
|
913
|
+
locationRef: { source: 'location', required: true },
|
|
914
|
+
onHand: { source: 'qty', resolver: 'sdk.parseInt' },
|
|
915
|
+
status: { source: 'status', resolver: 'sdk.uppercase' }
|
|
916
|
+
}
|
|
917
|
+
}, {
|
|
918
|
+
customResolvers: {
|
|
919
|
+
'custom.buildRef': (value, data) => `${data.sku}-${data.location}`
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
const mappedRecords = [];
|
|
924
|
+
const mappingErrors = [];
|
|
925
|
+
|
|
926
|
+
for (const record of changedRecords) {
|
|
927
|
+
const result = await mapper.map(record);
|
|
928
|
+
if (result.success) {
|
|
929
|
+
mappedRecords.push(result.data);
|
|
930
|
+
} else {
|
|
931
|
+
mappingErrors.push({ record, errors: result.errors });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (mappingErrors.length > 0) {
|
|
936
|
+
log.warn('Mapping errors encountered', { errorCount: mappingErrors.length });
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
log.info('Mapping complete', { mappedRecords: mappedRecords.length });
|
|
940
|
+
|
|
941
|
+
// Create Fluent client
|
|
942
|
+
const client = await createClient({
|
|
943
|
+
connection: connections.fluent_commerce,
|
|
944
|
+
logger: log
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
// Create Batch job
|
|
948
|
+
const job = await client.createJob({
|
|
949
|
+
name: `Delta Sync - ${new Date().toISOString()}`,
|
|
950
|
+
retailerId: '2'
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
log.info('Created Batch job', { jobId: job.id });
|
|
954
|
+
|
|
955
|
+
// Send batches
|
|
956
|
+
const BATCH_SIZE = 100;
|
|
957
|
+
let batchCount = 0;
|
|
958
|
+
|
|
959
|
+
for (let i = 0; i < mappedRecords.length; i += BATCH_SIZE) {
|
|
960
|
+
const batch = mappedRecords.slice(i, i + BATCH_SIZE);
|
|
961
|
+
|
|
962
|
+
await client.sendBatch(job.id, { entities: batch });
|
|
963
|
+
batchCount++;
|
|
964
|
+
|
|
965
|
+
log.info('Sent batch', { batchNumber: batchCount, recordCount: batch.length });
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Poll status
|
|
969
|
+
let status = await client.getJobStatus(job.id);
|
|
970
|
+
let pollCount = 0;
|
|
971
|
+
|
|
972
|
+
while (status.status === 'PENDING' || status.status === 'PROCESSING') {
|
|
973
|
+
pollCount++;
|
|
974
|
+
|
|
975
|
+
log.info('Polling job status', {
|
|
976
|
+
status: status.status,
|
|
977
|
+
completedBatches: status.completedBatches,
|
|
978
|
+
totalBatches: status.totalBatches,
|
|
979
|
+
pollCount
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
await new Promise(resolve => setTimeout(resolve, 30000)); // 30 seconds
|
|
983
|
+
|
|
984
|
+
status = await client.getJobStatus(job.id);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (status.status === 'COMPLETED') {
|
|
988
|
+
// Update last sync timestamp
|
|
989
|
+
const currentTime = new Date().toISOString();
|
|
990
|
+
await stateService.setState('last-sync-timestamp', currentTime);
|
|
991
|
+
|
|
992
|
+
log.info('Delta sync completed successfully', {
|
|
993
|
+
recordsProcessed: mappedRecords.length,
|
|
994
|
+
errorCount: status.errorSummary?.totalErrors || 0,
|
|
995
|
+
nextSyncAfter: currentTime
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
status: 200,
|
|
1000
|
+
body: {
|
|
1001
|
+
success: true,
|
|
1002
|
+
totalRecords: allRecords.length,
|
|
1003
|
+
changedRecords: changedRecords.length,
|
|
1004
|
+
processedRecords: mappedRecords.length,
|
|
1005
|
+
jobId: job.id,
|
|
1006
|
+
nextSyncAfter: currentTime
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
} else {
|
|
1010
|
+
throw new Error(`Job failed with status: ${status.status}`);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
} catch (error: any) {
|
|
1014
|
+
log.error('Delta sync failed', error);
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
status: 500,
|
|
1018
|
+
body: {
|
|
1019
|
+
success: false,
|
|
1020
|
+
error: error.message
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
---
|
|
1028
|
+
|
|
1029
|
+
## Performance Optimization
|
|
1030
|
+
|
|
1031
|
+
### Memory-Efficient Hash Calculation
|
|
1032
|
+
|
|
1033
|
+
```typescript
|
|
1034
|
+
/**
|
|
1035
|
+
* Stream-based hash calculation for large datasets
|
|
1036
|
+
*/
|
|
1037
|
+
async function streamHashCalculation(
|
|
1038
|
+
csvStream: ReadableStream,
|
|
1039
|
+
hashFields: string[]
|
|
1040
|
+
): Promise<Map<string, { hash: string; record: any }>> {
|
|
1041
|
+
const parser = new CSVParserService({ headers: true });
|
|
1042
|
+
const hashes = new Map();
|
|
1043
|
+
|
|
1044
|
+
for await (const record of parser.streamParse(csvStream)) {
|
|
1045
|
+
const key = `${record.sku}-${record.location}`;
|
|
1046
|
+
const values = hashFields.map(f => String(record[f] || ''));
|
|
1047
|
+
const hash = fastHash(values.join('|'));
|
|
1048
|
+
|
|
1049
|
+
hashes.set(key, { hash, record });
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return hashes;
|
|
1053
|
+
}
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
### Incremental State Updates
|
|
1057
|
+
|
|
1058
|
+
```typescript
|
|
1059
|
+
/**
|
|
1060
|
+
* Update state incrementally instead of all at once
|
|
1061
|
+
*/
|
|
1062
|
+
async function updateStateIncremental(
|
|
1063
|
+
stateService: StateService,
|
|
1064
|
+
records: any[],
|
|
1065
|
+
batchSize = 1000
|
|
1066
|
+
) {
|
|
1067
|
+
const hashes: Record<string, string> = {};
|
|
1068
|
+
|
|
1069
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
1070
|
+
const batch = records.slice(i, i + batchSize);
|
|
1071
|
+
|
|
1072
|
+
for (const record of batch) {
|
|
1073
|
+
const key = `${record.sku}-${record.location}`;
|
|
1074
|
+
hashes[key] = calculateRecordHash(record, ['sku', 'location', 'qty']);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Update state every batch (more resilient to failures)
|
|
1078
|
+
await stateService.setState('record-hashes', hashes);
|
|
1079
|
+
|
|
1080
|
+
console.log(`Updated state for ${i + batch.length} records`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
---
|
|
1086
|
+
|
|
1087
|
+
## Next Steps
|
|
1088
|
+
|
|
1089
|
+
Now that you understand delta sync, you're ready to explore webhook patterns for event-driven architectures!
|
|
1090
|
+
|
|
1091
|
+
**Continue to:** [Module 4: Webhook Patterns →](./integration-patterns-04-webhook-patterns.md)
|
|
1092
|
+
|
|
1093
|
+
Or explore:
|
|
1094
|
+
- [Module 5: Error Handling](./integration-patterns-05-error-handling.md) - Resilience strategies
|
|
1095
|
+
- [Complete Example: Delta Sync](../examples/delta-sync.ts)
|
|
1096
|
+
- [Complete Example: Versori Delta Sync](../../../01-TEMPLATES/versori/workflows/readme.md) - See delta sync patterns
|
|
1097
|
+
|
|
1098
|
+
---
|
|
1099
|
+
|
|
1100
|
+
## Additional Resources
|
|
1101
|
+
|
|
1102
|
+
- [StateService API Reference](../../../02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md#state-management-service)
|
|
1103
|
+
- [File Tracking Patterns](../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md)
|
|
1104
|
+
- [Performance Optimization Guide](../../../02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md)
|
|
1105
|
+
|
|
1106
|
+
---
|
|
1107
|
+
|
|
1108
|
+
[← Back to Index](../integration-patterns-readme.md) | [Previous: Batch Processing →](./integration-patterns-02-batch-processing.md) | [Next: Webhook Patterns →](./integration-patterns-04-webhook-patterns.md)
|