@open-mercato/core 0.9.1-develop.7474.1.51db10479d → 0.9.1-develop.7475.1.28ca93ed97
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/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/catalog_price_history_entry/index.js +53 -0
- package/dist/generated/entities/catalog_price_history_entry/index.js.map +7 -0
- package/dist/generated/entities/customer_group_terms/index.js +2 -0
- package/dist/generated/entities/customer_group_terms/index.js.map +2 -2
- package/dist/generated/entities/ecommerce_store/index.js +33 -0
- package/dist/generated/entities/ecommerce_store/index.js.map +7 -0
- package/dist/generated/entities/ecommerce_store_channel_binding/index.js +29 -0
- package/dist/generated/entities/ecommerce_store_channel_binding/index.js.map +7 -0
- package/dist/generated/entities/ecommerce_store_domain_binding/index.js +23 -0
- package/dist/generated/entities/ecommerce_store_domain_binding/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +8 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +72 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/customerGroupsFixtures.js.map +2 -2
- package/dist/modules/catalog/acl.js +8 -1
- package/dist/modules/catalog/acl.js.map +2 -2
- package/dist/modules/catalog/api/config/omnibus/route.js +184 -0
- package/dist/modules/catalog/api/config/omnibus/route.js.map +7 -0
- package/dist/modules/catalog/api/prices/history/route.js +131 -0
- package/dist/modules/catalog/api/prices/history/route.js.map +7 -0
- package/dist/modules/catalog/api/prices/omnibus-preview/route.js +184 -0
- package/dist/modules/catalog/api/prices/omnibus-preview/route.js.map +7 -0
- package/dist/modules/catalog/api/products/route.js +54 -245
- package/dist/modules/catalog/api/products/route.js.map +3 -3
- package/dist/modules/catalog/api/tags/route.js +6 -0
- package/dist/modules/catalog/api/tags/route.js.map +2 -2
- package/dist/modules/catalog/backend/config/catalog/page.js +3 -1
- package/dist/modules/catalog/backend/config/catalog/page.js.map +2 -2
- package/dist/modules/catalog/cli.js +80 -2
- package/dist/modules/catalog/cli.js.map +2 -2
- package/dist/modules/catalog/commands/categories.js +51 -2
- package/dist/modules/catalog/commands/categories.js.map +2 -2
- package/dist/modules/catalog/commands/prices.js +43 -1
- package/dist/modules/catalog/commands/prices.js.map +2 -2
- package/dist/modules/catalog/commands/products.js +71 -0
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/commands/variants.js +7 -0
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/catalog/components/OmnibusSettings.js +703 -0
- package/dist/modules/catalog/components/OmnibusSettings.js.map +7 -0
- package/dist/modules/catalog/components/PriceEditorOmnibusRow.js +152 -0
- package/dist/modules/catalog/components/PriceEditorOmnibusRow.js.map +7 -0
- package/dist/modules/catalog/components/lookupSources.js +31 -0
- package/dist/modules/catalog/components/lookupSources.js.map +7 -0
- package/dist/modules/catalog/components/prices/PriceScopeSelectors.js +8 -2
- package/dist/modules/catalog/components/prices/PriceScopeSelectors.js.map +2 -2
- package/dist/modules/catalog/components/products/VariantBuilder.js +11 -1
- package/dist/modules/catalog/components/products/VariantBuilder.js.map +2 -2
- package/dist/modules/catalog/data/entities.js +122 -1
- package/dist/modules/catalog/data/entities.js.map +2 -2
- package/dist/modules/catalog/data/validators.js +78 -0
- package/dist/modules/catalog/data/validators.js.map +2 -2
- package/dist/modules/catalog/di.js +18 -0
- package/dist/modules/catalog/di.js.map +2 -2
- package/dist/modules/catalog/lib/omnibus.js +285 -0
- package/dist/modules/catalog/lib/omnibus.js.map +7 -0
- package/dist/modules/catalog/lib/omnibusBackfill.js +146 -0
- package/dist/modules/catalog/lib/omnibusBackfill.js.map +7 -0
- package/dist/modules/catalog/lib/omnibusCache.js +89 -0
- package/dist/modules/catalog/lib/omnibusCache.js.map +7 -0
- package/dist/modules/catalog/lib/omnibusConfig.js +64 -0
- package/dist/modules/catalog/lib/omnibusConfig.js.map +7 -0
- package/dist/modules/catalog/lib/omnibusHistoryQueries.js +191 -0
- package/dist/modules/catalog/lib/omnibusHistoryQueries.js.map +7 -0
- package/dist/modules/catalog/lib/omnibusPresentedEntry.js +73 -0
- package/dist/modules/catalog/lib/omnibusPresentedEntry.js.map +7 -0
- package/dist/modules/catalog/lib/omnibusProductListEnrichment.js +94 -0
- package/dist/modules/catalog/lib/omnibusProductListEnrichment.js.map +7 -0
- package/dist/modules/catalog/lib/omnibusTypes.js +115 -0
- package/dist/modules/catalog/lib/omnibusTypes.js.map +7 -0
- package/dist/modules/catalog/lib/optionSchemaTranslations.js +55 -0
- package/dist/modules/catalog/lib/optionSchemaTranslations.js.map +7 -0
- package/dist/modules/catalog/lib/priceHistoryQuery.js +103 -0
- package/dist/modules/catalog/lib/priceHistoryQuery.js.map +7 -0
- package/dist/modules/catalog/lib/pricing.js +26 -2
- package/dist/modules/catalog/lib/pricing.js.map +2 -2
- package/dist/modules/catalog/lib/productFilters.js +273 -0
- package/dist/modules/catalog/lib/productFilters.js.map +7 -0
- package/dist/modules/catalog/lib/productScopeKeys.js +105 -0
- package/dist/modules/catalog/lib/productScopeKeys.js.map +7 -0
- package/dist/modules/catalog/lib/productScopeReindex.js +73 -0
- package/dist/modules/catalog/lib/productScopeReindex.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20261005105352_catalog.js +39 -0
- package/dist/modules/catalog/migrations/Migration20261005105352_catalog.js.map +7 -0
- package/dist/modules/catalog/migrations/Migration20261005150824_catalog.js +39 -0
- package/dist/modules/catalog/migrations/Migration20261005150824_catalog.js.map +7 -0
- package/dist/modules/catalog/services/catalogOmnibusService.js +480 -0
- package/dist/modules/catalog/services/catalogOmnibusService.js.map +7 -0
- package/dist/modules/catalog/setup.js +3 -1
- package/dist/modules/catalog/setup.js.map +2 -2
- package/dist/modules/catalog/subscribers/product-scope-keys-category-deleted.js +16 -0
- package/dist/modules/catalog/subscribers/product-scope-keys-category-deleted.js.map +7 -0
- package/dist/modules/catalog/subscribers/product-scope-keys-category-updated.js +17 -0
- package/dist/modules/catalog/subscribers/product-scope-keys-category-updated.js.map +7 -0
- package/dist/modules/catalog/translations.js +10 -0
- package/dist/modules/catalog/translations.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +28 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_groups/api/customer-groups/[id]/terms/route.js +5 -0
- package/dist/modules/customer_groups/api/customer-groups/[id]/terms/route.js.map +2 -2
- package/dist/modules/customer_groups/components/CustomerGroupTermsSection.js +111 -3
- package/dist/modules/customer_groups/components/CustomerGroupTermsSection.js.map +2 -2
- package/dist/modules/customer_groups/components/customerGroupAssortmentScope.js +43 -0
- package/dist/modules/customer_groups/components/customerGroupAssortmentScope.js.map +7 -0
- package/dist/modules/customer_groups/data/entities.js +3 -0
- package/dist/modules/customer_groups/data/entities.js.map +2 -2
- package/dist/modules/customer_groups/data/validators.js +9 -0
- package/dist/modules/customer_groups/data/validators.js.map +2 -2
- package/dist/modules/customer_groups/migrations/Migration20261005110250_customer_groups.js +17 -0
- package/dist/modules/customer_groups/migrations/Migration20261005110250_customer_groups.js.map +7 -0
- package/dist/modules/customer_groups/services/customerGroupsService.js +62 -15
- package/dist/modules/customer_groups/services/customerGroupsService.js.map +2 -2
- package/dist/modules/ecommerce/acl.js +13 -0
- package/dist/modules/ecommerce/acl.js.map +7 -0
- package/dist/modules/ecommerce/api/domain-mappings/route.js +72 -0
- package/dist/modules/ecommerce/api/domain-mappings/route.js.map +7 -0
- package/dist/modules/ecommerce/api/openapi.js +27 -0
- package/dist/modules/ecommerce/api/openapi.js.map +7 -0
- package/dist/modules/ecommerce/api/store-channel-bindings/[id]/assortment-count/route.js +109 -0
- package/dist/modules/ecommerce/api/store-channel-bindings/[id]/assortment-count/route.js.map +7 -0
- package/dist/modules/ecommerce/api/store-channel-bindings/crud.js +265 -0
- package/dist/modules/ecommerce/api/store-channel-bindings/crud.js.map +7 -0
- package/dist/modules/ecommerce/api/store-channel-bindings/route.js +67 -0
- package/dist/modules/ecommerce/api/store-channel-bindings/route.js.map +7 -0
- package/dist/modules/ecommerce/api/store-domain-bindings/crud.js +296 -0
- package/dist/modules/ecommerce/api/store-domain-bindings/crud.js.map +7 -0
- package/dist/modules/ecommerce/api/store-domain-bindings/route.js +73 -0
- package/dist/modules/ecommerce/api/store-domain-bindings/route.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js +87 -0
- package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/categories/openapiSchemas.js +50 -0
- package/dist/modules/ecommerce/api/storefront/categories/openapiSchemas.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/categories/route.js +75 -0
- package/dist/modules/ecommerce/api/storefront/categories/route.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/context/route.js +163 -0
- package/dist/modules/ecommerce/api/storefront/context/route.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js +87 -0
- package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/products/openapiSchemas.js +177 -0
- package/dist/modules/ecommerce/api/storefront/products/openapiSchemas.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/products/route.js +78 -0
- package/dist/modules/ecommerce/api/storefront/products/route.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/search/suggest/openapiSchemas.js +25 -0
- package/dist/modules/ecommerce/api/storefront/search/suggest/openapiSchemas.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/search/suggest/route.js +75 -0
- package/dist/modules/ecommerce/api/storefront/search/suggest/route.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/storefrontRouteSupport.js +58 -0
- package/dist/modules/ecommerce/api/storefront/storefrontRouteSupport.js.map +7 -0
- package/dist/modules/ecommerce/api/stores/[id]/branding/route.js +183 -0
- package/dist/modules/ecommerce/api/stores/[id]/branding/route.js.map +7 -0
- package/dist/modules/ecommerce/api/stores/[id]/preview-branding/route.js +92 -0
- package/dist/modules/ecommerce/api/stores/[id]/preview-branding/route.js.map +7 -0
- package/dist/modules/ecommerce/api/stores/crud.js +401 -0
- package/dist/modules/ecommerce/api/stores/crud.js.map +7 -0
- package/dist/modules/ecommerce/api/stores/route.js +64 -0
- package/dist/modules/ecommerce/api/stores/route.js.map +7 -0
- package/dist/modules/ecommerce/backend/config/ecommerce/[id]/page.js +9 -0
- package/dist/modules/ecommerce/backend/config/ecommerce/[id]/page.js.map +7 -0
- package/dist/modules/ecommerce/backend/config/ecommerce/[id]/page.meta.js +16 -0
- package/dist/modules/ecommerce/backend/config/ecommerce/[id]/page.meta.js.map +7 -0
- package/dist/modules/ecommerce/backend/config/ecommerce/page.js +9 -0
- package/dist/modules/ecommerce/backend/config/ecommerce/page.js.map +7 -0
- package/dist/modules/ecommerce/backend/config/ecommerce/page.meta.js +16 -0
- package/dist/modules/ecommerce/backend/config/ecommerce/page.meta.js.map +7 -0
- package/dist/modules/ecommerce/commands/stores.js +110 -0
- package/dist/modules/ecommerce/commands/stores.js.map +7 -0
- package/dist/modules/ecommerce/components/ChannelAssortmentCount.js +90 -0
- package/dist/modules/ecommerce/components/ChannelAssortmentCount.js.map +7 -0
- package/dist/modules/ecommerce/components/ChannelBindingFormSections.js +33 -0
- package/dist/modules/ecommerce/components/ChannelBindingFormSections.js.map +7 -0
- package/dist/modules/ecommerce/components/ChannelLookupPicker.js +11 -0
- package/dist/modules/ecommerce/components/ChannelLookupPicker.js.map +7 -0
- package/dist/modules/ecommerce/components/DomainStatusWarning.js +24 -0
- package/dist/modules/ecommerce/components/DomainStatusWarning.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreAvailabilityDefaultsSection.js +190 -0
- package/dist/modules/ecommerce/components/StoreAvailabilityDefaultsSection.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreBrandingControls.js +67 -0
- package/dist/modules/ecommerce/components/StoreBrandingControls.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreBrandingPreview.js +69 -0
- package/dist/modules/ecommerce/components/StoreBrandingPreview.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreBrandingTab.js +218 -0
- package/dist/modules/ecommerce/components/StoreBrandingTab.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreChannelBindingDialog.js +270 -0
- package/dist/modules/ecommerce/components/StoreChannelBindingDialog.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreChannelsTab.js +313 -0
- package/dist/modules/ecommerce/components/StoreChannelsTab.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreCreateDialog.js +164 -0
- package/dist/modules/ecommerce/components/StoreCreateDialog.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreDomainBindingDialog.js +162 -0
- package/dist/modules/ecommerce/components/StoreDomainBindingDialog.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreDomainsTab.js +357 -0
- package/dist/modules/ecommerce/components/StoreDomainsTab.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreEditPage.js +155 -0
- package/dist/modules/ecommerce/components/StoreEditPage.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreGeneralTab.js +167 -0
- package/dist/modules/ecommerce/components/StoreGeneralTab.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreListPage.js +290 -0
- package/dist/modules/ecommerce/components/StoreListPage.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreSeoTab.js +132 -0
- package/dist/modules/ecommerce/components/StoreSeoTab.js.map +7 -0
- package/dist/modules/ecommerce/components/StoreStatusBadge.js +20 -0
- package/dist/modules/ecommerce/components/StoreStatusBadge.js.map +7 -0
- package/dist/modules/ecommerce/components/channelLookups.js +28 -0
- package/dist/modules/ecommerce/components/channelLookups.js.map +7 -0
- package/dist/modules/ecommerce/components/storeAdmin.js +41 -0
- package/dist/modules/ecommerce/components/storeAdmin.js.map +7 -0
- package/dist/modules/ecommerce/components/storeBrandingForm.js +197 -0
- package/dist/modules/ecommerce/components/storeBrandingForm.js.map +7 -0
- package/dist/modules/ecommerce/components/storeChannels.js +140 -0
- package/dist/modules/ecommerce/components/storeChannels.js.map +7 -0
- package/dist/modules/ecommerce/components/storeDomains.js +167 -0
- package/dist/modules/ecommerce/components/storeDomains.js.map +7 -0
- package/dist/modules/ecommerce/components/storeEditTabs.js +62 -0
- package/dist/modules/ecommerce/components/storeEditTabs.js.map +7 -0
- package/dist/modules/ecommerce/components/storeGeneral.js +95 -0
- package/dist/modules/ecommerce/components/storeGeneral.js.map +7 -0
- package/dist/modules/ecommerce/components/storeSeo.js +69 -0
- package/dist/modules/ecommerce/components/storeSeo.js.map +7 -0
- package/dist/modules/ecommerce/components/useChannelAssortmentCount.js +45 -0
- package/dist/modules/ecommerce/components/useChannelAssortmentCount.js.map +7 -0
- package/dist/modules/ecommerce/components/useStoreAccess.js +28 -0
- package/dist/modules/ecommerce/components/useStoreAccess.js.map +7 -0
- package/dist/modules/ecommerce/data/enrichers.js +157 -0
- package/dist/modules/ecommerce/data/enrichers.js.map +7 -0
- package/dist/modules/ecommerce/data/entities.js +197 -0
- package/dist/modules/ecommerce/data/entities.js.map +7 -0
- package/dist/modules/ecommerce/data/validators.js +406 -0
- package/dist/modules/ecommerce/data/validators.js.map +7 -0
- package/dist/modules/ecommerce/di.js +11 -0
- package/dist/modules/ecommerce/di.js.map +7 -0
- package/dist/modules/ecommerce/events.js +27 -0
- package/dist/modules/ecommerce/events.js.map +7 -0
- package/dist/modules/ecommerce/index.js +16 -0
- package/dist/modules/ecommerce/index.js.map +7 -0
- package/dist/modules/ecommerce/lib/assortmentCount.js +112 -0
- package/dist/modules/ecommerce/lib/assortmentCount.js.map +7 -0
- package/dist/modules/ecommerce/lib/brandingStyles.js +184 -0
- package/dist/modules/ecommerce/lib/brandingStyles.js.map +7 -0
- package/dist/modules/ecommerce/lib/buyerContext.js +284 -0
- package/dist/modules/ecommerce/lib/buyerContext.js.map +7 -0
- package/dist/modules/ecommerce/lib/cacheInvalidation.js +186 -0
- package/dist/modules/ecommerce/lib/cacheInvalidation.js.map +7 -0
- package/dist/modules/ecommerce/lib/cacheKeys.js +236 -0
- package/dist/modules/ecommerce/lib/cacheKeys.js.map +7 -0
- package/dist/modules/ecommerce/lib/createConflictRecheck.js +29 -0
- package/dist/modules/ecommerce/lib/createConflictRecheck.js.map +7 -0
- package/dist/modules/ecommerce/lib/crudEvents.js +94 -0
- package/dist/modules/ecommerce/lib/crudEvents.js.map +7 -0
- package/dist/modules/ecommerce/lib/crudSupport.js +64 -0
- package/dist/modules/ecommerce/lib/crudSupport.js.map +7 -0
- package/dist/modules/ecommerce/lib/domainBindingRebind.js +99 -0
- package/dist/modules/ecommerce/lib/domainBindingRebind.js.map +7 -0
- package/dist/modules/ecommerce/lib/domainMappingSummaries.js +39 -0
- package/dist/modules/ecommerce/lib/domainMappingSummaries.js.map +7 -0
- package/dist/modules/ecommerce/lib/exclusiveFlag.js +57 -0
- package/dist/modules/ecommerce/lib/exclusiveFlag.js.map +7 -0
- package/dist/modules/ecommerce/lib/references.js +70 -0
- package/dist/modules/ecommerce/lib/references.js.map +7 -0
- package/dist/modules/ecommerce/lib/seedDraftStore.js +99 -0
- package/dist/modules/ecommerce/lib/seedDraftStore.js.map +7 -0
- package/dist/modules/ecommerce/lib/storeBindingCascade.js +109 -0
- package/dist/modules/ecommerce/lib/storeBindingCascade.js.map +7 -0
- package/dist/modules/ecommerce/lib/storeBranding.js +40 -0
- package/dist/modules/ecommerce/lib/storeBranding.js.map +7 -0
- package/dist/modules/ecommerce/lib/storeBrandingRoute.js +76 -0
- package/dist/modules/ecommerce/lib/storeBrandingRoute.js.map +7 -0
- package/dist/modules/ecommerce/lib/storeContext.js +393 -0
- package/dist/modules/ecommerce/lib/storeContext.js.map +7 -0
- package/dist/modules/ecommerce/lib/storeContextService.js +43 -0
- package/dist/modules/ecommerce/lib/storeContextService.js.map +7 -0
- package/dist/modules/ecommerce/lib/storeLocale.js +60 -0
- package/dist/modules/ecommerce/lib/storeLocale.js.map +7 -0
- package/dist/modules/ecommerce/lib/storeNotifications.js +41 -0
- package/dist/modules/ecommerce/lib/storeNotifications.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontCatalogSupport.js +248 -0
- package/dist/modules/ecommerce/lib/storefrontCatalogSupport.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontCategories.js +241 -0
- package/dist/modules/ecommerce/lib/storefrontCategories.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontCategoryCache.js +71 -0
- package/dist/modules/ecommerce/lib/storefrontCategoryCache.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontDetail.js +447 -0
- package/dist/modules/ecommerce/lib/storefrontDetail.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontFacets.js +441 -0
- package/dist/modules/ecommerce/lib/storefrontFacets.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontOptionFilter.js +15 -0
- package/dist/modules/ecommerce/lib/storefrontOptionFilter.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontPricing.js +393 -0
- package/dist/modules/ecommerce/lib/storefrontPricing.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontProductCache.js +90 -0
- package/dist/modules/ecommerce/lib/storefrontProductCache.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontProductScope.js +107 -0
- package/dist/modules/ecommerce/lib/storefrontProductScope.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontProducts.js +670 -0
- package/dist/modules/ecommerce/lib/storefrontProducts.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontQuery.js +156 -0
- package/dist/modules/ecommerce/lib/storefrontQuery.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontRateLimit.js +99 -0
- package/dist/modules/ecommerce/lib/storefrontRateLimit.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontSearch.js +303 -0
- package/dist/modules/ecommerce/lib/storefrontSearch.js.map +7 -0
- package/dist/modules/ecommerce/lib/storefrontSearchCache.js +35 -0
- package/dist/modules/ecommerce/lib/storefrontSearchCache.js.map +7 -0
- package/dist/modules/ecommerce/lib/subscriberSupport.js +33 -0
- package/dist/modules/ecommerce/lib/subscriberSupport.js.map +7 -0
- package/dist/modules/ecommerce/lib/types.js +1 -0
- package/dist/modules/ecommerce/lib/types.js.map +7 -0
- package/dist/modules/ecommerce/migrations/Migration20261005113233_ecommerce.js +32 -0
- package/dist/modules/ecommerce/migrations/Migration20261005113233_ecommerce.js.map +7 -0
- package/dist/modules/ecommerce/migrations/Migration20261006112733_ecommerce.js +17 -0
- package/dist/modules/ecommerce/migrations/Migration20261006112733_ecommerce.js.map +7 -0
- package/dist/modules/ecommerce/notifications.client.js +47 -0
- package/dist/modules/ecommerce/notifications.client.js.map +7 -0
- package/dist/modules/ecommerce/notifications.js +48 -0
- package/dist/modules/ecommerce/notifications.js.map +7 -0
- package/dist/modules/ecommerce/search.js +83 -0
- package/dist/modules/ecommerce/search.js.map +7 -0
- package/dist/modules/ecommerce/setup.js +17 -0
- package/dist/modules/ecommerce/setup.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/assortment-empty-notifications.js +14 -0
- package/dist/modules/ecommerce/subscribers/assortment-empty-notifications.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/availability-policy-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/availability-policy-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/catalog-category-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/catalog-category-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/catalog-price-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/catalog-price-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/catalog-product-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/catalog-product-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/catalog-variant-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/catalog-variant-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/customer-group-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/customer-group-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/customer-group-membership-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/customer-group-membership-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/customer-group-terms-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/customer-group-terms-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/customer-user-deleted-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/customer-user-deleted-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/customer-user-updated-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/customer-user-updated-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/domain-binding-rebind.js +16 -0
- package/dist/modules/ecommerce/subscribers/domain-binding-rebind.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/domain-mapping-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/domain-mapping-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/store-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/store-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/store-channel-binding-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/store-channel-binding-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/store-domain-binding-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/store-domain-binding-cache-invalidation.js.map +7 -0
- package/dist/modules/ecommerce/subscribers/store-notifications.js +14 -0
- package/dist/modules/ecommerce/subscribers/store-notifications.js.map +7 -0
- package/dist/modules/query_index/lib/batch.js +23 -9
- package/dist/modules/query_index/lib/batch.js.map +2 -2
- package/dist/modules/query_index/lib/doc-enrichers.js +212 -0
- package/dist/modules/query_index/lib/doc-enrichers.js.map +7 -0
- package/dist/modules/query_index/lib/document.js +2 -0
- package/dist/modules/query_index/lib/document.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +68 -1
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +7 -0
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +2 -0
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/query_index/migrations/Migration20261005201500_query_index.js +17 -0
- package/dist/modules/query_index/migrations/Migration20261005201500_query_index.js.map +7 -0
- package/dist/modules/translations/components/TranslationManager.js +3 -3
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/translations/data/validators.js +1 -1
- package/dist/modules/translations/data/validators.js.map +1 -1
- package/dist/modules/translations/lib/batch.js +21 -1
- package/dist/modules/translations/lib/batch.js.map +2 -2
- package/dist/modules/translations/lib/helpers.js +3 -0
- package/dist/modules/translations/lib/helpers.js.map +2 -2
- package/dist/modules/translations/lib/resolve-field-list.js +14 -2
- package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
- package/generated/entities/catalog_price_history_entry/index.ts +25 -0
- package/generated/entities/customer_group_terms/index.ts +1 -0
- package/generated/entities/ecommerce_store/index.ts +15 -0
- package/generated/entities/ecommerce_store_channel_binding/index.ts +13 -0
- package/generated/entities/ecommerce_store_domain_binding/index.ts +10 -0
- package/generated/entities.ids.generated.ts +8 -1
- package/generated/entity-fields-registry.ts +72 -0
- package/package.json +7 -7
- package/src/helpers/integration/customerGroupsFixtures.ts +2 -0
- package/src/modules/auth/i18n/de.json +8 -0
- package/src/modules/auth/i18n/en.json +8 -0
- package/src/modules/auth/i18n/es.json +8 -0
- package/src/modules/auth/i18n/ko.json +8 -0
- package/src/modules/auth/i18n/pl.json +8 -0
- package/src/modules/catalog/AGENTS.md +5 -2
- package/src/modules/catalog/acl.ts +7 -0
- package/src/modules/catalog/api/config/omnibus/route.ts +204 -0
- package/src/modules/catalog/api/prices/history/route.ts +139 -0
- package/src/modules/catalog/api/prices/omnibus-preview/route.ts +202 -0
- package/src/modules/catalog/api/products/route.ts +53 -308
- package/src/modules/catalog/api/tags/route.ts +6 -0
- package/src/modules/catalog/backend/config/catalog/page.tsx +2 -0
- package/src/modules/catalog/cli.ts +84 -1
- package/src/modules/catalog/commands/categories.ts +54 -2
- package/src/modules/catalog/commands/prices.ts +43 -1
- package/src/modules/catalog/commands/products.ts +84 -1
- package/src/modules/catalog/commands/variants.ts +7 -0
- package/src/modules/catalog/components/OmnibusSettings.tsx +832 -0
- package/src/modules/catalog/components/PriceEditorOmnibusRow.tsx +219 -0
- package/src/modules/catalog/components/lookupSources.ts +29 -0
- package/src/modules/catalog/components/prices/PriceScopeSelectors.tsx +9 -0
- package/src/modules/catalog/components/products/VariantBuilder.tsx +9 -0
- package/src/modules/catalog/data/entities.ts +123 -1
- package/src/modules/catalog/data/validators.ts +118 -0
- package/src/modules/catalog/di.ts +23 -0
- package/src/modules/catalog/i18n/de.json +77 -0
- package/src/modules/catalog/i18n/en.json +77 -0
- package/src/modules/catalog/i18n/es.json +77 -0
- package/src/modules/catalog/i18n/ko.json +77 -0
- package/src/modules/catalog/i18n/pl.json +77 -0
- package/src/modules/catalog/lib/omnibus.ts +406 -0
- package/src/modules/catalog/lib/omnibusBackfill.ts +198 -0
- package/src/modules/catalog/lib/omnibusCache.ts +111 -0
- package/src/modules/catalog/lib/omnibusConfig.ts +73 -0
- package/src/modules/catalog/lib/omnibusHistoryQueries.ts +248 -0
- package/src/modules/catalog/lib/omnibusPresentedEntry.ts +76 -0
- package/src/modules/catalog/lib/omnibusProductListEnrichment.ts +125 -0
- package/src/modules/catalog/lib/omnibusTypes.ts +202 -0
- package/src/modules/catalog/lib/optionSchemaTranslations.ts +55 -0
- package/src/modules/catalog/lib/priceHistoryQuery.ts +155 -0
- package/src/modules/catalog/lib/pricing.ts +42 -4
- package/src/modules/catalog/lib/productFilters.ts +349 -0
- package/src/modules/catalog/lib/productScopeKeys.ts +162 -0
- package/src/modules/catalog/lib/productScopeReindex.ts +139 -0
- package/src/modules/catalog/migrations/.snapshot-open-mercato.json +572 -0
- package/src/modules/catalog/migrations/Migration20261005105352_catalog.ts +39 -0
- package/src/modules/catalog/migrations/Migration20261005150824_catalog.ts +39 -0
- package/src/modules/catalog/services/catalogOmnibusService.ts +642 -0
- package/src/modules/catalog/setup.ts +2 -0
- package/src/modules/catalog/subscribers/product-scope-keys-category-deleted.ts +18 -0
- package/src/modules/catalog/subscribers/product-scope-keys-category-updated.ts +19 -0
- package/src/modules/catalog/translations.ts +12 -0
- package/src/modules/configs/i18n/de.json +5 -1
- package/src/modules/configs/i18n/en.json +5 -1
- package/src/modules/configs/i18n/es.json +5 -1
- package/src/modules/configs/i18n/ko.json +5 -1
- package/src/modules/configs/i18n/pl.json +5 -1
- package/src/modules/configs/lib/upgrade-actions.ts +35 -0
- package/src/modules/customer_groups/api/customer-groups/[id]/terms/route.ts +7 -0
- package/src/modules/customer_groups/components/CustomerGroupTermsSection.tsx +124 -2
- package/src/modules/customer_groups/components/customerGroupAssortmentScope.ts +46 -0
- package/src/modules/customer_groups/data/entities.ts +4 -0
- package/src/modules/customer_groups/data/validators.ts +14 -0
- package/src/modules/customer_groups/i18n/de.json +12 -0
- package/src/modules/customer_groups/i18n/en.json +12 -0
- package/src/modules/customer_groups/i18n/es.json +12 -0
- package/src/modules/customer_groups/i18n/ko.json +12 -0
- package/src/modules/customer_groups/i18n/pl.json +12 -0
- package/src/modules/customer_groups/migrations/.snapshot-open-mercato.json +17 -0
- package/src/modules/customer_groups/migrations/Migration20261005110250_customer_groups.ts +15 -0
- package/src/modules/customer_groups/services/customerGroupsService.ts +74 -23
- package/src/modules/ecommerce/acl.ts +9 -0
- package/src/modules/ecommerce/api/domain-mappings/route.ts +76 -0
- package/src/modules/ecommerce/api/openapi.ts +28 -0
- package/src/modules/ecommerce/api/store-channel-bindings/[id]/assortment-count/route.ts +141 -0
- package/src/modules/ecommerce/api/store-channel-bindings/crud.ts +283 -0
- package/src/modules/ecommerce/api/store-channel-bindings/route.ts +67 -0
- package/src/modules/ecommerce/api/store-domain-bindings/crud.ts +324 -0
- package/src/modules/ecommerce/api/store-domain-bindings/route.ts +73 -0
- package/src/modules/ecommerce/api/storefront/categories/[slug]/route.ts +94 -0
- package/src/modules/ecommerce/api/storefront/categories/openapiSchemas.ts +63 -0
- package/src/modules/ecommerce/api/storefront/categories/route.ts +78 -0
- package/src/modules/ecommerce/api/storefront/context/route.ts +203 -0
- package/src/modules/ecommerce/api/storefront/products/[idOrHandle]/route.ts +94 -0
- package/src/modules/ecommerce/api/storefront/products/openapiSchemas.ts +193 -0
- package/src/modules/ecommerce/api/storefront/products/route.ts +81 -0
- package/src/modules/ecommerce/api/storefront/search/suggest/openapiSchemas.ts +25 -0
- package/src/modules/ecommerce/api/storefront/search/suggest/route.ts +78 -0
- package/src/modules/ecommerce/api/storefront/storefrontRouteSupport.ts +68 -0
- package/src/modules/ecommerce/api/stores/[id]/branding/route.ts +205 -0
- package/src/modules/ecommerce/api/stores/[id]/preview-branding/route.ts +99 -0
- package/src/modules/ecommerce/api/stores/crud.ts +457 -0
- package/src/modules/ecommerce/api/stores/route.ts +66 -0
- package/src/modules/ecommerce/backend/config/ecommerce/[id]/page.meta.ts +12 -0
- package/src/modules/ecommerce/backend/config/ecommerce/[id]/page.tsx +5 -0
- package/src/modules/ecommerce/backend/config/ecommerce/page.meta.ts +12 -0
- package/src/modules/ecommerce/backend/config/ecommerce/page.tsx +5 -0
- package/src/modules/ecommerce/commands/stores.ts +154 -0
- package/src/modules/ecommerce/components/ChannelAssortmentCount.tsx +138 -0
- package/src/modules/ecommerce/components/ChannelBindingFormSections.tsx +40 -0
- package/src/modules/ecommerce/components/ChannelLookupPicker.tsx +5 -0
- package/src/modules/ecommerce/components/DomainStatusWarning.tsx +32 -0
- package/src/modules/ecommerce/components/StoreAvailabilityDefaultsSection.tsx +219 -0
- package/src/modules/ecommerce/components/StoreBrandingControls.tsx +73 -0
- package/src/modules/ecommerce/components/StoreBrandingPreview.tsx +77 -0
- package/src/modules/ecommerce/components/StoreBrandingTab.tsx +242 -0
- package/src/modules/ecommerce/components/StoreChannelBindingDialog.tsx +306 -0
- package/src/modules/ecommerce/components/StoreChannelsTab.tsx +375 -0
- package/src/modules/ecommerce/components/StoreCreateDialog.tsx +204 -0
- package/src/modules/ecommerce/components/StoreDomainBindingDialog.tsx +198 -0
- package/src/modules/ecommerce/components/StoreDomainsTab.tsx +426 -0
- package/src/modules/ecommerce/components/StoreEditPage.tsx +193 -0
- package/src/modules/ecommerce/components/StoreGeneralTab.tsx +189 -0
- package/src/modules/ecommerce/components/StoreListPage.tsx +326 -0
- package/src/modules/ecommerce/components/StoreSeoTab.tsx +152 -0
- package/src/modules/ecommerce/components/StoreStatusBadge.tsx +22 -0
- package/src/modules/ecommerce/components/channelLookups.ts +24 -0
- package/src/modules/ecommerce/components/storeAdmin.ts +56 -0
- package/src/modules/ecommerce/components/storeBrandingForm.ts +230 -0
- package/src/modules/ecommerce/components/storeChannels.ts +210 -0
- package/src/modules/ecommerce/components/storeDomains.ts +221 -0
- package/src/modules/ecommerce/components/storeEditTabs.tsx +76 -0
- package/src/modules/ecommerce/components/storeGeneral.ts +155 -0
- package/src/modules/ecommerce/components/storeSeo.ts +78 -0
- package/src/modules/ecommerce/components/useChannelAssortmentCount.ts +67 -0
- package/src/modules/ecommerce/components/useStoreAccess.ts +36 -0
- package/src/modules/ecommerce/data/enrichers.ts +213 -0
- package/src/modules/ecommerce/data/entities.ts +168 -0
- package/src/modules/ecommerce/data/validators.ts +587 -0
- package/src/modules/ecommerce/di.ts +9 -0
- package/src/modules/ecommerce/events.ts +27 -0
- package/src/modules/ecommerce/i18n/de.json +341 -0
- package/src/modules/ecommerce/i18n/en.json +341 -0
- package/src/modules/ecommerce/i18n/es.json +341 -0
- package/src/modules/ecommerce/i18n/ko.json +341 -0
- package/src/modules/ecommerce/i18n/pl.json +341 -0
- package/src/modules/ecommerce/index.ts +14 -0
- package/src/modules/ecommerce/lib/assortmentCount.ts +175 -0
- package/src/modules/ecommerce/lib/brandingStyles.ts +215 -0
- package/src/modules/ecommerce/lib/buyerContext.ts +401 -0
- package/src/modules/ecommerce/lib/cacheInvalidation.ts +302 -0
- package/src/modules/ecommerce/lib/cacheKeys.ts +340 -0
- package/src/modules/ecommerce/lib/createConflictRecheck.ts +31 -0
- package/src/modules/ecommerce/lib/crudEvents.ts +128 -0
- package/src/modules/ecommerce/lib/crudSupport.ts +85 -0
- package/src/modules/ecommerce/lib/domainBindingRebind.ts +138 -0
- package/src/modules/ecommerce/lib/domainMappingSummaries.ts +71 -0
- package/src/modules/ecommerce/lib/exclusiveFlag.ts +90 -0
- package/src/modules/ecommerce/lib/references.ts +93 -0
- package/src/modules/ecommerce/lib/seedDraftStore.ts +125 -0
- package/src/modules/ecommerce/lib/storeBindingCascade.ts +138 -0
- package/src/modules/ecommerce/lib/storeBranding.ts +48 -0
- package/src/modules/ecommerce/lib/storeBrandingRoute.ts +91 -0
- package/src/modules/ecommerce/lib/storeContext.ts +669 -0
- package/src/modules/ecommerce/lib/storeContextService.ts +71 -0
- package/src/modules/ecommerce/lib/storeLocale.ts +88 -0
- package/src/modules/ecommerce/lib/storeNotifications.ts +55 -0
- package/src/modules/ecommerce/lib/storefrontCatalogSupport.ts +427 -0
- package/src/modules/ecommerce/lib/storefrontCategories.ts +419 -0
- package/src/modules/ecommerce/lib/storefrontCategoryCache.ts +113 -0
- package/src/modules/ecommerce/lib/storefrontDetail.ts +654 -0
- package/src/modules/ecommerce/lib/storefrontFacets.ts +697 -0
- package/src/modules/ecommerce/lib/storefrontOptionFilter.ts +44 -0
- package/src/modules/ecommerce/lib/storefrontPricing.ts +583 -0
- package/src/modules/ecommerce/lib/storefrontProductCache.ts +126 -0
- package/src/modules/ecommerce/lib/storefrontProductScope.ts +181 -0
- package/src/modules/ecommerce/lib/storefrontProducts.ts +963 -0
- package/src/modules/ecommerce/lib/storefrontQuery.ts +215 -0
- package/src/modules/ecommerce/lib/storefrontRateLimit.ts +126 -0
- package/src/modules/ecommerce/lib/storefrontSearch.ts +454 -0
- package/src/modules/ecommerce/lib/storefrontSearchCache.ts +49 -0
- package/src/modules/ecommerce/lib/subscriberSupport.ts +41 -0
- package/src/modules/ecommerce/lib/types.ts +69 -0
- package/src/modules/ecommerce/migrations/.snapshot-open-mercato.json +829 -0
- package/src/modules/ecommerce/migrations/Migration20261005113233_ecommerce.ts +32 -0
- package/src/modules/ecommerce/migrations/Migration20261006112733_ecommerce.ts +15 -0
- package/src/modules/ecommerce/notifications.client.ts +46 -0
- package/src/modules/ecommerce/notifications.ts +46 -0
- package/src/modules/ecommerce/search.ts +93 -0
- package/src/modules/ecommerce/setup.ts +15 -0
- package/src/modules/ecommerce/subscribers/assortment-empty-notifications.ts +12 -0
- package/src/modules/ecommerce/subscribers/availability-policy-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/catalog-category-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/catalog-price-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/catalog-product-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/catalog-variant-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/customer-group-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/customer-group-membership-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/customer-group-terms-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/customer-user-deleted-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/customer-user-updated-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/domain-binding-rebind.ts +14 -0
- package/src/modules/ecommerce/subscribers/domain-mapping-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/store-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/store-channel-binding-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/store-domain-binding-cache-invalidation.ts +12 -0
- package/src/modules/ecommerce/subscribers/store-notifications.ts +12 -0
- package/src/modules/query_index/lib/batch.ts +25 -9
- package/src/modules/query_index/lib/doc-enrichers.ts +306 -0
- package/src/modules/query_index/lib/document.ts +2 -0
- package/src/modules/query_index/lib/engine.ts +58 -1
- package/src/modules/query_index/lib/indexer.ts +8 -0
- package/src/modules/query_index/lib/search-tokens.ts +2 -0
- package/src/modules/query_index/migrations/Migration20261005201500_query_index.ts +42 -0
- package/src/modules/translations/components/TranslationManager.tsx +3 -3
- package/src/modules/translations/data/validators.ts +1 -1
- package/src/modules/translations/lib/batch.ts +47 -0
- package/src/modules/translations/lib/helpers.ts +4 -0
- package/src/modules/translations/lib/resolve-field-list.ts +16 -2
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
4
|
+
import {
|
|
5
|
+
catalogCategoryTag,
|
|
6
|
+
catalogPriceTag,
|
|
7
|
+
catalogProductTag,
|
|
8
|
+
catalogProductsTag,
|
|
9
|
+
ecommerceCustomerGroupTag,
|
|
10
|
+
ecommerceCustomerTag,
|
|
11
|
+
ecommerceCustomerUserTag,
|
|
12
|
+
ecommerceDomainMappingTag,
|
|
13
|
+
ecommerceDomainTag,
|
|
14
|
+
ecommerceStoreTag,
|
|
15
|
+
ecommerceUngroupedBuyerTag,
|
|
16
|
+
invalidateEcommerceCacheTags,
|
|
17
|
+
storefrontAvailabilityTag,
|
|
18
|
+
} from './cacheKeys'
|
|
19
|
+
import {
|
|
20
|
+
asPayloadRecord,
|
|
21
|
+
eventAction,
|
|
22
|
+
readEventScope,
|
|
23
|
+
readPayloadString,
|
|
24
|
+
tryResolveService,
|
|
25
|
+
type EcommerceSubscriberContext,
|
|
26
|
+
} from './subscriberSupport'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Event → cache-tag mapping for the storefront caches (SPEC-029 §8 table). Every eviction goes
|
|
30
|
+
* through `invalidateEcommerceCacheTags`, which clears the tenant-agnostic resolution cache and the
|
|
31
|
+
* emitting tenant's buyer/storefront caches.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const logger = createLogger('ecommerce').child({ component: 'cache-invalidation' })
|
|
35
|
+
|
|
36
|
+
type TagDeriver = (payload: Record<string, unknown>, action: string | null, tenantId: string | null) => string[]
|
|
37
|
+
|
|
38
|
+
const STORE_CACHE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted', 'branding_updated'])
|
|
39
|
+
const CUSTOMER_GROUP_CACHE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted'])
|
|
40
|
+
const CUSTOMER_GROUP_DEFAULT_CANDIDATE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated'])
|
|
41
|
+
const CATALOG_CRUD_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted'])
|
|
42
|
+
const CUSTOMER_USER_CACHE_ACTIONS: ReadonlySet<string> = new Set(['updated', 'deleted'])
|
|
43
|
+
|
|
44
|
+
function tagsFrom(values: Array<string | null>, toTag: (value: string) => string): string[] {
|
|
45
|
+
return values.filter((value): value is string => value !== null).map(toTag)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const storeEventTags: TagDeriver = (payload, action) => {
|
|
49
|
+
if (action && !STORE_CACHE_ACTIONS.has(action)) return []
|
|
50
|
+
return tagsFrom([readPayloadString(payload, 'id')], ecommerceStoreTag)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const storeDomainBindingEventTags: TagDeriver = (payload) => [
|
|
54
|
+
...tagsFrom(
|
|
55
|
+
[readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')],
|
|
56
|
+
ecommerceStoreTag,
|
|
57
|
+
),
|
|
58
|
+
...tagsFrom(
|
|
59
|
+
[readPayloadString(payload, 'domainMappingId'), readPayloadString(payload, 'previousDomainMappingId')],
|
|
60
|
+
ecommerceDomainMappingTag,
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
export const storeChannelBindingEventTags: TagDeriver = (payload) =>
|
|
65
|
+
tagsFrom([readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')], ecommerceStoreTag)
|
|
66
|
+
|
|
67
|
+
export const domainMappingEventTags: TagDeriver = (payload) => [
|
|
68
|
+
...tagsFrom(
|
|
69
|
+
[readPayloadString(payload, 'id'), readPayloadString(payload, 'replacedDomainId')],
|
|
70
|
+
ecommerceDomainMappingTag,
|
|
71
|
+
),
|
|
72
|
+
...tagsFrom(
|
|
73
|
+
[readPayloadString(payload, 'hostname'), readPayloadString(payload, 'replacedHostname')],
|
|
74
|
+
ecommerceDomainTag,
|
|
75
|
+
),
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `customer_accounts.user.updated|deleted`: the user's buyer contexts, so a deactivated or deleted
|
|
80
|
+
* portal user stops resolving to their cached identity (and contract prices) before the TTL ends.
|
|
81
|
+
*/
|
|
82
|
+
export const customerUserEventTags: TagDeriver = (payload, action) => {
|
|
83
|
+
if (action && !CUSTOMER_USER_CACHE_ACTIONS.has(action)) return []
|
|
84
|
+
return tagsFrom([readPayloadString(payload, 'id')], ecommerceCustomerUserTag)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const customerGroupMembershipEventTags: TagDeriver = (payload) =>
|
|
88
|
+
tagsFrom([readPayloadString(payload, 'customerId')], ecommerceCustomerTag)
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* `customer_groups.group.*`: the group's buyer contexts. The payload carries no `isDefault`, so a
|
|
92
|
+
* create or update also evicts the tenant's ungrouped buyer contexts, which would fall back to the
|
|
93
|
+
* group if it became the tenant default; a replaced default is announced as its own update.
|
|
94
|
+
*/
|
|
95
|
+
export const customerGroupEventTags: TagDeriver = (payload, action, tenantId) => {
|
|
96
|
+
if (action && !CUSTOMER_GROUP_CACHE_ACTIONS.has(action)) return []
|
|
97
|
+
const tags = action === 'created' ? [] : tagsFrom([readPayloadString(payload, 'id')], ecommerceCustomerGroupTag)
|
|
98
|
+
if (tenantId && (!action || CUSTOMER_GROUP_DEFAULT_CANDIDATE_ACTIONS.has(action))) {
|
|
99
|
+
tags.push(ecommerceUngroupedBuyerTag(tenantId))
|
|
100
|
+
}
|
|
101
|
+
return tags
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const customerGroupTermsEventTags: TagDeriver = (payload) =>
|
|
105
|
+
tagsFrom([readPayloadString(payload, 'groupId')], ecommerceCustomerGroupTag)
|
|
106
|
+
|
|
107
|
+
export async function invalidateForEvent(
|
|
108
|
+
payload: unknown,
|
|
109
|
+
ctx: EcommerceSubscriberContext,
|
|
110
|
+
deriveTags: TagDeriver,
|
|
111
|
+
): Promise<number> {
|
|
112
|
+
const record = asPayloadRecord(payload)
|
|
113
|
+
const { tenantId } = readEventScope(record, ctx)
|
|
114
|
+
const tags = deriveTags(record, eventAction(ctx.eventName), tenantId)
|
|
115
|
+
if (tags.length === 0) return 0
|
|
116
|
+
return invalidateEcommerceCacheTags(ctx, { tenantId, tags })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type CatalogLookupDatabase = {
|
|
120
|
+
catalog_product_variant_prices: {
|
|
121
|
+
id: string
|
|
122
|
+
tenant_id: string
|
|
123
|
+
organization_id: string
|
|
124
|
+
customer_id: string | null
|
|
125
|
+
product_id: string | null
|
|
126
|
+
variant_id: string | null
|
|
127
|
+
}
|
|
128
|
+
catalog_product_variants: {
|
|
129
|
+
id: string
|
|
130
|
+
tenant_id: string
|
|
131
|
+
organization_id: string
|
|
132
|
+
product_id: string
|
|
133
|
+
}
|
|
134
|
+
availability_policies: {
|
|
135
|
+
id: string
|
|
136
|
+
tenant_id: string
|
|
137
|
+
organization_id: string
|
|
138
|
+
product_id: string | null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type PriceOwner = { customerId: string | null; productId: string | null }
|
|
143
|
+
|
|
144
|
+
type LookupScope = { tenantId: string; organizationId: string | null }
|
|
145
|
+
|
|
146
|
+
async function readPriceOwner(em: EntityManager, priceId: string, scope: LookupScope): Promise<PriceOwner | null> {
|
|
147
|
+
let query = em
|
|
148
|
+
.getKysely<CatalogLookupDatabase>()
|
|
149
|
+
.selectFrom('catalog_product_variant_prices as price')
|
|
150
|
+
.leftJoin('catalog_product_variants as variant', 'variant.id', 'price.variant_id')
|
|
151
|
+
.select(['price.customer_id as customerId', 'price.product_id as productId', 'variant.product_id as variantProductId'])
|
|
152
|
+
.where('price.id', '=', priceId)
|
|
153
|
+
.where('price.tenant_id', '=', scope.tenantId)
|
|
154
|
+
if (scope.organizationId) query = query.where('price.organization_id', '=', scope.organizationId)
|
|
155
|
+
const row = await query.executeTakeFirst()
|
|
156
|
+
if (!row) return null
|
|
157
|
+
return { customerId: row.customerId ?? null, productId: row.productId ?? row.variantProductId ?? null }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function readVariantProductId(em: EntityManager, variantId: string, scope: LookupScope): Promise<string | null> {
|
|
161
|
+
let query = em
|
|
162
|
+
.getKysely<CatalogLookupDatabase>()
|
|
163
|
+
.selectFrom('catalog_product_variants')
|
|
164
|
+
.select('product_id')
|
|
165
|
+
.where('id', '=', variantId)
|
|
166
|
+
.where('tenant_id', '=', scope.tenantId)
|
|
167
|
+
if (scope.organizationId) query = query.where('organization_id', '=', scope.organizationId)
|
|
168
|
+
const row = await query.executeTakeFirst()
|
|
169
|
+
return row?.product_id ?? null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function readPolicyProductId(em: EntityManager, policyId: string, scope: LookupScope): Promise<string | null> {
|
|
173
|
+
let query = em
|
|
174
|
+
.getKysely<CatalogLookupDatabase>()
|
|
175
|
+
.selectFrom('availability_policies')
|
|
176
|
+
.select('product_id')
|
|
177
|
+
.where('id', '=', policyId)
|
|
178
|
+
.where('tenant_id', '=', scope.tenantId)
|
|
179
|
+
if (scope.organizationId) query = query.where('organization_id', '=', scope.organizationId)
|
|
180
|
+
const row = await query.executeTakeFirst()
|
|
181
|
+
return row?.product_id ?? null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Reads the row an id-only catalog/availability payload points at. A missing `em`, a missing row
|
|
186
|
+
* and a failed lookup all answer `undefined`, so the caller falls back to its broader tag.
|
|
187
|
+
*/
|
|
188
|
+
async function lookupForEvent<T>(
|
|
189
|
+
ctx: EcommerceSubscriberContext,
|
|
190
|
+
id: string,
|
|
191
|
+
scope: LookupScope,
|
|
192
|
+
lookup: (em: EntityManager, id: string, scope: LookupScope) => Promise<T | null>,
|
|
193
|
+
failureCode: string,
|
|
194
|
+
): Promise<T | undefined> {
|
|
195
|
+
const em = tryResolveService<EntityManager>(ctx, 'em')
|
|
196
|
+
if (!em) return undefined
|
|
197
|
+
try {
|
|
198
|
+
return (await lookup(em, id, scope)) ?? undefined
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.warn('Storefront cache lookup failed; falling back to broader tags', { id, failureCode, err: error })
|
|
201
|
+
getTelemetryRuntime()?.reportError(error, {
|
|
202
|
+
module: 'ecommerce',
|
|
203
|
+
code: failureCode,
|
|
204
|
+
attributes: { id },
|
|
205
|
+
})
|
|
206
|
+
return undefined
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type ScopedEvent = { id: string; action: string | null; scope: LookupScope }
|
|
211
|
+
|
|
212
|
+
function readScopedEvent(payload: unknown, ctx: EcommerceSubscriberContext): ScopedEvent | null {
|
|
213
|
+
const record = asPayloadRecord(payload)
|
|
214
|
+
const id = readPayloadString(record, 'id')
|
|
215
|
+
const scope = readEventScope(record, ctx)
|
|
216
|
+
if (!id || !scope.tenantId) return null
|
|
217
|
+
return { id, action: eventAction(ctx.eventName), scope: { tenantId: scope.tenantId, organizationId: scope.organizationId } }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isCatalogCrudAction(action: string | null): boolean {
|
|
221
|
+
return action === null || CATALOG_CRUD_ACTIONS.has(action)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* `catalog.price.*` (§8 buyer context, Storefront Public API §9): evicts the product's
|
|
226
|
+
* `catalog-price` tag and every listing of the tenant, plus the owning customer's buyer context
|
|
227
|
+
* when the row is customer-specific (`customerOverlayId`). The payload carries ids only, so the row
|
|
228
|
+
* is read back; a deleted row is gone, so a delete evicts the listings only and the product detail
|
|
229
|
+
* and buyer context fall back to their TTLs (catalog declares no price-kind events either).
|
|
230
|
+
*/
|
|
231
|
+
export async function invalidateForPriceEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {
|
|
232
|
+
const event = readScopedEvent(payload, ctx)
|
|
233
|
+
if (!event || !isCatalogCrudAction(event.action)) return 0
|
|
234
|
+
const owner =
|
|
235
|
+
event.action === 'deleted'
|
|
236
|
+
? undefined
|
|
237
|
+
: await lookupForEvent(ctx, event.id, event.scope, readPriceOwner, 'ecommerce.price_cache_invalidation_failed')
|
|
238
|
+
const tags = [catalogProductsTag(event.scope.tenantId)]
|
|
239
|
+
if (owner?.productId) tags.push(catalogPriceTag(owner.productId))
|
|
240
|
+
if (owner?.customerId) tags.push(ecommerceCustomerTag(owner.customerId))
|
|
241
|
+
return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** `catalog.product.created|updated|deleted`: the product's detail entries and every listing of the tenant. */
|
|
245
|
+
export async function invalidateForProductEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {
|
|
246
|
+
const event = readScopedEvent(payload, ctx)
|
|
247
|
+
if (!event || !isCatalogCrudAction(event.action)) return 0
|
|
248
|
+
return invalidateEcommerceCacheTags(ctx, {
|
|
249
|
+
tenantId: event.scope.tenantId,
|
|
250
|
+
tags: [catalogProductTag(event.id), catalogProductsTag(event.scope.tenantId)],
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* `catalog.variant.*`: the variant's product (read back — the payload carries ids only) and every
|
|
256
|
+
* listing of the tenant. A deleted variant row is gone, so its product detail falls back to its TTL.
|
|
257
|
+
*/
|
|
258
|
+
export async function invalidateForVariantEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {
|
|
259
|
+
const event = readScopedEvent(payload, ctx)
|
|
260
|
+
if (!event || !isCatalogCrudAction(event.action)) return 0
|
|
261
|
+
const productId =
|
|
262
|
+
event.action === 'deleted'
|
|
263
|
+
? undefined
|
|
264
|
+
: await lookupForEvent(ctx, event.id, event.scope, readVariantProductId, 'ecommerce.variant_cache_invalidation_failed')
|
|
265
|
+
const tags = [catalogProductsTag(event.scope.tenantId)]
|
|
266
|
+
if (productId) tags.push(catalogProductTag(productId))
|
|
267
|
+
return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** `catalog.category.*`: details showing the category and every listing (category names, filters, assortment). */
|
|
271
|
+
export async function invalidateForCategoryEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {
|
|
272
|
+
const event = readScopedEvent(payload, ctx)
|
|
273
|
+
if (!event || !isCatalogCrudAction(event.action)) return 0
|
|
274
|
+
return invalidateEcommerceCacheTags(ctx, {
|
|
275
|
+
tenantId: event.scope.tenantId,
|
|
276
|
+
tags: [catalogCategoryTag(event.id), catalogProductsTag(event.scope.tenantId)],
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* `availability.policy.*`: a policy tied to a product evicts that product and every listing; a
|
|
282
|
+
* store- or tenant-wide policy (or one that cannot be read back) evicts every storefront product
|
|
283
|
+
* entry of the tenant. Policies are soft-deleted, so a delete still reads back its product.
|
|
284
|
+
*/
|
|
285
|
+
export async function invalidateForAvailabilityPolicyEvent(
|
|
286
|
+
payload: unknown,
|
|
287
|
+
ctx: EcommerceSubscriberContext,
|
|
288
|
+
): Promise<number> {
|
|
289
|
+
const event = readScopedEvent(payload, ctx)
|
|
290
|
+
if (!event || !isCatalogCrudAction(event.action)) return 0
|
|
291
|
+
const productId = await lookupForEvent(
|
|
292
|
+
ctx,
|
|
293
|
+
event.id,
|
|
294
|
+
event.scope,
|
|
295
|
+
readPolicyProductId,
|
|
296
|
+
'ecommerce.availability_policy_cache_invalidation_failed',
|
|
297
|
+
)
|
|
298
|
+
const tags = productId
|
|
299
|
+
? [catalogProductTag(productId), catalogProductsTag(event.scope.tenantId)]
|
|
300
|
+
: [storefrontAvailabilityTag(event.scope.tenantId)]
|
|
301
|
+
return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })
|
|
302
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
|
|
3
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
4
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
5
|
+
import type { BuyerContext, StoreContext } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The ONLY file in the ecommerce module allowed to resolve the DI `cache` (SPEC-029 §6.1
|
|
9
|
+
* "Enforcement", D7). Every storefront key is built by `buildStorefrontCacheKey`; callers pass
|
|
10
|
+
* `parts`, never a raw key. `__tests__/no-raw-cache-access.test.ts` fails on any other access.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type StorefrontCacheScope = 'digest' | 'assortment' | 'priceScope' | 'store'
|
|
14
|
+
|
|
15
|
+
export type StorefrontCacheKeyOptions = { scope?: StorefrontCacheScope }
|
|
16
|
+
|
|
17
|
+
export type StorefrontCacheSetOptions = {
|
|
18
|
+
ttlMs: number
|
|
19
|
+
tags?: string[]
|
|
20
|
+
scope?: StorefrontCacheScope
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type StorefrontCache = {
|
|
24
|
+
get<T>(parts: string[], options?: StorefrontCacheKeyOptions): Promise<T | null>
|
|
25
|
+
set<T>(parts: string[], value: T, options: StorefrontCacheSetOptions): Promise<void>
|
|
26
|
+
deleteByTags(tags: string[]): Promise<number>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type EcommerceResolutionCacheSetOptions = { ttlMs: number; tags?: string[] }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Store-resolution cache (host/slug → store + default channel binding, §8.1 steps 3–5). It runs
|
|
33
|
+
* BEFORE a `StoreContext` exists, so it is tenant-agnostic and MUST NEVER carry buyer data.
|
|
34
|
+
*/
|
|
35
|
+
export type EcommerceResolutionCache = {
|
|
36
|
+
get<T>(parts: string[]): Promise<T | null>
|
|
37
|
+
set<T>(parts: string[], value: T, options: EcommerceResolutionCacheSetOptions): Promise<void>
|
|
38
|
+
deleteByTags(tags: string[]): Promise<number>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Buyer-context cache (§8, step 6): one entry per (store, customerUserId | anonymous), holding the
|
|
43
|
+
* locale-independent buyer layer only. Tagged per store, per portal user, per identity in
|
|
44
|
+
* `customerIds` and per contributing group so user, membership, terms, group and price events can
|
|
45
|
+
* evict it.
|
|
46
|
+
*/
|
|
47
|
+
export type BuyerContextCache = {
|
|
48
|
+
get(customerUserId: string | null): Promise<BuyerContext | null>
|
|
49
|
+
set(buyer: BuyerContext): Promise<void>
|
|
50
|
+
deleteByTags(tags: string[]): Promise<number>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type BuyerContextCacheScope = { tenantId: string; storeId: string }
|
|
54
|
+
|
|
55
|
+
export type CacheContainer = { resolve: (name: string) => unknown }
|
|
56
|
+
|
|
57
|
+
export const BUYER_CONTEXT_TTL_MS = 60_000
|
|
58
|
+
|
|
59
|
+
const STOREFRONT_KEY_NAMESPACE = 'ecommerce:storefront'
|
|
60
|
+
const RESOLUTION_KEY_NAMESPACE = 'ecommerce:resolution'
|
|
61
|
+
const BUYER_KEY_NAMESPACE = 'ecommerce:buyer'
|
|
62
|
+
const ANONYMOUS_SEGMENT = 'anonymous'
|
|
63
|
+
const NULL_SEGMENT = '-'
|
|
64
|
+
|
|
65
|
+
const SCOPE_SEGMENTS: Record<StorefrontCacheScope, string> = {
|
|
66
|
+
digest: 'digest',
|
|
67
|
+
assortment: 'assortment',
|
|
68
|
+
priceScope: 'price-scope',
|
|
69
|
+
store: 'store',
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const logger = createLogger('ecommerce').child({ component: 'storefront-cache' })
|
|
73
|
+
|
|
74
|
+
function encodeSegment(value: string): string {
|
|
75
|
+
return encodeURIComponent(value)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function encodeParts(parts: string[]): string {
|
|
79
|
+
if (parts.length === 0) {
|
|
80
|
+
throw new Error('[internal] ecommerce cache keys require at least one part')
|
|
81
|
+
}
|
|
82
|
+
return parts.map(encodeSegment).join(':')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function scopeSegments(ctx: StoreContext, scope: StorefrontCacheScope): string[] {
|
|
86
|
+
const storeId = ctx.store.id
|
|
87
|
+
switch (scope) {
|
|
88
|
+
case 'digest':
|
|
89
|
+
return [storeId, ctx.digest]
|
|
90
|
+
case 'assortment':
|
|
91
|
+
return [storeId, ctx.effectiveLocale, ctx.buyer.assortmentScopeHash]
|
|
92
|
+
case 'priceScope':
|
|
93
|
+
return [storeId, ctx.buyer.priceScopeKey, ctx.buyer.customerOverlayId ?? NULL_SEGMENT]
|
|
94
|
+
case 'store':
|
|
95
|
+
return [storeId, ctx.effectiveLocale]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildStorefrontCacheKey(
|
|
100
|
+
ctx: StoreContext,
|
|
101
|
+
parts: string[],
|
|
102
|
+
options?: StorefrontCacheKeyOptions,
|
|
103
|
+
): string {
|
|
104
|
+
const scope = options?.scope ?? 'digest'
|
|
105
|
+
const prefix = [SCOPE_SEGMENTS[scope], ...scopeSegments(ctx, scope).map(encodeSegment)].join(':')
|
|
106
|
+
return `${STOREFRONT_KEY_NAMESPACE}:${prefix}:${encodeParts(parts)}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function canonicalCacheValue(value: unknown): unknown {
|
|
110
|
+
if (Array.isArray(value)) return value.map(canonicalCacheValue)
|
|
111
|
+
if (value && typeof value === 'object') {
|
|
112
|
+
return Object.fromEntries(
|
|
113
|
+
Object.entries(value as Record<string, unknown>)
|
|
114
|
+
.filter((entry) => entry[1] !== undefined)
|
|
115
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
116
|
+
.map(([key, entry]) => [key, canonicalCacheValue(entry)]),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
return value
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A fixed-length key part for a structured value: object keys sorted, `undefined` entries dropped, sha256-hashed. */
|
|
123
|
+
export function storefrontCacheValueHash(value: unknown): string {
|
|
124
|
+
return createHash('sha256').update(JSON.stringify(canonicalCacheValue(value))).digest('hex')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function buildEcommerceResolutionCacheKey(parts: string[]): string {
|
|
128
|
+
return `${RESOLUTION_KEY_NAMESPACE}:${encodeParts(parts)}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function buildBuyerContextCacheKey(storeId: string, customerUserId: string | null): string {
|
|
132
|
+
return `${BUYER_KEY_NAMESPACE}:${encodeParts([storeId, customerUserId ?? ANONYMOUS_SEGMENT])}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function ecommerceStoreTag(storeId: string): string {
|
|
136
|
+
return `ecommerce-store:${storeId}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function ecommerceDomainMappingTag(domainMappingId: string): string {
|
|
140
|
+
return `ecommerce-domain-mapping:${domainMappingId}`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function ecommerceDomainTag(hostname: string): string {
|
|
144
|
+
return `ecommerce-domain:${hostname}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function ecommerceCustomerTag(customerId: string): string {
|
|
148
|
+
return `customer:${customerId}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** One portal user's buyer contexts: evicted when the user is updated (e.g. deactivated) or deleted. */
|
|
152
|
+
export function ecommerceCustomerUserTag(customerUserId: string): string {
|
|
153
|
+
return `customer-user:${customerUserId}`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function ecommerceCustomerGroupTag(customerGroupId: string): string {
|
|
157
|
+
return `customer-group:${customerGroupId}`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function catalogProductTag(productId: string): string {
|
|
161
|
+
return `catalog-product:${productId}`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Every storefront listing of a tenant: evicted by any product, variant, category or price change. */
|
|
165
|
+
export function catalogProductsTag(tenantId: string): string {
|
|
166
|
+
return `catalog-products:${tenantId}`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function catalogCategoryTag(categoryId: string): string {
|
|
170
|
+
return `catalog-category:${categoryId}`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function catalogPriceTag(productId: string): string {
|
|
174
|
+
return `catalog-price:${productId}`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Every storefront product entry of a tenant: evicted by an availability policy not tied to one product. */
|
|
178
|
+
export function storefrontAvailabilityTag(tenantId: string): string {
|
|
179
|
+
return `availability:${tenantId}`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Buyer contexts of a tenant that resolved to no customer group (anonymous or unassigned buyers
|
|
184
|
+
* while the tenant has no default group): evicted when a group is created or updated, since that
|
|
185
|
+
* group may have become the default they fall back to.
|
|
186
|
+
*/
|
|
187
|
+
export function ecommerceUngroupedBuyerTag(tenantId: string): string {
|
|
188
|
+
return `customer-group-none:${tenantId}`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function buyerContextTags(
|
|
192
|
+
buyer: Pick<BuyerContext, 'customerIds' | 'customerGroupIds'> & { customerUserId?: string | null },
|
|
193
|
+
tenantId?: string,
|
|
194
|
+
): string[] {
|
|
195
|
+
return [
|
|
196
|
+
...(buyer.customerUserId ? [ecommerceCustomerUserTag(buyer.customerUserId)] : []),
|
|
197
|
+
...buyer.customerIds.map(ecommerceCustomerTag),
|
|
198
|
+
...buyer.customerGroupIds.map(ecommerceCustomerGroupTag),
|
|
199
|
+
...(tenantId && buyer.customerGroupIds.length === 0 ? [ecommerceUngroupedBuyerTag(tenantId)] : []),
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveCacheService(container: CacheContainer | null | undefined): CacheStrategy | null {
|
|
204
|
+
if (!container?.resolve) return null
|
|
205
|
+
try {
|
|
206
|
+
const candidate = container.resolve('cache') as CacheStrategy | null | undefined
|
|
207
|
+
if (
|
|
208
|
+
candidate &&
|
|
209
|
+
typeof candidate.get === 'function' &&
|
|
210
|
+
typeof candidate.set === 'function' &&
|
|
211
|
+
typeof candidate.deleteByTags === 'function'
|
|
212
|
+
) {
|
|
213
|
+
return candidate
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function reportCacheFailure(operation: string, error: unknown): void {
|
|
222
|
+
logger.warn('Ecommerce cache operation failed; degrading to a cache miss', { operation, err: error })
|
|
223
|
+
getTelemetryRuntime()?.reportError(error, {
|
|
224
|
+
module: 'ecommerce',
|
|
225
|
+
code: 'ecommerce.cache_operation_failed',
|
|
226
|
+
attributes: { operation },
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
type TenantScopedCacheOptions = {
|
|
231
|
+
cache: CacheStrategy | null
|
|
232
|
+
tenantId: string | null
|
|
233
|
+
baseTags: string[]
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function createTenantScopedAccess({ cache, tenantId, baseTags }: TenantScopedCacheOptions) {
|
|
237
|
+
return {
|
|
238
|
+
async get<T>(key: string): Promise<T | null> {
|
|
239
|
+
if (!cache) return null
|
|
240
|
+
try {
|
|
241
|
+
const value = await runWithCacheTenant(tenantId, () => cache.get(key))
|
|
242
|
+
return (value ?? null) as T | null
|
|
243
|
+
} catch (error) {
|
|
244
|
+
reportCacheFailure('get', error)
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
async set<T>(key: string, value: T, ttlMs: number, tags: string[] | undefined): Promise<void> {
|
|
249
|
+
if (!cache) return
|
|
250
|
+
const mergedTags = Array.from(new Set([...baseTags, ...(tags ?? [])]))
|
|
251
|
+
try {
|
|
252
|
+
await runWithCacheTenant(tenantId, () => cache.set(key, value, { ttl: ttlMs, tags: mergedTags }))
|
|
253
|
+
} catch (error) {
|
|
254
|
+
reportCacheFailure('set', error)
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
async deleteByTags(tags: string[]): Promise<number> {
|
|
258
|
+
if (!cache || tags.length === 0) return 0
|
|
259
|
+
try {
|
|
260
|
+
return await runWithCacheTenant(tenantId, () => cache.deleteByTags(tags))
|
|
261
|
+
} catch (error) {
|
|
262
|
+
reportCacheFailure('deleteByTags', error)
|
|
263
|
+
return 0
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function storefrontCache(container: CacheContainer | null | undefined, ctx: StoreContext): StorefrontCache {
|
|
270
|
+
const access = createTenantScopedAccess({
|
|
271
|
+
cache: resolveCacheService(container),
|
|
272
|
+
tenantId: ctx.tenantId,
|
|
273
|
+
baseTags: [ecommerceStoreTag(ctx.store.id)],
|
|
274
|
+
})
|
|
275
|
+
return {
|
|
276
|
+
get: (parts, options) => access.get(buildStorefrontCacheKey(ctx, parts, options)),
|
|
277
|
+
set: (parts, value, options) =>
|
|
278
|
+
access.set(buildStorefrontCacheKey(ctx, parts, { scope: options.scope }), value, options.ttlMs, options.tags),
|
|
279
|
+
deleteByTags: (tags) => access.deleteByTags(tags),
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function ecommerceResolutionCache(container: CacheContainer | null | undefined): EcommerceResolutionCache {
|
|
284
|
+
const access = createTenantScopedAccess({
|
|
285
|
+
cache: resolveCacheService(container),
|
|
286
|
+
tenantId: null,
|
|
287
|
+
baseTags: [],
|
|
288
|
+
})
|
|
289
|
+
return {
|
|
290
|
+
get: (parts) => access.get(buildEcommerceResolutionCacheKey(parts)),
|
|
291
|
+
set: (parts, value, options) =>
|
|
292
|
+
access.set(buildEcommerceResolutionCacheKey(parts), value, options.ttlMs, options.tags),
|
|
293
|
+
deleteByTags: (tags) => access.deleteByTags(tags),
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export type EcommerceCacheInvalidation = { tenantId: string | null; tags: string[] }
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Event-driven invalidation (§8): evicts `tags` from the tenant-agnostic resolution cache and, when
|
|
301
|
+
* the tenant is known, from that tenant's storefront and buyer-context caches. Tags are tenant
|
|
302
|
+
* scoped by the cache layer, so each scope is cleared inside its own tenant.
|
|
303
|
+
*/
|
|
304
|
+
export async function invalidateEcommerceCacheTags(
|
|
305
|
+
container: CacheContainer | null | undefined,
|
|
306
|
+
invalidation: EcommerceCacheInvalidation,
|
|
307
|
+
): Promise<number> {
|
|
308
|
+
const tags = Array.from(new Set(invalidation.tags.filter((tag) => tag.length > 0)))
|
|
309
|
+
if (tags.length === 0) return 0
|
|
310
|
+
const cache = resolveCacheService(container)
|
|
311
|
+
if (!cache) return 0
|
|
312
|
+
const tenantIds = invalidation.tenantId ? [null, invalidation.tenantId] : [null]
|
|
313
|
+
let deleted = 0
|
|
314
|
+
for (const tenantId of tenantIds) {
|
|
315
|
+
deleted += await createTenantScopedAccess({ cache, tenantId, baseTags: [] }).deleteByTags(tags)
|
|
316
|
+
}
|
|
317
|
+
return deleted
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function buyerContextCache(
|
|
321
|
+
container: CacheContainer | null | undefined,
|
|
322
|
+
scope: BuyerContextCacheScope,
|
|
323
|
+
): BuyerContextCache {
|
|
324
|
+
const access = createTenantScopedAccess({
|
|
325
|
+
cache: resolveCacheService(container),
|
|
326
|
+
tenantId: scope.tenantId,
|
|
327
|
+
baseTags: [ecommerceStoreTag(scope.storeId)],
|
|
328
|
+
})
|
|
329
|
+
return {
|
|
330
|
+
get: (customerUserId) => access.get<BuyerContext>(buildBuyerContextCacheKey(scope.storeId, customerUserId)),
|
|
331
|
+
set: (buyer) =>
|
|
332
|
+
access.set(
|
|
333
|
+
buildBuyerContextCacheKey(scope.storeId, buyer.customerUserId),
|
|
334
|
+
buyer,
|
|
335
|
+
BUYER_CONTEXT_TTL_MS,
|
|
336
|
+
buyerContextTags(buyer, scope.tenantId),
|
|
337
|
+
),
|
|
338
|
+
deleteByTags: (tags) => access.deleteByTags(tags),
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
3
|
+
|
|
4
|
+
type ConflictRecheck = () => Promise<void>
|
|
5
|
+
|
|
6
|
+
const recheckByRequest = new WeakMap<Request, ConflictRecheck>()
|
|
7
|
+
|
|
8
|
+
export function registerCreateConflictRecheck(request: Request | undefined, recheck: ConflictRecheck): void {
|
|
9
|
+
if (request) recheckByRequest.set(request, recheck)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function clearCreateConflictRecheck(request: Request | undefined): void {
|
|
13
|
+
if (request) recheckByRequest.delete(request)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function withCreateConflictRecheck(
|
|
17
|
+
handler: (request: Request) => Promise<Response>,
|
|
18
|
+
): (request: Request) => Promise<Response> {
|
|
19
|
+
return async (request: Request): Promise<Response> => {
|
|
20
|
+
const response = await handler(request)
|
|
21
|
+
const recheck = recheckByRequest.get(request)
|
|
22
|
+
recheckByRequest.delete(request)
|
|
23
|
+
if (!recheck || response.status !== 500) return response
|
|
24
|
+
try {
|
|
25
|
+
await recheck()
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (isCrudHttpError(err) && err.status === 409) return NextResponse.json(err.body, { status: 409 })
|
|
28
|
+
}
|
|
29
|
+
return response
|
|
30
|
+
}
|
|
31
|
+
}
|