@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,273 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { raw } from "@mikro-orm/postgresql";
|
|
3
|
+
import { buildCustomFieldFiltersFromQuery } from "@open-mercato/shared/lib/crud/custom-fields";
|
|
4
|
+
import { buildScopedWhere } from "@open-mercato/shared/lib/api/crud";
|
|
5
|
+
import { parseBooleanFlag } from "@open-mercato/shared/lib/boolean";
|
|
6
|
+
import { sanitizeSearchTerm } from "@open-mercato/shared/lib/query/sanitizeSearchTerm";
|
|
7
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
8
|
+
import { warnOnEncryptedLikeFilter } from "@open-mercato/shared/lib/encryption/likeFilterWarning";
|
|
9
|
+
import { buildAccentInsensitiveContainsPatternSql } from "@open-mercato/shared/lib/db/accentInsensitiveSearch";
|
|
10
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
11
|
+
import { fieldsetCodeRegex } from "@open-mercato/core/modules/entities/data/validators";
|
|
12
|
+
import { E } from "../../../generated/entities.ids.generated.js";
|
|
13
|
+
import {
|
|
14
|
+
CatalogOffer,
|
|
15
|
+
CatalogProduct,
|
|
16
|
+
CatalogProductCategory,
|
|
17
|
+
CatalogProductCategoryAssignment,
|
|
18
|
+
CatalogProductTagAssignment
|
|
19
|
+
} from "../data/entities.js";
|
|
20
|
+
import { CATALOG_PRODUCT_TYPES } from "../data/types.js";
|
|
21
|
+
import { PRODUCT_SEARCH_COLUMNS, PRODUCT_SEARCH_EXPRESSION_SQL } from "./productSearch.js";
|
|
22
|
+
const logger = createLogger("catalog");
|
|
23
|
+
const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
|
|
24
|
+
const catalogProductFilterQuerySchema = z.object({
|
|
25
|
+
page: z.coerce.number().min(1).default(1),
|
|
26
|
+
pageSize: z.coerce.number().min(1).max(100).default(50),
|
|
27
|
+
id: z.string().uuid().optional(),
|
|
28
|
+
search: z.string().optional(),
|
|
29
|
+
status: z.string().optional(),
|
|
30
|
+
isActive: z.string().optional(),
|
|
31
|
+
configurable: z.string().optional(),
|
|
32
|
+
productType: z.enum(CATALOG_PRODUCT_TYPES).optional(),
|
|
33
|
+
channelIds: z.string().optional(),
|
|
34
|
+
channelId: z.string().uuid().optional(),
|
|
35
|
+
categoryIds: z.string().optional(),
|
|
36
|
+
tagIds: z.string().optional(),
|
|
37
|
+
offerId: z.string().uuid().optional(),
|
|
38
|
+
userId: z.string().uuid().optional(),
|
|
39
|
+
userGroupId: z.string().uuid().optional(),
|
|
40
|
+
customerId: z.string().uuid().optional(),
|
|
41
|
+
customerGroupId: z.string().uuid().optional(),
|
|
42
|
+
quantity: z.coerce.number().min(1).max(1e5).optional(),
|
|
43
|
+
quantityUnit: z.string().trim().max(50).optional(),
|
|
44
|
+
priceDate: z.string().optional(),
|
|
45
|
+
sortField: z.string().optional(),
|
|
46
|
+
sortDir: z.enum(["asc", "desc"]).optional(),
|
|
47
|
+
withDeleted: z.coerce.boolean().optional(),
|
|
48
|
+
customFieldset: z.string().regex(fieldsetCodeRegex).optional()
|
|
49
|
+
}).passthrough();
|
|
50
|
+
function parseIdList(raw2) {
|
|
51
|
+
if (!raw2) return [];
|
|
52
|
+
return raw2.split(",").map((value) => value.trim()).filter((value) => UUID_REGEX.test(value));
|
|
53
|
+
}
|
|
54
|
+
async function buildProductFilters(query, ctx, options = {}) {
|
|
55
|
+
const filters = {};
|
|
56
|
+
const em = ctx.container.resolve("em").fork();
|
|
57
|
+
const restrictedProductIds = { value: null };
|
|
58
|
+
const intersectProductIds = (ids) => {
|
|
59
|
+
const normalized = ids.filter(
|
|
60
|
+
(id) => typeof id === "string" && id.trim().length > 0
|
|
61
|
+
);
|
|
62
|
+
const current = new Set(normalized);
|
|
63
|
+
if (!current.size) {
|
|
64
|
+
restrictedProductIds.value = /* @__PURE__ */ new Set();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!restrictedProductIds.value) {
|
|
68
|
+
restrictedProductIds.value = current;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
restrictedProductIds.value = new Set(
|
|
72
|
+
Array.from(restrictedProductIds.value).filter((id) => current.has(id))
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
const applyRestrictedProducts = () => {
|
|
76
|
+
if (!restrictedProductIds.value) return;
|
|
77
|
+
if (restrictedProductIds.value.size === 0) {
|
|
78
|
+
filters.id = { $eq: "00000000-0000-0000-0000-000000000000" };
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const ids = Array.from(restrictedProductIds.value);
|
|
82
|
+
const existing = filters.id;
|
|
83
|
+
if (existing && typeof existing === "object") {
|
|
84
|
+
if ("$eq" in existing && typeof existing.$eq === "string") {
|
|
85
|
+
const target = existing.$eq;
|
|
86
|
+
if (!restrictedProductIds.value.has(target)) {
|
|
87
|
+
filters.id = { $eq: "00000000-0000-0000-0000-000000000000" };
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if ("$in" in existing && Array.isArray(existing.$in)) {
|
|
92
|
+
const subset = existing.$in.filter(
|
|
93
|
+
(id) => restrictedProductIds.value.has(id)
|
|
94
|
+
);
|
|
95
|
+
filters.id = subset.length ? { $in: subset } : { $eq: "00000000-0000-0000-0000-000000000000" };
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
filters.id = ids.length === 1 ? { $eq: ids[0] } : { $in: ids };
|
|
100
|
+
};
|
|
101
|
+
if (query.id) {
|
|
102
|
+
filters.id = { $eq: query.id };
|
|
103
|
+
}
|
|
104
|
+
if (query.status && query.status.trim()) {
|
|
105
|
+
filters.status_entry_id = { $eq: query.status.trim() };
|
|
106
|
+
}
|
|
107
|
+
const active = parseBooleanFlag(query.isActive);
|
|
108
|
+
if (active !== void 0) {
|
|
109
|
+
filters.is_active = active;
|
|
110
|
+
}
|
|
111
|
+
const configurable = parseBooleanFlag(query.configurable);
|
|
112
|
+
if (configurable !== void 0) {
|
|
113
|
+
filters.is_configurable = configurable;
|
|
114
|
+
}
|
|
115
|
+
if (query.productType) {
|
|
116
|
+
filters.product_type = { $eq: query.productType };
|
|
117
|
+
}
|
|
118
|
+
const scope = {
|
|
119
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
120
|
+
tenantId: ctx.auth?.tenantId ?? null
|
|
121
|
+
};
|
|
122
|
+
const scopeWhere = buildScopedWhere(
|
|
123
|
+
{},
|
|
124
|
+
{
|
|
125
|
+
organizationId: scope.organizationId,
|
|
126
|
+
organizationIds: ctx.organizationIds ?? void 0,
|
|
127
|
+
tenantId: scope.tenantId,
|
|
128
|
+
orgField: ctx.organizationIds === null ? null : void 0,
|
|
129
|
+
softDeleteField: null
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
const term = sanitizeSearchTerm(query.search);
|
|
133
|
+
const channelFilterIds = parseIdList(query.channelIds);
|
|
134
|
+
const categoryFilterIds = parseIdList(query.categoryIds);
|
|
135
|
+
const tagFilterIds = parseIdList(query.tagIds);
|
|
136
|
+
const customFieldset = typeof query.customFieldset === "string" && query.customFieldset.trim().length ? query.customFieldset.trim() : null;
|
|
137
|
+
const tenantId = ctx.auth?.tenantId ?? null;
|
|
138
|
+
const searchTask = async () => {
|
|
139
|
+
if (!term) return null;
|
|
140
|
+
const [searchMatches] = await Promise.all([
|
|
141
|
+
findWithDecryption(
|
|
142
|
+
em,
|
|
143
|
+
CatalogProduct,
|
|
144
|
+
{
|
|
145
|
+
...scopeWhere,
|
|
146
|
+
...query.withDeleted ? {} : { deletedAt: null },
|
|
147
|
+
[raw(PRODUCT_SEARCH_EXPRESSION_SQL)]: {
|
|
148
|
+
$ilike: raw(buildAccentInsensitiveContainsPatternSql(), [term])
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{ fields: ["id"] },
|
|
152
|
+
scope
|
|
153
|
+
),
|
|
154
|
+
warnOnEncryptedLikeFilter({
|
|
155
|
+
em,
|
|
156
|
+
entityName: CatalogProduct,
|
|
157
|
+
likeFields: [...PRODUCT_SEARCH_COLUMNS],
|
|
158
|
+
tenantId: scope.tenantId
|
|
159
|
+
})
|
|
160
|
+
]);
|
|
161
|
+
return searchMatches.map((product) => product.id).filter((id) => typeof id === "string" && id.length > 0);
|
|
162
|
+
};
|
|
163
|
+
const channelTask = async () => {
|
|
164
|
+
if (!channelFilterIds.length) return null;
|
|
165
|
+
const offerRows = await findWithDecryption(
|
|
166
|
+
em,
|
|
167
|
+
CatalogOffer,
|
|
168
|
+
{
|
|
169
|
+
channelId: { $in: channelFilterIds },
|
|
170
|
+
deletedAt: null,
|
|
171
|
+
...scopeWhere
|
|
172
|
+
},
|
|
173
|
+
{ fields: ["id", "product"] },
|
|
174
|
+
scope
|
|
175
|
+
);
|
|
176
|
+
return offerRows.map(
|
|
177
|
+
(offer) => typeof offer.product === "string" ? offer.product : offer.product?.id ?? null
|
|
178
|
+
).filter((id) => !!id);
|
|
179
|
+
};
|
|
180
|
+
const expandCategoryIds = async () => {
|
|
181
|
+
if (!options.includeCategoryDescendants) return categoryFilterIds;
|
|
182
|
+
const categories = await findWithDecryption(
|
|
183
|
+
em,
|
|
184
|
+
CatalogProductCategory,
|
|
185
|
+
{ id: { $in: categoryFilterIds }, deletedAt: null, ...scopeWhere },
|
|
186
|
+
{ fields: ["id", "descendantIds"] },
|
|
187
|
+
scope
|
|
188
|
+
);
|
|
189
|
+
const expanded = new Set(categoryFilterIds);
|
|
190
|
+
for (const category of categories) {
|
|
191
|
+
const descendants = Array.isArray(category.descendantIds) ? category.descendantIds : [];
|
|
192
|
+
for (const descendantId of descendants) {
|
|
193
|
+
if (typeof descendantId === "string" && descendantId.length > 0) {
|
|
194
|
+
expanded.add(descendantId);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return Array.from(expanded);
|
|
199
|
+
};
|
|
200
|
+
const categoryTask = async () => {
|
|
201
|
+
if (!categoryFilterIds.length) return null;
|
|
202
|
+
const categoryIds2 = await expandCategoryIds();
|
|
203
|
+
const assignments = await findWithDecryption(
|
|
204
|
+
em,
|
|
205
|
+
CatalogProductCategoryAssignment,
|
|
206
|
+
{ category: { $in: categoryIds2 }, ...scopeWhere },
|
|
207
|
+
{ fields: ["id", "product"] },
|
|
208
|
+
scope
|
|
209
|
+
);
|
|
210
|
+
return assignments.map(
|
|
211
|
+
(assignment) => typeof assignment.product === "string" ? assignment.product : assignment.product?.id ?? null
|
|
212
|
+
).filter((id) => !!id);
|
|
213
|
+
};
|
|
214
|
+
const tagTask = async () => {
|
|
215
|
+
if (!tagFilterIds.length) return null;
|
|
216
|
+
const assignments = await findWithDecryption(
|
|
217
|
+
em,
|
|
218
|
+
CatalogProductTagAssignment,
|
|
219
|
+
{ tag: { $in: tagFilterIds }, ...scopeWhere },
|
|
220
|
+
{ fields: ["id", "product"] },
|
|
221
|
+
scope
|
|
222
|
+
);
|
|
223
|
+
return assignments.map(
|
|
224
|
+
(assignment) => typeof assignment.product === "string" ? assignment.product : assignment.product?.id ?? null
|
|
225
|
+
).filter((id) => !!id);
|
|
226
|
+
};
|
|
227
|
+
const customFieldTask = async () => {
|
|
228
|
+
try {
|
|
229
|
+
const scopedEm = ctx.container.resolve("em");
|
|
230
|
+
return await buildCustomFieldFiltersFromQuery({
|
|
231
|
+
entityIds: [E.catalog.catalog_product],
|
|
232
|
+
query,
|
|
233
|
+
em: scopedEm,
|
|
234
|
+
tenantId,
|
|
235
|
+
fieldset: customFieldset ?? void 0
|
|
236
|
+
});
|
|
237
|
+
} catch (err) {
|
|
238
|
+
logger.debug("catalog.products custom field filter error", { err });
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const [searchIds, channelIds, categoryIds, tagIds, cfFilters] = await Promise.all([
|
|
243
|
+
searchTask(),
|
|
244
|
+
channelTask(),
|
|
245
|
+
categoryTask(),
|
|
246
|
+
tagTask(),
|
|
247
|
+
customFieldTask()
|
|
248
|
+
]);
|
|
249
|
+
for (const productIds of [searchIds, channelIds, categoryIds, tagIds]) {
|
|
250
|
+
if (productIds) intersectProductIds(productIds);
|
|
251
|
+
}
|
|
252
|
+
Object.assign(filters, cfFilters);
|
|
253
|
+
applyRestrictedProducts();
|
|
254
|
+
return filters;
|
|
255
|
+
}
|
|
256
|
+
function scoreProductSearchRelevance(needle, title, sku) {
|
|
257
|
+
const t = (title ?? "").toLowerCase();
|
|
258
|
+
const s = (sku ?? "").toLowerCase();
|
|
259
|
+
if (t === needle) return 0;
|
|
260
|
+
if (s === needle) return 1;
|
|
261
|
+
if (t.startsWith(needle)) return 2;
|
|
262
|
+
if (s.startsWith(needle)) return 3;
|
|
263
|
+
if (t.includes(needle)) return 4;
|
|
264
|
+
if (s.includes(needle)) return 5;
|
|
265
|
+
return 6;
|
|
266
|
+
}
|
|
267
|
+
export {
|
|
268
|
+
buildProductFilters,
|
|
269
|
+
catalogProductFilterQuerySchema,
|
|
270
|
+
parseIdList,
|
|
271
|
+
scoreProductSearchRelevance
|
|
272
|
+
};
|
|
273
|
+
//# sourceMappingURL=productFilters.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/catalog/lib/productFilters.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from \"zod\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { raw } from \"@mikro-orm/postgresql\";\nimport { buildCustomFieldFiltersFromQuery } from \"@open-mercato/shared/lib/crud/custom-fields\";\nimport type { CrudCtx } from \"@open-mercato/shared/lib/crud/factory\";\nimport { buildScopedWhere } from \"@open-mercato/shared/lib/api/crud\";\nimport { parseBooleanFlag } from \"@open-mercato/shared/lib/boolean\";\nimport { sanitizeSearchTerm } from \"@open-mercato/shared/lib/query/sanitizeSearchTerm\";\nimport { findWithDecryption } from \"@open-mercato/shared/lib/encryption/find\";\nimport { warnOnEncryptedLikeFilter } from \"@open-mercato/shared/lib/encryption/likeFilterWarning\";\nimport { buildAccentInsensitiveContainsPatternSql } from \"@open-mercato/shared/lib/db/accentInsensitiveSearch\";\nimport { createLogger } from \"@open-mercato/shared/lib/logger\";\nimport { fieldsetCodeRegex } from \"@open-mercato/core/modules/entities/data/validators\";\nimport { E } from \"#generated/entities.ids.generated\";\nimport {\n CatalogOffer,\n CatalogProduct,\n CatalogProductCategory,\n CatalogProductCategoryAssignment,\n CatalogProductTagAssignment,\n} from \"../data/entities\";\nimport { CATALOG_PRODUCT_TYPES } from \"../data/types\";\nimport { PRODUCT_SEARCH_COLUMNS, PRODUCT_SEARCH_EXPRESSION_SQL } from \"./productSearch\";\n\nconst logger = createLogger('catalog')\n\nconst UUID_REGEX =\n /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;\n\nexport const catalogProductFilterQuerySchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n id: z.string().uuid().optional(),\n search: z.string().optional(),\n status: z.string().optional(),\n isActive: z.string().optional(),\n configurable: z.string().optional(),\n productType: z.enum(CATALOG_PRODUCT_TYPES).optional(),\n channelIds: z.string().optional(),\n channelId: z.string().uuid().optional(),\n categoryIds: z.string().optional(),\n tagIds: z.string().optional(),\n offerId: z.string().uuid().optional(),\n userId: z.string().uuid().optional(),\n userGroupId: z.string().uuid().optional(),\n customerId: z.string().uuid().optional(),\n customerGroupId: z.string().uuid().optional(),\n quantity: z.coerce.number().min(1).max(100000).optional(),\n quantityUnit: z.string().trim().max(50).optional(),\n priceDate: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum([\"asc\", \"desc\"]).optional(),\n withDeleted: z.coerce.boolean().optional(),\n customFieldset: z.string().regex(fieldsetCodeRegex).optional(),\n })\n .passthrough();\n\nexport type CatalogProductFilterQuery = z.infer<typeof catalogProductFilterQuerySchema>;\n\nexport type BuildProductFiltersOptions = {\n includeCategoryDescendants?: boolean;\n};\n\nexport function parseIdList(raw?: string): string[] {\n if (!raw) return [];\n return raw\n .split(\",\")\n .map((value) => value.trim())\n .filter((value) => UUID_REGEX.test(value));\n}\n\nexport async function buildProductFilters(\n query: CatalogProductFilterQuery,\n ctx: CrudCtx,\n options: BuildProductFiltersOptions = {},\n): Promise<Record<string, unknown>> {\n const filters: Record<string, unknown> = {};\n const em = (ctx.container.resolve(\"em\") as EntityManager).fork();\n const restrictedProductIds: { value: Set<string> | null } = { value: null };\n\n const intersectProductIds = (ids: string[]) => {\n const normalized = ids.filter(\n (id): id is string => typeof id === \"string\" && id.trim().length > 0,\n );\n const current = new Set(normalized);\n if (!current.size) {\n restrictedProductIds.value = new Set();\n return;\n }\n if (!restrictedProductIds.value) {\n restrictedProductIds.value = current;\n return;\n }\n restrictedProductIds.value = new Set(\n Array.from(restrictedProductIds.value).filter((id) => current.has(id)),\n );\n };\n\n const applyRestrictedProducts = () => {\n if (!restrictedProductIds.value) return;\n if (restrictedProductIds.value.size === 0) {\n filters.id = { $eq: \"00000000-0000-0000-0000-000000000000\" };\n return;\n }\n const ids = Array.from(restrictedProductIds.value);\n const existing = filters.id as Record<string, unknown> | undefined;\n if (existing && typeof existing === \"object\") {\n if (\n \"$eq\" in existing &&\n typeof (existing as { $eq?: unknown }).$eq === \"string\"\n ) {\n const target = (existing as { $eq: string }).$eq;\n if (!restrictedProductIds.value.has(target)) {\n filters.id = { $eq: \"00000000-0000-0000-0000-000000000000\" };\n }\n return;\n }\n if (\n \"$in\" in existing &&\n Array.isArray((existing as { $in?: unknown }).$in)\n ) {\n const subset = (existing as { $in: string[] }).$in.filter((id) =>\n restrictedProductIds.value!.has(id),\n );\n filters.id = subset.length\n ? { $in: subset }\n : { $eq: \"00000000-0000-0000-0000-000000000000\" };\n return;\n }\n }\n filters.id = ids.length === 1 ? { $eq: ids[0] } : { $in: ids };\n };\n if (query.id) {\n filters.id = { $eq: query.id };\n }\n if (query.status && query.status.trim()) {\n filters.status_entry_id = { $eq: query.status.trim() };\n }\n const active = parseBooleanFlag(query.isActive);\n if (active !== undefined) {\n filters.is_active = active;\n }\n const configurable = parseBooleanFlag(query.configurable);\n if (configurable !== undefined) {\n filters.is_configurable = configurable;\n }\n if (query.productType) {\n filters.product_type = { $eq: query.productType };\n }\n const scope = {\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n tenantId: ctx.auth?.tenantId ?? null,\n };\n const scopeWhere = buildScopedWhere(\n {},\n {\n organizationId: scope.organizationId,\n organizationIds: ctx.organizationIds ?? undefined,\n tenantId: scope.tenantId,\n orgField: ctx.organizationIds === null ? null : undefined,\n softDeleteField: null,\n },\n );\n const term = sanitizeSearchTerm(query.search);\n const channelFilterIds = parseIdList(query.channelIds);\n const categoryFilterIds = parseIdList(query.categoryIds);\n const tagFilterIds = parseIdList(query.tagIds);\n const customFieldset =\n typeof query.customFieldset === \"string\" &&\n query.customFieldset.trim().length\n ? query.customFieldset.trim()\n : null;\n const tenantId = ctx.auth?.tenantId ?? null;\n\n // These prequeries are independent \u2014 each only feeds the final product-id\n // intersection, none depends on another's result \u2014 so dispatch them together\n // instead of awaiting one after another (#3179). A task returns null when its\n // filter is inactive (intersection skipped) or the matched product-id list\n // (possibly empty) when active, preserving the \"active filter with no matches\n // => empty result\" behavior.\n const searchTask = async (): Promise<string[] | null> => {\n if (!term) return null;\n // The predicate hides behind a raw() symbol key, which the filter walker in\n // findWithDecryption cannot see (Object.entries skips symbols), so the\n // encrypted-ILIKE diagnostic is raised here with the field list instead.\n // Without it, a tenant that encrypts one of these columns at rest gets an\n // empty result indistinguishable from a genuine no-match (#5051). It runs\n // alongside the query rather than before it: it is a development-only\n // diagnostic and must not add a round trip to the request path.\n const [searchMatches] = await Promise.all([\n findWithDecryption(\n em,\n CatalogProduct,\n {\n ...scopeWhere,\n ...(query.withDeleted ? {} : { deletedAt: null }),\n [raw(PRODUCT_SEARCH_EXPRESSION_SQL)]: {\n $ilike: raw(buildAccentInsensitiveContainsPatternSql(), [term]),\n },\n },\n { fields: [\"id\"] },\n scope,\n ),\n warnOnEncryptedLikeFilter({\n em,\n entityName: CatalogProduct,\n likeFields: [...PRODUCT_SEARCH_COLUMNS],\n tenantId: scope.tenantId,\n }),\n ]);\n return searchMatches\n .map((product) => product.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0);\n };\n\n const channelTask = async (): Promise<string[] | null> => {\n if (!channelFilterIds.length) return null;\n const offerRows = await findWithDecryption(\n em,\n CatalogOffer,\n {\n channelId: { $in: channelFilterIds },\n deletedAt: null,\n ...scopeWhere,\n },\n { fields: [\"id\", \"product\"] },\n scope,\n );\n return offerRows\n .map((offer) =>\n typeof offer.product === \"string\"\n ? offer.product\n : (offer.product?.id ?? null),\n )\n .filter((id): id is string => !!id);\n };\n\n const expandCategoryIds = async (): Promise<string[]> => {\n if (!options.includeCategoryDescendants) return categoryFilterIds;\n const categories = await findWithDecryption(\n em,\n CatalogProductCategory,\n { id: { $in: categoryFilterIds }, deletedAt: null, ...scopeWhere },\n { fields: [\"id\", \"descendantIds\"] },\n scope,\n );\n const expanded = new Set(categoryFilterIds);\n for (const category of categories) {\n const descendants = Array.isArray(category.descendantIds) ? category.descendantIds : [];\n for (const descendantId of descendants) {\n if (typeof descendantId === \"string\" && descendantId.length > 0) {\n expanded.add(descendantId);\n }\n }\n }\n return Array.from(expanded);\n };\n\n const categoryTask = async (): Promise<string[] | null> => {\n if (!categoryFilterIds.length) return null;\n const categoryIds = await expandCategoryIds();\n const assignments = await findWithDecryption(\n em,\n CatalogProductCategoryAssignment,\n { category: { $in: categoryIds }, ...scopeWhere },\n { fields: [\"id\", \"product\"] },\n scope,\n );\n return assignments\n .map((assignment) =>\n typeof assignment.product === \"string\"\n ? assignment.product\n : (assignment.product?.id ?? null),\n )\n .filter((id): id is string => !!id);\n };\n\n const tagTask = async (): Promise<string[] | null> => {\n if (!tagFilterIds.length) return null;\n const assignments = await findWithDecryption(\n em,\n CatalogProductTagAssignment,\n { tag: { $in: tagFilterIds }, ...scopeWhere },\n { fields: [\"id\", \"product\"] },\n scope,\n );\n return assignments\n .map((assignment) =>\n typeof assignment.product === \"string\"\n ? assignment.product\n : (assignment.product?.id ?? null),\n )\n .filter((id): id is string => !!id);\n };\n\n const customFieldTask = async (): Promise<Record<string, unknown>> => {\n try {\n const scopedEm = ctx.container.resolve(\"em\") as EntityManager;\n return await buildCustomFieldFiltersFromQuery({\n entityIds: [E.catalog.catalog_product],\n query,\n em: scopedEm,\n tenantId,\n fieldset: customFieldset ?? undefined,\n });\n } catch (err) {\n // Custom field filter parsing may fail for non-existent or misconfigured fields.\n // Fall back to base filters to avoid blocking the product listing.\n logger.debug('catalog.products custom field filter error', { err });\n return {};\n }\n };\n\n const [searchIds, channelIds, categoryIds, tagIds, cfFilters] =\n await Promise.all([\n searchTask(),\n channelTask(),\n categoryTask(),\n tagTask(),\n customFieldTask(),\n ]);\n\n // Apply intersections in the original order; intersection is commutative, so\n // the result is identical to the previous sequential pass. An empty array is\n // still intersected (active filter that matched nothing); null is skipped.\n for (const productIds of [searchIds, channelIds, categoryIds, tagIds]) {\n if (productIds) intersectProductIds(productIds);\n }\n Object.assign(filters, cfFilters);\n applyRestrictedProducts();\n return filters;\n}\n\nexport function scoreProductSearchRelevance(\n needle: string,\n title: string | null | undefined,\n sku: string | null | undefined,\n): number {\n const t = (title ?? \"\").toLowerCase();\n const s = (sku ?? \"\").toLowerCase();\n if (t === needle) return 0;\n if (s === needle) return 1;\n if (t.startsWith(needle)) return 2;\n if (s.startsWith(needle)) return 3;\n if (t.includes(needle)) return 4;\n if (s.includes(needle)) return 5;\n return 6;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["raw", "categoryIds"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
2
|
+
const PRODUCT_SCOPE_KEYS_ENTITY_TYPE = "catalog:catalog_product";
|
|
3
|
+
const PRODUCT_SCOPE_KEYS_DOC_KEY = "scope_keys";
|
|
4
|
+
const PRODUCT_SCOPE_KEYS_ENRICHER_ID = "catalog.product_scope_keys";
|
|
5
|
+
const PRODUCT_SCOPE_CATEGORY_PREFIX = "cat:";
|
|
6
|
+
const PRODUCT_SCOPE_TAG_PREFIX = "tag:";
|
|
7
|
+
function compareCodeUnits(left, right) {
|
|
8
|
+
if (left < right) return -1;
|
|
9
|
+
if (left > right) return 1;
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
function categoryScopeKey(categoryId) {
|
|
13
|
+
return `${PRODUCT_SCOPE_CATEGORY_PREFIX}${categoryId}`;
|
|
14
|
+
}
|
|
15
|
+
function tagScopeKey(tagId) {
|
|
16
|
+
return `${PRODUCT_SCOPE_TAG_PREFIX}${tagId}`;
|
|
17
|
+
}
|
|
18
|
+
function buildProductScopeKeys(input) {
|
|
19
|
+
const keys = /* @__PURE__ */ new Set();
|
|
20
|
+
for (const category of input.categories) {
|
|
21
|
+
if (category.categoryId) keys.add(categoryScopeKey(category.categoryId));
|
|
22
|
+
for (const ancestorId of category.ancestorIds) {
|
|
23
|
+
if (ancestorId) keys.add(categoryScopeKey(ancestorId));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const tagId of input.tagIds) {
|
|
27
|
+
if (tagId) keys.add(tagScopeKey(tagId));
|
|
28
|
+
}
|
|
29
|
+
return Array.from(keys).sort(compareCodeUnits);
|
|
30
|
+
}
|
|
31
|
+
function isProductScopeIndexed(doc) {
|
|
32
|
+
if (!doc) return false;
|
|
33
|
+
const keys = doc[PRODUCT_SCOPE_KEYS_DOC_KEY];
|
|
34
|
+
if (!Array.isArray(keys)) return false;
|
|
35
|
+
return keys.every((entry) => typeof entry === "string");
|
|
36
|
+
}
|
|
37
|
+
function readJsonIdList(value) {
|
|
38
|
+
let parsed = value;
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(value);
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(parsed)) return [];
|
|
47
|
+
return parsed.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
48
|
+
}
|
|
49
|
+
function scopeEquals(column, value) {
|
|
50
|
+
return value === null ? sql`${sql.ref(column)} is null` : sql`${sql.ref(column)} = ${value}`;
|
|
51
|
+
}
|
|
52
|
+
async function computeProductScopeKeys(db, productIds, scope) {
|
|
53
|
+
const ids = Array.from(new Set(productIds.filter((id) => typeof id === "string" && id.length > 0)));
|
|
54
|
+
const result = /* @__PURE__ */ new Map();
|
|
55
|
+
if (!ids.length) return result;
|
|
56
|
+
const categoryRows = await db.selectFrom("catalog_product_category_assignments as a").innerJoin("catalog_product_categories as c", "c.id", "a.category_id").select(["a.product_id as product_id", "a.category_id as category_id", "c.ancestor_ids as ancestor_ids"]).where("a.product_id", "in", ids).where("c.deleted_at", "is", null).where(scopeEquals("a.tenant_id", scope.tenantId)).where(scopeEquals("a.organization_id", scope.organizationId)).where(scopeEquals("c.tenant_id", scope.tenantId)).where(scopeEquals("c.organization_id", scope.organizationId)).execute();
|
|
57
|
+
const tagRows = await db.selectFrom("catalog_product_tag_assignments as t").select(["t.product_id as product_id", "t.tag_id as tag_id"]).where("t.product_id", "in", ids).where(scopeEquals("t.tenant_id", scope.tenantId)).where(scopeEquals("t.organization_id", scope.organizationId)).execute();
|
|
58
|
+
const inputs = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const id of ids) inputs.set(id, { categories: [], tagIds: [] });
|
|
60
|
+
for (const row of categoryRows) {
|
|
61
|
+
const entry = inputs.get(String(row.product_id));
|
|
62
|
+
if (!entry) continue;
|
|
63
|
+
entry.categories.push({ categoryId: String(row.category_id), ancestorIds: readJsonIdList(row.ancestor_ids) });
|
|
64
|
+
}
|
|
65
|
+
for (const row of tagRows) {
|
|
66
|
+
const entry = inputs.get(String(row.product_id));
|
|
67
|
+
if (!entry) continue;
|
|
68
|
+
entry.tagIds.push(String(row.tag_id));
|
|
69
|
+
}
|
|
70
|
+
for (const [id, input] of inputs) result.set(id, buildProductScopeKeys(input));
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
const catalogProductScopeKeysEnricher = {
|
|
74
|
+
id: PRODUCT_SCOPE_KEYS_ENRICHER_ID,
|
|
75
|
+
entityType: PRODUCT_SCOPE_KEYS_ENTITY_TYPE,
|
|
76
|
+
keys: [PRODUCT_SCOPE_KEYS_DOC_KEY],
|
|
77
|
+
async enrich(records, ctx) {
|
|
78
|
+
const keysByProduct = await computeProductScopeKeys(
|
|
79
|
+
ctx.db,
|
|
80
|
+
records.map((record) => record.recordId),
|
|
81
|
+
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId }
|
|
82
|
+
);
|
|
83
|
+
const out = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const [productId, keys] of keysByProduct) {
|
|
85
|
+
out.set(productId, { [PRODUCT_SCOPE_KEYS_DOC_KEY]: keys });
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
export {
|
|
91
|
+
PRODUCT_SCOPE_CATEGORY_PREFIX,
|
|
92
|
+
PRODUCT_SCOPE_KEYS_DOC_KEY,
|
|
93
|
+
PRODUCT_SCOPE_KEYS_ENRICHER_ID,
|
|
94
|
+
PRODUCT_SCOPE_KEYS_ENTITY_TYPE,
|
|
95
|
+
PRODUCT_SCOPE_TAG_PREFIX,
|
|
96
|
+
buildProductScopeKeys,
|
|
97
|
+
catalogProductScopeKeysEnricher,
|
|
98
|
+
categoryScopeKey,
|
|
99
|
+
compareCodeUnits,
|
|
100
|
+
computeProductScopeKeys,
|
|
101
|
+
isProductScopeIndexed,
|
|
102
|
+
readJsonIdList,
|
|
103
|
+
tagScopeKey
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=productScopeKeys.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/catalog/lib/productScopeKeys.ts"],
|
|
4
|
+
"sourcesContent": ["import { sql, type Kysely, type RawBuilder } from 'kysely'\nimport type {\n IndexDocEnricher,\n IndexDocEnricherContext,\n IndexDocEnricherRecord,\n IndexDocEnrichment,\n} from '@open-mercato/core/modules/query_index/lib/doc-enrichers'\n\n/**\n * Denormalized assortment keys on the `catalog:catalog_product` index document\n * (Storefront Public API \u00A73.3, decision D9).\n *\n * `scope_keys` is a sorted, de-duplicated jsonb string array:\n * - `cat:<uuid>` for every assigned, non-deleted category AND each of its `ancestor_ids`\n * (so \"a category includes its descendants\" is a plain overlap test);\n * - `tag:<uuid>` for every assigned tag.\n *\n * Ids only \u2014 never encrypted, never translated. A document whose `scope_keys` is `null`\n * (enricher failure) or absent (not reindexed since the enricher shipped) is NOT indexed:\n * consumers MUST fail closed on it, never read it as \"no assignments\" \u2014 see\n * `isProductScopeIndexed`.\n */\nexport const PRODUCT_SCOPE_KEYS_ENTITY_TYPE = 'catalog:catalog_product'\nexport const PRODUCT_SCOPE_KEYS_DOC_KEY = 'scope_keys'\nexport const PRODUCT_SCOPE_KEYS_ENRICHER_ID = 'catalog.product_scope_keys'\nexport const PRODUCT_SCOPE_CATEGORY_PREFIX = 'cat:'\nexport const PRODUCT_SCOPE_TAG_PREFIX = 'tag:'\n\nexport function compareCodeUnits(left: string, right: string): number {\n if (left < right) return -1\n if (left > right) return 1\n return 0\n}\n\nexport function categoryScopeKey(categoryId: string): string {\n return `${PRODUCT_SCOPE_CATEGORY_PREFIX}${categoryId}`\n}\n\nexport function tagScopeKey(tagId: string): string {\n return `${PRODUCT_SCOPE_TAG_PREFIX}${tagId}`\n}\n\nexport type ProductScopeKeyInput = {\n categories: ReadonlyArray<{ categoryId: string; ancestorIds: readonly string[] }>\n tagIds: readonly string[]\n}\n\nexport function buildProductScopeKeys(input: ProductScopeKeyInput): string[] {\n const keys = new Set<string>()\n for (const category of input.categories) {\n if (category.categoryId) keys.add(categoryScopeKey(category.categoryId))\n for (const ancestorId of category.ancestorIds) {\n if (ancestorId) keys.add(categoryScopeKey(ancestorId))\n }\n }\n for (const tagId of input.tagIds) {\n if (tagId) keys.add(tagScopeKey(tagId))\n }\n return Array.from(keys).sort(compareCodeUnits)\n}\n\nexport function isProductScopeIndexed(doc: Readonly<Record<string, unknown>> | null | undefined): boolean {\n if (!doc) return false\n const keys = doc[PRODUCT_SCOPE_KEYS_DOC_KEY]\n if (!Array.isArray(keys)) return false\n return keys.every((entry) => typeof entry === 'string')\n}\n\nexport function readJsonIdList(value: unknown): string[] {\n let parsed: unknown = value\n if (typeof value === 'string') {\n try {\n parsed = JSON.parse(value)\n } catch {\n return []\n }\n }\n if (!Array.isArray(parsed)) return []\n return parsed.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n}\n\ntype ScopeFilter = { tenantId: string | null; organizationId: string | null }\n\nfunction scopeEquals(column: string, value: string | null): RawBuilder<boolean> {\n return value === null\n ? sql<boolean>`${sql.ref(column)} is null`\n : sql<boolean>`${sql.ref(column)} = ${value}`\n}\n\ntype CategoryAssignmentRow = { product_id: string; category_id: string; ancestor_ids: unknown }\ntype TagAssignmentRow = { product_id: string; tag_id: string }\n\n/**\n * Computes `scope_keys` for a batch of products in two queries (category assignments joined\n * with their categories, then tag assignments). Every requested product gets an entry \u2014 a\n * product without assignments maps to `[]`, which is \"indexed, no keys\", not \"unknown\".\n */\nexport async function computeProductScopeKeys(\n db: Kysely<any>,\n productIds: readonly string[],\n scope: ScopeFilter,\n): Promise<Map<string, string[]>> {\n const ids = Array.from(new Set(productIds.filter((id) => typeof id === 'string' && id.length > 0)))\n const result = new Map<string, string[]>()\n if (!ids.length) return result\n\n const categoryRows = (await db\n .selectFrom('catalog_product_category_assignments as a')\n .innerJoin('catalog_product_categories as c', 'c.id', 'a.category_id')\n .select(['a.product_id as product_id', 'a.category_id as category_id', 'c.ancestor_ids as ancestor_ids'])\n .where('a.product_id', 'in', ids)\n .where('c.deleted_at', 'is', null)\n .where(scopeEquals('a.tenant_id', scope.tenantId))\n .where(scopeEquals('a.organization_id', scope.organizationId))\n .where(scopeEquals('c.tenant_id', scope.tenantId))\n .where(scopeEquals('c.organization_id', scope.organizationId))\n .execute()) as CategoryAssignmentRow[]\n\n const tagRows = (await db\n .selectFrom('catalog_product_tag_assignments as t')\n .select(['t.product_id as product_id', 't.tag_id as tag_id'])\n .where('t.product_id', 'in', ids)\n .where(scopeEquals('t.tenant_id', scope.tenantId))\n .where(scopeEquals('t.organization_id', scope.organizationId))\n .execute()) as TagAssignmentRow[]\n\n const inputs = new Map<string, { categories: Array<{ categoryId: string; ancestorIds: string[] }>; tagIds: string[] }>()\n for (const id of ids) inputs.set(id, { categories: [], tagIds: [] })\n for (const row of categoryRows) {\n const entry = inputs.get(String(row.product_id))\n if (!entry) continue\n entry.categories.push({ categoryId: String(row.category_id), ancestorIds: readJsonIdList(row.ancestor_ids) })\n }\n for (const row of tagRows) {\n const entry = inputs.get(String(row.product_id))\n if (!entry) continue\n entry.tagIds.push(String(row.tag_id))\n }\n for (const [id, input] of inputs) result.set(id, buildProductScopeKeys(input))\n return result\n}\n\nexport const catalogProductScopeKeysEnricher: IndexDocEnricher = {\n id: PRODUCT_SCOPE_KEYS_ENRICHER_ID,\n entityType: PRODUCT_SCOPE_KEYS_ENTITY_TYPE,\n keys: [PRODUCT_SCOPE_KEYS_DOC_KEY],\n async enrich(\n records: readonly IndexDocEnricherRecord[],\n ctx: IndexDocEnricherContext,\n ): Promise<Map<string, IndexDocEnrichment>> {\n const keysByProduct = await computeProductScopeKeys(\n ctx.db,\n records.map((record) => record.recordId),\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId },\n )\n const out = new Map<string, IndexDocEnrichment>()\n for (const [productId, keys] of keysByProduct) {\n out.set(productId, { [PRODUCT_SCOPE_KEYS_DOC_KEY]: keys })\n }\n return out\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
2
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
3
|
+
import { PRODUCT_SCOPE_KEYS_ENTITY_TYPE, readJsonIdList } from "./productScopeKeys.js";
|
|
4
|
+
const logger = createLogger("catalog").child({ component: "product-scope-reindex" });
|
|
5
|
+
const PRODUCT_SCOPE_REINDEX_UPSERT_LIMIT = 500;
|
|
6
|
+
function readNonEmptyString(value) {
|
|
7
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
8
|
+
}
|
|
9
|
+
function parseCategoryScopeTarget(payload) {
|
|
10
|
+
const categoryId = readNonEmptyString(payload?.id);
|
|
11
|
+
const tenantId = readNonEmptyString(payload?.tenantId);
|
|
12
|
+
const organizationId = readNonEmptyString(payload?.organizationId);
|
|
13
|
+
if (!categoryId || !tenantId || !organizationId) return null;
|
|
14
|
+
return {
|
|
15
|
+
categoryId,
|
|
16
|
+
tenantId,
|
|
17
|
+
organizationId,
|
|
18
|
+
extraCategoryIds: readJsonIdList(payload?.previousDescendantIds)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function collectCategorySubtreeProductIds(db, target, limit) {
|
|
22
|
+
const category = await db.selectFrom("catalog_product_categories").select(["id", "descendant_ids"]).where("id", "=", target.categoryId).where("tenant_id", "=", target.tenantId).where("organization_id", "=", target.organizationId).executeTakeFirst();
|
|
23
|
+
const categoryIds = Array.from(/* @__PURE__ */ new Set([
|
|
24
|
+
target.categoryId,
|
|
25
|
+
...readJsonIdList(category?.descendant_ids),
|
|
26
|
+
...target.extraCategoryIds
|
|
27
|
+
]));
|
|
28
|
+
const rows = await db.selectFrom("catalog_product_category_assignments as a").innerJoin("catalog_products as p", "p.id", "a.product_id").select("a.product_id as product_id").distinct().where("a.category_id", "in", categoryIds).where("a.tenant_id", "=", target.tenantId).where("a.organization_id", "=", target.organizationId).where(sql`${sql.ref("p.deleted_at")} is null`).limit(limit + 1).execute();
|
|
29
|
+
return rows.map((row) => String(row.product_id));
|
|
30
|
+
}
|
|
31
|
+
async function reindexProductsForCategorySubtree(payload, ctx, limit = PRODUCT_SCOPE_REINDEX_UPSERT_LIMIT) {
|
|
32
|
+
const target = parseCategoryScopeTarget(payload);
|
|
33
|
+
if (!target) {
|
|
34
|
+
logger.warn("Category scope change without id/tenant/organization; product scope keys not refreshed", {
|
|
35
|
+
categoryId: readNonEmptyString(payload?.id)
|
|
36
|
+
});
|
|
37
|
+
return { mode: "skipped", productCount: 0 };
|
|
38
|
+
}
|
|
39
|
+
const em = ctx.resolve("em");
|
|
40
|
+
const db = em.getKysely();
|
|
41
|
+
const bus = ctx.resolve("eventBus");
|
|
42
|
+
const productIds = await collectCategorySubtreeProductIds(db, target, limit);
|
|
43
|
+
if (!productIds.length) return { mode: "skipped", productCount: 0 };
|
|
44
|
+
if (productIds.length > limit) {
|
|
45
|
+
await bus.emitEvent(
|
|
46
|
+
"query_index.reindex",
|
|
47
|
+
{
|
|
48
|
+
entityType: PRODUCT_SCOPE_KEYS_ENTITY_TYPE,
|
|
49
|
+
tenantId: target.tenantId,
|
|
50
|
+
organizationId: target.organizationId
|
|
51
|
+
},
|
|
52
|
+
{ persistent: true, deliverInline: false }
|
|
53
|
+
);
|
|
54
|
+
return { mode: "reindex", productCount: productIds.length };
|
|
55
|
+
}
|
|
56
|
+
for (const productId of productIds) {
|
|
57
|
+
await bus.emitEvent("query_index.upsert_one", {
|
|
58
|
+
entityType: PRODUCT_SCOPE_KEYS_ENTITY_TYPE,
|
|
59
|
+
recordId: productId,
|
|
60
|
+
tenantId: target.tenantId,
|
|
61
|
+
organizationId: target.organizationId,
|
|
62
|
+
crudAction: "updated"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return { mode: "upsert", productCount: productIds.length };
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
PRODUCT_SCOPE_REINDEX_UPSERT_LIMIT,
|
|
69
|
+
collectCategorySubtreeProductIds,
|
|
70
|
+
parseCategoryScopeTarget,
|
|
71
|
+
reindexProductsForCategorySubtree
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=productScopeReindex.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/catalog/lib/productScopeReindex.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql, type Kysely } from 'kysely'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { PRODUCT_SCOPE_KEYS_ENTITY_TYPE, readJsonIdList } from './productScopeKeys'\n\nconst logger = createLogger('catalog').child({ component: 'product-scope-reindex' })\n\n/**\n * Above this many affected products a category hierarchy change requests one scoped\n * `query_index.reindex` job for `catalog:catalog_product` instead of per-record upserts.\n */\nexport const PRODUCT_SCOPE_REINDEX_UPSERT_LIMIT = 500\n\nexport type CategoryScopeChangePayload = {\n id?: unknown\n tenantId?: unknown\n organizationId?: unknown\n hierarchyChanged?: unknown\n previousDescendantIds?: unknown\n}\n\nexport type ProductScopeReindexContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype EventBusLike = {\n emitEvent: (event: string, payload: unknown, options?: Record<string, unknown>) => Promise<void>\n}\n\ntype CategoryScopeTarget = {\n categoryId: string\n tenantId: string\n organizationId: string\n extraCategoryIds: string[]\n}\n\nexport type ProductScopeReindexResult =\n | { mode: 'skipped'; productCount: 0 }\n | { mode: 'upsert'; productCount: number }\n | { mode: 'reindex'; productCount: number }\n\nfunction readNonEmptyString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nexport function parseCategoryScopeTarget(payload: CategoryScopeChangePayload | null | undefined): CategoryScopeTarget | null {\n const categoryId = readNonEmptyString(payload?.id)\n const tenantId = readNonEmptyString(payload?.tenantId)\n const organizationId = readNonEmptyString(payload?.organizationId)\n if (!categoryId || !tenantId || !organizationId) return null\n return {\n categoryId,\n tenantId,\n organizationId,\n extraCategoryIds: readJsonIdList(payload?.previousDescendantIds),\n }\n}\n\nexport async function collectCategorySubtreeProductIds(\n db: Kysely<any>,\n target: CategoryScopeTarget,\n limit: number,\n): Promise<string[]> {\n const category = (await db\n .selectFrom('catalog_product_categories')\n .select(['id', 'descendant_ids'])\n .where('id', '=', target.categoryId)\n .where('tenant_id', '=', target.tenantId)\n .where('organization_id', '=', target.organizationId)\n .executeTakeFirst()) as { id: string; descendant_ids: unknown } | undefined\n\n const categoryIds = Array.from(new Set([\n target.categoryId,\n ...readJsonIdList(category?.descendant_ids),\n ...target.extraCategoryIds,\n ]))\n\n const rows = (await db\n .selectFrom('catalog_product_category_assignments as a')\n .innerJoin('catalog_products as p', 'p.id', 'a.product_id')\n .select('a.product_id as product_id')\n .distinct()\n .where('a.category_id', 'in', categoryIds)\n .where('a.tenant_id', '=', target.tenantId)\n .where('a.organization_id', '=', target.organizationId)\n .where(sql<boolean>`${sql.ref('p.deleted_at')} is null`)\n .limit(limit + 1)\n .execute()) as Array<{ product_id: string }>\n\n return rows.map((row) => String(row.product_id))\n}\n\n/**\n * Re-projects `scope_keys` for every product assigned anywhere in a category's subtree\n * (Storefront Public API \u00A73.3, R16). Runs from persistent `catalog.category.*` subscribers,\n * i.e. in the events worker, never in the request that changed the hierarchy.\n */\nexport async function reindexProductsForCategorySubtree(\n payload: CategoryScopeChangePayload | null | undefined,\n ctx: ProductScopeReindexContext,\n limit: number = PRODUCT_SCOPE_REINDEX_UPSERT_LIMIT,\n): Promise<ProductScopeReindexResult> {\n const target = parseCategoryScopeTarget(payload)\n if (!target) {\n logger.warn('Category scope change without id/tenant/organization; product scope keys not refreshed', {\n categoryId: readNonEmptyString(payload?.id),\n })\n return { mode: 'skipped', productCount: 0 }\n }\n const em = ctx.resolve<EntityManager>('em')\n const db = em.getKysely() as Kysely<any>\n const bus = ctx.resolve<EventBusLike>('eventBus')\n const productIds = await collectCategorySubtreeProductIds(db, target, limit)\n if (!productIds.length) return { mode: 'skipped', productCount: 0 }\n\n if (productIds.length > limit) {\n await bus.emitEvent(\n 'query_index.reindex',\n {\n entityType: PRODUCT_SCOPE_KEYS_ENTITY_TYPE,\n tenantId: target.tenantId,\n organizationId: target.organizationId,\n },\n { persistent: true, deliverInline: false },\n )\n return { mode: 'reindex', productCount: productIds.length }\n }\n\n for (const productId of productIds) {\n await bus.emitEvent('query_index.upsert_one', {\n entityType: PRODUCT_SCOPE_KEYS_ENTITY_TYPE,\n recordId: productId,\n tenantId: target.tenantId,\n organizationId: target.organizationId,\n crudAction: 'updated',\n })\n }\n return { mode: 'upsert', productCount: productIds.length }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,WAAwB;AACjC,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC,sBAAsB;AAE/D,MAAM,SAAS,aAAa,SAAS,EAAE,MAAM,EAAE,WAAW,wBAAwB,CAAC;AAM5E,MAAM,qCAAqC;AA8BlD,SAAS,mBAAmB,OAA+B;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAEO,SAAS,yBAAyB,SAAoF;AAC3H,QAAM,aAAa,mBAAmB,SAAS,EAAE;AACjD,QAAM,WAAW,mBAAmB,SAAS,QAAQ;AACrD,QAAM,iBAAiB,mBAAmB,SAAS,cAAc;AACjE,MAAI,CAAC,cAAc,CAAC,YAAY,CAAC,eAAgB,QAAO;AACxD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,kBAAkB,eAAe,SAAS,qBAAqB;AAAA,EACjE;AACF;AAEA,eAAsB,iCACpB,IACA,QACA,OACmB;AACnB,QAAM,WAAY,MAAM,GACrB,WAAW,4BAA4B,EACvC,OAAO,CAAC,MAAM,gBAAgB,CAAC,EAC/B,MAAM,MAAM,KAAK,OAAO,UAAU,EAClC,MAAM,aAAa,KAAK,OAAO,QAAQ,EACvC,MAAM,mBAAmB,KAAK,OAAO,cAAc,EACnD,iBAAiB;AAEpB,QAAM,cAAc,MAAM,KAAK,oBAAI,IAAI;AAAA,IACrC,OAAO;AAAA,IACP,GAAG,eAAe,UAAU,cAAc;AAAA,IAC1C,GAAG,OAAO;AAAA,EACZ,CAAC,CAAC;AAEF,QAAM,OAAQ,MAAM,GACjB,WAAW,2CAA2C,EACtD,UAAU,yBAAyB,QAAQ,cAAc,EACzD,OAAO,4BAA4B,EACnC,SAAS,EACT,MAAM,iBAAiB,MAAM,WAAW,EACxC,MAAM,eAAe,KAAK,OAAO,QAAQ,EACzC,MAAM,qBAAqB,KAAK,OAAO,cAAc,EACrD,MAAM,MAAe,IAAI,IAAI,cAAc,CAAC,UAAU,EACtD,MAAM,QAAQ,CAAC,EACf,QAAQ;AAEX,SAAO,KAAK,IAAI,CAAC,QAAQ,OAAO,IAAI,UAAU,CAAC;AACjD;AAOA,eAAsB,kCACpB,SACA,KACA,QAAgB,oCACoB;AACpC,QAAM,SAAS,yBAAyB,OAAO;AAC/C,MAAI,CAAC,QAAQ;AACX,WAAO,KAAK,0FAA0F;AAAA,MACpG,YAAY,mBAAmB,SAAS,EAAE;AAAA,IAC5C,CAAC;AACD,WAAO,EAAE,MAAM,WAAW,cAAc,EAAE;AAAA,EAC5C;AACA,QAAM,KAAK,IAAI,QAAuB,IAAI;AAC1C,QAAM,KAAK,GAAG,UAAU;AACxB,QAAM,MAAM,IAAI,QAAsB,UAAU;AAChD,QAAM,aAAa,MAAM,iCAAiC,IAAI,QAAQ,KAAK;AAC3E,MAAI,CAAC,WAAW,OAAQ,QAAO,EAAE,MAAM,WAAW,cAAc,EAAE;AAElE,MAAI,WAAW,SAAS,OAAO;AAC7B,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,QACE,YAAY;AAAA,QACZ,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,MACzB;AAAA,MACA,EAAE,YAAY,MAAM,eAAe,MAAM;AAAA,IAC3C;AACA,WAAO,EAAE,MAAM,WAAW,cAAc,WAAW,OAAO;AAAA,EAC5D;AAEA,aAAW,aAAa,YAAY;AAClC,UAAM,IAAI,UAAU,0BAA0B;AAAA,MAC5C,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AACA,SAAO,EAAE,MAAM,UAAU,cAAc,WAAW,OAAO;AAC3D;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
const PRICE_ROW_INDEXES = [
|
|
3
|
+
{
|
|
4
|
+
name: "catalog_product_variant_prices_customer_idx",
|
|
5
|
+
definition: '("customer_id", "organization_id", "tenant_id") where "customer_id" is not null'
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
name: "catalog_product_variant_prices_customer_group_idx",
|
|
9
|
+
definition: '("customer_group_id", "organization_id", "tenant_id") where "customer_group_id" is not null'
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: "catalog_product_variant_prices_product_lookup_idx",
|
|
13
|
+
definition: '("product_id", "currency_code", "channel_id", "organization_id", "tenant_id")'
|
|
14
|
+
}
|
|
15
|
+
];
|
|
16
|
+
class Migration20261005105352_catalog extends Migration {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
this.name = "Migration20261005105352";
|
|
20
|
+
}
|
|
21
|
+
isTransactional() {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
up() {
|
|
25
|
+
for (const index of PRICE_ROW_INDEXES) {
|
|
26
|
+
this.addSql(`drop index concurrently if exists "${index.name}";`);
|
|
27
|
+
this.addSql(`create index concurrently "${index.name}" on "catalog_product_variant_prices" ${index.definition};`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
down() {
|
|
31
|
+
for (const index of PRICE_ROW_INDEXES) {
|
|
32
|
+
this.addSql(`drop index concurrently if exists "${index.name}";`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
Migration20261005105352_catalog
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=Migration20261005105352_catalog.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/catalog/migrations/Migration20261005105352_catalog.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nconst PRICE_ROW_INDEXES = [\n {\n name: 'catalog_product_variant_prices_customer_idx',\n definition: '(\"customer_id\", \"organization_id\", \"tenant_id\") where \"customer_id\" is not null',\n },\n {\n name: 'catalog_product_variant_prices_customer_group_idx',\n definition: '(\"customer_group_id\", \"organization_id\", \"tenant_id\") where \"customer_group_id\" is not null',\n },\n {\n name: 'catalog_product_variant_prices_product_lookup_idx',\n definition: '(\"product_id\", \"currency_code\", \"channel_id\", \"organization_id\", \"tenant_id\")',\n },\n] as const;\n\nexport class Migration20261005105352_catalog extends Migration {\n\n override name = 'Migration20261005105352';\n\n override isTransactional(): boolean {\n return false;\n }\n\n override up(): void | Promise<void> {\n for (const index of PRICE_ROW_INDEXES) {\n this.addSql(`drop index concurrently if exists \"${index.name}\";`);\n this.addSql(`create index concurrently \"${index.name}\" on \"catalog_product_variant_prices\" ${index.definition};`);\n }\n }\n\n override down(): void | Promise<void> {\n for (const index of PRICE_ROW_INDEXES) {\n this.addSql(`drop index concurrently if exists \"${index.name}\";`);\n }\n }\n\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAE1B,MAAM,oBAAoB;AAAA,EACxB;AAAA,IACE,MAAM;AAAA,IACN,YAAY;AAAA,EACd;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,YAAY;AAAA,EACd;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,YAAY;AAAA,EACd;AACF;AAEO,MAAM,wCAAwC,UAAU;AAAA,EAAxD;AAAA;AAEL,SAAS,OAAO;AAAA;AAAA,EAEP,kBAA2B;AAClC,WAAO;AAAA,EACT;AAAA,EAES,KAA2B;AAClC,eAAW,SAAS,mBAAmB;AACrC,WAAK,OAAO,sCAAsC,MAAM,IAAI,IAAI;AAChE,WAAK,OAAO,8BAA8B,MAAM,IAAI,yCAAyC,MAAM,UAAU,GAAG;AAAA,IAClH;AAAA,EACF;AAAA,EAES,OAA6B;AACpC,eAAW,SAAS,mBAAmB;AACrC,WAAK,OAAO,sCAAsC,MAAM,IAAI,IAAI;AAAA,IAClE;AAAA,EACF;AAEF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20261005150824_catalog extends Migration {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.name = "Migration20261005150824";
|
|
6
|
+
}
|
|
7
|
+
up() {
|
|
8
|
+
this.addSql(`create table "catalog_price_history_entries" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid not null, "organization_id" uuid not null, "price_id" uuid not null, "product_id" uuid not null, "variant_id" uuid null, "offer_id" uuid null, "channel_id" uuid null, "price_kind_id" uuid not null, "price_kind_code" text not null, "currency_code" varchar(3) not null, "unit_price_net" numeric(16,4) null, "unit_price_gross" numeric(16,4) null, "tax_rate" numeric(7,4) null, "tax_amount" numeric(16,4) null, "min_quantity" int null, "max_quantity" int null, "starts_at" timestamptz null, "ends_at" timestamptz null, "recorded_at" timestamptz not null, "change_type" text not null, "source" text not null, "is_announced" boolean null, "idempotency_key" text null, "metadata" jsonb null, primary key ("id"));`);
|
|
9
|
+
this.addSql(`create index "catalog_price_history_price_idx" on "catalog_price_history_entries" ("tenant_id", "organization_id", "price_id");`);
|
|
10
|
+
this.addSql(`create index "catalog_price_history_offer_idx" on "catalog_price_history_entries" ("tenant_id", "organization_id", "offer_id", "price_kind_id", "currency_code", "recorded_at" desc);`);
|
|
11
|
+
this.addSql(`create index "catalog_price_history_variant_channel_idx" on "catalog_price_history_entries" ("tenant_id", "organization_id", "variant_id", "channel_id", "price_kind_id", "currency_code", "recorded_at" desc);`);
|
|
12
|
+
this.addSql(`create index "catalog_price_history_variant_idx" on "catalog_price_history_entries" ("tenant_id", "organization_id", "variant_id", "price_kind_id", "currency_code", "recorded_at" desc);`);
|
|
13
|
+
this.addSql(`create index "catalog_price_history_product_channel_idx" on "catalog_price_history_entries" ("tenant_id", "organization_id", "product_id", "channel_id", "price_kind_id", "currency_code", "recorded_at" desc);`);
|
|
14
|
+
this.addSql(`create index "catalog_price_history_product_idx" on "catalog_price_history_entries" ("tenant_id", "organization_id", "product_id", "price_kind_id", "currency_code", "recorded_at" desc);`);
|
|
15
|
+
this.addSql(`alter table "catalog_price_history_entries" add constraint "catalog_price_history_entries_change_type_check" check ("change_type" in ('create', 'update', 'delete', 'undo'));`);
|
|
16
|
+
this.addSql(`alter table "catalog_price_history_entries" add constraint "catalog_price_history_entries_source_check" check ("source" in ('api', 'system'));`);
|
|
17
|
+
this.addSql(`create unique index "catalog_price_history_idempotency_uq" on "catalog_price_history_entries" ("tenant_id", "organization_id", "idempotency_key") where "idempotency_key" is not null;`);
|
|
18
|
+
this.addSql(`-- MANUAL DDL: immutability trigger and role restriction
|
|
19
|
+
CREATE OR REPLACE FUNCTION catalog_price_history_prevent_modification() RETURNS trigger AS $$
|
|
20
|
+
BEGIN
|
|
21
|
+
RAISE EXCEPTION 'catalog_price_history_entries is immutable';
|
|
22
|
+
END;
|
|
23
|
+
$$ LANGUAGE plpgsql;`);
|
|
24
|
+
this.addSql(`-- DEPLOY RUNBOOK (production): grant the app role INSERT/SELECT only.
|
|
25
|
+
-- REVOKE UPDATE, DELETE ON catalog_price_history_entries FROM <app_db_role>;
|
|
26
|
+
CREATE OR REPLACE TRIGGER history_immutable
|
|
27
|
+
BEFORE UPDATE OR DELETE ON catalog_price_history_entries
|
|
28
|
+
FOR EACH ROW EXECUTE FUNCTION catalog_price_history_prevent_modification();`);
|
|
29
|
+
}
|
|
30
|
+
down() {
|
|
31
|
+
this.addSql(`DROP TRIGGER IF EXISTS history_immutable ON catalog_price_history_entries;`);
|
|
32
|
+
this.addSql(`DROP FUNCTION IF EXISTS catalog_price_history_prevent_modification();`);
|
|
33
|
+
this.addSql(`drop table if exists "catalog_price_history_entries" cascade;`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
Migration20261005150824_catalog
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=Migration20261005150824_catalog.js.map
|