@open-mercato/core 0.9.1-develop.7473.1.de5b22888f → 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,697 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import { sql } from 'kysely'
|
|
4
|
+
import type { AvailabilityState } from '@open-mercato/shared/lib/availability'
|
|
5
|
+
import type { EffectiveAssortmentScope } from '@open-mercato/shared/lib/catalog-visibility'
|
|
6
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
7
|
+
import { defaultLocale } from '@open-mercato/shared/lib/i18n/config'
|
|
8
|
+
import { resolveSupportedLocale } from '@open-mercato/shared/lib/i18n/locale'
|
|
9
|
+
import { loadDictionary } from '@open-mercato/shared/lib/i18n/server'
|
|
10
|
+
import { createFallbackTranslator } from '@open-mercato/shared/lib/i18n/translate'
|
|
11
|
+
import type { QueryEngine, Where } from '@open-mercato/shared/lib/query/types'
|
|
12
|
+
import { E } from '#generated/entities.ids.generated'
|
|
13
|
+
import {
|
|
14
|
+
CatalogOptionSchemaTemplate,
|
|
15
|
+
CatalogProductCategory,
|
|
16
|
+
CatalogProductVariant,
|
|
17
|
+
} from '@open-mercato/core/modules/catalog/data/entities'
|
|
18
|
+
import {
|
|
19
|
+
CATALOG_PRODUCT_TYPES,
|
|
20
|
+
type CatalogProductOptionDefinition,
|
|
21
|
+
} from '@open-mercato/core/modules/catalog/data/types'
|
|
22
|
+
import {
|
|
23
|
+
optionChoiceLabelTranslationField,
|
|
24
|
+
optionLabelTranslationField,
|
|
25
|
+
} from '@open-mercato/core/modules/catalog/lib/optionSchemaTranslations'
|
|
26
|
+
import { compareCodeUnits } from '@open-mercato/core/modules/catalog/lib/productScopeKeys'
|
|
27
|
+
import { catalogCategoryTag, catalogProductsTag, storefrontCache, storefrontCacheValueHash } from './cacheKeys'
|
|
28
|
+
import {
|
|
29
|
+
STOREFRONT_CATEGORY_ENTITY_TYPE,
|
|
30
|
+
STOREFRONT_OPTION_SCHEMA_ENTITY_TYPE,
|
|
31
|
+
STOREFRONT_TAG_ENTITY_TYPE,
|
|
32
|
+
isCategoryInAssortment,
|
|
33
|
+
localize,
|
|
34
|
+
nonEmptyString,
|
|
35
|
+
referenceId,
|
|
36
|
+
stringList,
|
|
37
|
+
type TranslationMap,
|
|
38
|
+
} from './storefrontCatalogSupport'
|
|
39
|
+
import type { StorefrontProductScope } from './storefrontProductScope'
|
|
40
|
+
import type { StoreContext } from './types'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Listing facets (Storefront Public API rev 4 §5.3, §5.4, §9.1).
|
|
44
|
+
*
|
|
45
|
+
* The four count facets (`categories`, `tags`, `options`, `productTypes`) are buyer-independent
|
|
46
|
+
* beyond the assortment, so they are computed once per assortment scope and cached under
|
|
47
|
+
* `assortmentScopeHash` (`scope: 'assortment'`). `priceRange` is buyer-priced and is computed and
|
|
48
|
+
* cached with the items on the full digest — never here. `availability` counts the returned page
|
|
49
|
+
* only (D21) and is computed by the listing.
|
|
50
|
+
*
|
|
51
|
+
* Cross-exclusion: the facet universe is the assortment scope plus the search term (the only
|
|
52
|
+
* non-facet product filter). Each dimension then applies every active filter except its own, in
|
|
53
|
+
* memory, over that universe — so with no filters every dimension shares one universe load, and
|
|
54
|
+
* with filters no dimension costs an extra query. Option codes are separate dimensions: the
|
|
55
|
+
* counts for `options[color]` keep `options[size]` applied (on the same variant) but not
|
|
56
|
+
* `options[color]`. The price filter is not applied to count facets — it depends on the buyer's
|
|
57
|
+
* prices, which the assortment-keyed entry must not.
|
|
58
|
+
*
|
|
59
|
+
* The universe is hard-capped at `STOREFRONT_FACET_UNIVERSE_CAP` products: past it the universe is
|
|
60
|
+
* never loaded and the count facets degrade to empty lists (§10 query budget over completeness).
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
export const STOREFRONT_COUNT_FACETS_TTL_MS = 30_000
|
|
64
|
+
|
|
65
|
+
const COUNT_FACETS_CACHE_SEGMENT = 'products-count-facets'
|
|
66
|
+
/** The most products a facet or category-count universe ever loads; larger universes are not counted. */
|
|
67
|
+
export const STOREFRONT_FACET_UNIVERSE_CAP = 10_000
|
|
68
|
+
const VARIANT_ID_CHUNK = 10_000
|
|
69
|
+
const CATALOG_PRODUCT_TYPE_LABEL_PREFIX = 'catalog.products.types.'
|
|
70
|
+
|
|
71
|
+
export type StorefrontFacets = {
|
|
72
|
+
categories: Array<{ id: string; name: string; slug: string | null; depth: number; parentId: string | null; count: number }>
|
|
73
|
+
tags: Array<{ slug: string; label: string; count: number }>
|
|
74
|
+
priceRange: { min: number; max: number; currencyCode: string } | null
|
|
75
|
+
options: Array<{ code: string; label: string; values: Array<{ code: string; label: string; count: number }> }>
|
|
76
|
+
productTypes: Array<{ type: string; label: string; count: number }>
|
|
77
|
+
availability: Array<{ state: AvailabilityState; count: number }>
|
|
78
|
+
availabilityScope: 'page'
|
|
79
|
+
total: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type StorefrontCountFacets = Pick<StorefrontFacets, 'categories' | 'tags' | 'options' | 'productTypes'>
|
|
83
|
+
|
|
84
|
+
/** The facet filters the listing actually applied (unknown or out-of-assortment selections already dropped). */
|
|
85
|
+
export type StorefrontFacetSelection = {
|
|
86
|
+
categoryId: string | null
|
|
87
|
+
tagIds: string[]
|
|
88
|
+
productType: string | null
|
|
89
|
+
options: Record<string, string[]>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type StorefrontFacetRuntime = {
|
|
93
|
+
ctx: StoreContext
|
|
94
|
+
em: EntityManager
|
|
95
|
+
queryEngine: QueryEngine
|
|
96
|
+
scope: StorefrontProductScope
|
|
97
|
+
decryptionScope: { tenantId: string; organizationId: string }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type StorefrontFacetProduct = {
|
|
101
|
+
id: string
|
|
102
|
+
productType: string
|
|
103
|
+
templateId: string | null
|
|
104
|
+
categoryIds: string[]
|
|
105
|
+
tagIds: string[]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type StorefrontFacetVariant = { id: string; optionValues: Record<string, string> }
|
|
109
|
+
|
|
110
|
+
export type StorefrontFacetCategory = {
|
|
111
|
+
id: string
|
|
112
|
+
name: string
|
|
113
|
+
slug: string | null
|
|
114
|
+
depth: number
|
|
115
|
+
parentId: string | null
|
|
116
|
+
isActive: boolean
|
|
117
|
+
ancestorIds: string[]
|
|
118
|
+
descendantIds: string[]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type StorefrontFacetTag = { id: string; slug: string; label: string }
|
|
122
|
+
|
|
123
|
+
export type StorefrontFacetSource = {
|
|
124
|
+
products: StorefrontFacetProduct[]
|
|
125
|
+
variants: Map<string, StorefrontFacetVariant[]>
|
|
126
|
+
categories: Map<string, StorefrontFacetCategory>
|
|
127
|
+
tags: Map<string, StorefrontFacetTag>
|
|
128
|
+
templates: Map<string, CatalogProductOptionDefinition[]>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type StorefrontRawCountFacets = {
|
|
132
|
+
categories: Map<string, number>
|
|
133
|
+
tags: Map<string, number>
|
|
134
|
+
productTypes: Map<string, number>
|
|
135
|
+
options: Map<string, Map<string, number>>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type UniverseRecord = { id?: unknown; product_type?: unknown; option_schema_id?: unknown }
|
|
139
|
+
|
|
140
|
+
type FacetAssignmentDatabase = {
|
|
141
|
+
catalog_product_category_assignments: {
|
|
142
|
+
product_id: string
|
|
143
|
+
category_id: string
|
|
144
|
+
tenant_id: string
|
|
145
|
+
organization_id: string
|
|
146
|
+
}
|
|
147
|
+
catalog_product_tag_assignments: {
|
|
148
|
+
product_id: string
|
|
149
|
+
tag_id: string
|
|
150
|
+
tenant_id: string
|
|
151
|
+
organization_id: string
|
|
152
|
+
}
|
|
153
|
+
catalog_product_tags: {
|
|
154
|
+
id: string
|
|
155
|
+
slug: string
|
|
156
|
+
label: string
|
|
157
|
+
tenant_id: string
|
|
158
|
+
organization_id: string
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type AssignmentRow = { product_id: string; kind: string; ref_id: string; slug: string | null; label: string | null }
|
|
163
|
+
|
|
164
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
165
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function chunk<T>(values: T[], size: number): T[][] {
|
|
169
|
+
const chunks: T[][] = []
|
|
170
|
+
for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size))
|
|
171
|
+
return chunks
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function increment<K>(counts: Map<K, number>, key: K): void {
|
|
175
|
+
counts.set(key, (counts.get(key) ?? 0) + 1)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The count-facet key part: the resolved facet selection (unknown or out-of-assortment category
|
|
180
|
+
* and tag slugs already dropped; option values are validator-bounded, sorted and de-duplicated)
|
|
181
|
+
* plus the sanitized search term. Price,
|
|
182
|
+
* sort, paging, availability and locale are excluded (locale is already part of the assortment
|
|
183
|
+
* scope segment).
|
|
184
|
+
*/
|
|
185
|
+
export function storefrontCountFacetCacheParts(selection: StorefrontFacetSelection, searchTerm: string | null): string[] {
|
|
186
|
+
const options: Record<string, string[]> = {}
|
|
187
|
+
for (const code of Object.keys(selection.options).sort(compareCodeUnits)) {
|
|
188
|
+
const values = Array.from(new Set(selection.options[code])).sort(compareCodeUnits)
|
|
189
|
+
if (values.length) options[code] = values
|
|
190
|
+
}
|
|
191
|
+
return [
|
|
192
|
+
COUNT_FACETS_CACHE_SEGMENT,
|
|
193
|
+
storefrontCacheValueHash({
|
|
194
|
+
search: searchTerm ?? null,
|
|
195
|
+
categoryId: selection.categoryId,
|
|
196
|
+
tagIds: Array.from(new Set(selection.tagIds)).sort(compareCodeUnits),
|
|
197
|
+
productType: selection.productType,
|
|
198
|
+
options,
|
|
199
|
+
}),
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function storefrontCountFacetCacheTags(ctx: StoreContext, selection: Pick<StorefrontFacetSelection, 'categoryId'>): string[] {
|
|
204
|
+
return [catalogProductsTag(ctx.tenantId), ...(selection.categoryId ? [catalogCategoryTag(selection.categoryId)] : [])]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function readCachedStorefrontCountFacets(
|
|
208
|
+
container: AwilixContainer,
|
|
209
|
+
ctx: StoreContext,
|
|
210
|
+
selection: StorefrontFacetSelection,
|
|
211
|
+
searchTerm: string | null,
|
|
212
|
+
): Promise<StorefrontCountFacets | null> {
|
|
213
|
+
return storefrontCache(container, ctx).get<StorefrontCountFacets>(storefrontCountFacetCacheParts(selection, searchTerm), {
|
|
214
|
+
scope: 'assortment',
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function writeCachedStorefrontCountFacets(
|
|
219
|
+
container: AwilixContainer,
|
|
220
|
+
ctx: StoreContext,
|
|
221
|
+
selection: StorefrontFacetSelection,
|
|
222
|
+
searchTerm: string | null,
|
|
223
|
+
facets: StorefrontCountFacets,
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
await storefrontCache(container, ctx).set(storefrontCountFacetCacheParts(selection, searchTerm), facets, {
|
|
226
|
+
scope: 'assortment',
|
|
227
|
+
ttlMs: STOREFRONT_COUNT_FACETS_TTL_MS,
|
|
228
|
+
tags: storefrontCountFacetCacheTags(ctx, selection),
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The scoped product ids of a facet or category-count universe, or `null` when it holds more than
|
|
234
|
+
* `cap` products — one query either way, never more than `cap` rows.
|
|
235
|
+
*/
|
|
236
|
+
export async function queryStorefrontProductUniverse(
|
|
237
|
+
runtime: StorefrontFacetRuntime,
|
|
238
|
+
filters: Where,
|
|
239
|
+
cap: number = STOREFRONT_FACET_UNIVERSE_CAP,
|
|
240
|
+
): Promise<UniverseRecord[] | null> {
|
|
241
|
+
const result = await runtime.queryEngine.query<UniverseRecord>(E.catalog.catalog_product, {
|
|
242
|
+
tenantId: runtime.scope.tenantId,
|
|
243
|
+
organizationId: runtime.scope.organizationId,
|
|
244
|
+
withDeleted: runtime.scope.withDeleted,
|
|
245
|
+
filters,
|
|
246
|
+
fields: ['id', 'product_type', 'option_schema_id'],
|
|
247
|
+
page: { page: 1, pageSize: Math.max(1, cap) },
|
|
248
|
+
})
|
|
249
|
+
return result.total > result.items.length ? null : result.items
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function loadAssignments(runtime: StorefrontFacetRuntime, productIds: string[]): Promise<AssignmentRow[]> {
|
|
253
|
+
const { tenantId, organizationId } = runtime.decryptionScope
|
|
254
|
+
const db = runtime.em.getKysely<FacetAssignmentDatabase>()
|
|
255
|
+
const categoryRows = db
|
|
256
|
+
.selectFrom('catalog_product_category_assignments as ca')
|
|
257
|
+
.select([
|
|
258
|
+
'ca.product_id as product_id',
|
|
259
|
+
sql<string>`'category'`.as('kind'),
|
|
260
|
+
'ca.category_id as ref_id',
|
|
261
|
+
sql<string | null>`null`.as('slug'),
|
|
262
|
+
sql<string | null>`null`.as('label'),
|
|
263
|
+
])
|
|
264
|
+
.where(sql<boolean>`${sql.ref('ca.product_id')} = any(${productIds}::uuid[])`)
|
|
265
|
+
.where('ca.tenant_id', '=', tenantId)
|
|
266
|
+
.where('ca.organization_id', '=', organizationId)
|
|
267
|
+
const tagRows = db
|
|
268
|
+
.selectFrom('catalog_product_tag_assignments as ta')
|
|
269
|
+
.innerJoin('catalog_product_tags as tg', 'tg.id', 'ta.tag_id')
|
|
270
|
+
.select([
|
|
271
|
+
'ta.product_id as product_id',
|
|
272
|
+
sql<string>`'tag'`.as('kind'),
|
|
273
|
+
'ta.tag_id as ref_id',
|
|
274
|
+
sql<string | null>`${sql.ref('tg.slug')}`.as('slug'),
|
|
275
|
+
sql<string | null>`${sql.ref('tg.label')}`.as('label'),
|
|
276
|
+
])
|
|
277
|
+
.where(sql<boolean>`${sql.ref('ta.product_id')} = any(${productIds}::uuid[])`)
|
|
278
|
+
.where('ta.tenant_id', '=', tenantId)
|
|
279
|
+
.where('ta.organization_id', '=', organizationId)
|
|
280
|
+
.where('tg.tenant_id', '=', tenantId)
|
|
281
|
+
.where('tg.organization_id', '=', organizationId)
|
|
282
|
+
return categoryRows.unionAll(tagRows).execute()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function loadCategories(runtime: StorefrontFacetRuntime): Promise<Map<string, StorefrontFacetCategory>> {
|
|
286
|
+
const { ctx } = runtime
|
|
287
|
+
const rows = await findWithDecryption(
|
|
288
|
+
runtime.em,
|
|
289
|
+
CatalogProductCategory,
|
|
290
|
+
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId, deletedAt: null },
|
|
291
|
+
{ fields: ['id', 'name', 'slug', 'depth', 'parentId', 'isActive', 'ancestorIds', 'descendantIds'] },
|
|
292
|
+
runtime.decryptionScope,
|
|
293
|
+
)
|
|
294
|
+
return new Map(
|
|
295
|
+
rows.map((row) => [
|
|
296
|
+
row.id,
|
|
297
|
+
{
|
|
298
|
+
id: row.id,
|
|
299
|
+
name: row.name,
|
|
300
|
+
slug: row.slug ?? null,
|
|
301
|
+
depth: typeof row.depth === 'number' ? row.depth : 0,
|
|
302
|
+
parentId: row.parentId ?? null,
|
|
303
|
+
isActive: row.isActive !== false,
|
|
304
|
+
ancestorIds: stringList(row.ancestorIds),
|
|
305
|
+
descendantIds: stringList(row.descendantIds),
|
|
306
|
+
},
|
|
307
|
+
]),
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function loadVariants(
|
|
312
|
+
runtime: StorefrontFacetRuntime,
|
|
313
|
+
productIds: string[],
|
|
314
|
+
): Promise<Map<string, StorefrontFacetVariant[]>> {
|
|
315
|
+
const { ctx } = runtime
|
|
316
|
+
const batches = await Promise.all(
|
|
317
|
+
chunk(productIds, VARIANT_ID_CHUNK).map((ids) =>
|
|
318
|
+
findWithDecryption(
|
|
319
|
+
runtime.em,
|
|
320
|
+
CatalogProductVariant,
|
|
321
|
+
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId, deletedAt: null, isActive: true, product: { $in: ids } },
|
|
322
|
+
{ fields: ['id', 'product', 'optionValues'] },
|
|
323
|
+
runtime.decryptionScope,
|
|
324
|
+
),
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
const byProduct = new Map<string, StorefrontFacetVariant[]>()
|
|
328
|
+
for (const variant of batches.flat()) {
|
|
329
|
+
const productId = referenceId(variant.product)
|
|
330
|
+
if (!productId) continue
|
|
331
|
+
const optionValues: Record<string, string> = {}
|
|
332
|
+
if (isRecord(variant.optionValues)) {
|
|
333
|
+
for (const [code, value] of Object.entries(variant.optionValues)) {
|
|
334
|
+
const text = nonEmptyString(value)
|
|
335
|
+
if (text !== null) optionValues[code] = text
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const bucket = byProduct.get(productId) ?? []
|
|
339
|
+
bucket.push({ id: variant.id, optionValues })
|
|
340
|
+
byProduct.set(productId, bucket)
|
|
341
|
+
}
|
|
342
|
+
return byProduct
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function loadTemplates(
|
|
346
|
+
runtime: StorefrontFacetRuntime,
|
|
347
|
+
templateIds: string[],
|
|
348
|
+
): Promise<Map<string, CatalogProductOptionDefinition[]>> {
|
|
349
|
+
const templates = new Map<string, CatalogProductOptionDefinition[]>()
|
|
350
|
+
if (!templateIds.length) return templates
|
|
351
|
+
const { ctx } = runtime
|
|
352
|
+
const rows = await findWithDecryption(
|
|
353
|
+
runtime.em,
|
|
354
|
+
CatalogOptionSchemaTemplate,
|
|
355
|
+
{ id: { $in: templateIds }, tenantId: ctx.tenantId, organizationId: ctx.organizationId, deletedAt: null, isActive: true },
|
|
356
|
+
{ fields: ['id', 'schema'] },
|
|
357
|
+
runtime.decryptionScope,
|
|
358
|
+
)
|
|
359
|
+
for (const row of rows) {
|
|
360
|
+
const options = isRecord(row.schema) && Array.isArray(row.schema.options) ? row.schema.options : []
|
|
361
|
+
templates.set(
|
|
362
|
+
row.id,
|
|
363
|
+
options.filter((option): option is CatalogProductOptionDefinition => isRecord(option) && nonEmptyString(option.code) !== null),
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
return templates
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The facet universe and everything the count facets read: the scoped products (one query-engine
|
|
371
|
+
* call composing `filters`, normally the assortment scope plus the search clause), their category
|
|
372
|
+
* and tag assignments (one union query), the tenant's categories, the products' active variants
|
|
373
|
+
* and the option schema templates they use. Five queries for a universe of at most `cap` products;
|
|
374
|
+
* `null` after the single universe query when it is larger. The variant list doubles as the
|
|
375
|
+
* listing's variant index.
|
|
376
|
+
*/
|
|
377
|
+
export async function loadStorefrontFacetSource(
|
|
378
|
+
runtime: StorefrontFacetRuntime,
|
|
379
|
+
filters: Where,
|
|
380
|
+
cap: number = STOREFRONT_FACET_UNIVERSE_CAP,
|
|
381
|
+
): Promise<StorefrontFacetSource | null> {
|
|
382
|
+
const records = await queryStorefrontProductUniverse(runtime, filters, cap)
|
|
383
|
+
if (!records) return null
|
|
384
|
+
const products = new Map<string, StorefrontFacetProduct>()
|
|
385
|
+
for (const record of records) {
|
|
386
|
+
if (typeof record.id !== 'string' || products.has(record.id)) continue
|
|
387
|
+
products.set(record.id, {
|
|
388
|
+
id: record.id,
|
|
389
|
+
productType: nonEmptyString(record.product_type) ?? 'simple',
|
|
390
|
+
templateId: nonEmptyString(record.option_schema_id),
|
|
391
|
+
categoryIds: [],
|
|
392
|
+
tagIds: [],
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
const productIds = Array.from(products.keys())
|
|
396
|
+
if (!productIds.length) {
|
|
397
|
+
return { products: [], variants: new Map(), categories: new Map(), tags: new Map(), templates: new Map() }
|
|
398
|
+
}
|
|
399
|
+
const templateIds = Array.from(
|
|
400
|
+
new Set(Array.from(products.values()).flatMap((product) => (product.templateId ? [product.templateId] : []))),
|
|
401
|
+
)
|
|
402
|
+
const [assignments, categories, variants, templates] = await Promise.all([
|
|
403
|
+
loadAssignments(runtime, productIds),
|
|
404
|
+
loadCategories(runtime),
|
|
405
|
+
loadVariants(runtime, productIds),
|
|
406
|
+
loadTemplates(runtime, templateIds),
|
|
407
|
+
])
|
|
408
|
+
const tags = new Map<string, StorefrontFacetTag>()
|
|
409
|
+
for (const row of assignments) {
|
|
410
|
+
const product = products.get(String(row.product_id))
|
|
411
|
+
const refId = String(row.ref_id)
|
|
412
|
+
if (!product) continue
|
|
413
|
+
if (row.kind === 'category') {
|
|
414
|
+
if (!product.categoryIds.includes(refId)) product.categoryIds.push(refId)
|
|
415
|
+
continue
|
|
416
|
+
}
|
|
417
|
+
if (!product.tagIds.includes(refId)) product.tagIds.push(refId)
|
|
418
|
+
if (!tags.has(refId)) tags.set(refId, { id: refId, slug: row.slug ?? '', label: row.label ?? '' })
|
|
419
|
+
}
|
|
420
|
+
return { products: Array.from(products.values()), variants, categories, tags, templates }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function storefrontFacetVariantIndex(source: StorefrontFacetSource): Map<string, string[]> {
|
|
424
|
+
const index = new Map<string, string[]>()
|
|
425
|
+
for (const product of source.products) {
|
|
426
|
+
index.set(product.id, (source.variants.get(product.id) ?? []).map((variant) => variant.id))
|
|
427
|
+
}
|
|
428
|
+
return index
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function variantMatches(
|
|
432
|
+
variant: StorefrontFacetVariant,
|
|
433
|
+
options: Record<string, string[]>,
|
|
434
|
+
exceptCode: string | null,
|
|
435
|
+
): boolean {
|
|
436
|
+
return Object.entries(options).every(([code, values]) => {
|
|
437
|
+
if (code === exceptCode) return true
|
|
438
|
+
const value = variant.optionValues[code]
|
|
439
|
+
return value !== undefined && values.includes(value)
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Cross-facet exclusion (§5.4) over the facet universe: each dimension counts the products that
|
|
445
|
+
* match every active filter except its own. Category counts include descendants (a product counts
|
|
446
|
+
* for a category when it is assigned to it or to one of its descendants, as the filter does); option
|
|
447
|
+
* counts are per option code and count a product once per value carried by any of its variants that
|
|
448
|
+
* also matches the other option filters.
|
|
449
|
+
*/
|
|
450
|
+
export function countStorefrontFacets(
|
|
451
|
+
source: StorefrontFacetSource,
|
|
452
|
+
selection: StorefrontFacetSelection,
|
|
453
|
+
): StorefrontRawCountFacets {
|
|
454
|
+
const selectedCategory = selection.categoryId ? source.categories.get(selection.categoryId) : undefined
|
|
455
|
+
const categoryReach = selection.categoryId
|
|
456
|
+
? new Set([selection.categoryId, ...(selectedCategory?.descendantIds ?? [])])
|
|
457
|
+
: null
|
|
458
|
+
const tagSet = selection.tagIds.length ? new Set(selection.tagIds) : null
|
|
459
|
+
const optionCodes = Object.keys(selection.options)
|
|
460
|
+
const variantsOf = (product: StorefrontFacetProduct) => source.variants.get(product.id) ?? []
|
|
461
|
+
|
|
462
|
+
const inCategory = (product: StorefrontFacetProduct) =>
|
|
463
|
+
!categoryReach || product.categoryIds.some((id) => categoryReach.has(id))
|
|
464
|
+
const inTags = (product: StorefrontFacetProduct) => !tagSet || product.tagIds.some((id) => tagSet.has(id))
|
|
465
|
+
const inType = (product: StorefrontFacetProduct) =>
|
|
466
|
+
!selection.productType || product.productType === selection.productType
|
|
467
|
+
const inOptions = (product: StorefrontFacetProduct) =>
|
|
468
|
+
optionCodes.length === 0 || variantsOf(product).some((variant) => variantMatches(variant, selection.options, null))
|
|
469
|
+
|
|
470
|
+
const contributors = new Map<string, string[]>()
|
|
471
|
+
for (const category of source.categories.values()) {
|
|
472
|
+
for (const id of [category.id, ...category.descendantIds]) {
|
|
473
|
+
const bucket = contributors.get(id) ?? []
|
|
474
|
+
bucket.push(category.id)
|
|
475
|
+
contributors.set(id, bucket)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const raw: StorefrontRawCountFacets = {
|
|
480
|
+
categories: new Map(),
|
|
481
|
+
tags: new Map(),
|
|
482
|
+
productTypes: new Map(),
|
|
483
|
+
options: new Map(),
|
|
484
|
+
}
|
|
485
|
+
const codes = new Set<string>(optionCodes)
|
|
486
|
+
for (const product of source.products) {
|
|
487
|
+
for (const variant of variantsOf(product)) for (const code of Object.keys(variant.optionValues)) codes.add(code)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for (const product of source.products) {
|
|
491
|
+
const category = inCategory(product)
|
|
492
|
+
const tags = inTags(product)
|
|
493
|
+
const type = inType(product)
|
|
494
|
+
const options = inOptions(product)
|
|
495
|
+
if (tags && type && options) {
|
|
496
|
+
const reached = new Set<string>()
|
|
497
|
+
for (const assignedId of product.categoryIds) {
|
|
498
|
+
for (const categoryId of contributors.get(assignedId) ?? []) reached.add(categoryId)
|
|
499
|
+
}
|
|
500
|
+
for (const categoryId of reached) increment(raw.categories, categoryId)
|
|
501
|
+
}
|
|
502
|
+
if (category && type && options) {
|
|
503
|
+
for (const tagId of product.tagIds) increment(raw.tags, tagId)
|
|
504
|
+
}
|
|
505
|
+
if (category && tags && options) increment(raw.productTypes, product.productType)
|
|
506
|
+
if (!(category && tags && type)) continue
|
|
507
|
+
for (const code of codes) {
|
|
508
|
+
const values = new Set<string>()
|
|
509
|
+
for (const variant of variantsOf(product)) {
|
|
510
|
+
const value = variant.optionValues[code]
|
|
511
|
+
if (value === undefined || !variantMatches(variant, selection.options, code)) continue
|
|
512
|
+
values.add(value)
|
|
513
|
+
}
|
|
514
|
+
if (!values.size) continue
|
|
515
|
+
const counts = raw.options.get(code) ?? new Map<string, number>()
|
|
516
|
+
for (const value of values) increment(counts, value)
|
|
517
|
+
raw.options.set(code, counts)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return raw
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function sortedTemplateIds(source: StorefrontFacetSource): string[] {
|
|
524
|
+
return Array.from(source.templates.keys()).sort(compareCodeUnits)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The overlays the count-facet labels read, for the listing's single translation query (§7.2). */
|
|
528
|
+
export function storefrontFacetTranslationRequests(
|
|
529
|
+
source: StorefrontFacetSource,
|
|
530
|
+
raw: StorefrontRawCountFacets,
|
|
531
|
+
): Array<{ entityType: string; ids: string[] }> {
|
|
532
|
+
return [
|
|
533
|
+
{ entityType: STOREFRONT_CATEGORY_ENTITY_TYPE, ids: Array.from(raw.categories.keys()) },
|
|
534
|
+
{ entityType: STOREFRONT_TAG_ENTITY_TYPE, ids: Array.from(raw.tags.keys()) },
|
|
535
|
+
{ entityType: STOREFRONT_OPTION_SCHEMA_ENTITY_TYPE, ids: sortedTemplateIds(source) },
|
|
536
|
+
]
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export type StorefrontFacetLabelContext = {
|
|
540
|
+
locales: string[]
|
|
541
|
+
translations: Map<string, TranslationMap>
|
|
542
|
+
assortmentScope: EffectiveAssortmentScope
|
|
543
|
+
productTypeLabel: (type: string) => string
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
type OptionDefinitionRef = { templateId: string; option: CatalogProductOptionDefinition }
|
|
547
|
+
|
|
548
|
+
function optionDefinitions(source: StorefrontFacetSource): Map<string, OptionDefinitionRef> {
|
|
549
|
+
const definitions = new Map<string, OptionDefinitionRef>()
|
|
550
|
+
for (const templateId of sortedTemplateIds(source)) {
|
|
551
|
+
for (const option of source.templates.get(templateId) ?? []) {
|
|
552
|
+
if (!definitions.has(option.code)) definitions.set(option.code, { templateId, option })
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return definitions
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function compareLabels(left: { label: string; code: string }, right: { label: string; code: string }): number {
|
|
559
|
+
return left.label.localeCompare(right.label) || compareCodeUnits(left.code, right.code)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function productTypeRank(type: string): number {
|
|
563
|
+
const index = (CATALOG_PRODUCT_TYPES as readonly string[]).indexOf(type)
|
|
564
|
+
return index === -1 ? CATALOG_PRODUCT_TYPES.length : index
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Turns raw counts into the payload: categories that are active, under active ancestors and inside the buyer's assortment,
|
|
569
|
+
* with localized names (depth, then name order); tags with localized labels (count, then label);
|
|
570
|
+
* options with the template's translated option and choice labels (choices in template order;
|
|
571
|
+
* values no template defines fall back to the stored value); product types with catalog labels.
|
|
572
|
+
* Only entries with a nonzero count are listed.
|
|
573
|
+
*/
|
|
574
|
+
export function labelStorefrontCountFacets(
|
|
575
|
+
source: StorefrontFacetSource,
|
|
576
|
+
raw: StorefrontRawCountFacets,
|
|
577
|
+
context: StorefrontFacetLabelContext,
|
|
578
|
+
): StorefrontCountFacets {
|
|
579
|
+
const { locales, translations } = context
|
|
580
|
+
const categoryTranslations = translations.get(STOREFRONT_CATEGORY_ENTITY_TYPE)
|
|
581
|
+
const tagTranslations = translations.get(STOREFRONT_TAG_ENTITY_TYPE)
|
|
582
|
+
const templateTranslations = translations.get(STOREFRONT_OPTION_SCHEMA_ENTITY_TYPE)
|
|
583
|
+
|
|
584
|
+
const categories: StorefrontCountFacets['categories'] = []
|
|
585
|
+
for (const [id, count] of raw.categories) {
|
|
586
|
+
const category = source.categories.get(id)
|
|
587
|
+
if (!category || !category.isActive || count <= 0) continue
|
|
588
|
+
if (!category.ancestorIds.every((ancestorId) => source.categories.get(ancestorId)?.isActive === true)) continue
|
|
589
|
+
const lineage = { id, ancestorIds: category.ancestorIds, descendantIds: category.descendantIds }
|
|
590
|
+
if (!isCategoryInAssortment(lineage, context.assortmentScope)) continue
|
|
591
|
+
categories.push({
|
|
592
|
+
id,
|
|
593
|
+
name: localize(category.name, categoryTranslations?.get(id), 'name', locales) ?? category.name,
|
|
594
|
+
slug: category.slug,
|
|
595
|
+
depth: category.depth,
|
|
596
|
+
parentId: category.parentId,
|
|
597
|
+
count,
|
|
598
|
+
})
|
|
599
|
+
}
|
|
600
|
+
categories.sort(
|
|
601
|
+
(left, right) => left.depth - right.depth || left.name.localeCompare(right.name) || compareCodeUnits(left.id, right.id),
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
const tags: StorefrontCountFacets['tags'] = []
|
|
605
|
+
for (const [id, count] of raw.tags) {
|
|
606
|
+
const tag = source.tags.get(id)
|
|
607
|
+
if (!tag || !tag.slug || count <= 0) continue
|
|
608
|
+
tags.push({ slug: tag.slug, label: localize(tag.label, tagTranslations?.get(id), 'label', locales) ?? tag.slug, count })
|
|
609
|
+
}
|
|
610
|
+
tags.sort(
|
|
611
|
+
(left, right) => right.count - left.count || left.label.localeCompare(right.label) || compareCodeUnits(left.slug, right.slug),
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
const definitions = optionDefinitions(source)
|
|
615
|
+
const options: StorefrontCountFacets['options'] = []
|
|
616
|
+
for (const [code, counts] of raw.options) {
|
|
617
|
+
const definition = definitions.get(code)
|
|
618
|
+
const overlay = definition ? templateTranslations?.get(definition.templateId) : undefined
|
|
619
|
+
const choices = Array.isArray(definition?.option.choices) ? definition.option.choices : []
|
|
620
|
+
const values: Array<{ code: string; label: string; count: number; rank: number }> = []
|
|
621
|
+
for (const [value, count] of counts) {
|
|
622
|
+
if (count <= 0) continue
|
|
623
|
+
let rank = choices.findIndex((choice) => choice.code === value)
|
|
624
|
+
if (rank === -1) rank = choices.findIndex((choice) => choice.label === value)
|
|
625
|
+
const choice = rank === -1 ? null : choices[rank]
|
|
626
|
+
const label = choice
|
|
627
|
+
? localize(choice.label, overlay, optionChoiceLabelTranslationField(code, choice.code), locales) ?? value
|
|
628
|
+
: value
|
|
629
|
+
values.push({ code: value, label, count, rank: rank === -1 ? choices.length : rank })
|
|
630
|
+
}
|
|
631
|
+
if (!values.length) continue
|
|
632
|
+
values.sort((left, right) => left.rank - right.rank || compareLabels(left, right))
|
|
633
|
+
options.push({
|
|
634
|
+
code,
|
|
635
|
+
label: definition
|
|
636
|
+
? localize(definition.option.label, overlay, optionLabelTranslationField(code), locales) ?? code
|
|
637
|
+
: code,
|
|
638
|
+
values: values.map((value) => ({ code: value.code, label: value.label, count: value.count })),
|
|
639
|
+
})
|
|
640
|
+
}
|
|
641
|
+
options.sort(compareLabels)
|
|
642
|
+
|
|
643
|
+
const productTypes: StorefrontCountFacets['productTypes'] = []
|
|
644
|
+
for (const [type, count] of raw.productTypes) {
|
|
645
|
+
if (count <= 0) continue
|
|
646
|
+
productTypes.push({ type, label: context.productTypeLabel(type), count })
|
|
647
|
+
}
|
|
648
|
+
productTypes.sort(
|
|
649
|
+
(left, right) => productTypeRank(left.type) - productTypeRank(right.type) || compareCodeUnits(left.type, right.type),
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
return { categories, tags, options, productTypes }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Catalog product-type labels (`catalog.products.types.<type>`) in the response locale; the type code when untranslated. */
|
|
656
|
+
export async function loadStorefrontProductTypeLabeler(locale: string): Promise<(type: string) => string> {
|
|
657
|
+
const translate = createFallbackTranslator(await loadDictionary(resolveSupportedLocale(locale) ?? defaultLocale))
|
|
658
|
+
return (type) => translate(`${CATALOG_PRODUCT_TYPE_LABEL_PREFIX}${type}`, type)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** `priceRange` over the amounts the price filter compares against (each product's from-price). */
|
|
662
|
+
export function storefrontPriceRangeFacet(
|
|
663
|
+
amounts: Iterable<number | null>,
|
|
664
|
+
currencyCode: string,
|
|
665
|
+
): StorefrontFacets['priceRange'] {
|
|
666
|
+
let min: number | null = null
|
|
667
|
+
let max: number | null = null
|
|
668
|
+
for (const amount of amounts) {
|
|
669
|
+
if (amount === null || !Number.isFinite(amount)) continue
|
|
670
|
+
if (min === null || amount < min) min = amount
|
|
671
|
+
if (max === null || amount > max) max = amount
|
|
672
|
+
}
|
|
673
|
+
return min === null || max === null ? null : { min, max, currencyCode }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const AVAILABILITY_STATE_ORDER: readonly AvailabilityState[] = [
|
|
677
|
+
'in_stock',
|
|
678
|
+
'low_stock',
|
|
679
|
+
'backorder',
|
|
680
|
+
'preorder',
|
|
681
|
+
'not_tracked',
|
|
682
|
+
'out_of_stock',
|
|
683
|
+
]
|
|
684
|
+
|
|
685
|
+
function availabilityRank(state: AvailabilityState): number {
|
|
686
|
+
const index = AVAILABILITY_STATE_ORDER.indexOf(state)
|
|
687
|
+
return index === -1 ? AVAILABILITY_STATE_ORDER.length : index
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Page-scoped availability counts (D21): the states of the returned page before the availability filter. */
|
|
691
|
+
export function storefrontAvailabilityFacet(states: Iterable<AvailabilityState>): StorefrontFacets['availability'] {
|
|
692
|
+
const counts = new Map<AvailabilityState, number>()
|
|
693
|
+
for (const state of states) increment(counts, state)
|
|
694
|
+
return Array.from(counts.entries())
|
|
695
|
+
.map(([state, count]) => ({ state, count }))
|
|
696
|
+
.sort((left, right) => availabilityRank(left.state) - availabilityRank(right.state))
|
|
697
|
+
}
|