@open-mercato/core 0.10.1-develop.7954.1.48da4a7d13 → 0.10.1-develop.7972.1.03f813eaaf
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 +1 -1
- package/dist/modules/auth/api/logout.js +3 -3
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +42 -31
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/communication_channels/setup.js +49 -45
- package/dist/modules/communication_channels/setup.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +55 -29
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +8 -3
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +3 -3
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +19 -7
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +5 -5
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/directory/subscribers/seedOrganizationDefaults.js +67 -0
- package/dist/modules/directory/subscribers/seedOrganizationDefaults.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/categories/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/categories/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/context/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/context/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/products/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/products/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/search/suggest/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/search/suggest/route.js.map +2 -2
- package/dist/modules/ecommerce/lib/cacheInvalidation.js +3 -0
- package/dist/modules/ecommerce/lib/cacheInvalidation.js.map +2 -2
- package/dist/modules/ecommerce/lib/cacheKeys.js +4 -0
- package/dist/modules/ecommerce/lib/cacheKeys.js.map +2 -2
- package/dist/modules/ecommerce/lib/storeContext.js +36 -12
- package/dist/modules/ecommerce/lib/storeContext.js.map +2 -2
- package/dist/modules/ecommerce/subscribers/sales-channel-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/sales-channel-cache-invalidation.js.map +7 -0
- package/dist/modules/integrations/setup.js +34 -24
- package/dist/modules/integrations/setup.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -1
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js +3 -3
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/body-panel.js +2 -2
- package/dist/modules/messages/components/message-detail/panels/body-panel.js.map +2 -2
- package/dist/modules/payment_gateways/setup.js +3 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +30 -11
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +3 -3
- package/dist/modules/warranty_claims/setup.js +34 -30
- package/dist/modules/warranty_claims/setup.js.map +2 -2
- package/dist/modules/workflows/setup.js +7 -0
- package/dist/modules/workflows/setup.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/logout.ts +3 -3
- package/src/modules/auth/commands/roles.ts +43 -32
- package/src/modules/communication_channels/setup.ts +59 -53
- package/src/modules/customer_accounts/services/domainMappingService.ts +57 -29
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +8 -3
- package/src/modules/customers/components/detail/ActivityCard.tsx +3 -3
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +20 -8
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +7 -7
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/directory/subscribers/seedOrganizationDefaults.ts +84 -0
- package/src/modules/ecommerce/api/storefront/categories/[slug]/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/categories/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/context/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/products/[idOrHandle]/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/products/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/search/suggest/route.ts +1 -1
- package/src/modules/ecommerce/lib/cacheInvalidation.ts +4 -0
- package/src/modules/ecommerce/lib/cacheKeys.ts +4 -0
- package/src/modules/ecommerce/lib/storeContext.ts +56 -10
- package/src/modules/ecommerce/subscribers/sales-channel-cache-invalidation.ts +12 -0
- package/src/modules/integrations/setup.ts +41 -24
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +1 -1
- package/src/modules/messages/components/message-detail/panels/MainMessageHeader.tsx +3 -3
- package/src/modules/messages/components/message-detail/panels/body-panel.tsx +2 -2
- package/src/modules/payment_gateways/setup.ts +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +55 -28
- package/src/modules/warranty_claims/setup.ts +40 -30
- package/src/modules/workflows/setup.ts +8 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ecommerceDomainMappingTag,
|
|
15
15
|
ecommerceDomainTag,
|
|
16
16
|
ecommerceResolutionCache,
|
|
17
|
+
ecommerceSalesChannelTag,
|
|
17
18
|
ecommerceStoreTag
|
|
18
19
|
} from "./cacheKeys.js";
|
|
19
20
|
import { resolveStoreLocale } from "./storeLocale.js";
|
|
@@ -50,7 +51,8 @@ const STORE_COLUMNS = [
|
|
|
50
51
|
"c.price_kind_id as channel_price_kind_id",
|
|
51
52
|
"c.assortment_scope as channel_assortment_scope",
|
|
52
53
|
"c.price_sort_fallback as channel_price_sort_fallback",
|
|
53
|
-
"c.require_authentication as channel_require_authentication"
|
|
54
|
+
"c.require_authentication as channel_require_authentication",
|
|
55
|
+
"sc.id as channel_live_sales_channel_id"
|
|
54
56
|
];
|
|
55
57
|
const FORCE_HOST_HEADER = "x-force-host";
|
|
56
58
|
const FORCE_HOST_SECRET_HEADER = "x-force-host-secret";
|
|
@@ -184,7 +186,8 @@ function toCandidate(row) {
|
|
|
184
186
|
defaultCurrencyCode: row.store_default_currency_code,
|
|
185
187
|
settings: parseSettings(row.store_settings, row.store_id)
|
|
186
188
|
};
|
|
187
|
-
const
|
|
189
|
+
const salesChannelMissing = !!row.channel_binding_id && !!row.channel_sales_channel_id && !row.channel_live_sales_channel_id;
|
|
190
|
+
const channel = row.channel_binding_id && row.channel_sales_channel_id && !salesChannelMissing ? {
|
|
188
191
|
channelBindingId: row.channel_binding_id,
|
|
189
192
|
salesChannelId: row.channel_sales_channel_id,
|
|
190
193
|
priceKindId: row.channel_price_kind_id ?? null,
|
|
@@ -193,7 +196,7 @@ function toCandidate(row) {
|
|
|
193
196
|
requireAuthentication: row.channel_require_authentication === true
|
|
194
197
|
} : null;
|
|
195
198
|
const binding = row.binding_id ? { id: row.binding_id, pathPrefix: row.binding_path_prefix ?? null, isPrimary: row.binding_is_primary === true } : null;
|
|
196
|
-
return { store, channel, binding };
|
|
199
|
+
return { store, channel, binding, missingSalesChannelId: salesChannelMissing ? row.channel_sales_channel_id : null };
|
|
197
200
|
}
|
|
198
201
|
async function queryDomainCandidates(em, mapping) {
|
|
199
202
|
const db = em.getKysely();
|
|
@@ -203,6 +206,9 @@ async function queryDomainCandidates(em, mapping) {
|
|
|
203
206
|
).leftJoin(
|
|
204
207
|
"ecommerce_store_channel_bindings as c",
|
|
205
208
|
(join) => join.onRef("c.store_id", "=", "s.id").onRef("c.tenant_id", "=", "s.tenant_id").onRef("c.organization_id", "=", "s.organization_id").on("c.is_default", "=", true).on("c.deleted_at", "is", null)
|
|
209
|
+
).leftJoin(
|
|
210
|
+
"sales_channels as sc",
|
|
211
|
+
(join) => join.onRef("sc.id", "=", "c.sales_channel_id").onRef("sc.tenant_id", "=", "c.tenant_id").onRef("sc.organization_id", "=", "c.organization_id").on("sc.deleted_at", "is", null)
|
|
206
212
|
).select([
|
|
207
213
|
...STORE_COLUMNS,
|
|
208
214
|
"b.id as binding_id",
|
|
@@ -215,10 +221,16 @@ async function querySlugCandidates(em, slug, tenantId) {
|
|
|
215
221
|
let query = db.selectFrom("ecommerce_stores as s").leftJoin(
|
|
216
222
|
"ecommerce_store_channel_bindings as c",
|
|
217
223
|
(join) => join.onRef("c.store_id", "=", "s.id").onRef("c.tenant_id", "=", "s.tenant_id").onRef("c.organization_id", "=", "s.organization_id").on("c.is_default", "=", true).on("c.deleted_at", "is", null)
|
|
224
|
+
).leftJoin(
|
|
225
|
+
"sales_channels as sc",
|
|
226
|
+
(join) => join.onRef("sc.id", "=", "c.sales_channel_id").onRef("sc.tenant_id", "=", "c.tenant_id").onRef("sc.organization_id", "=", "c.organization_id").on("sc.deleted_at", "is", null)
|
|
218
227
|
).select([...STORE_COLUMNS]).where("s.slug", "=", slug).where("s.deleted_at", "is", null);
|
|
219
228
|
if (tenantId) query = query.where("s.tenant_id", "=", tenantId);
|
|
220
229
|
return query.limit(2).execute();
|
|
221
230
|
}
|
|
231
|
+
function salesChannelTags(rows) {
|
|
232
|
+
return rows.flatMap((row) => row.channel_sales_channel_id ? [ecommerceSalesChannelTag(row.channel_sales_channel_id)] : []);
|
|
233
|
+
}
|
|
222
234
|
async function loadDomainCandidates(container, mapping) {
|
|
223
235
|
const cache = ecommerceResolutionCache(container);
|
|
224
236
|
const key = ["domain-mapping", mapping.domainMappingId];
|
|
@@ -229,7 +241,8 @@ async function loadDomainCandidates(container, mapping) {
|
|
|
229
241
|
const tags = [
|
|
230
242
|
ecommerceDomainMappingTag(mapping.domainMappingId),
|
|
231
243
|
ecommerceDomainTag(mapping.hostname),
|
|
232
|
-
...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id))
|
|
244
|
+
...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)),
|
|
245
|
+
...salesChannelTags(rows)
|
|
233
246
|
];
|
|
234
247
|
await cache.set(
|
|
235
248
|
key,
|
|
@@ -251,23 +264,34 @@ async function loadSlugCandidates(container, slug, tenantId) {
|
|
|
251
264
|
{ candidates },
|
|
252
265
|
{
|
|
253
266
|
ttlMs: STORE_RESOLUTION_TTL_MS,
|
|
254
|
-
tags:
|
|
267
|
+
tags: Array.from(
|
|
268
|
+
/* @__PURE__ */ new Set([...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)), ...salesChannelTags(rows)])
|
|
269
|
+
)
|
|
255
270
|
}
|
|
256
271
|
);
|
|
257
272
|
}
|
|
258
273
|
return candidates;
|
|
259
274
|
}
|
|
260
|
-
async function reportMisconfiguredStore(container, store) {
|
|
275
|
+
async function reportMisconfiguredStore(container, store, missingSalesChannelId) {
|
|
261
276
|
const cache = ecommerceResolutionCache(container);
|
|
262
277
|
const key = ["misconfigured", "channel-binding-missing", store.id];
|
|
263
278
|
const alreadyReported = await cache.get(key);
|
|
264
279
|
if (alreadyReported) return;
|
|
265
280
|
await cache.set(key, { reportedAt: (/* @__PURE__ */ new Date()).toISOString() }, { ttlMs: STORE_MISCONFIGURED_THROTTLE_MS });
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
if (missingSalesChannelId) {
|
|
282
|
+
logger.warn("Store default channel binding references a sales channel that no longer exists; answering 503", {
|
|
283
|
+
storeId: store.id,
|
|
284
|
+
tenantId: store.tenantId,
|
|
285
|
+
organizationId: store.organizationId,
|
|
286
|
+
salesChannelId: missingSalesChannelId
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
logger.warn("Store has no default channel binding; answering 503", {
|
|
290
|
+
storeId: store.id,
|
|
291
|
+
tenantId: store.tenantId,
|
|
292
|
+
organizationId: store.organizationId
|
|
293
|
+
});
|
|
294
|
+
}
|
|
271
295
|
try {
|
|
272
296
|
await emitEcommerceEvent(
|
|
273
297
|
"ecommerce.store.misconfigured",
|
|
@@ -297,7 +321,7 @@ async function finalizeCandidate(container, candidate, source, domain, localeCan
|
|
|
297
321
|
}
|
|
298
322
|
if (store.status === "archived") throw new StorefrontResolutionError(410, "store_archived");
|
|
299
323
|
if (!candidate.channel) {
|
|
300
|
-
await reportMisconfiguredStore(container, store);
|
|
324
|
+
await reportMisconfiguredStore(container, store, candidate.missingSalesChannelId ?? null);
|
|
301
325
|
throw new StorefrontResolutionError(503, "store_misconfigured");
|
|
302
326
|
}
|
|
303
327
|
const locale = resolveStoreLocale(store, localeCandidates);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ecommerce/lib/storeContext.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AssortmentScope } from '@open-mercato/shared/lib/catalog-visibility'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'\nimport {\n ECOMMERCE_STORE_SLUG_PATTERN,\n ecommercePriceSortFallbackSchema,\n ecommerceStoreSettingsSchema,\n ecommerceStoreStatusSchema,\n type EcommercePriceSortFallback,\n type EcommerceStoreSettings,\n type EcommerceStoreStatus,\n} from '../data/validators'\nimport { emitEcommerceEvent } from '../events'\nimport {\n ecommerceDomainMappingTag,\n ecommerceDomainTag,\n ecommerceResolutionCache,\n ecommerceStoreTag,\n type CacheContainer,\n} from './cacheKeys'\nimport { resolveStoreLocale, type StoreLocaleCandidates } from './storeLocale'\nimport type { StoreContextChannel, StoreContextStore } from './types'\n\n/**\n * Store layer of storefront resolution (SPEC-029 \u00A74.1 steps 1\u20135 and 7, \u00A76.2, \u00A78, \u00A78.1).\n *\n * Host flow: normalized Host \u2192 `customer_accounts` `domainMappingService.resolveByHostname`\n * (returns `active` mappings only, D1) \u2192 ONE ecommerce query returning every binding of that\n * mapping joined with its store and the store's default channel binding (D6) \u2192 longest\n * `path_prefix` match on a path-segment boundary (R6). The candidate list is cached per mapping.\n *\n * Dev slug flow (D18): `?storeSlug=` only when `OM_ECOMMERCE_DEV_STORE_SLUG` is true, otherwise\n * `400`. A slug is unique within a tenant (\u00A75.1), so slug resolution is tenant-scoped when the\n * caller supplies a tenant and otherwise requires the slug to be unique across tenants \u2014 an\n * ambiguous slug answers `404`.\n *\n * The buyer layer (Step 2.7) composes `ResolvedStore` into a full `StoreContext`.\n */\n\nexport const DEV_STORE_SLUG_ENV = 'OM_ECOMMERCE_DEV_STORE_SLUG'\nexport const STORE_RESOLUTION_TTL_MS = 300_000\nexport const STORE_MISCONFIGURED_THROTTLE_MS = 3_600_000\n\nexport type StorefrontResolutionErrorCode =\n | 'store_slug_not_allowed'\n | 'store_not_found'\n | 'store_draft'\n | 'store_archived'\n | 'store_misconfigured'\n | 'portal_session_scope_mismatch'\n | 'portal_session_invalid'\n\nexport type StorefrontResolutionStatus = 400 | 401 | 403 | 404 | 410 | 503\n\nconst STOREFRONT_RESOLUTION_ERROR_MARKER = Symbol.for('@open-mercato/ecommerce/StorefrontResolutionError')\n\nexport class StorefrontResolutionError extends Error {\n readonly [STOREFRONT_RESOLUTION_ERROR_MARKER] = true\n readonly status: StorefrontResolutionStatus\n readonly code: StorefrontResolutionErrorCode\n\n constructor(status: StorefrontResolutionStatus, code: StorefrontResolutionErrorCode) {\n super(`[internal] storefront store resolution failed: ${code}`)\n this.name = 'StorefrontResolutionError'\n this.status = status\n this.code = code\n }\n}\n\n/**\n * Marker-based so it holds across bundle layers: `storeContextService` is resolved from the DI\n * container, whose registrars may have been loaded from another bundle layer than the route, so\n * the error class it throws is not always the route's `StorefrontResolutionError`.\n */\nexport function isStorefrontResolutionError(error: unknown): error is StorefrontResolutionError {\n return (\n !!error &&\n typeof error === 'object' &&\n (error as Record<symbol, unknown>)[STOREFRONT_RESOLUTION_ERROR_MARKER] === true\n )\n}\n\nexport type ResolvedStoreChannel = StoreContextChannel & {\n assortmentScope: AssortmentScope | null\n requireAuthentication: boolean\n}\n\nexport type ResolvedStoreDomain = {\n domainMappingId: string\n hostname: string\n bindingId: string\n pathPrefix: string | null\n isPrimary: boolean\n}\n\nexport type ResolvedStore = {\n source: 'host' | 'slug'\n store: StoreContextStore\n tenantId: string\n organizationId: string\n channel: ResolvedStoreChannel\n domain: ResolvedStoreDomain | null\n effectiveLocale: string\n requestedLocale: string | null\n currencyCode: string\n}\n\nexport type StoreContextContainer = CacheContainer\n\nexport type ResolveStoreFromRequestOptions = {\n pathname?: string | null\n}\n\nexport type ResolveStoreBySlugOptions = {\n tenantId?: string | null\n locale?: string | null\n headerLocale?: string | null\n acceptLanguage?: string | null\n}\n\ntype DomainMappingResolution = {\n domainMappingId: string\n hostname: string\n tenantId: string\n organizationId: string\n status: string\n}\n\ntype DomainMappingResolver = {\n resolveByHostname(hostname: string): Promise<DomainMappingResolution | null>\n}\n\ntype CandidateStore = Omit<StoreContextStore, 'status'> & {\n status: EcommerceStoreStatus\n tenantId: string\n organizationId: string\n}\n\ntype CandidateBinding = { id: string; pathPrefix: string | null; isPrimary: boolean }\n\ntype StoreCandidate = {\n store: CandidateStore\n channel: ResolvedStoreChannel | null\n binding: CandidateBinding | null\n}\n\ntype CachedStoreCandidates = { candidates: StoreCandidate[] }\n\ntype StoreResolutionDatabase = {\n ecommerce_stores: {\n id: string\n tenant_id: string\n organization_id: string\n code: string\n name: string\n slug: string\n status: string\n default_locale: string\n supported_locales: unknown\n default_currency_code: string\n settings: unknown\n deleted_at: Date | null\n }\n ecommerce_store_domain_bindings: {\n id: string\n tenant_id: string\n organization_id: string\n store_id: string\n domain_mapping_id: string\n path_prefix: string | null\n is_primary: boolean\n deleted_at: Date | null\n }\n ecommerce_store_channel_bindings: {\n id: string\n tenant_id: string\n organization_id: string\n store_id: string\n sales_channel_id: string\n price_kind_id: string | null\n assortment_scope: unknown\n price_sort_fallback: string\n is_default: boolean\n require_authentication: boolean\n deleted_at: Date | null\n }\n}\n\ntype StoreResolutionRow = {\n store_id: string\n store_tenant_id: string\n store_organization_id: string\n store_code: string\n store_name: string\n store_slug: string\n store_status: string\n store_default_locale: string\n store_supported_locales: unknown\n store_default_currency_code: string\n store_settings: unknown\n channel_binding_id: string | null\n channel_sales_channel_id: string | null\n channel_price_kind_id: string | null\n channel_assortment_scope: unknown\n channel_price_sort_fallback: string | null\n channel_require_authentication?: boolean | null\n binding_id?: string | null\n binding_path_prefix?: string | null\n binding_is_primary?: boolean | null\n}\n\nconst STORE_COLUMNS = [\n 's.id as store_id',\n 's.tenant_id as store_tenant_id',\n 's.organization_id as store_organization_id',\n 's.code as store_code',\n 's.name as store_name',\n 's.slug as store_slug',\n 's.status as store_status',\n 's.default_locale as store_default_locale',\n 's.supported_locales as store_supported_locales',\n 's.default_currency_code as store_default_currency_code',\n 's.settings as store_settings',\n 'c.id as channel_binding_id',\n 'c.sales_channel_id as channel_sales_channel_id',\n 'c.price_kind_id as channel_price_kind_id',\n 'c.assortment_scope as channel_assortment_scope',\n 'c.price_sort_fallback as channel_price_sort_fallback',\n 'c.require_authentication as channel_require_authentication',\n] as const\n\nconst FORCE_HOST_HEADER = 'x-force-host'\nconst FORCE_HOST_SECRET_HEADER = 'x-force-host-secret'\nconst SLUG_MAX_LENGTH = 120\n\nconst logger = createLogger('ecommerce').child({ component: 'store-context' })\n\ntype NotFoundReason =\n | 'host_missing'\n | 'domain_routing_unavailable'\n | 'domain_mapping_not_active'\n | 'domain_binding_not_found'\n | 'store_draft_hidden'\n | 'slug_invalid'\n | 'slug_not_found'\n | 'slug_ambiguous'\n\nconst WARN_NOT_FOUND_REASONS: ReadonlySet<NotFoundReason> = new Set([\n 'domain_routing_unavailable',\n 'domain_binding_not_found',\n 'slug_ambiguous',\n])\n\nfunction storeNotFound(reason: NotFoundReason, details: Record<string, unknown>): StorefrontResolutionError {\n const meta = { reason, ...details }\n if (WARN_NOT_FOUND_REASONS.has(reason)) {\n logger.warn('Storefront store not resolved', meta)\n } else {\n logger.debug('Storefront store not resolved', meta)\n }\n return new StorefrontResolutionError(404, 'store_not_found')\n}\n\nfunction tryResolve<T>(container: StoreContextContainer, name: string): T | null {\n try {\n const resolved = container.resolve(name) as T | null | undefined\n return resolved ?? null\n } catch {\n return null\n }\n}\n\nfunction resolveEntityManager(container: StoreContextContainer): EntityManager {\n const em = tryResolve<EntityManager>(container, 'em')\n if (!em) throw new Error('[internal] ecommerce store resolution requires the DI entity manager')\n return em\n}\n\nexport function isDevStoreSlugEnabled(): boolean {\n return parseBooleanWithDefault(process.env[DEV_STORE_SLUG_ENV], false)\n}\n\nexport function normalizeRequestHost(raw: string | null | undefined): string | null {\n if (typeof raw !== 'string') return null\n const first = raw.split(',')[0]?.trim().toLowerCase() ?? ''\n if (!first.length) return null\n const withoutPort = first.startsWith('[')\n ? first.replace(/^(\\[[^\\]]*\\])(?::\\d+)?$/, '$1')\n : first.replace(/:\\d+$/, '')\n const withoutTrailingDot = withoutPort.replace(/\\.+$/, '')\n return withoutTrailingDot.length ? withoutTrailingDot : null\n}\n\nexport function normalizeRequestPathname(pathname: string | null | undefined): string {\n if (typeof pathname !== 'string' || !pathname.length) return '/'\n const lowered = pathname.toLowerCase()\n const withLeadingSlash = lowered.startsWith('/') ? lowered : `/${lowered}`\n const withoutTrailingSlash = withLeadingSlash.replace(/\\/+$/, '')\n return withoutTrailingSlash.length ? withoutTrailingSlash : '/'\n}\n\nexport function pathPrefixMatches(pathPrefix: string | null, pathname: string): boolean {\n if (!pathPrefix) return true\n const normalizedPrefix = normalizeRequestPathname(pathPrefix)\n if (normalizedPrefix === '/') return true\n return pathname === normalizedPrefix || pathname.startsWith(`${normalizedPrefix}/`)\n}\n\nexport function selectLongestPrefixMatch<T extends { binding: { pathPrefix: string | null } | null }>(\n candidates: T[],\n pathname: string,\n): T | null {\n let best: T | null = null\n let bestLength = -1\n for (const candidate of candidates) {\n const pathPrefix = candidate.binding?.pathPrefix ?? null\n if (!pathPrefixMatches(pathPrefix, pathname)) continue\n const length = pathPrefix ? normalizeRequestPathname(pathPrefix).length : 0\n if (length > bestLength) {\n best = candidate\n bestLength = length\n }\n }\n return best\n}\n\nfunction readForcedHost(request: Request): string | null {\n if (process.env.NODE_ENV !== 'test') return null\n const expected = process.env.FORCE_HOST_SECRET\n if (!expected) return null\n if (!secretEqual(request.headers.get(FORCE_HOST_SECRET_HEADER), expected)) return null\n const forced = request.headers.get(FORCE_HOST_HEADER)\n return forced && forced.length > 0 ? forced : null\n}\n\nfunction readLocaleCandidates(request: Request, url: URL): StoreLocaleCandidates {\n return {\n queryLocale: url.searchParams.get('locale'),\n headerLocale: request.headers.get('x-locale'),\n acceptLanguage: request.headers.get('accept-language'),\n }\n}\n\nfunction parseJsonColumn(value: unknown): unknown {\n if (typeof value !== 'string') return value\n try {\n return JSON.parse(value)\n } catch {\n return null\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction parseSettings(raw: unknown, storeId: string): EcommerceStoreSettings {\n const parsed = ecommerceStoreSettingsSchema.safeParse(parseJsonColumn(raw) ?? {})\n if (parsed.success) return parsed.data\n logger.warn('Store settings failed validation; serving defaults', { storeId })\n return ecommerceStoreSettingsSchema.parse({})\n}\n\nfunction parseSupportedLocales(raw: unknown, defaultLocale: string): string[] {\n const value = parseJsonColumn(raw)\n const locales = Array.isArray(value)\n ? value.filter((locale): locale is string => typeof locale === 'string' && locale.length > 0)\n : []\n return locales.includes(defaultLocale) ? locales : [defaultLocale, ...locales]\n}\n\nfunction parseStatus(raw: string): EcommerceStoreStatus {\n const parsed = ecommerceStoreStatusSchema.safeParse(raw)\n return parsed.success ? parsed.data : 'draft'\n}\n\nfunction parsePriceSortFallback(raw: string | null): EcommercePriceSortFallback {\n const parsed = ecommercePriceSortFallbackSchema.safeParse(raw)\n return parsed.success ? parsed.data : 'approximate'\n}\n\nfunction parseAssortmentScope(raw: unknown): AssortmentScope | null {\n const value = parseJsonColumn(raw)\n return isRecord(value) ? (value as AssortmentScope) : null\n}\n\nfunction toCandidate(row: StoreResolutionRow): StoreCandidate {\n const store: CandidateStore = {\n id: row.store_id,\n tenantId: row.store_tenant_id,\n organizationId: row.store_organization_id,\n code: row.store_code,\n name: row.store_name,\n slug: row.store_slug,\n status: parseStatus(row.store_status),\n defaultLocale: row.store_default_locale,\n supportedLocales: parseSupportedLocales(row.store_supported_locales, row.store_default_locale),\n defaultCurrencyCode: row.store_default_currency_code,\n settings: parseSettings(row.store_settings, row.store_id),\n }\n const channel: ResolvedStoreChannel | null =\n row.channel_binding_id && row.channel_sales_channel_id\n ? {\n channelBindingId: row.channel_binding_id,\n salesChannelId: row.channel_sales_channel_id,\n priceKindId: row.channel_price_kind_id ?? null,\n priceSortFallback: parsePriceSortFallback(row.channel_price_sort_fallback),\n assortmentScope: parseAssortmentScope(row.channel_assortment_scope),\n requireAuthentication: row.channel_require_authentication === true,\n }\n : null\n const binding: CandidateBinding | null = row.binding_id\n ? { id: row.binding_id, pathPrefix: row.binding_path_prefix ?? null, isPrimary: row.binding_is_primary === true }\n : null\n return { store, channel, binding }\n}\n\nasync function queryDomainCandidates(\n em: EntityManager,\n mapping: DomainMappingResolution,\n): Promise<StoreResolutionRow[]> {\n const db = em.getKysely<StoreResolutionDatabase>()\n return db\n .selectFrom('ecommerce_store_domain_bindings as b')\n .innerJoin('ecommerce_stores as s', (join) =>\n join\n .onRef('s.id', '=', 'b.store_id')\n .onRef('s.tenant_id', '=', 'b.tenant_id')\n .onRef('s.organization_id', '=', 'b.organization_id')\n .on('s.deleted_at', 'is', null),\n )\n .leftJoin('ecommerce_store_channel_bindings as c', (join) =>\n join\n .onRef('c.store_id', '=', 's.id')\n .onRef('c.tenant_id', '=', 's.tenant_id')\n .onRef('c.organization_id', '=', 's.organization_id')\n .on('c.is_default', '=', true)\n .on('c.deleted_at', 'is', null),\n )\n .select([\n ...STORE_COLUMNS,\n 'b.id as binding_id',\n 'b.path_prefix as binding_path_prefix',\n 'b.is_primary as binding_is_primary',\n ])\n .where('b.domain_mapping_id', '=', mapping.domainMappingId)\n .where('b.tenant_id', '=', mapping.tenantId)\n .where('b.organization_id', '=', mapping.organizationId)\n .where('b.deleted_at', 'is', null)\n .execute()\n}\n\nasync function querySlugCandidates(\n em: EntityManager,\n slug: string,\n tenantId: string | null,\n): Promise<StoreResolutionRow[]> {\n const db = em.getKysely<StoreResolutionDatabase>()\n let query = db\n .selectFrom('ecommerce_stores as s')\n .leftJoin('ecommerce_store_channel_bindings as c', (join) =>\n join\n .onRef('c.store_id', '=', 's.id')\n .onRef('c.tenant_id', '=', 's.tenant_id')\n .onRef('c.organization_id', '=', 's.organization_id')\n .on('c.is_default', '=', true)\n .on('c.deleted_at', 'is', null),\n )\n .select([...STORE_COLUMNS])\n .where('s.slug', '=', slug)\n .where('s.deleted_at', 'is', null)\n if (tenantId) query = query.where('s.tenant_id', '=', tenantId)\n return query.limit(2).execute()\n}\n\nasync function loadDomainCandidates(\n container: StoreContextContainer,\n mapping: DomainMappingResolution,\n): Promise<StoreCandidate[]> {\n const cache = ecommerceResolutionCache(container)\n const key = ['domain-mapping', mapping.domainMappingId]\n const cached = await cache.get<CachedStoreCandidates>(key)\n if (cached && Array.isArray(cached.candidates)) return cached.candidates\n const rows = await queryDomainCandidates(resolveEntityManager(container), mapping)\n const candidates = rows.map(toCandidate)\n const tags = [\n ecommerceDomainMappingTag(mapping.domainMappingId),\n ecommerceDomainTag(mapping.hostname),\n ...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)),\n ]\n await cache.set<CachedStoreCandidates>(\n key,\n { candidates },\n { ttlMs: STORE_RESOLUTION_TTL_MS, tags: Array.from(new Set(tags)) },\n )\n return candidates\n}\n\nasync function loadSlugCandidates(\n container: StoreContextContainer,\n slug: string,\n tenantId: string | null,\n): Promise<StoreCandidate[]> {\n const cache = ecommerceResolutionCache(container)\n const key = ['slug', tenantId ?? '*', slug]\n const cached = await cache.get<CachedStoreCandidates>(key)\n if (cached && Array.isArray(cached.candidates)) return cached.candidates\n const rows = await querySlugCandidates(resolveEntityManager(container), slug, tenantId)\n const candidates = rows.map(toCandidate)\n if (candidates.length > 0) {\n await cache.set<CachedStoreCandidates>(\n key,\n { candidates },\n {\n ttlMs: STORE_RESOLUTION_TTL_MS,\n tags: candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)),\n },\n )\n }\n return candidates\n}\n\nasync function reportMisconfiguredStore(container: StoreContextContainer, store: CandidateStore): Promise<void> {\n const cache = ecommerceResolutionCache(container)\n const key = ['misconfigured', 'channel-binding-missing', store.id]\n const alreadyReported = await cache.get<{ reportedAt: string }>(key)\n if (alreadyReported) return\n await cache.set(key, { reportedAt: new Date().toISOString() }, { ttlMs: STORE_MISCONFIGURED_THROTTLE_MS })\n logger.warn('Store has no default channel binding; answering 503', {\n storeId: store.id,\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n })\n try {\n await emitEcommerceEvent(\n 'ecommerce.store.misconfigured',\n {\n id: store.id,\n storeId: store.id,\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n reason: 'channel_binding_missing',\n },\n { persistent: true, tenantId: store.tenantId, organizationId: store.organizationId },\n )\n } catch (error) {\n logger.warn('Failed to emit store misconfiguration event', { storeId: store.id, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: 'ecommerce.store_misconfigured_emit_failed',\n attributes: { storeId: store.id },\n })\n }\n}\n\nasync function finalizeCandidate(\n container: StoreContextContainer,\n candidate: StoreCandidate,\n source: ResolvedStore['source'],\n domain: ResolvedStoreDomain | null,\n localeCandidates: StoreLocaleCandidates,\n): Promise<ResolvedStore> {\n const { store } = candidate\n if (store.status === 'draft') {\n if (isDevStoreSlugEnabled()) throw new StorefrontResolutionError(403, 'store_draft')\n throw storeNotFound('store_draft_hidden', { source, storeId: store.id })\n }\n if (store.status === 'archived') throw new StorefrontResolutionError(410, 'store_archived')\n if (!candidate.channel) {\n await reportMisconfiguredStore(container, store)\n throw new StorefrontResolutionError(503, 'store_misconfigured')\n }\n const locale = resolveStoreLocale(store, localeCandidates)\n return {\n source,\n store: {\n id: store.id,\n code: store.code,\n name: store.name,\n slug: store.slug,\n status: 'active',\n defaultLocale: store.defaultLocale,\n supportedLocales: store.supportedLocales,\n defaultCurrencyCode: store.defaultCurrencyCode,\n settings: store.settings,\n },\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n channel: candidate.channel,\n domain,\n effectiveLocale: locale.effectiveLocale,\n requestedLocale: locale.requestedLocale,\n currencyCode: store.defaultCurrencyCode,\n }\n}\n\nasync function resolveSlugWithCandidates(\n container: StoreContextContainer,\n rawSlug: string,\n tenantId: string | null,\n localeCandidates: StoreLocaleCandidates,\n): Promise<ResolvedStore> {\n if (!isDevStoreSlugEnabled()) throw new StorefrontResolutionError(400, 'store_slug_not_allowed')\n const slug = rawSlug.trim().toLowerCase()\n if (!slug.length || slug.length > SLUG_MAX_LENGTH || !ECOMMERCE_STORE_SLUG_PATTERN.test(slug)) {\n throw storeNotFound('slug_invalid', {})\n }\n const candidates = await loadSlugCandidates(container, slug, tenantId)\n if (candidates.length === 0) throw storeNotFound('slug_not_found', { slug })\n if (candidates.length > 1) throw storeNotFound('slug_ambiguous', { slug })\n return finalizeCandidate(container, candidates[0], 'slug', null, localeCandidates)\n}\n\nexport async function resolveStoreBySlug(\n container: StoreContextContainer,\n slug: string,\n options: ResolveStoreBySlugOptions = {},\n): Promise<ResolvedStore> {\n return resolveSlugWithCandidates(container, slug, options.tenantId ?? null, {\n queryLocale: options.locale ?? null,\n headerLocale: options.headerLocale ?? null,\n acceptLanguage: options.acceptLanguage ?? null,\n })\n}\n\nexport async function resolveStoreFromRequest(\n container: StoreContextContainer,\n request: Request,\n options: ResolveStoreFromRequestOptions = {},\n): Promise<ResolvedStore> {\n const url = new URL(request.url)\n const localeCandidates = readLocaleCandidates(request, url)\n const storeSlug = url.searchParams.get('storeSlug')\n if (storeSlug !== null) return resolveSlugWithCandidates(container, storeSlug, null, localeCandidates)\n\n const host = normalizeRequestHost(readForcedHost(request) ?? request.headers.get('host'))\n if (!host) throw storeNotFound('host_missing', {})\n const domainMappingService = tryResolve<DomainMappingResolver>(container, 'domainMappingService')\n if (!domainMappingService) throw storeNotFound('domain_routing_unavailable', { host })\n const mapping = await domainMappingService.resolveByHostname(host)\n if (!mapping || mapping.status !== 'active') throw storeNotFound('domain_mapping_not_active', { host })\n\n const candidates = await loadDomainCandidates(container, mapping)\n const pathname = normalizeRequestPathname(options.pathname ?? url.pathname)\n const candidate = selectLongestPrefixMatch(candidates, pathname)\n if (!candidate?.binding) {\n throw storeNotFound('domain_binding_not_found', {\n host,\n domainMappingId: mapping.domainMappingId,\n bindingCount: candidates.length,\n })\n }\n return finalizeCandidate(\n container,\n candidate,\n 'host',\n {\n domainMappingId: mapping.domainMappingId,\n hostname: mapping.hostname,\n bindingId: candidate.binding.id,\n pathPrefix: candidate.binding.pathPrefix,\n isPrimary: candidate.binding.isPrimary,\n },\n localeCandidates,\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AssortmentScope } from '@open-mercato/shared/lib/catalog-visibility'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'\nimport {\n ECOMMERCE_STORE_SLUG_PATTERN,\n ecommercePriceSortFallbackSchema,\n ecommerceStoreSettingsSchema,\n ecommerceStoreStatusSchema,\n type EcommercePriceSortFallback,\n type EcommerceStoreSettings,\n type EcommerceStoreStatus,\n} from '../data/validators'\nimport { emitEcommerceEvent } from '../events'\nimport {\n ecommerceDomainMappingTag,\n ecommerceDomainTag,\n ecommerceResolutionCache,\n ecommerceSalesChannelTag,\n ecommerceStoreTag,\n type CacheContainer,\n} from './cacheKeys'\nimport { resolveStoreLocale, type StoreLocaleCandidates } from './storeLocale'\nimport type { StoreContextChannel, StoreContextStore } from './types'\n\n/**\n * Store layer of storefront resolution (SPEC-029 \u00A74.1 steps 1\u20135 and 7, \u00A76.2, \u00A78, \u00A78.1).\n *\n * Host flow: normalized Host \u2192 `customer_accounts` `domainMappingService.resolveByHostname`\n * (returns `active` mappings only, D1) \u2192 ONE ecommerce query returning every binding of that\n * mapping joined with its store and the store's default channel binding (D6) \u2192 longest\n * `path_prefix` match on a path-segment boundary (R6). The candidate list is cached per mapping.\n *\n * Dev slug flow (D18): `?storeSlug=` only when `OM_ECOMMERCE_DEV_STORE_SLUG` is true, otherwise\n * `400`. A slug is unique within a tenant (\u00A75.1), so slug resolution is tenant-scoped when the\n * caller supplies a tenant and otherwise requires the slug to be unique across tenants \u2014 an\n * ambiguous slug answers `404`.\n *\n * The buyer layer (Step 2.7) composes `ResolvedStore` into a full `StoreContext`.\n */\n\nexport const DEV_STORE_SLUG_ENV = 'OM_ECOMMERCE_DEV_STORE_SLUG'\nexport const STORE_RESOLUTION_TTL_MS = 300_000\nexport const STORE_MISCONFIGURED_THROTTLE_MS = 3_600_000\n\nexport type StorefrontResolutionErrorCode =\n | 'store_slug_not_allowed'\n | 'store_not_found'\n | 'store_draft'\n | 'store_archived'\n | 'store_misconfigured'\n | 'portal_session_scope_mismatch'\n | 'portal_session_invalid'\n\nexport type StorefrontResolutionStatus = 400 | 401 | 403 | 404 | 410 | 503\n\nconst STOREFRONT_RESOLUTION_ERROR_MARKER = Symbol.for('@open-mercato/ecommerce/StorefrontResolutionError')\n\nexport class StorefrontResolutionError extends Error {\n readonly [STOREFRONT_RESOLUTION_ERROR_MARKER] = true\n readonly status: StorefrontResolutionStatus\n readonly code: StorefrontResolutionErrorCode\n\n constructor(status: StorefrontResolutionStatus, code: StorefrontResolutionErrorCode) {\n super(`[internal] storefront store resolution failed: ${code}`)\n this.name = 'StorefrontResolutionError'\n this.status = status\n this.code = code\n }\n}\n\n/**\n * Marker-based so it holds across bundle layers: `storeContextService` is resolved from the DI\n * container, whose registrars may have been loaded from another bundle layer than the route, so\n * the error class it throws is not always the route's `StorefrontResolutionError`.\n */\nexport function isStorefrontResolutionError(error: unknown): error is StorefrontResolutionError {\n return (\n !!error &&\n typeof error === 'object' &&\n (error as Record<symbol, unknown>)[STOREFRONT_RESOLUTION_ERROR_MARKER] === true\n )\n}\n\nexport type ResolvedStoreChannel = StoreContextChannel & {\n assortmentScope: AssortmentScope | null\n requireAuthentication: boolean\n}\n\nexport type ResolvedStoreDomain = {\n domainMappingId: string\n hostname: string\n bindingId: string\n pathPrefix: string | null\n isPrimary: boolean\n}\n\nexport type ResolvedStore = {\n source: 'host' | 'slug'\n store: StoreContextStore\n tenantId: string\n organizationId: string\n channel: ResolvedStoreChannel\n domain: ResolvedStoreDomain | null\n effectiveLocale: string\n requestedLocale: string | null\n currencyCode: string\n}\n\nexport type StoreContextContainer = CacheContainer\n\nexport type ResolveStoreFromRequestOptions = {\n pathname?: string | null\n}\n\nexport type ResolveStoreBySlugOptions = {\n tenantId?: string | null\n locale?: string | null\n headerLocale?: string | null\n acceptLanguage?: string | null\n}\n\ntype DomainMappingResolution = {\n domainMappingId: string\n hostname: string\n tenantId: string\n organizationId: string\n status: string\n}\n\ntype DomainMappingResolver = {\n resolveByHostname(hostname: string): Promise<DomainMappingResolution | null>\n}\n\ntype CandidateStore = Omit<StoreContextStore, 'status'> & {\n status: EcommerceStoreStatus\n tenantId: string\n organizationId: string\n}\n\ntype CandidateBinding = { id: string; pathPrefix: string | null; isPrimary: boolean }\n\ntype StoreCandidate = {\n store: CandidateStore\n channel: ResolvedStoreChannel | null\n binding: CandidateBinding | null\n missingSalesChannelId?: string | null\n}\n\ntype CachedStoreCandidates = { candidates: StoreCandidate[] }\n\ntype StoreResolutionDatabase = {\n ecommerce_stores: {\n id: string\n tenant_id: string\n organization_id: string\n code: string\n name: string\n slug: string\n status: string\n default_locale: string\n supported_locales: unknown\n default_currency_code: string\n settings: unknown\n deleted_at: Date | null\n }\n ecommerce_store_domain_bindings: {\n id: string\n tenant_id: string\n organization_id: string\n store_id: string\n domain_mapping_id: string\n path_prefix: string | null\n is_primary: boolean\n deleted_at: Date | null\n }\n ecommerce_store_channel_bindings: {\n id: string\n tenant_id: string\n organization_id: string\n store_id: string\n sales_channel_id: string\n price_kind_id: string | null\n assortment_scope: unknown\n price_sort_fallback: string\n is_default: boolean\n require_authentication: boolean\n deleted_at: Date | null\n }\n sales_channels: {\n id: string\n tenant_id: string\n organization_id: string\n deleted_at: Date | null\n }\n}\n\ntype StoreResolutionRow = {\n store_id: string\n store_tenant_id: string\n store_organization_id: string\n store_code: string\n store_name: string\n store_slug: string\n store_status: string\n store_default_locale: string\n store_supported_locales: unknown\n store_default_currency_code: string\n store_settings: unknown\n channel_binding_id: string | null\n channel_sales_channel_id: string | null\n channel_price_kind_id: string | null\n channel_assortment_scope: unknown\n channel_price_sort_fallback: string | null\n channel_require_authentication?: boolean | null\n channel_live_sales_channel_id?: string | null\n binding_id?: string | null\n binding_path_prefix?: string | null\n binding_is_primary?: boolean | null\n}\n\nconst STORE_COLUMNS = [\n 's.id as store_id',\n 's.tenant_id as store_tenant_id',\n 's.organization_id as store_organization_id',\n 's.code as store_code',\n 's.name as store_name',\n 's.slug as store_slug',\n 's.status as store_status',\n 's.default_locale as store_default_locale',\n 's.supported_locales as store_supported_locales',\n 's.default_currency_code as store_default_currency_code',\n 's.settings as store_settings',\n 'c.id as channel_binding_id',\n 'c.sales_channel_id as channel_sales_channel_id',\n 'c.price_kind_id as channel_price_kind_id',\n 'c.assortment_scope as channel_assortment_scope',\n 'c.price_sort_fallback as channel_price_sort_fallback',\n 'c.require_authentication as channel_require_authentication',\n 'sc.id as channel_live_sales_channel_id',\n] as const\n\nconst FORCE_HOST_HEADER = 'x-force-host'\nconst FORCE_HOST_SECRET_HEADER = 'x-force-host-secret'\nconst SLUG_MAX_LENGTH = 120\n\nconst logger = createLogger('ecommerce').child({ component: 'store-context' })\n\ntype NotFoundReason =\n | 'host_missing'\n | 'domain_routing_unavailable'\n | 'domain_mapping_not_active'\n | 'domain_binding_not_found'\n | 'store_draft_hidden'\n | 'slug_invalid'\n | 'slug_not_found'\n | 'slug_ambiguous'\n\nconst WARN_NOT_FOUND_REASONS: ReadonlySet<NotFoundReason> = new Set([\n 'domain_routing_unavailable',\n 'domain_binding_not_found',\n 'slug_ambiguous',\n])\n\nfunction storeNotFound(reason: NotFoundReason, details: Record<string, unknown>): StorefrontResolutionError {\n const meta = { reason, ...details }\n if (WARN_NOT_FOUND_REASONS.has(reason)) {\n logger.warn('Storefront store not resolved', meta)\n } else {\n logger.debug('Storefront store not resolved', meta)\n }\n return new StorefrontResolutionError(404, 'store_not_found')\n}\n\nfunction tryResolve<T>(container: StoreContextContainer, name: string): T | null {\n try {\n const resolved = container.resolve(name) as T | null | undefined\n return resolved ?? null\n } catch {\n return null\n }\n}\n\nfunction resolveEntityManager(container: StoreContextContainer): EntityManager {\n const em = tryResolve<EntityManager>(container, 'em')\n if (!em) throw new Error('[internal] ecommerce store resolution requires the DI entity manager')\n return em\n}\n\nexport function isDevStoreSlugEnabled(): boolean {\n return parseBooleanWithDefault(process.env[DEV_STORE_SLUG_ENV], false)\n}\n\nexport function normalizeRequestHost(raw: string | null | undefined): string | null {\n if (typeof raw !== 'string') return null\n const first = raw.split(',')[0]?.trim().toLowerCase() ?? ''\n if (!first.length) return null\n const withoutPort = first.startsWith('[')\n ? first.replace(/^(\\[[^\\]]*\\])(?::\\d+)?$/, '$1')\n : first.replace(/:\\d+$/, '')\n const withoutTrailingDot = withoutPort.replace(/\\.+$/, '')\n return withoutTrailingDot.length ? withoutTrailingDot : null\n}\n\nexport function normalizeRequestPathname(pathname: string | null | undefined): string {\n if (typeof pathname !== 'string' || !pathname.length) return '/'\n const lowered = pathname.toLowerCase()\n const withLeadingSlash = lowered.startsWith('/') ? lowered : `/${lowered}`\n const withoutTrailingSlash = withLeadingSlash.replace(/\\/+$/, '')\n return withoutTrailingSlash.length ? withoutTrailingSlash : '/'\n}\n\nexport function pathPrefixMatches(pathPrefix: string | null, pathname: string): boolean {\n if (!pathPrefix) return true\n const normalizedPrefix = normalizeRequestPathname(pathPrefix)\n if (normalizedPrefix === '/') return true\n return pathname === normalizedPrefix || pathname.startsWith(`${normalizedPrefix}/`)\n}\n\nexport function selectLongestPrefixMatch<T extends { binding: { pathPrefix: string | null } | null }>(\n candidates: T[],\n pathname: string,\n): T | null {\n let best: T | null = null\n let bestLength = -1\n for (const candidate of candidates) {\n const pathPrefix = candidate.binding?.pathPrefix ?? null\n if (!pathPrefixMatches(pathPrefix, pathname)) continue\n const length = pathPrefix ? normalizeRequestPathname(pathPrefix).length : 0\n if (length > bestLength) {\n best = candidate\n bestLength = length\n }\n }\n return best\n}\n\nfunction readForcedHost(request: Request): string | null {\n if (process.env.NODE_ENV !== 'test') return null\n const expected = process.env.FORCE_HOST_SECRET\n if (!expected) return null\n if (!secretEqual(request.headers.get(FORCE_HOST_SECRET_HEADER), expected)) return null\n const forced = request.headers.get(FORCE_HOST_HEADER)\n return forced && forced.length > 0 ? forced : null\n}\n\nfunction readLocaleCandidates(request: Request, url: URL): StoreLocaleCandidates {\n return {\n queryLocale: url.searchParams.get('locale'),\n headerLocale: request.headers.get('x-locale'),\n acceptLanguage: request.headers.get('accept-language'),\n }\n}\n\nfunction parseJsonColumn(value: unknown): unknown {\n if (typeof value !== 'string') return value\n try {\n return JSON.parse(value)\n } catch {\n return null\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction parseSettings(raw: unknown, storeId: string): EcommerceStoreSettings {\n const parsed = ecommerceStoreSettingsSchema.safeParse(parseJsonColumn(raw) ?? {})\n if (parsed.success) return parsed.data\n logger.warn('Store settings failed validation; serving defaults', { storeId })\n return ecommerceStoreSettingsSchema.parse({})\n}\n\nfunction parseSupportedLocales(raw: unknown, defaultLocale: string): string[] {\n const value = parseJsonColumn(raw)\n const locales = Array.isArray(value)\n ? value.filter((locale): locale is string => typeof locale === 'string' && locale.length > 0)\n : []\n return locales.includes(defaultLocale) ? locales : [defaultLocale, ...locales]\n}\n\nfunction parseStatus(raw: string): EcommerceStoreStatus {\n const parsed = ecommerceStoreStatusSchema.safeParse(raw)\n return parsed.success ? parsed.data : 'draft'\n}\n\nfunction parsePriceSortFallback(raw: string | null): EcommercePriceSortFallback {\n const parsed = ecommercePriceSortFallbackSchema.safeParse(raw)\n return parsed.success ? parsed.data : 'approximate'\n}\n\nfunction parseAssortmentScope(raw: unknown): AssortmentScope | null {\n const value = parseJsonColumn(raw)\n return isRecord(value) ? (value as AssortmentScope) : null\n}\n\nfunction toCandidate(row: StoreResolutionRow): StoreCandidate {\n const store: CandidateStore = {\n id: row.store_id,\n tenantId: row.store_tenant_id,\n organizationId: row.store_organization_id,\n code: row.store_code,\n name: row.store_name,\n slug: row.store_slug,\n status: parseStatus(row.store_status),\n defaultLocale: row.store_default_locale,\n supportedLocales: parseSupportedLocales(row.store_supported_locales, row.store_default_locale),\n defaultCurrencyCode: row.store_default_currency_code,\n settings: parseSettings(row.store_settings, row.store_id),\n }\n const salesChannelMissing =\n !!row.channel_binding_id && !!row.channel_sales_channel_id && !row.channel_live_sales_channel_id\n const channel: ResolvedStoreChannel | null =\n row.channel_binding_id && row.channel_sales_channel_id && !salesChannelMissing\n ? {\n channelBindingId: row.channel_binding_id,\n salesChannelId: row.channel_sales_channel_id,\n priceKindId: row.channel_price_kind_id ?? null,\n priceSortFallback: parsePriceSortFallback(row.channel_price_sort_fallback),\n assortmentScope: parseAssortmentScope(row.channel_assortment_scope),\n requireAuthentication: row.channel_require_authentication === true,\n }\n : null\n const binding: CandidateBinding | null = row.binding_id\n ? { id: row.binding_id, pathPrefix: row.binding_path_prefix ?? null, isPrimary: row.binding_is_primary === true }\n : null\n return { store, channel, binding, missingSalesChannelId: salesChannelMissing ? row.channel_sales_channel_id : null }\n}\n\nasync function queryDomainCandidates(\n em: EntityManager,\n mapping: DomainMappingResolution,\n): Promise<StoreResolutionRow[]> {\n const db = em.getKysely<StoreResolutionDatabase>()\n return db\n .selectFrom('ecommerce_store_domain_bindings as b')\n .innerJoin('ecommerce_stores as s', (join) =>\n join\n .onRef('s.id', '=', 'b.store_id')\n .onRef('s.tenant_id', '=', 'b.tenant_id')\n .onRef('s.organization_id', '=', 'b.organization_id')\n .on('s.deleted_at', 'is', null),\n )\n .leftJoin('ecommerce_store_channel_bindings as c', (join) =>\n join\n .onRef('c.store_id', '=', 's.id')\n .onRef('c.tenant_id', '=', 's.tenant_id')\n .onRef('c.organization_id', '=', 's.organization_id')\n .on('c.is_default', '=', true)\n .on('c.deleted_at', 'is', null),\n )\n .leftJoin('sales_channels as sc', (join) =>\n join\n .onRef('sc.id', '=', 'c.sales_channel_id')\n .onRef('sc.tenant_id', '=', 'c.tenant_id')\n .onRef('sc.organization_id', '=', 'c.organization_id')\n .on('sc.deleted_at', 'is', null),\n )\n .select([\n ...STORE_COLUMNS,\n 'b.id as binding_id',\n 'b.path_prefix as binding_path_prefix',\n 'b.is_primary as binding_is_primary',\n ])\n .where('b.domain_mapping_id', '=', mapping.domainMappingId)\n .where('b.tenant_id', '=', mapping.tenantId)\n .where('b.organization_id', '=', mapping.organizationId)\n .where('b.deleted_at', 'is', null)\n .execute()\n}\n\nasync function querySlugCandidates(\n em: EntityManager,\n slug: string,\n tenantId: string | null,\n): Promise<StoreResolutionRow[]> {\n const db = em.getKysely<StoreResolutionDatabase>()\n let query = db\n .selectFrom('ecommerce_stores as s')\n .leftJoin('ecommerce_store_channel_bindings as c', (join) =>\n join\n .onRef('c.store_id', '=', 's.id')\n .onRef('c.tenant_id', '=', 's.tenant_id')\n .onRef('c.organization_id', '=', 's.organization_id')\n .on('c.is_default', '=', true)\n .on('c.deleted_at', 'is', null),\n )\n .leftJoin('sales_channels as sc', (join) =>\n join\n .onRef('sc.id', '=', 'c.sales_channel_id')\n .onRef('sc.tenant_id', '=', 'c.tenant_id')\n .onRef('sc.organization_id', '=', 'c.organization_id')\n .on('sc.deleted_at', 'is', null),\n )\n .select([...STORE_COLUMNS])\n .where('s.slug', '=', slug)\n .where('s.deleted_at', 'is', null)\n if (tenantId) query = query.where('s.tenant_id', '=', tenantId)\n return query.limit(2).execute()\n}\n\nfunction salesChannelTags(rows: StoreResolutionRow[]): string[] {\n return rows.flatMap((row) => (row.channel_sales_channel_id ? [ecommerceSalesChannelTag(row.channel_sales_channel_id)] : []))\n}\n\nasync function loadDomainCandidates(\n container: StoreContextContainer,\n mapping: DomainMappingResolution,\n): Promise<StoreCandidate[]> {\n const cache = ecommerceResolutionCache(container)\n const key = ['domain-mapping', mapping.domainMappingId]\n const cached = await cache.get<CachedStoreCandidates>(key)\n if (cached && Array.isArray(cached.candidates)) return cached.candidates\n const rows = await queryDomainCandidates(resolveEntityManager(container), mapping)\n const candidates = rows.map(toCandidate)\n const tags = [\n ecommerceDomainMappingTag(mapping.domainMappingId),\n ecommerceDomainTag(mapping.hostname),\n ...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)),\n ...salesChannelTags(rows),\n ]\n await cache.set<CachedStoreCandidates>(\n key,\n { candidates },\n { ttlMs: STORE_RESOLUTION_TTL_MS, tags: Array.from(new Set(tags)) },\n )\n return candidates\n}\n\nasync function loadSlugCandidates(\n container: StoreContextContainer,\n slug: string,\n tenantId: string | null,\n): Promise<StoreCandidate[]> {\n const cache = ecommerceResolutionCache(container)\n const key = ['slug', tenantId ?? '*', slug]\n const cached = await cache.get<CachedStoreCandidates>(key)\n if (cached && Array.isArray(cached.candidates)) return cached.candidates\n const rows = await querySlugCandidates(resolveEntityManager(container), slug, tenantId)\n const candidates = rows.map(toCandidate)\n if (candidates.length > 0) {\n await cache.set<CachedStoreCandidates>(\n key,\n { candidates },\n {\n ttlMs: STORE_RESOLUTION_TTL_MS,\n tags: Array.from(\n new Set([...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)), ...salesChannelTags(rows)]),\n ),\n },\n )\n }\n return candidates\n}\n\nasync function reportMisconfiguredStore(\n container: StoreContextContainer,\n store: CandidateStore,\n missingSalesChannelId: string | null,\n): Promise<void> {\n const cache = ecommerceResolutionCache(container)\n const key = ['misconfigured', 'channel-binding-missing', store.id]\n const alreadyReported = await cache.get<{ reportedAt: string }>(key)\n if (alreadyReported) return\n await cache.set(key, { reportedAt: new Date().toISOString() }, { ttlMs: STORE_MISCONFIGURED_THROTTLE_MS })\n if (missingSalesChannelId) {\n logger.warn('Store default channel binding references a sales channel that no longer exists; answering 503', {\n storeId: store.id,\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n salesChannelId: missingSalesChannelId,\n })\n } else {\n logger.warn('Store has no default channel binding; answering 503', {\n storeId: store.id,\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n })\n }\n try {\n await emitEcommerceEvent(\n 'ecommerce.store.misconfigured',\n {\n id: store.id,\n storeId: store.id,\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n reason: 'channel_binding_missing',\n },\n { persistent: true, tenantId: store.tenantId, organizationId: store.organizationId },\n )\n } catch (error) {\n logger.warn('Failed to emit store misconfiguration event', { storeId: store.id, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: 'ecommerce.store_misconfigured_emit_failed',\n attributes: { storeId: store.id },\n })\n }\n}\n\nasync function finalizeCandidate(\n container: StoreContextContainer,\n candidate: StoreCandidate,\n source: ResolvedStore['source'],\n domain: ResolvedStoreDomain | null,\n localeCandidates: StoreLocaleCandidates,\n): Promise<ResolvedStore> {\n const { store } = candidate\n if (store.status === 'draft') {\n if (isDevStoreSlugEnabled()) throw new StorefrontResolutionError(403, 'store_draft')\n throw storeNotFound('store_draft_hidden', { source, storeId: store.id })\n }\n if (store.status === 'archived') throw new StorefrontResolutionError(410, 'store_archived')\n if (!candidate.channel) {\n await reportMisconfiguredStore(container, store, candidate.missingSalesChannelId ?? null)\n throw new StorefrontResolutionError(503, 'store_misconfigured')\n }\n const locale = resolveStoreLocale(store, localeCandidates)\n return {\n source,\n store: {\n id: store.id,\n code: store.code,\n name: store.name,\n slug: store.slug,\n status: 'active',\n defaultLocale: store.defaultLocale,\n supportedLocales: store.supportedLocales,\n defaultCurrencyCode: store.defaultCurrencyCode,\n settings: store.settings,\n },\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n channel: candidate.channel,\n domain,\n effectiveLocale: locale.effectiveLocale,\n requestedLocale: locale.requestedLocale,\n currencyCode: store.defaultCurrencyCode,\n }\n}\n\nasync function resolveSlugWithCandidates(\n container: StoreContextContainer,\n rawSlug: string,\n tenantId: string | null,\n localeCandidates: StoreLocaleCandidates,\n): Promise<ResolvedStore> {\n if (!isDevStoreSlugEnabled()) throw new StorefrontResolutionError(400, 'store_slug_not_allowed')\n const slug = rawSlug.trim().toLowerCase()\n if (!slug.length || slug.length > SLUG_MAX_LENGTH || !ECOMMERCE_STORE_SLUG_PATTERN.test(slug)) {\n throw storeNotFound('slug_invalid', {})\n }\n const candidates = await loadSlugCandidates(container, slug, tenantId)\n if (candidates.length === 0) throw storeNotFound('slug_not_found', { slug })\n if (candidates.length > 1) throw storeNotFound('slug_ambiguous', { slug })\n return finalizeCandidate(container, candidates[0], 'slug', null, localeCandidates)\n}\n\nexport async function resolveStoreBySlug(\n container: StoreContextContainer,\n slug: string,\n options: ResolveStoreBySlugOptions = {},\n): Promise<ResolvedStore> {\n return resolveSlugWithCandidates(container, slug, options.tenantId ?? null, {\n queryLocale: options.locale ?? null,\n headerLocale: options.headerLocale ?? null,\n acceptLanguage: options.acceptLanguage ?? null,\n })\n}\n\nexport async function resolveStoreFromRequest(\n container: StoreContextContainer,\n request: Request,\n options: ResolveStoreFromRequestOptions = {},\n): Promise<ResolvedStore> {\n const url = new URL(request.url)\n const localeCandidates = readLocaleCandidates(request, url)\n const storeSlug = url.searchParams.get('storeSlug')\n if (storeSlug !== null) return resolveSlugWithCandidates(container, storeSlug, null, localeCandidates)\n\n const host = normalizeRequestHost(readForcedHost(request) ?? request.headers.get('host'))\n if (!host) throw storeNotFound('host_missing', {})\n const domainMappingService = tryResolve<DomainMappingResolver>(container, 'domainMappingService')\n if (!domainMappingService) throw storeNotFound('domain_routing_unavailable', { host })\n const mapping = await domainMappingService.resolveByHostname(host)\n if (!mapping || mapping.status !== 'active') throw storeNotFound('domain_mapping_not_active', { host })\n\n const candidates = await loadDomainCandidates(container, mapping)\n const pathname = normalizeRequestPathname(options.pathname ?? url.pathname)\n const candidate = selectLongestPrefixMatch(candidates, pathname)\n if (!candidate?.binding) {\n throw storeNotFound('domain_binding_not_found', {\n host,\n domainMappingId: mapping.domainMappingId,\n bindingCount: candidates.length,\n })\n }\n return finalizeCandidate(\n container,\n candidate,\n 'host',\n {\n domainMappingId: mapping.domainMappingId,\n hostname: mapping.hostname,\n bindingId: candidate.binding.id,\n pathPrefix: candidate.binding.pathPrefix,\n isPrimary: candidate.binding.isPrimary,\n },\n localeCandidates,\n )\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAEA,SAAS,+BAA+B;AACxC,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,0BAAsD;AAmBxD,MAAM,qBAAqB;AAC3B,MAAM,0BAA0B;AAChC,MAAM,kCAAkC;AAa/C,MAAM,qCAAqC,uBAAO,IAAI,mDAAmD;AAElG,MAAM,mCAAkC,YACnC,yCADmC,IAAM;AAAA,EAKnD,YAAY,QAAoC,MAAqC;AACnF,UAAM,kDAAkD,IAAI,EAAE;AALhE,SAAU,MAAsC;AAM9C,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACd;AACF;AAOO,SAAS,4BAA4B,OAAoD;AAC9F,SACE,CAAC,CAAC,SACF,OAAO,UAAU,YAChB,MAAkC,kCAAkC,MAAM;AAE/E;AA2IA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,oBAAoB;AAC1B,MAAM,2BAA2B;AACjC,MAAM,kBAAkB;AAExB,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,gBAAgB,CAAC;AAY7E,MAAM,yBAAsD,oBAAI,IAAI;AAAA,EAClE;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,cAAc,QAAwB,SAA6D;AAC1G,QAAM,OAAO,EAAE,QAAQ,GAAG,QAAQ;AAClC,MAAI,uBAAuB,IAAI,MAAM,GAAG;AACtC,WAAO,KAAK,iCAAiC,IAAI;AAAA,EACnD,OAAO;AACL,WAAO,MAAM,iCAAiC,IAAI;AAAA,EACpD;AACA,SAAO,IAAI,0BAA0B,KAAK,iBAAiB;AAC7D;AAEA,SAAS,WAAc,WAAkC,MAAwB;AAC/E,MAAI;AACF,UAAM,WAAW,UAAU,QAAQ,IAAI;AACvC,WAAO,YAAY;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,qBAAqB,WAAiD;AAC7E,QAAM,KAAK,WAA0B,WAAW,IAAI;AACpD,MAAI,CAAC,GAAI,OAAM,IAAI,MAAM,sEAAsE;AAC/F,SAAO;AACT;AAEO,SAAS,wBAAiC;AAC/C,SAAO,wBAAwB,QAAQ,IAAI,kBAAkB,GAAG,KAAK;AACvE;AAEO,SAAS,qBAAqB,KAA+C;AAClF,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,YAAY,KAAK;AACzD,MAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,QAAM,cAAc,MAAM,WAAW,GAAG,IACpC,MAAM,QAAQ,2BAA2B,IAAI,IAC7C,MAAM,QAAQ,SAAS,EAAE;AAC7B,QAAM,qBAAqB,YAAY,QAAQ,QAAQ,EAAE;AACzD,SAAO,mBAAmB,SAAS,qBAAqB;AAC1D;AAEO,SAAS,yBAAyB,UAA6C;AACpF,MAAI,OAAO,aAAa,YAAY,CAAC,SAAS,OAAQ,QAAO;AAC7D,QAAM,UAAU,SAAS,YAAY;AACrC,QAAM,mBAAmB,QAAQ,WAAW,GAAG,IAAI,UAAU,IAAI,OAAO;AACxE,QAAM,uBAAuB,iBAAiB,QAAQ,QAAQ,EAAE;AAChE,SAAO,qBAAqB,SAAS,uBAAuB;AAC9D;AAEO,SAAS,kBAAkB,YAA2B,UAA2B;AACtF,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,mBAAmB,yBAAyB,UAAU;AAC5D,MAAI,qBAAqB,IAAK,QAAO;AACrC,SAAO,aAAa,oBAAoB,SAAS,WAAW,GAAG,gBAAgB,GAAG;AACpF;AAEO,SAAS,yBACd,YACA,UACU;AACV,MAAI,OAAiB;AACrB,MAAI,aAAa;AACjB,aAAW,aAAa,YAAY;AAClC,UAAM,aAAa,UAAU,SAAS,cAAc;AACpD,QAAI,CAAC,kBAAkB,YAAY,QAAQ,EAAG;AAC9C,UAAM,SAAS,aAAa,yBAAyB,UAAU,EAAE,SAAS;AAC1E,QAAI,SAAS,YAAY;AACvB,aAAO;AACP,mBAAa;AAAA,IACf;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,eAAe,SAAiC;AACvD,MAAI,QAAQ,IAAI,aAAa,OAAQ,QAAO;AAC5C,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,CAAC,YAAY,QAAQ,QAAQ,IAAI,wBAAwB,GAAG,QAAQ,EAAG,QAAO;AAClF,QAAM,SAAS,QAAQ,QAAQ,IAAI,iBAAiB;AACpD,SAAO,UAAU,OAAO,SAAS,IAAI,SAAS;AAChD;AAEA,SAAS,qBAAqB,SAAkB,KAAiC;AAC/E,SAAO;AAAA,IACL,aAAa,IAAI,aAAa,IAAI,QAAQ;AAAA,IAC1C,cAAc,QAAQ,QAAQ,IAAI,UAAU;AAAA,IAC5C,gBAAgB,QAAQ,QAAQ,IAAI,iBAAiB;AAAA,EACvD;AACF;AAEA,SAAS,gBAAgB,OAAyB;AAChD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI;AACF,WAAO,KAAK,MAAM,KAAK;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,cAAc,KAAc,SAAyC;AAC5E,QAAM,SAAS,6BAA6B,UAAU,gBAAgB,GAAG,KAAK,CAAC,CAAC;AAChF,MAAI,OAAO,QAAS,QAAO,OAAO;AAClC,SAAO,KAAK,sDAAsD,EAAE,QAAQ,CAAC;AAC7E,SAAO,6BAA6B,MAAM,CAAC,CAAC;AAC9C;AAEA,SAAS,sBAAsB,KAAc,eAAiC;AAC5E,QAAM,QAAQ,gBAAgB,GAAG;AACjC,QAAM,UAAU,MAAM,QAAQ,KAAK,IAC/B,MAAM,OAAO,CAAC,WAA6B,OAAO,WAAW,YAAY,OAAO,SAAS,CAAC,IAC1F,CAAC;AACL,SAAO,QAAQ,SAAS,aAAa,IAAI,UAAU,CAAC,eAAe,GAAG,OAAO;AAC/E;AAEA,SAAS,YAAY,KAAmC;AACtD,QAAM,SAAS,2BAA2B,UAAU,GAAG;AACvD,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;AAEA,SAAS,uBAAuB,KAAgD;AAC9E,QAAM,SAAS,iCAAiC,UAAU,GAAG;AAC7D,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;AAEA,SAAS,qBAAqB,KAAsC;AAClE,QAAM,QAAQ,gBAAgB,GAAG;AACjC,SAAO,SAAS,KAAK,IAAK,QAA4B;AACxD;AAEA,SAAS,YAAY,KAAyC;AAC5D,QAAM,QAAwB;AAAA,IAC5B,IAAI,IAAI;AAAA,IACR,UAAU,IAAI;AAAA,IACd,gBAAgB,IAAI;AAAA,IACpB,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,QAAQ,YAAY,IAAI,YAAY;AAAA,IACpC,eAAe,IAAI;AAAA,IACnB,kBAAkB,sBAAsB,IAAI,yBAAyB,IAAI,oBAAoB;AAAA,IAC7F,qBAAqB,IAAI;AAAA,IACzB,UAAU,cAAc,IAAI,gBAAgB,IAAI,QAAQ;AAAA,EAC1D;AACA,QAAM,sBACJ,CAAC,CAAC,IAAI,sBAAsB,CAAC,CAAC,IAAI,4BAA4B,CAAC,IAAI;AACrE,QAAM,UACJ,IAAI,sBAAsB,IAAI,4BAA4B,CAAC,sBACvD;AAAA,IACE,kBAAkB,IAAI;AAAA,IACtB,gBAAgB,IAAI;AAAA,IACpB,aAAa,IAAI,yBAAyB;AAAA,IAC1C,mBAAmB,uBAAuB,IAAI,2BAA2B;AAAA,IACzE,iBAAiB,qBAAqB,IAAI,wBAAwB;AAAA,IAClE,uBAAuB,IAAI,mCAAmC;AAAA,EAChE,IACA;AACN,QAAM,UAAmC,IAAI,aACzC,EAAE,IAAI,IAAI,YAAY,YAAY,IAAI,uBAAuB,MAAM,WAAW,IAAI,uBAAuB,KAAK,IAC9G;AACJ,SAAO,EAAE,OAAO,SAAS,SAAS,uBAAuB,sBAAsB,IAAI,2BAA2B,KAAK;AACrH;AAEA,eAAe,sBACb,IACA,SAC+B;AAC/B,QAAM,KAAK,GAAG,UAAmC;AACjD,SAAO,GACJ,WAAW,sCAAsC,EACjD;AAAA,IAAU;AAAA,IAAyB,CAAC,SACnC,KACG,MAAM,QAAQ,KAAK,YAAY,EAC/B,MAAM,eAAe,KAAK,aAAa,EACvC,MAAM,qBAAqB,KAAK,mBAAmB,EACnD,GAAG,gBAAgB,MAAM,IAAI;AAAA,EAClC,EACC;AAAA,IAAS;AAAA,IAAyC,CAAC,SAClD,KACG,MAAM,cAAc,KAAK,MAAM,EAC/B,MAAM,eAAe,KAAK,aAAa,EACvC,MAAM,qBAAqB,KAAK,mBAAmB,EACnD,GAAG,gBAAgB,KAAK,IAAI,EAC5B,GAAG,gBAAgB,MAAM,IAAI;AAAA,EAClC,EACC;AAAA,IAAS;AAAA,IAAwB,CAAC,SACjC,KACG,MAAM,SAAS,KAAK,oBAAoB,EACxC,MAAM,gBAAgB,KAAK,aAAa,EACxC,MAAM,sBAAsB,KAAK,mBAAmB,EACpD,GAAG,iBAAiB,MAAM,IAAI;AAAA,EACnC,EACC,OAAO;AAAA,IACN,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EACA,MAAM,uBAAuB,KAAK,QAAQ,eAAe,EACzD,MAAM,eAAe,KAAK,QAAQ,QAAQ,EAC1C,MAAM,qBAAqB,KAAK,QAAQ,cAAc,EACtD,MAAM,gBAAgB,MAAM,IAAI,EAChC,QAAQ;AACb;AAEA,eAAe,oBACb,IACA,MACA,UAC+B;AAC/B,QAAM,KAAK,GAAG,UAAmC;AACjD,MAAI,QAAQ,GACT,WAAW,uBAAuB,EAClC;AAAA,IAAS;AAAA,IAAyC,CAAC,SAClD,KACG,MAAM,cAAc,KAAK,MAAM,EAC/B,MAAM,eAAe,KAAK,aAAa,EACvC,MAAM,qBAAqB,KAAK,mBAAmB,EACnD,GAAG,gBAAgB,KAAK,IAAI,EAC5B,GAAG,gBAAgB,MAAM,IAAI;AAAA,EAClC,EACC;AAAA,IAAS;AAAA,IAAwB,CAAC,SACjC,KACG,MAAM,SAAS,KAAK,oBAAoB,EACxC,MAAM,gBAAgB,KAAK,aAAa,EACxC,MAAM,sBAAsB,KAAK,mBAAmB,EACpD,GAAG,iBAAiB,MAAM,IAAI;AAAA,EACnC,EACC,OAAO,CAAC,GAAG,aAAa,CAAC,EACzB,MAAM,UAAU,KAAK,IAAI,EACzB,MAAM,gBAAgB,MAAM,IAAI;AACnC,MAAI,SAAU,SAAQ,MAAM,MAAM,eAAe,KAAK,QAAQ;AAC9D,SAAO,MAAM,MAAM,CAAC,EAAE,QAAQ;AAChC;AAEA,SAAS,iBAAiB,MAAsC;AAC9D,SAAO,KAAK,QAAQ,CAAC,QAAS,IAAI,2BAA2B,CAAC,yBAAyB,IAAI,wBAAwB,CAAC,IAAI,CAAC,CAAE;AAC7H;AAEA,eAAe,qBACb,WACA,SAC2B;AAC3B,QAAM,QAAQ,yBAAyB,SAAS;AAChD,QAAM,MAAM,CAAC,kBAAkB,QAAQ,eAAe;AACtD,QAAM,SAAS,MAAM,MAAM,IAA2B,GAAG;AACzD,MAAI,UAAU,MAAM,QAAQ,OAAO,UAAU,EAAG,QAAO,OAAO;AAC9D,QAAM,OAAO,MAAM,sBAAsB,qBAAqB,SAAS,GAAG,OAAO;AACjF,QAAM,aAAa,KAAK,IAAI,WAAW;AACvC,QAAM,OAAO;AAAA,IACX,0BAA0B,QAAQ,eAAe;AAAA,IACjD,mBAAmB,QAAQ,QAAQ;AAAA,IACnC,GAAG,WAAW,IAAI,CAAC,cAAc,kBAAkB,UAAU,MAAM,EAAE,CAAC;AAAA,IACtE,GAAG,iBAAiB,IAAI;AAAA,EAC1B;AACA,QAAM,MAAM;AAAA,IACV;AAAA,IACA,EAAE,WAAW;AAAA,IACb,EAAE,OAAO,yBAAyB,MAAM,MAAM,KAAK,IAAI,IAAI,IAAI,CAAC,EAAE;AAAA,EACpE;AACA,SAAO;AACT;AAEA,eAAe,mBACb,WACA,MACA,UAC2B;AAC3B,QAAM,QAAQ,yBAAyB,SAAS;AAChD,QAAM,MAAM,CAAC,QAAQ,YAAY,KAAK,IAAI;AAC1C,QAAM,SAAS,MAAM,MAAM,IAA2B,GAAG;AACzD,MAAI,UAAU,MAAM,QAAQ,OAAO,UAAU,EAAG,QAAO,OAAO;AAC9D,QAAM,OAAO,MAAM,oBAAoB,qBAAqB,SAAS,GAAG,MAAM,QAAQ;AACtF,QAAM,aAAa,KAAK,IAAI,WAAW;AACvC,MAAI,WAAW,SAAS,GAAG;AACzB,UAAM,MAAM;AAAA,MACV;AAAA,MACA,EAAE,WAAW;AAAA,MACb;AAAA,QACE,OAAO;AAAA,QACP,MAAM,MAAM;AAAA,UACV,oBAAI,IAAI,CAAC,GAAG,WAAW,IAAI,CAAC,cAAc,kBAAkB,UAAU,MAAM,EAAE,CAAC,GAAG,GAAG,iBAAiB,IAAI,CAAC,CAAC;AAAA,QAC9G;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,yBACb,WACA,OACA,uBACe;AACf,QAAM,QAAQ,yBAAyB,SAAS;AAChD,QAAM,MAAM,CAAC,iBAAiB,2BAA2B,MAAM,EAAE;AACjE,QAAM,kBAAkB,MAAM,MAAM,IAA4B,GAAG;AACnE,MAAI,gBAAiB;AACrB,QAAM,MAAM,IAAI,KAAK,EAAE,aAAY,oBAAI,KAAK,GAAE,YAAY,EAAE,GAAG,EAAE,OAAO,gCAAgC,CAAC;AACzG,MAAI,uBAAuB;AACzB,WAAO,KAAK,iGAAiG;AAAA,MAC3G,SAAS,MAAM;AAAA,MACf,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH,OAAO;AACL,WAAO,KAAK,uDAAuD;AAAA,MACjE,SAAS,MAAM;AAAA,MACf,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,IACxB,CAAC;AAAA,EACH;AACA,MAAI;AACF,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,QACE,IAAI,MAAM;AAAA,QACV,SAAS,MAAM;AAAA,QACf,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ;AAAA,MACV;AAAA,MACA,EAAE,YAAY,MAAM,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,IACrF;AAAA,EACF,SAAS,OAAO;AACd,WAAO,KAAK,+CAA+C,EAAE,SAAS,MAAM,IAAI,KAAK,MAAM,CAAC;AAC5F,wBAAoB,GAAG,YAAY,OAAO;AAAA,MACxC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,YAAY,EAAE,SAAS,MAAM,GAAG;AAAA,IAClC,CAAC;AAAA,EACH;AACF;AAEA,eAAe,kBACb,WACA,WACA,QACA,QACA,kBACwB;AACxB,QAAM,EAAE,MAAM,IAAI;AAClB,MAAI,MAAM,WAAW,SAAS;AAC5B,QAAI,sBAAsB,EAAG,OAAM,IAAI,0BAA0B,KAAK,aAAa;AACnF,UAAM,cAAc,sBAAsB,EAAE,QAAQ,SAAS,MAAM,GAAG,CAAC;AAAA,EACzE;AACA,MAAI,MAAM,WAAW,WAAY,OAAM,IAAI,0BAA0B,KAAK,gBAAgB;AAC1F,MAAI,CAAC,UAAU,SAAS;AACtB,UAAM,yBAAyB,WAAW,OAAO,UAAU,yBAAyB,IAAI;AACxF,UAAM,IAAI,0BAA0B,KAAK,qBAAqB;AAAA,EAChE;AACA,QAAM,SAAS,mBAAmB,OAAO,gBAAgB;AACzD,SAAO;AAAA,IACL;AAAA,IACA,OAAO;AAAA,MACL,IAAI,MAAM;AAAA,MACV,MAAM,MAAM;AAAA,MACZ,MAAM,MAAM;AAAA,MACZ,MAAM,MAAM;AAAA,MACZ,QAAQ;AAAA,MACR,eAAe,MAAM;AAAA,MACrB,kBAAkB,MAAM;AAAA,MACxB,qBAAqB,MAAM;AAAA,MAC3B,UAAU,MAAM;AAAA,IAClB;AAAA,IACA,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,SAAS,UAAU;AAAA,IACnB;AAAA,IACA,iBAAiB,OAAO;AAAA,IACxB,iBAAiB,OAAO;AAAA,IACxB,cAAc,MAAM;AAAA,EACtB;AACF;AAEA,eAAe,0BACb,WACA,SACA,UACA,kBACwB;AACxB,MAAI,CAAC,sBAAsB,EAAG,OAAM,IAAI,0BAA0B,KAAK,wBAAwB;AAC/F,QAAM,OAAO,QAAQ,KAAK,EAAE,YAAY;AACxC,MAAI,CAAC,KAAK,UAAU,KAAK,SAAS,mBAAmB,CAAC,6BAA6B,KAAK,IAAI,GAAG;AAC7F,UAAM,cAAc,gBAAgB,CAAC,CAAC;AAAA,EACxC;AACA,QAAM,aAAa,MAAM,mBAAmB,WAAW,MAAM,QAAQ;AACrE,MAAI,WAAW,WAAW,EAAG,OAAM,cAAc,kBAAkB,EAAE,KAAK,CAAC;AAC3E,MAAI,WAAW,SAAS,EAAG,OAAM,cAAc,kBAAkB,EAAE,KAAK,CAAC;AACzE,SAAO,kBAAkB,WAAW,WAAW,CAAC,GAAG,QAAQ,MAAM,gBAAgB;AACnF;AAEA,eAAsB,mBACpB,WACA,MACA,UAAqC,CAAC,GACd;AACxB,SAAO,0BAA0B,WAAW,MAAM,QAAQ,YAAY,MAAM;AAAA,IAC1E,aAAa,QAAQ,UAAU;AAAA,IAC/B,cAAc,QAAQ,gBAAgB;AAAA,IACtC,gBAAgB,QAAQ,kBAAkB;AAAA,EAC5C,CAAC;AACH;AAEA,eAAsB,wBACpB,WACA,SACA,UAA0C,CAAC,GACnB;AACxB,QAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,QAAM,mBAAmB,qBAAqB,SAAS,GAAG;AAC1D,QAAM,YAAY,IAAI,aAAa,IAAI,WAAW;AAClD,MAAI,cAAc,KAAM,QAAO,0BAA0B,WAAW,WAAW,MAAM,gBAAgB;AAErG,QAAM,OAAO,qBAAqB,eAAe,OAAO,KAAK,QAAQ,QAAQ,IAAI,MAAM,CAAC;AACxF,MAAI,CAAC,KAAM,OAAM,cAAc,gBAAgB,CAAC,CAAC;AACjD,QAAM,uBAAuB,WAAkC,WAAW,sBAAsB;AAChG,MAAI,CAAC,qBAAsB,OAAM,cAAc,8BAA8B,EAAE,KAAK,CAAC;AACrF,QAAM,UAAU,MAAM,qBAAqB,kBAAkB,IAAI;AACjE,MAAI,CAAC,WAAW,QAAQ,WAAW,SAAU,OAAM,cAAc,6BAA6B,EAAE,KAAK,CAAC;AAEtG,QAAM,aAAa,MAAM,qBAAqB,WAAW,OAAO;AAChE,QAAM,WAAW,yBAAyB,QAAQ,YAAY,IAAI,QAAQ;AAC1E,QAAM,YAAY,yBAAyB,YAAY,QAAQ;AAC/D,MAAI,CAAC,WAAW,SAAS;AACvB,UAAM,cAAc,4BAA4B;AAAA,MAC9C;AAAA,MACA,iBAAiB,QAAQ;AAAA,MACzB,cAAc,WAAW;AAAA,IAC3B,CAAC;AAAA,EACH;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,MACE,iBAAiB,QAAQ;AAAA,MACzB,UAAU,QAAQ;AAAA,MAClB,WAAW,UAAU,QAAQ;AAAA,MAC7B,YAAY,UAAU,QAAQ;AAAA,MAC9B,WAAW,UAAU,QAAQ;AAAA,IAC/B;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { invalidateForEvent, salesChannelEventTags } from "../lib/cacheInvalidation.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "sales.channel.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "ecommerce:sales-channel-cache-invalidation"
|
|
6
|
+
};
|
|
7
|
+
async function handle(payload, ctx) {
|
|
8
|
+
await invalidateForEvent(payload, ctx, salesChannelEventTags);
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
handle as default,
|
|
12
|
+
metadata
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=sales-channel-cache-invalidation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ecommerce/subscribers/sales-channel-cache-invalidation.ts"],
|
|
4
|
+
"sourcesContent": ["import { invalidateForEvent, salesChannelEventTags } from '../lib/cacheInvalidation'\nimport type { EcommerceSubscriberContext } from '../lib/subscriberSupport'\n\nexport const metadata = {\n event: 'sales.channel.*',\n persistent: false,\n id: 'ecommerce:sales-channel-cache-invalidation',\n}\n\nexport default async function handle(payload: unknown, ctx: EcommerceSubscriberContext): Promise<void> {\n await invalidateForEvent(payload, ctx, salesChannelEventTags)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB,6BAA6B;AAGnD,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,eAAO,OAA8B,SAAkB,KAAgD;AACrG,QAAM,mBAAmB,SAAS,KAAK,qBAAqB;AAC9D;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -6,6 +6,33 @@ function stableUuidFromString(input) {
|
|
|
6
6
|
const hex = Buffer.from(bytes).toString("hex");
|
|
7
7
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
8
8
|
}
|
|
9
|
+
async function ensureHealthProbeSchedule(ctx, legacyScheduleId) {
|
|
10
|
+
const cradle = ctx.container;
|
|
11
|
+
if (typeof cradle.hasRegistration !== "function" || !cradle.hasRegistration("schedulerService")) return;
|
|
12
|
+
const schedulerService = ctx.container.resolve("schedulerService");
|
|
13
|
+
if (legacyScheduleId && typeof schedulerService.unregister === "function") {
|
|
14
|
+
await schedulerService.unregister(legacyScheduleId);
|
|
15
|
+
}
|
|
16
|
+
await schedulerService.register({
|
|
17
|
+
id: stableUuidFromString(`integrations:health-probe:${ctx.tenantId}:${ctx.organizationId}`),
|
|
18
|
+
name: "Integration health probes",
|
|
19
|
+
description: "Runs outbound health checks for enabled integrations every 15 minutes.",
|
|
20
|
+
scopeType: "organization",
|
|
21
|
+
organizationId: ctx.organizationId,
|
|
22
|
+
tenantId: ctx.tenantId,
|
|
23
|
+
scheduleType: "interval",
|
|
24
|
+
scheduleValue: "15m",
|
|
25
|
+
timezone: "UTC",
|
|
26
|
+
targetType: "queue",
|
|
27
|
+
targetQueue: "integration-health-probe",
|
|
28
|
+
targetPayload: {
|
|
29
|
+
scope: { organizationId: ctx.organizationId, tenantId: ctx.tenantId }
|
|
30
|
+
},
|
|
31
|
+
sourceType: "module",
|
|
32
|
+
sourceModule: "integrations",
|
|
33
|
+
isEnabled: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
9
36
|
const setup = {
|
|
10
37
|
defaultRoleFeatures: {
|
|
11
38
|
superadmin: ["integrations.*", "integrations.view", "integrations.manage", "integrations.credentials.manage"],
|
|
@@ -13,30 +40,13 @@ const setup = {
|
|
|
13
40
|
employee: ["integrations.view"]
|
|
14
41
|
},
|
|
15
42
|
async seedDefaults({ container, organizationId, tenantId }) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
name: "Integration health probes",
|
|
24
|
-
description: "Runs outbound health checks for enabled integrations every 15 minutes.",
|
|
25
|
-
scopeType: "organization",
|
|
26
|
-
organizationId,
|
|
27
|
-
tenantId,
|
|
28
|
-
scheduleType: "interval",
|
|
29
|
-
scheduleValue: "15m",
|
|
30
|
-
timezone: "UTC",
|
|
31
|
-
targetType: "queue",
|
|
32
|
-
targetQueue: "integration-health-probe",
|
|
33
|
-
targetPayload: {
|
|
34
|
-
scope: { organizationId, tenantId }
|
|
35
|
-
},
|
|
36
|
-
sourceType: "module",
|
|
37
|
-
sourceModule: "integrations",
|
|
38
|
-
isEnabled: true
|
|
39
|
-
});
|
|
43
|
+
await ensureHealthProbeSchedule(
|
|
44
|
+
{ container, organizationId, tenantId },
|
|
45
|
+
stableUuidFromString(`integrations:health-probe:${tenantId}`)
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
async onOrganizationCreated({ container, organizationId, tenantId }) {
|
|
49
|
+
await ensureHealthProbeSchedule({ container, organizationId, tenantId });
|
|
40
50
|
}
|
|
41
51
|
};
|
|
42
52
|
var setup_default = setup;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/integrations/setup.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport crypto from 'node:crypto'\n\nfunction stableUuidFromString(input: string): string {\n const bytes = crypto.createHash('sha256').update(input).digest().subarray(0, 16)\n // RFC4122: set version (5) and variant (10xx)\n bytes[6] = (bytes[6] & 0x0f) | 0x50\n bytes[8] = (bytes[8] & 0x3f) | 0x80\n const hex = Buffer.from(bytes).toString('hex') // 32 hex chars\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'organization'\n organizationId: string\n tenantId: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload: Record<string, unknown>\n requireFeature?: string\n sourceType: 'module'\n sourceModule: string\n isEnabled?: boolean\n }) => Promise<void>\n}\n\
|
|
5
|
-
"mappings": "AACA,OAAO,YAAY;AAEnB,SAAS,qBAAqB,OAAuB;AACnD,QAAM,QAAQ,OAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,GAAG,EAAE;AAE/E,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,MAAM,OAAO,KAAK,KAAK,EAAE,SAAS,KAAK;AAC7C,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;
|
|
4
|
+
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport crypto from 'node:crypto'\n\nfunction stableUuidFromString(input: string): string {\n const bytes = crypto.createHash('sha256').update(input).digest().subarray(0, 16)\n // RFC4122: set version (5) and variant (10xx)\n bytes[6] = (bytes[6] & 0x0f) | 0x50\n bytes[8] = (bytes[8] & 0x3f) | 0x80\n const hex = Buffer.from(bytes).toString('hex') // 32 hex chars\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\ntype SchedulerServiceLike = {\n unregister?: (scheduleId: string) => Promise<void>\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'organization'\n organizationId: string\n tenantId: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload: Record<string, unknown>\n requireFeature?: string\n sourceType: 'module'\n sourceModule: string\n isEnabled?: boolean\n }) => Promise<void>\n}\n\nasync function ensureHealthProbeSchedule(ctx: {\n container: import('awilix').AwilixContainer\n organizationId: string\n tenantId: string\n}, legacyScheduleId?: string): Promise<void> {\n const cradle = ctx.container as { hasRegistration?: (name: string) => boolean }\n if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) return\n\n const schedulerService = ctx.container.resolve('schedulerService') as SchedulerServiceLike\n if (legacyScheduleId && typeof schedulerService.unregister === 'function') {\n await schedulerService.unregister(legacyScheduleId)\n }\n await schedulerService.register({\n id: stableUuidFromString(`integrations:health-probe:${ctx.tenantId}:${ctx.organizationId}`),\n name: 'Integration health probes',\n description: 'Runs outbound health checks for enabled integrations every 15 minutes.',\n scopeType: 'organization',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'integration-health-probe',\n targetPayload: {\n scope: { organizationId: ctx.organizationId, tenantId: ctx.tenantId },\n },\n sourceType: 'module',\n sourceModule: 'integrations',\n isEnabled: true,\n })\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: ['integrations.*', 'integrations.view', 'integrations.manage', 'integrations.credentials.manage'],\n admin: ['integrations.*', 'integrations.view', 'integrations.manage', 'integrations.credentials.manage'],\n employee: ['integrations.view'],\n },\n\n async seedDefaults({ container, organizationId, tenantId }) {\n await ensureHealthProbeSchedule(\n { container, organizationId, tenantId },\n stableUuidFromString(`integrations:health-probe:${tenantId}`),\n )\n },\n\n async onOrganizationCreated({ container, organizationId, tenantId }) {\n await ensureHealthProbeSchedule({ container, organizationId, tenantId })\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AACA,OAAO,YAAY;AAEnB,SAAS,qBAAqB,OAAuB;AACnD,QAAM,QAAQ,OAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,GAAG,EAAE;AAE/E,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,MAAM,OAAO,KAAK,KAAK,EAAE,SAAS,KAAK;AAC7C,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;AAwBA,eAAe,0BAA0B,KAItC,kBAA0C;AAC3C,QAAM,SAAS,IAAI;AACnB,MAAI,OAAO,OAAO,oBAAoB,cAAc,CAAC,OAAO,gBAAgB,kBAAkB,EAAG;AAEjG,QAAM,mBAAmB,IAAI,UAAU,QAAQ,kBAAkB;AACjE,MAAI,oBAAoB,OAAO,iBAAiB,eAAe,YAAY;AACzE,UAAM,iBAAiB,WAAW,gBAAgB;AAAA,EACpD;AACA,QAAM,iBAAiB,SAAS;AAAA,IAC9B,IAAI,qBAAqB,6BAA6B,IAAI,QAAQ,IAAI,IAAI,cAAc,EAAE;AAAA,IAC1F,MAAM;AAAA,IACN,aAAa;AAAA,IACb,WAAW;AAAA,IACX,gBAAgB,IAAI;AAAA,IACpB,UAAU,IAAI;AAAA,IACd,cAAc;AAAA,IACd,eAAe;AAAA,IACf,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,eAAe;AAAA,MACb,OAAO,EAAE,gBAAgB,IAAI,gBAAgB,UAAU,IAAI,SAAS;AAAA,IACtE;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,WAAW;AAAA,EACb,CAAC;AACH;AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,YAAY,CAAC,kBAAkB,qBAAqB,uBAAuB,iCAAiC;AAAA,IAC5G,OAAO,CAAC,kBAAkB,qBAAqB,uBAAuB,iCAAiC;AAAA,IACvG,UAAU,CAAC,mBAAmB;AAAA,EAChC;AAAA,EAEA,MAAM,aAAa,EAAE,WAAW,gBAAgB,SAAS,GAAG;AAC1D,UAAM;AAAA,MACJ,EAAE,WAAW,gBAAgB,SAAS;AAAA,MACtC,qBAAqB,6BAA6B,QAAQ,EAAE;AAAA,IAC9D;AAAA,EACF;AAAA,EAEA,MAAM,sBAAsB,EAAE,WAAW,gBAAgB,SAAS,GAAG;AACnE,UAAM,0BAA0B,EAAE,WAAW,gBAAgB,SAAS,CAAC;AAAA,EACzE;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -268,7 +268,7 @@ function MessagesInboxPageClient() {
|
|
|
268
268
|
const total = listQuery.data?.total ?? 0;
|
|
269
269
|
const totalIsCapped = listQuery.data?.totalIsCapped === true;
|
|
270
270
|
const totalPages = listQuery.data?.totalPages ?? 0;
|
|
271
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
271
|
+
return /* @__PURE__ */ jsxs("div", { className: "min-w-0 max-w-full space-y-4 [&_.isolate]:overflow-x-auto", children: [
|
|
272
272
|
/* @__PURE__ */ jsx(
|
|
273
273
|
DataTable,
|
|
274
274
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/components/MessagesInboxPageClient.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/messages/extension-points'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { useAppEventCoalesced } from '@open-mercato/ui/backend/injection/useAppEventCoalesced'\nimport { Archive, ChevronDown, FilePenLine, Inbox, Layers, Send } from 'lucide-react'\nimport { getMessageUiComponentRegistry } from './utils/typeUiRegistry'\nimport { DefaultMessageListItem } from './defaults/DefaultMessageListItem'\nimport { getMessageListParticipantLabel } from './messageListLabels'\nimport { toErrorMessage } from './message-detail/utils'\nimport { useMessagesInboxBulkActions, type MessageFolder } from './useMessagesInboxBulkActions'\nimport {\n buildMessagesInboxFilters,\n buildMessagesListParams,\n buildSenderOptionsFromMessages,\n type SenderOption,\n} from './inboxFilters'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('messages').child({ component: 'MessagesInboxPageClient' })\n\ntype MessageListItem = {\n id: string\n type: string\n subject: string\n bodyPreview: string\n senderUserId: string\n senderName?: string | null\n senderEmail?: string | null\n externalName?: string | null\n externalEmail?: string | null\n sourceEntityType?: string | null\n priority: string\n status: string\n isDraft: boolean\n hasObjects: boolean\n objectCount: number\n hasAttachments: boolean\n attachmentCount: number\n recipientCount?: number\n hasActions: boolean\n actionTaken?: string | null\n sentAt?: string | null\n readAt?: string | null\n threadId?: string | null\n}\n\ntype MessageListResponse = {\n items?: MessageListItem[]\n total?: number\n page?: number\n pageSize?: number\n totalPages?: number\n totalIsCapped?: boolean\n}\n\ntype MessageTypeItem = {\n type: string\n module: string\n labelKey: string\n ui?: {\n listItemComponent?: string | null\n } | null\n}\n\ntype UserListItem = {\n id: string\n email?: string | null\n name?: string | null\n}\n\nexport function MessagesInboxPageClient() {\n const router = useRouter()\n const t = useT()\n const queryClient = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n\n const invalidateMessageListQueries = React.useCallback(() => {\n void queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })\n }, [queryClient])\n\n useAppEventCoalesced('messages.message.*', invalidateMessageListQueries, [invalidateMessageListQueries])\n\n // Reconnect means events were missed while offline, so it refreshes uncoalesced.\n useAppEvent('om:bridge:reconnected', invalidateMessageListQueries, [invalidateMessageListQueries])\n\n const [folder, setFolder] = React.useState<MessageFolder>('inbox')\n const [folderMenuOpen, setFolderMenuOpen] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [page, setPage] = React.useState(1)\n const pageSize = 20\n const folderMenuRef = React.useRef<HTMLDivElement | null>(null)\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n const { bulkActions, selectionScopeKey, injectionContext, ConfirmDialogElement } = useMessagesInboxBulkActions<MessageListItem>({\n folder,\n page,\n search,\n filterValues,\n })\n\n const listQuery = useQuery({\n queryKey: [\n 'messages',\n 'list',\n folder,\n search,\n page,\n pageSize,\n JSON.stringify(filterValues),\n scopeVersion,\n ],\n queryFn: async () => {\n const params = buildMessagesListParams({\n folder,\n page,\n pageSize,\n search,\n filterValues,\n })\n\n const call = await apiCall<MessageListResponse>(`/api/messages?${params.toString()}`)\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadListFailed', 'Failed to load messages.'),\n )\n }\n\n return {\n items: Array.isArray(call.result?.items) ? call.result?.items ?? [] : [],\n total: Number(call.result?.total ?? 0),\n page: Number(call.result?.page ?? page),\n pageSize: Number(call.result?.pageSize ?? pageSize),\n totalPages: Number(call.result?.totalPages ?? 0),\n totalIsCapped: call.result?.totalIsCapped === true,\n }\n },\n })\n\n const messageTypesQuery = useQuery({\n queryKey: ['messages', 'types', scopeVersion],\n queryFn: async () => {\n const call = await apiCall<{ items?: MessageTypeItem[] }>('/api/messages/types')\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n )\n }\n return Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n },\n })\n\n React.useEffect(() => {\n if (!listQuery.error) return\n flash(\n listQuery.error instanceof Error\n ? listQuery.error.message\n : t('messages.errors.loadListFailed', 'Failed to load messages.'),\n 'error',\n )\n }, [listQuery.error, t])\n\n React.useEffect(() => {\n if (!messageTypesQuery.error) return\n flash(\n messageTypesQuery.error instanceof Error\n ? messageTypesQuery.error.message\n : t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n 'error',\n )\n }, [messageTypesQuery.error, t])\n\n const messageTypeLabelMap = React.useMemo(() => {\n const map: Record<string, string> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = t(item.labelKey, item.type)\n }\n return map\n }, [messageTypesQuery.data, t])\n\n const [senderOptions, setSenderOptions] = React.useState<SenderOption[]>([])\n const senderOptionsScopeRef = React.useRef(scopeVersion)\n\n const mergeSenderOptions = React.useCallback((incoming: SenderOption[]) => {\n if (incoming.length === 0) return\n setSenderOptions((prev) => {\n const map = new Map<string, SenderOption>()\n for (const opt of prev) map.set(opt.value, opt)\n for (const opt of incoming) map.set(opt.value, opt)\n return Array.from(map.values())\n })\n }, [])\n\n const loadSenderOptions = React.useCallback(async (query?: string) => {\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '20')\n if (query && query.trim().length > 0) {\n params.set('search', query.trim())\n }\n\n const call = await apiCall<{ items?: UserListItem[] }>(\n `/api/auth/users?${params.toString()}`,\n {\n headers: {\n 'x-om-forbidden-redirect': '0',\n },\n },\n ).catch(() => null)\n if (!call) return []\n if (!call.ok) return []\n\n const items = Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n const next: SenderOption[] = items.flatMap((item) => {\n if (!item || typeof item.id !== 'string' || item.id.trim().length === 0) return []\n const name = typeof item.name === 'string' && item.name.trim().length > 0 ? item.name.trim() : null\n const email = typeof item.email === 'string' && item.email.trim().length > 0 ? item.email.trim() : null\n const label = name ?? email ?? item.id\n return [{\n value: item.id,\n label,\n description: email && email !== label ? email : null,\n }]\n })\n if (senderOptionsScopeRef.current === scopeVersion) {\n mergeSenderOptions(next)\n }\n return next\n }, [mergeSenderOptions, scopeVersion])\n\n React.useEffect(() => {\n const items = listQuery.data?.items ?? []\n mergeSenderOptions(buildSenderOptionsFromMessages(items))\n }, [listQuery.data?.items, mergeSenderOptions])\n\n React.useEffect(() => {\n senderOptionsScopeRef.current = scopeVersion\n setSenderOptions([])\n loadSenderOptions().catch((error: unknown) => {\n logger.warn('Failed to load sender filter options', { err: error })\n })\n }, [loadSenderOptions, scopeVersion])\n\n const listItemComponentKeyByType = React.useMemo(() => {\n const map: Record<string, string | null> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = item.ui?.listItemComponent ?? null\n }\n return map\n }, [messageTypesQuery.data])\n\n const filters = React.useMemo<FilterDef[]>(() => {\n const typeOptions = (messageTypesQuery.data ?? []).map((item) => ({\n value: item.type,\n label: t(item.labelKey, item.type),\n }))\n\n return buildMessagesInboxFilters({\n t,\n typeOptions,\n senderOptions,\n loadSenderOptions,\n })\n }, [loadSenderOptions, messageTypesQuery.data, senderOptions, t])\n\n const columns = React.useMemo<ColumnDef<MessageListItem>[]>(() => [\n {\n accessorKey: 'message',\n header: t('messages.title', 'Messages'),\n meta: {\n truncate: false,\n maxWidth: '100%',\n },\n cell: ({ row }) => {\n const item = row.original\n const listItemComponentKey = listItemComponentKeyByType[item.type]\n const ListItemComponent = listItemComponentKey\n ? messageUiRegistry.listItemComponents[listItemComponentKey] ?? null\n : null\n const ComponentToUse = ListItemComponent || DefaultMessageListItem\n\n return (\n <ComponentToUse\n message={{\n id: item.id,\n type: item.type,\n typeLabel: messageTypeLabelMap[item.type] ?? item.type,\n subject: item.subject,\n body: item.bodyPreview,\n bodyFormat: 'text' as const,\n priority: (item.priority as 'low' | 'normal' | 'high' | 'urgent') ?? 'normal',\n sentAt: item.sentAt ? new Date(item.sentAt) : null,\n isDraft: item.isDraft,\n senderName: getMessageListParticipantLabel(item, folder, t),\n hasObjects: item.hasObjects,\n objectCount: item.objectCount,\n hasAttachments: item.hasAttachments,\n attachmentCount: item.attachmentCount,\n recipientCount: item.recipientCount ?? 0,\n hasActions: item.hasActions,\n actionTaken: item.actionTaken ?? null,\n unread: item.status === 'unread',\n }}\n onClick={() => router.push(`/backend/messages/${item.id}`)}\n />\n )\n },\n },\n ], [folder, listItemComponentKeyByType, messageTypeLabelMap, messageUiRegistry, router, t])\n\n const folderOptions = React.useMemo(() => [\n { id: 'inbox' as const, label: t('messages.folder.inbox', 'Inbox'), icon: Inbox },\n { id: 'sent' as const, label: t('messages.folder.sent', 'Sent'), icon: Send },\n { id: 'drafts' as const, label: t('messages.folder.drafts', 'Drafts'), icon: FilePenLine },\n { id: 'archived' as const, label: t('messages.folder.archived', 'Archived'), icon: Archive },\n { id: 'all' as const, label: t('messages.folder.all', 'All'), icon: Layers },\n ], [t])\n\n const activeFolderOption = folderOptions.find((option) => option.id === folder) ?? folderOptions[0]\n const ActiveFolderIcon = activeFolderOption.icon\n\n React.useEffect(() => {\n if (!folderMenuOpen) return\n const handleClickOutside = (event: MouseEvent) => {\n if (!folderMenuRef.current) return\n const target = event.target\n if (target instanceof Node && !folderMenuRef.current.contains(target)) {\n setFolderMenuOpen(false)\n }\n }\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setFolderMenuOpen(false)\n }\n document.addEventListener('mousedown', handleClickOutside)\n document.addEventListener('keydown', handleEscape)\n return () => {\n document.removeEventListener('mousedown', handleClickOutside)\n document.removeEventListener('keydown', handleEscape)\n }\n }, [folderMenuOpen])\n\n const rows = listQuery.data?.items ?? []\n const total = listQuery.data?.total ?? 0\n const totalIsCapped = listQuery.data?.totalIsCapped === true\n const totalPages = listQuery.data?.totalPages ?? 0\n\n return (\n <div className=\"space-y-4\">\n <DataTable\n title={t('messages.title', 'Messages')}\n titleHeadingLevel={1}\n // UMES extension surface \u2014 opt into widget injection at:\n // data-table:messages:columns / :row-actions / :bulk-actions / :filters / :toolbar / :search-trailing\n // (SPEC-045d \u00A79.3a \u2014 communication_channels hub renders channel badge + delivery status here)\n extensionTableId={extensionPoints.hosts.inboxTable.tableId}\n columns={columns}\n data={rows}\n bulkActions={bulkActions}\n selectionScopeKey={selectionScopeKey}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n searchPlaceholder={t('messages.searchPlaceholder', 'Search messages')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(value) => {\n setFilterValues(value)\n setPage(1)\n }}\n onFiltersClear={() => {\n setFilterValues({})\n setPage(1)\n }}\n injectionContext={injectionContext}\n isLoading={listQuery.isLoading || listQuery.isFetching}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n totalIsCapped,\n onPageChange: setPage,\n }}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative\" ref={folderMenuRef}>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n aria-expanded={folderMenuOpen}\n aria-haspopup=\"menu\"\n onClick={() => setFolderMenuOpen((value) => !value)}\n >\n <ActiveFolderIcon className=\"h-4 w-4\" aria-hidden />\n <span>{t('messages.folder.selector', 'Folder')}:</span>\n <span>{activeFolderOption.label}</span>\n <ChevronDown className=\"h-4 w-4 opacity-70\" aria-hidden />\n </Button>\n {folderMenuOpen ? (\n <div\n className=\"absolute right-0 z-dropdown mt-1 min-w-52 rounded-md border bg-background p-1 shadow\"\n role=\"menu\"\n >\n {folderOptions.map((option) => {\n const Icon = option.icon\n const isActive = option.id === folder\n return (\n <Button\n key={option.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n role=\"menuitemradio\"\n aria-checked={isActive}\n className={`w-full justify-start h-auto px-2 py-1.5 text-sm font-normal ${isActive ? 'bg-accent/60' : ''}`}\n onClick={() => {\n setFolder(option.id)\n setPage(1)\n setFolderMenuOpen(false)\n }}\n >\n <Icon className=\"h-4 w-4\" aria-hidden />\n <span>{option.label}</span>\n </Button>\n )\n })}\n </div>\n ) : null}\n </div>\n <Button asChild>\n <Link href=\"/backend/messages/compose\">{t('messages.compose', 'Compose message')}</Link>\n </Button>\n </div>\n }\n onRowClick={(row) => {\n router.push(`/backend/messages/${row.id}`)\n }}\n emptyState={(\n <ListEmptyState\n entityName={t('messages.title', 'Messages')}\n createHref=\"/backend/messages/compose\"\n createLabel={t('messages.compose', 'Compose message')}\n />\n )}\n embedded\n />\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/messages/extension-points'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { useAppEventCoalesced } from '@open-mercato/ui/backend/injection/useAppEventCoalesced'\nimport { Archive, ChevronDown, FilePenLine, Inbox, Layers, Send } from 'lucide-react'\nimport { getMessageUiComponentRegistry } from './utils/typeUiRegistry'\nimport { DefaultMessageListItem } from './defaults/DefaultMessageListItem'\nimport { getMessageListParticipantLabel } from './messageListLabels'\nimport { toErrorMessage } from './message-detail/utils'\nimport { useMessagesInboxBulkActions, type MessageFolder } from './useMessagesInboxBulkActions'\nimport {\n buildMessagesInboxFilters,\n buildMessagesListParams,\n buildSenderOptionsFromMessages,\n type SenderOption,\n} from './inboxFilters'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('messages').child({ component: 'MessagesInboxPageClient' })\n\ntype MessageListItem = {\n id: string\n type: string\n subject: string\n bodyPreview: string\n senderUserId: string\n senderName?: string | null\n senderEmail?: string | null\n externalName?: string | null\n externalEmail?: string | null\n sourceEntityType?: string | null\n priority: string\n status: string\n isDraft: boolean\n hasObjects: boolean\n objectCount: number\n hasAttachments: boolean\n attachmentCount: number\n recipientCount?: number\n hasActions: boolean\n actionTaken?: string | null\n sentAt?: string | null\n readAt?: string | null\n threadId?: string | null\n}\n\ntype MessageListResponse = {\n items?: MessageListItem[]\n total?: number\n page?: number\n pageSize?: number\n totalPages?: number\n totalIsCapped?: boolean\n}\n\ntype MessageTypeItem = {\n type: string\n module: string\n labelKey: string\n ui?: {\n listItemComponent?: string | null\n } | null\n}\n\ntype UserListItem = {\n id: string\n email?: string | null\n name?: string | null\n}\n\nexport function MessagesInboxPageClient() {\n const router = useRouter()\n const t = useT()\n const queryClient = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n\n const invalidateMessageListQueries = React.useCallback(() => {\n void queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })\n }, [queryClient])\n\n useAppEventCoalesced('messages.message.*', invalidateMessageListQueries, [invalidateMessageListQueries])\n\n // Reconnect means events were missed while offline, so it refreshes uncoalesced.\n useAppEvent('om:bridge:reconnected', invalidateMessageListQueries, [invalidateMessageListQueries])\n\n const [folder, setFolder] = React.useState<MessageFolder>('inbox')\n const [folderMenuOpen, setFolderMenuOpen] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [page, setPage] = React.useState(1)\n const pageSize = 20\n const folderMenuRef = React.useRef<HTMLDivElement | null>(null)\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n const { bulkActions, selectionScopeKey, injectionContext, ConfirmDialogElement } = useMessagesInboxBulkActions<MessageListItem>({\n folder,\n page,\n search,\n filterValues,\n })\n\n const listQuery = useQuery({\n queryKey: [\n 'messages',\n 'list',\n folder,\n search,\n page,\n pageSize,\n JSON.stringify(filterValues),\n scopeVersion,\n ],\n queryFn: async () => {\n const params = buildMessagesListParams({\n folder,\n page,\n pageSize,\n search,\n filterValues,\n })\n\n const call = await apiCall<MessageListResponse>(`/api/messages?${params.toString()}`)\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadListFailed', 'Failed to load messages.'),\n )\n }\n\n return {\n items: Array.isArray(call.result?.items) ? call.result?.items ?? [] : [],\n total: Number(call.result?.total ?? 0),\n page: Number(call.result?.page ?? page),\n pageSize: Number(call.result?.pageSize ?? pageSize),\n totalPages: Number(call.result?.totalPages ?? 0),\n totalIsCapped: call.result?.totalIsCapped === true,\n }\n },\n })\n\n const messageTypesQuery = useQuery({\n queryKey: ['messages', 'types', scopeVersion],\n queryFn: async () => {\n const call = await apiCall<{ items?: MessageTypeItem[] }>('/api/messages/types')\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n )\n }\n return Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n },\n })\n\n React.useEffect(() => {\n if (!listQuery.error) return\n flash(\n listQuery.error instanceof Error\n ? listQuery.error.message\n : t('messages.errors.loadListFailed', 'Failed to load messages.'),\n 'error',\n )\n }, [listQuery.error, t])\n\n React.useEffect(() => {\n if (!messageTypesQuery.error) return\n flash(\n messageTypesQuery.error instanceof Error\n ? messageTypesQuery.error.message\n : t('messages.errors.loadTypesFailed', 'Failed to load message types.'),\n 'error',\n )\n }, [messageTypesQuery.error, t])\n\n const messageTypeLabelMap = React.useMemo(() => {\n const map: Record<string, string> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = t(item.labelKey, item.type)\n }\n return map\n }, [messageTypesQuery.data, t])\n\n const [senderOptions, setSenderOptions] = React.useState<SenderOption[]>([])\n const senderOptionsScopeRef = React.useRef(scopeVersion)\n\n const mergeSenderOptions = React.useCallback((incoming: SenderOption[]) => {\n if (incoming.length === 0) return\n setSenderOptions((prev) => {\n const map = new Map<string, SenderOption>()\n for (const opt of prev) map.set(opt.value, opt)\n for (const opt of incoming) map.set(opt.value, opt)\n return Array.from(map.values())\n })\n }, [])\n\n const loadSenderOptions = React.useCallback(async (query?: string) => {\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '20')\n if (query && query.trim().length > 0) {\n params.set('search', query.trim())\n }\n\n const call = await apiCall<{ items?: UserListItem[] }>(\n `/api/auth/users?${params.toString()}`,\n {\n headers: {\n 'x-om-forbidden-redirect': '0',\n },\n },\n ).catch(() => null)\n if (!call) return []\n if (!call.ok) return []\n\n const items = Array.isArray(call.result?.items) ? call.result?.items ?? [] : []\n const next: SenderOption[] = items.flatMap((item) => {\n if (!item || typeof item.id !== 'string' || item.id.trim().length === 0) return []\n const name = typeof item.name === 'string' && item.name.trim().length > 0 ? item.name.trim() : null\n const email = typeof item.email === 'string' && item.email.trim().length > 0 ? item.email.trim() : null\n const label = name ?? email ?? item.id\n return [{\n value: item.id,\n label,\n description: email && email !== label ? email : null,\n }]\n })\n if (senderOptionsScopeRef.current === scopeVersion) {\n mergeSenderOptions(next)\n }\n return next\n }, [mergeSenderOptions, scopeVersion])\n\n React.useEffect(() => {\n const items = listQuery.data?.items ?? []\n mergeSenderOptions(buildSenderOptionsFromMessages(items))\n }, [listQuery.data?.items, mergeSenderOptions])\n\n React.useEffect(() => {\n senderOptionsScopeRef.current = scopeVersion\n setSenderOptions([])\n loadSenderOptions().catch((error: unknown) => {\n logger.warn('Failed to load sender filter options', { err: error })\n })\n }, [loadSenderOptions, scopeVersion])\n\n const listItemComponentKeyByType = React.useMemo(() => {\n const map: Record<string, string | null> = {}\n for (const item of messageTypesQuery.data ?? []) {\n map[item.type] = item.ui?.listItemComponent ?? null\n }\n return map\n }, [messageTypesQuery.data])\n\n const filters = React.useMemo<FilterDef[]>(() => {\n const typeOptions = (messageTypesQuery.data ?? []).map((item) => ({\n value: item.type,\n label: t(item.labelKey, item.type),\n }))\n\n return buildMessagesInboxFilters({\n t,\n typeOptions,\n senderOptions,\n loadSenderOptions,\n })\n }, [loadSenderOptions, messageTypesQuery.data, senderOptions, t])\n\n const columns = React.useMemo<ColumnDef<MessageListItem>[]>(() => [\n {\n accessorKey: 'message',\n header: t('messages.title', 'Messages'),\n meta: {\n truncate: false,\n maxWidth: '100%',\n },\n cell: ({ row }) => {\n const item = row.original\n const listItemComponentKey = listItemComponentKeyByType[item.type]\n const ListItemComponent = listItemComponentKey\n ? messageUiRegistry.listItemComponents[listItemComponentKey] ?? null\n : null\n const ComponentToUse = ListItemComponent || DefaultMessageListItem\n\n return (\n <ComponentToUse\n message={{\n id: item.id,\n type: item.type,\n typeLabel: messageTypeLabelMap[item.type] ?? item.type,\n subject: item.subject,\n body: item.bodyPreview,\n bodyFormat: 'text' as const,\n priority: (item.priority as 'low' | 'normal' | 'high' | 'urgent') ?? 'normal',\n sentAt: item.sentAt ? new Date(item.sentAt) : null,\n isDraft: item.isDraft,\n senderName: getMessageListParticipantLabel(item, folder, t),\n hasObjects: item.hasObjects,\n objectCount: item.objectCount,\n hasAttachments: item.hasAttachments,\n attachmentCount: item.attachmentCount,\n recipientCount: item.recipientCount ?? 0,\n hasActions: item.hasActions,\n actionTaken: item.actionTaken ?? null,\n unread: item.status === 'unread',\n }}\n onClick={() => router.push(`/backend/messages/${item.id}`)}\n />\n )\n },\n },\n ], [folder, listItemComponentKeyByType, messageTypeLabelMap, messageUiRegistry, router, t])\n\n const folderOptions = React.useMemo(() => [\n { id: 'inbox' as const, label: t('messages.folder.inbox', 'Inbox'), icon: Inbox },\n { id: 'sent' as const, label: t('messages.folder.sent', 'Sent'), icon: Send },\n { id: 'drafts' as const, label: t('messages.folder.drafts', 'Drafts'), icon: FilePenLine },\n { id: 'archived' as const, label: t('messages.folder.archived', 'Archived'), icon: Archive },\n { id: 'all' as const, label: t('messages.folder.all', 'All'), icon: Layers },\n ], [t])\n\n const activeFolderOption = folderOptions.find((option) => option.id === folder) ?? folderOptions[0]\n const ActiveFolderIcon = activeFolderOption.icon\n\n React.useEffect(() => {\n if (!folderMenuOpen) return\n const handleClickOutside = (event: MouseEvent) => {\n if (!folderMenuRef.current) return\n const target = event.target\n if (target instanceof Node && !folderMenuRef.current.contains(target)) {\n setFolderMenuOpen(false)\n }\n }\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setFolderMenuOpen(false)\n }\n document.addEventListener('mousedown', handleClickOutside)\n document.addEventListener('keydown', handleEscape)\n return () => {\n document.removeEventListener('mousedown', handleClickOutside)\n document.removeEventListener('keydown', handleEscape)\n }\n }, [folderMenuOpen])\n\n const rows = listQuery.data?.items ?? []\n const total = listQuery.data?.total ?? 0\n const totalIsCapped = listQuery.data?.totalIsCapped === true\n const totalPages = listQuery.data?.totalPages ?? 0\n\n return (\n <div className=\"min-w-0 max-w-full space-y-4 [&_.isolate]:overflow-x-auto\">\n <DataTable\n title={t('messages.title', 'Messages')}\n titleHeadingLevel={1}\n // UMES extension surface \u2014 opt into widget injection at:\n // data-table:messages:columns / :row-actions / :bulk-actions / :filters / :toolbar / :search-trailing\n // (SPEC-045d \u00A79.3a \u2014 communication_channels hub renders channel badge + delivery status here)\n extensionTableId={extensionPoints.hosts.inboxTable.tableId}\n columns={columns}\n data={rows}\n bulkActions={bulkActions}\n selectionScopeKey={selectionScopeKey}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n searchPlaceholder={t('messages.searchPlaceholder', 'Search messages')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(value) => {\n setFilterValues(value)\n setPage(1)\n }}\n onFiltersClear={() => {\n setFilterValues({})\n setPage(1)\n }}\n injectionContext={injectionContext}\n isLoading={listQuery.isLoading || listQuery.isFetching}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n totalIsCapped,\n onPageChange: setPage,\n }}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative\" ref={folderMenuRef}>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"gap-2\"\n aria-expanded={folderMenuOpen}\n aria-haspopup=\"menu\"\n onClick={() => setFolderMenuOpen((value) => !value)}\n >\n <ActiveFolderIcon className=\"h-4 w-4\" aria-hidden />\n <span>{t('messages.folder.selector', 'Folder')}:</span>\n <span>{activeFolderOption.label}</span>\n <ChevronDown className=\"h-4 w-4 opacity-70\" aria-hidden />\n </Button>\n {folderMenuOpen ? (\n <div\n className=\"absolute right-0 z-dropdown mt-1 min-w-52 rounded-md border bg-background p-1 shadow\"\n role=\"menu\"\n >\n {folderOptions.map((option) => {\n const Icon = option.icon\n const isActive = option.id === folder\n return (\n <Button\n key={option.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n role=\"menuitemradio\"\n aria-checked={isActive}\n className={`w-full justify-start h-auto px-2 py-1.5 text-sm font-normal ${isActive ? 'bg-accent/60' : ''}`}\n onClick={() => {\n setFolder(option.id)\n setPage(1)\n setFolderMenuOpen(false)\n }}\n >\n <Icon className=\"h-4 w-4\" aria-hidden />\n <span>{option.label}</span>\n </Button>\n )\n })}\n </div>\n ) : null}\n </div>\n <Button asChild>\n <Link href=\"/backend/messages/compose\">{t('messages.compose', 'Compose message')}</Link>\n </Button>\n </div>\n }\n onRowClick={(row) => {\n router.push(`/backend/messages/${row.id}`)\n }}\n emptyState={(\n <ListEmptyState\n entityName={t('messages.title', 'Messages')}\n createHref=\"/backend/messages/compose\"\n createLabel={t('messages.compose', 'Compose message')}\n />\n )}\n embedded\n />\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -68,9 +68,9 @@ function MainMessageHeader(props) {
|
|
|
68
68
|
mode: "detail",
|
|
69
69
|
backHref: "/backend/messages",
|
|
70
70
|
backLabel: t("messages.actions.backToList", "Back to messages"),
|
|
71
|
-
title: /* @__PURE__ */ jsxs("h1", { className: "
|
|
72
|
-
/* @__PURE__ */ jsx("span", { className: "
|
|
73
|
-
/* @__PURE__ */ jsx(PriorityBadge, { priority: props.priority })
|
|
71
|
+
title: /* @__PURE__ */ jsxs("h1", { className: "flex min-w-0 max-w-full flex-wrap items-center gap-2", children: [
|
|
72
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 break-words", title: props.subject, children: props.subject }),
|
|
73
|
+
/* @__PURE__ */ jsx(PriorityBadge, { priority: props.priority, className: "shrink-0" })
|
|
74
74
|
] }),
|
|
75
75
|
utilityActions: props.canReply ? /* @__PURE__ */ jsx(
|
|
76
76
|
IconButton,
|