@open-mercato/core 0.10.1-develop.7961.1.d65694a8e6 → 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/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/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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
import { getModules } from "@open-mercato/shared/lib/modules/registry";
|
|
3
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
4
|
+
const logger = createLogger("directory");
|
|
5
|
+
function createSeedContainer(ctx, em) {
|
|
6
|
+
const resolve = (name) => {
|
|
7
|
+
if (name === "em") return em;
|
|
8
|
+
return ctx.resolve(name);
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
resolve,
|
|
12
|
+
hasRegistration: (name) => {
|
|
13
|
+
try {
|
|
14
|
+
resolve(name);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const metadata = {
|
|
23
|
+
event: "directory.organization.created",
|
|
24
|
+
persistent: true,
|
|
25
|
+
id: "directory:seed-organization-defaults"
|
|
26
|
+
};
|
|
27
|
+
async function seedOrganizationDefaults(_payload, ctx) {
|
|
28
|
+
const tenantId = typeof ctx.tenantId === "string" ? ctx.tenantId : null;
|
|
29
|
+
const organizationId = typeof ctx.organizationId === "string" ? ctx.organizationId : null;
|
|
30
|
+
if (!tenantId || !organizationId) return;
|
|
31
|
+
const rootEm = ctx.resolve("em");
|
|
32
|
+
const failures = [];
|
|
33
|
+
for (const moduleEntry of getModules()) {
|
|
34
|
+
const onOrganizationCreated = moduleEntry.setup?.onOrganizationCreated;
|
|
35
|
+
if (!onOrganizationCreated) continue;
|
|
36
|
+
const em = rootEm.fork();
|
|
37
|
+
try {
|
|
38
|
+
await onOrganizationCreated({
|
|
39
|
+
em,
|
|
40
|
+
tenantId,
|
|
41
|
+
organizationId,
|
|
42
|
+
container: createSeedContainer(ctx, em)
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
failures.push(error);
|
|
46
|
+
logger.error("Organization defaults provisioning failed", {
|
|
47
|
+
moduleId: moduleEntry.id,
|
|
48
|
+
tenantId,
|
|
49
|
+
organizationId,
|
|
50
|
+
err: error
|
|
51
|
+
});
|
|
52
|
+
getTelemetryRuntime()?.reportError(error, {
|
|
53
|
+
module: "directory",
|
|
54
|
+
code: "directory.organization_defaults_provisioning_failed",
|
|
55
|
+
attributes: { moduleId: moduleEntry.id, tenantId, organizationId }
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (failures.length > 0) {
|
|
60
|
+
throw new AggregateError(failures, "One or more organization default hooks failed");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
seedOrganizationDefaults as default,
|
|
65
|
+
metadata
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=seedOrganizationDefaults.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/directory/subscribers/seedOrganizationDefaults.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getModules } from '@open-mercato/shared/lib/modules/registry'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nconst logger = createLogger('directory')\n\ntype OrganizationCreatedContext = {\n resolve: <T = unknown>(name: string) => T\n tenantId?: string | null\n organizationId?: string | null\n}\n\nfunction createSeedContainer(\n ctx: OrganizationCreatedContext,\n em: EntityManager,\n): AwilixContainer {\n const resolve = <T = unknown>(name: string): T => {\n if (name === 'em') return em as T\n return ctx.resolve<T>(name)\n }\n\n return {\n resolve,\n hasRegistration: (name: string) => {\n try {\n resolve(name)\n return true\n } catch {\n return false\n }\n },\n } as unknown as AwilixContainer\n}\n\nexport const metadata = {\n event: 'directory.organization.created',\n persistent: true,\n id: 'directory:seed-organization-defaults',\n}\n\nexport default async function seedOrganizationDefaults(\n _payload: unknown,\n ctx: OrganizationCreatedContext,\n): Promise<void> {\n const tenantId = typeof ctx.tenantId === 'string' ? ctx.tenantId : null\n const organizationId = typeof ctx.organizationId === 'string' ? ctx.organizationId : null\n if (!tenantId || !organizationId) return\n\n const rootEm = ctx.resolve<EntityManager>('em')\n const failures: unknown[] = []\n for (const moduleEntry of getModules()) {\n const onOrganizationCreated = moduleEntry.setup?.onOrganizationCreated\n if (!onOrganizationCreated) continue\n\n const em = rootEm.fork()\n try {\n await onOrganizationCreated({\n em,\n tenantId,\n organizationId,\n container: createSeedContainer(ctx, em),\n })\n } catch (error) {\n failures.push(error)\n logger.error('Organization defaults provisioning failed', {\n moduleId: moduleEntry.id,\n tenantId,\n organizationId,\n err: error,\n })\n getTelemetryRuntime()?.reportError(error, {\n module: 'directory',\n code: 'directory.organization_defaults_provisioning_failed',\n attributes: { moduleId: moduleEntry.id, tenantId, organizationId },\n })\n }\n }\n\n if (failures.length > 0) {\n throw new AggregateError(failures, 'One or more organization default hooks failed')\n }\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AAEpC,MAAM,SAAS,aAAa,WAAW;AAQvC,SAAS,oBACP,KACA,IACiB;AACjB,QAAM,UAAU,CAAc,SAAoB;AAChD,QAAI,SAAS,KAAM,QAAO;AAC1B,WAAO,IAAI,QAAW,IAAI;AAAA,EAC5B;AAEA,SAAO;AAAA,IACL;AAAA,IACA,iBAAiB,CAAC,SAAiB;AACjC,UAAI;AACF,gBAAQ,IAAI;AACZ,eAAO;AAAA,MACT,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,eAAO,yBACL,UACA,KACe;AACf,QAAM,WAAW,OAAO,IAAI,aAAa,WAAW,IAAI,WAAW;AACnE,QAAM,iBAAiB,OAAO,IAAI,mBAAmB,WAAW,IAAI,iBAAiB;AACrF,MAAI,CAAC,YAAY,CAAC,eAAgB;AAElC,QAAM,SAAS,IAAI,QAAuB,IAAI;AAC9C,QAAM,WAAsB,CAAC;AAC7B,aAAW,eAAe,WAAW,GAAG;AACtC,UAAM,wBAAwB,YAAY,OAAO;AACjD,QAAI,CAAC,sBAAuB;AAE5B,UAAM,KAAK,OAAO,KAAK;AACvB,QAAI;AACF,YAAM,sBAAsB;AAAA,QAC1B;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,oBAAoB,KAAK,EAAE;AAAA,MACxC,CAAC;AAAA,IACH,SAAS,OAAO;AACd,eAAS,KAAK,KAAK;AACnB,aAAO,MAAM,6CAA6C;AAAA,QACxD,UAAU,YAAY;AAAA,QACtB;AAAA,QACA;AAAA,QACA,KAAK;AAAA,MACP,CAAC;AACD,0BAAoB,GAAG,YAAY,OAAO;AAAA,QACxC,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,YAAY,EAAE,UAAU,YAAY,IAAI,UAAU,eAAe;AAAA,MACnE,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,SAAS,SAAS,GAAG;AACvB,UAAM,IAAI,eAAe,UAAU,+CAA+C;AAAA,EACpF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -68,7 +68,7 @@ const storefrontCategoryGetDoc = {
|
|
|
68
68
|
{ status: 404, description: "No store serves this host or path, or the category is not visible to this buyer", schema: storefrontErrorSchema },
|
|
69
69
|
{ status: 410, description: "Store archived", schema: storefrontErrorSchema },
|
|
70
70
|
{ status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
|
|
71
|
-
{ status: 503, description: "Store misconfigured (no default channel binding)", schema: storefrontErrorSchema }
|
|
71
|
+
{ status: 503, description: "Store misconfigured (no usable default channel binding)", schema: storefrontErrorSchema }
|
|
72
72
|
]
|
|
73
73
|
};
|
|
74
74
|
const openApi = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/ecommerce/api/storefront/categories/%5Bslug%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n ecommerceStorefrontCategoryLandingQuerySchema,\n ecommerceStorefrontCategoryParamsSchema,\n} from '../../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../../lib/storeContextService'\nimport { cachedGetStorefrontCategoryLanding } from '../../../../lib/storefrontCategoryCache'\nimport { parseStorefrontCategoryLandingQuery } from '../../../../lib/storefrontQuery'\nimport {\n storefrontErrorResponse,\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../../storefrontRouteSupport'\nimport { storefrontCategoryLandingResponseSchema } from '../openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/categories/[slug]',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=30, stale-while-revalidate=30'\nconst CATEGORY_NOT_FOUND = { error: 'category_not_found' } as const\n\ntype CategoryRouteParams = { slug?: string }\n\ntype CategoryRouteContext = { params?: CategoryRouteParams | Promise<CategoryRouteParams> }\n\nexport async function GET(req: Request, routeContext: CategoryRouteContext = {}) {\n try {\n const query = parseStorefrontCategoryLandingQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'categoryLanding')\n if (rateLimited) return rateLimited\n const params = ecommerceStorefrontCategoryParamsSchema.safeParse((await routeContext.params) ?? {})\n if (!params.success) return storefrontErrorResponse(404, { ...CATEGORY_NOT_FOUND })\n const landing = await cachedGetStorefrontCategoryLanding(container, context, params.data.slug, query)\n if (!landing) return storefrontErrorResponse(404, { ...CATEGORY_NOT_FOUND })\n const headers = storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL)\n if (landing.products.sortApproximate) headers['X-Sort-Approximate'] = 'true'\n if (landing.products.sortUnavailable) headers['X-Sort-Unavailable'] = 'true'\n return NextResponse.json(landing, { headers })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront category landing failed',\n code: 'ecommerce.storefront_category_landing_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontCategoryTag = 'Ecommerce'\n\nconst storefrontCategoryGetDoc: OpenApiMethodDoc = {\n summary: 'Get one storefront category landing for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns the category by slug with its breadcrumb, visible non-empty children and descendant-inclusive `productCount` within the buyer\\'s assortment (null past 10 000 products in the assortment), plus a full `GET /products` response filtered to the category (`products`, same query grammar except `categoryId` / `categorySlug`). A slug that does not exist, is inactive, deleted, of another tenant, under an inactive ancestor or outside the buyer\\'s effective assortment answers the same 404 `{ \"error\": \"category_not_found\" }`. `seo` fields are null until the catalog stores category SEO data. Anonymous responses are `public, max-age=30, stale-while-revalidate=30`; authenticated responses are `private, no-store`. The category block is cached server-side for 60 s per assortment scope; the embedded listing shares the `GET /products` cache. Rate limited per IP and store.',\n tags: [storefrontCategoryTag],\n pathParams: ecommerceStorefrontCategoryParamsSchema,\n query: ecommerceStorefrontCategoryLandingQuerySchema,\n responses: [\n { status: 200, description: 'The category landing as this buyer sees it.', schema: storefrontCategoryLandingResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path, or the category is not visible to this buyer', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontCategoryTag,\n summary: 'Public storefront category landing',\n methods: {\n GET: storefrontCategoryGetDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kCAAkC;AAE3C,SAAS,0CAA0C;AACnD,SAAS,2CAA2C;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,+CAA+C;AAEjD,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAChC,MAAM,qBAAqB,EAAE,OAAO,qBAAqB;AAMzD,eAAsB,IAAI,KAAc,eAAqC,CAAC,GAAG;AAC/E,MAAI;AACF,UAAM,QAAQ,oCAAoC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC/E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,iBAAiB;AAC/F,UAAI,YAAa,QAAO;AACxB,YAAM,SAAS,wCAAwC,UAAW,MAAM,aAAa,UAAW,CAAC,CAAC;AAClG,UAAI,CAAC,OAAO,QAAS,QAAO,wBAAwB,KAAK,EAAE,GAAG,mBAAmB,CAAC;AAClF,YAAM,UAAU,MAAM,mCAAmC,WAAW,SAAS,OAAO,KAAK,MAAM,KAAK;AACpG,UAAI,CAAC,QAAS,QAAO,wBAAwB,KAAK,EAAE,GAAG,mBAAmB,CAAC;AAC3E,YAAM,UAAU,yBAAyB,SAAS,uBAAuB;AACzE,UAAI,QAAQ,SAAS,gBAAiB,SAAQ,oBAAoB,IAAI;AACtE,UAAI,QAAQ,SAAS,gBAAiB,SAAQ,oBAAoB,IAAI;AACtE,aAAO,aAAa,KAAK,SAAS,EAAE,QAAQ,CAAC;AAAA,IAC/C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,wBAAwB;AAE9B,MAAM,2BAA6C;AAAA,EACjD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,qBAAqB;AAAA,EAC5B,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,+CAA+C,QAAQ,wCAAwC;AAAA,EAC7H;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,mFAAmF,QAAQ,sBAAsB;AAAA,IAC7I,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n ecommerceStorefrontCategoryLandingQuerySchema,\n ecommerceStorefrontCategoryParamsSchema,\n} from '../../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../../lib/storeContextService'\nimport { cachedGetStorefrontCategoryLanding } from '../../../../lib/storefrontCategoryCache'\nimport { parseStorefrontCategoryLandingQuery } from '../../../../lib/storefrontQuery'\nimport {\n storefrontErrorResponse,\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../../storefrontRouteSupport'\nimport { storefrontCategoryLandingResponseSchema } from '../openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/categories/[slug]',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=30, stale-while-revalidate=30'\nconst CATEGORY_NOT_FOUND = { error: 'category_not_found' } as const\n\ntype CategoryRouteParams = { slug?: string }\n\ntype CategoryRouteContext = { params?: CategoryRouteParams | Promise<CategoryRouteParams> }\n\nexport async function GET(req: Request, routeContext: CategoryRouteContext = {}) {\n try {\n const query = parseStorefrontCategoryLandingQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'categoryLanding')\n if (rateLimited) return rateLimited\n const params = ecommerceStorefrontCategoryParamsSchema.safeParse((await routeContext.params) ?? {})\n if (!params.success) return storefrontErrorResponse(404, { ...CATEGORY_NOT_FOUND })\n const landing = await cachedGetStorefrontCategoryLanding(container, context, params.data.slug, query)\n if (!landing) return storefrontErrorResponse(404, { ...CATEGORY_NOT_FOUND })\n const headers = storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL)\n if (landing.products.sortApproximate) headers['X-Sort-Approximate'] = 'true'\n if (landing.products.sortUnavailable) headers['X-Sort-Unavailable'] = 'true'\n return NextResponse.json(landing, { headers })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront category landing failed',\n code: 'ecommerce.storefront_category_landing_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontCategoryTag = 'Ecommerce'\n\nconst storefrontCategoryGetDoc: OpenApiMethodDoc = {\n summary: 'Get one storefront category landing for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns the category by slug with its breadcrumb, visible non-empty children and descendant-inclusive `productCount` within the buyer\\'s assortment (null past 10 000 products in the assortment), plus a full `GET /products` response filtered to the category (`products`, same query grammar except `categoryId` / `categorySlug`). A slug that does not exist, is inactive, deleted, of another tenant, under an inactive ancestor or outside the buyer\\'s effective assortment answers the same 404 `{ \"error\": \"category_not_found\" }`. `seo` fields are null until the catalog stores category SEO data. Anonymous responses are `public, max-age=30, stale-while-revalidate=30`; authenticated responses are `private, no-store`. The category block is cached server-side for 60 s per assortment scope; the embedded listing shares the `GET /products` cache. Rate limited per IP and store.',\n tags: [storefrontCategoryTag],\n pathParams: ecommerceStorefrontCategoryParamsSchema,\n query: ecommerceStorefrontCategoryLandingQuerySchema,\n responses: [\n { status: 200, description: 'The category landing as this buyer sees it.', schema: storefrontCategoryLandingResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path, or the category is not visible to this buyer', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontCategoryTag,\n summary: 'Public storefront category landing',\n methods: {\n GET: storefrontCategoryGetDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kCAAkC;AAE3C,SAAS,0CAA0C;AACnD,SAAS,2CAA2C;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,+CAA+C;AAEjD,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAChC,MAAM,qBAAqB,EAAE,OAAO,qBAAqB;AAMzD,eAAsB,IAAI,KAAc,eAAqC,CAAC,GAAG;AAC/E,MAAI;AACF,UAAM,QAAQ,oCAAoC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC/E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,iBAAiB;AAC/F,UAAI,YAAa,QAAO;AACxB,YAAM,SAAS,wCAAwC,UAAW,MAAM,aAAa,UAAW,CAAC,CAAC;AAClG,UAAI,CAAC,OAAO,QAAS,QAAO,wBAAwB,KAAK,EAAE,GAAG,mBAAmB,CAAC;AAClF,YAAM,UAAU,MAAM,mCAAmC,WAAW,SAAS,OAAO,KAAK,MAAM,KAAK;AACpG,UAAI,CAAC,QAAS,QAAO,wBAAwB,KAAK,EAAE,GAAG,mBAAmB,CAAC;AAC3E,YAAM,UAAU,yBAAyB,SAAS,uBAAuB;AACzE,UAAI,QAAQ,SAAS,gBAAiB,SAAQ,oBAAoB,IAAI;AACtE,UAAI,QAAQ,SAAS,gBAAiB,SAAQ,oBAAoB,IAAI;AACtE,aAAO,aAAa,KAAK,SAAS,EAAE,QAAQ,CAAC;AAAA,IAC/C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,wBAAwB;AAE9B,MAAM,2BAA6C;AAAA,EACjD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,qBAAqB;AAAA,EAC5B,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,+CAA+C,QAAQ,wCAAwC;AAAA,EAC7H;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,mFAAmF,QAAQ,sBAAsB;AAAA,IAC7I,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,2DAA2D,QAAQ,sBAAsB;AAAA,EACvH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -56,7 +56,7 @@ const storefrontCategoriesGetDoc = {
|
|
|
56
56
|
{ status: 404, description: "No store serves this host or path", schema: storefrontErrorSchema },
|
|
57
57
|
{ status: 410, description: "Store archived", schema: storefrontErrorSchema },
|
|
58
58
|
{ status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
|
|
59
|
-
{ status: 503, description: "Store misconfigured (no default channel binding)", schema: storefrontErrorSchema }
|
|
59
|
+
{ status: 503, description: "Store misconfigured (no usable default channel binding)", schema: storefrontErrorSchema }
|
|
60
60
|
]
|
|
61
61
|
};
|
|
62
62
|
const openApi = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/ecommerce/api/storefront/categories/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontCategoryTreeQuerySchema } from '../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport { cachedGetStorefrontCategoryTree } from '../../../lib/storefrontCategoryCache'\nimport { parseStorefrontCategoryTreeQuery } from '../../../lib/storefrontQuery'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../storefrontRouteSupport'\nimport { storefrontCategoryTreeResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/categories',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=300, stale-while-revalidate=60'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontCategoryTreeQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'categories')\n if (rateLimited) return rateLimited\n const body = await cachedGetStorefrontCategoryTree(container, context, query)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront category tree failed',\n code: 'ecommerce.storefront_categories_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontCategoriesTag = 'Ecommerce'\n\nconst storefrontCategoriesGetDoc: OpenApiMethodDoc = {\n summary: 'List the storefront category tree for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns the visible category tree below `parentId` (the roots when omitted), `depth` levels deep (all levels when omitted). `productCount` is descendant-inclusive and counts only products inside the buyer\\'s effective assortment; categories the buyer cannot browse are absent, and empty categories are absent unless `includeEmpty=true`. Past 10 000 products in the assortment the counts are not computed: every `productCount` is null and no category is dropped as empty. A `parentId` that is unknown or not visible to the buyer yields an empty tree, identical to a nonexistent one. A nested node beyond `depth` has empty `children` and `hasChildren: true`. Anonymous responses are `public, max-age=300, stale-while-revalidate=60`; authenticated responses are `private, no-store`. Cached server-side for 300 s per assortment scope. Rate limited per IP and store.',\n tags: [storefrontCategoriesTag],\n query: ecommerceStorefrontCategoryTreeQuerySchema,\n responses: [\n { status: 200, description: 'The category tree within the buyer\\'s assortment.', schema: storefrontCategoryTreeResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontCategoriesTag,\n summary: 'Public storefront category tree',\n methods: {\n GET: storefrontCategoriesGetDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,kDAAkD;AAC3D,SAAS,kCAAkC;AAE3C,SAAS,uCAAuC;AAChD,SAAS,wCAAwC;AACjD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4CAA4C;AAE9C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,iCAAiC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC5E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,YAAY;AAC1F,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,gCAAgC,WAAW,SAAS,KAAK;AAC5E,aAAO,aAAa,KAAK,MAAM,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IACxG,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,0BAA0B;AAEhC,MAAM,6BAA+C;AAAA,EACnD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,uBAAuB;AAAA,EAC9B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,oDAAqD,QAAQ,qCAAqC;AAAA,EAChI;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontCategoryTreeQuerySchema } from '../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport { cachedGetStorefrontCategoryTree } from '../../../lib/storefrontCategoryCache'\nimport { parseStorefrontCategoryTreeQuery } from '../../../lib/storefrontQuery'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../storefrontRouteSupport'\nimport { storefrontCategoryTreeResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/categories',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=300, stale-while-revalidate=60'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontCategoryTreeQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'categories')\n if (rateLimited) return rateLimited\n const body = await cachedGetStorefrontCategoryTree(container, context, query)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront category tree failed',\n code: 'ecommerce.storefront_categories_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontCategoriesTag = 'Ecommerce'\n\nconst storefrontCategoriesGetDoc: OpenApiMethodDoc = {\n summary: 'List the storefront category tree for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns the visible category tree below `parentId` (the roots when omitted), `depth` levels deep (all levels when omitted). `productCount` is descendant-inclusive and counts only products inside the buyer\\'s effective assortment; categories the buyer cannot browse are absent, and empty categories are absent unless `includeEmpty=true`. Past 10 000 products in the assortment the counts are not computed: every `productCount` is null and no category is dropped as empty. A `parentId` that is unknown or not visible to the buyer yields an empty tree, identical to a nonexistent one. A nested node beyond `depth` has empty `children` and `hasChildren: true`. Anonymous responses are `public, max-age=300, stale-while-revalidate=60`; authenticated responses are `private, no-store`. Cached server-side for 300 s per assortment scope. Rate limited per IP and store.',\n tags: [storefrontCategoriesTag],\n query: ecommerceStorefrontCategoryTreeQuerySchema,\n responses: [\n { status: 200, description: 'The category tree within the buyer\\'s assortment.', schema: storefrontCategoryTreeResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontCategoriesTag,\n summary: 'Public storefront category tree',\n methods: {\n GET: storefrontCategoriesGetDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,kDAAkD;AAC3D,SAAS,kCAAkC;AAE3C,SAAS,uCAAuC;AAChD,SAAS,wCAAwC;AACjD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4CAA4C;AAE9C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,iCAAiC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC5E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,YAAY;AAC1F,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,gCAAgC,WAAW,SAAS,KAAK;AAC5E,aAAO,aAAa,KAAK,MAAM,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IACxG,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,0BAA0B;AAEhC,MAAM,6BAA+C;AAAA,EACnD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,uBAAuB;AAAA,EAC9B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,oDAAqD,QAAQ,qCAAqC;AAAA,EAChI;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,2DAA2D,QAAQ,sBAAsB;AAAA,EACvH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -142,7 +142,7 @@ const storefrontContextGetDoc = {
|
|
|
142
142
|
{ status: 404, description: "No store serves this host or path", schema: storefrontErrorSchema },
|
|
143
143
|
{ status: 410, description: "Store archived", schema: storefrontErrorSchema },
|
|
144
144
|
{ status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
|
|
145
|
-
{ status: 503, description: "Store misconfigured (no default channel binding)", schema: storefrontErrorSchema }
|
|
145
|
+
{ status: 503, description: "Store misconfigured (no usable default channel binding)", schema: storefrontErrorSchema }
|
|
146
146
|
]
|
|
147
147
|
};
|
|
148
148
|
const openApi = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/ecommerce/api/storefront/context/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { CustomerEntity } from '@open-mercato/core/modules/customers/data/entities'\nimport { ecommercePriceDisplayModeSchema, ecommerceStorefrontContextQuerySchema } from '../../../data/validators'\nimport { parseStorefrontContextQuery } from '../../../lib/storefrontQuery'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport type { StoreContext } from '../../../lib/types'\nimport {\n runInStoreCacheTenant,\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n} from '../storefrontRouteSupport'\n\nexport const metadata = {\n path: '/ecommerce/storefront/context',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=60'\n\nexport type StorefrontBuyerNames = {\n displayName: string | null\n companyName: string | null\n}\n\nexport type StorefrontContextResponse = {\n store: {\n id: string\n code: string\n name: string\n slug: string\n status: 'active'\n defaultLocale: string\n supportedLocales: string[]\n defaultCurrencyCode: string\n settings: StoreContext['store']['settings']\n }\n effectiveLocale: string\n requestedLocale: string | null\n supportedLocales: string[]\n currencyCode: string\n buyer: {\n isAuthenticated: boolean\n taxMode: StoreContext['buyer']['taxMode']\n displayName: string | null\n companyName: string | null\n allowPurchaseOnAccount: boolean\n }\n}\n\nexport function projectStorefrontContext(context: StoreContext, names: StorefrontBuyerNames): StorefrontContextResponse {\n const { store, buyer } = context\n return {\n store: {\n id: store.id,\n code: store.code,\n name: store.name,\n slug: store.slug,\n status: store.status,\n defaultLocale: store.defaultLocale,\n supportedLocales: [...store.supportedLocales],\n defaultCurrencyCode: store.defaultCurrencyCode,\n settings: store.settings,\n },\n effectiveLocale: context.effectiveLocale,\n requestedLocale: context.requestedLocale,\n supportedLocales: [...store.supportedLocales],\n currencyCode: context.currencyCode,\n buyer: {\n isAuthenticated: buyer.isAuthenticated,\n taxMode: buyer.taxMode,\n displayName: buyer.isAuthenticated ? names.displayName : null,\n companyName: buyer.isAuthenticated ? names.companyName : null,\n allowPurchaseOnAccount: buyer.allowPurchaseOnAccount,\n },\n }\n}\n\nfunction nonEmptyOrNull(value: string | null | undefined): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value : null\n}\n\nasync function loadBuyerNames(em: EntityManager, context: StoreContext): Promise<StorefrontBuyerNames> {\n const { buyer } = context\n if (!buyer.isAuthenticated || !buyer.customerUserId) return { displayName: null, companyName: null }\n const scope = { tenantId: context.tenantId, organizationId: context.organizationId }\n const user = await findOneWithDecryption(\n em,\n CustomerUser,\n { id: buyer.customerUserId, ...scope, deletedAt: null },\n undefined,\n scope,\n )\n const company = buyer.companyId\n ? await findOneWithDecryption(\n em,\n CustomerEntity,\n { id: buyer.companyId, ...scope, deletedAt: null },\n undefined,\n scope,\n )\n : null\n return {\n displayName: nonEmptyOrNull(user?.displayName),\n companyName: nonEmptyOrNull(company?.displayName),\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontContextQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'context')\n if (rateLimited) return rateLimited\n const em = container.resolve('em') as EntityManager\n const names = await loadBuyerNames(em, context)\n const body = projectStorefrontContext(context, names)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront context resolution failed',\n code: 'ecommerce.storefront_context_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontContextTag = 'Ecommerce'\n\nexport const storefrontContextResponseSchema = z.object({\n store: z.object({\n id: z.string().uuid(),\n code: z.string(),\n name: z.string(),\n slug: z.string(),\n status: z.literal('active'),\n defaultLocale: z.string(),\n supportedLocales: z.array(z.string()),\n defaultCurrencyCode: z.string(),\n settings: z.record(z.string(), z.unknown()),\n }),\n effectiveLocale: z.string(),\n requestedLocale: z.string().nullable(),\n supportedLocales: z.array(z.string()),\n currencyCode: z.string(),\n buyer: z.object({\n isAuthenticated: z.boolean(),\n taxMode: ecommercePriceDisplayModeSchema,\n displayName: z.string().nullable(),\n companyName: z.string().nullable(),\n allowPurchaseOnAccount: z.boolean(),\n }),\n})\n\nconst storefrontContextBadRequestSchema = z.union([storefrontInvalidQueryErrorSchema, storefrontErrorSchema])\n\nconst storefrontContextGetDoc: OpenApiMethodDoc = {\n summary: 'Resolve the storefront context for the current host',\n description:\n 'Public, unauthenticated. Resolves the store from the Host header (or `storeSlug` when OM_ECOMMERCE_DEV_STORE_SLUG=true) and the optional portal session, and returns a safe projection of the store and buyer context. Anonymous responses are `public, max-age=60`; authenticated responses are `private, no-store`. Rate limited per IP and store.',\n tags: [storefrontContextTag],\n query: ecommerceStorefrontContextQuerySchema,\n responses: [\n { status: 200, description: 'Storefront context resolved.', schema: storefrontContextResponseSchema },\n ],\n errors: [\n {\n status: 400,\n description: 'Unknown, repeated or malformed query parameters, or storeSlug used while the development flag is off',\n schema: storefrontContextBadRequestSchema,\n },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontContextTag,\n summary: 'Public storefront context',\n methods: {\n GET: storefrontContextGetDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { CustomerEntity } from '@open-mercato/core/modules/customers/data/entities'\nimport { ecommercePriceDisplayModeSchema, ecommerceStorefrontContextQuerySchema } from '../../../data/validators'\nimport { parseStorefrontContextQuery } from '../../../lib/storefrontQuery'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport type { StoreContext } from '../../../lib/types'\nimport {\n runInStoreCacheTenant,\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n} from '../storefrontRouteSupport'\n\nexport const metadata = {\n path: '/ecommerce/storefront/context',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=60'\n\nexport type StorefrontBuyerNames = {\n displayName: string | null\n companyName: string | null\n}\n\nexport type StorefrontContextResponse = {\n store: {\n id: string\n code: string\n name: string\n slug: string\n status: 'active'\n defaultLocale: string\n supportedLocales: string[]\n defaultCurrencyCode: string\n settings: StoreContext['store']['settings']\n }\n effectiveLocale: string\n requestedLocale: string | null\n supportedLocales: string[]\n currencyCode: string\n buyer: {\n isAuthenticated: boolean\n taxMode: StoreContext['buyer']['taxMode']\n displayName: string | null\n companyName: string | null\n allowPurchaseOnAccount: boolean\n }\n}\n\nexport function projectStorefrontContext(context: StoreContext, names: StorefrontBuyerNames): StorefrontContextResponse {\n const { store, buyer } = context\n return {\n store: {\n id: store.id,\n code: store.code,\n name: store.name,\n slug: store.slug,\n status: store.status,\n defaultLocale: store.defaultLocale,\n supportedLocales: [...store.supportedLocales],\n defaultCurrencyCode: store.defaultCurrencyCode,\n settings: store.settings,\n },\n effectiveLocale: context.effectiveLocale,\n requestedLocale: context.requestedLocale,\n supportedLocales: [...store.supportedLocales],\n currencyCode: context.currencyCode,\n buyer: {\n isAuthenticated: buyer.isAuthenticated,\n taxMode: buyer.taxMode,\n displayName: buyer.isAuthenticated ? names.displayName : null,\n companyName: buyer.isAuthenticated ? names.companyName : null,\n allowPurchaseOnAccount: buyer.allowPurchaseOnAccount,\n },\n }\n}\n\nfunction nonEmptyOrNull(value: string | null | undefined): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value : null\n}\n\nasync function loadBuyerNames(em: EntityManager, context: StoreContext): Promise<StorefrontBuyerNames> {\n const { buyer } = context\n if (!buyer.isAuthenticated || !buyer.customerUserId) return { displayName: null, companyName: null }\n const scope = { tenantId: context.tenantId, organizationId: context.organizationId }\n const user = await findOneWithDecryption(\n em,\n CustomerUser,\n { id: buyer.customerUserId, ...scope, deletedAt: null },\n undefined,\n scope,\n )\n const company = buyer.companyId\n ? await findOneWithDecryption(\n em,\n CustomerEntity,\n { id: buyer.companyId, ...scope, deletedAt: null },\n undefined,\n scope,\n )\n : null\n return {\n displayName: nonEmptyOrNull(user?.displayName),\n companyName: nonEmptyOrNull(company?.displayName),\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontContextQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'context')\n if (rateLimited) return rateLimited\n const em = container.resolve('em') as EntityManager\n const names = await loadBuyerNames(em, context)\n const body = projectStorefrontContext(context, names)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront context resolution failed',\n code: 'ecommerce.storefront_context_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontContextTag = 'Ecommerce'\n\nexport const storefrontContextResponseSchema = z.object({\n store: z.object({\n id: z.string().uuid(),\n code: z.string(),\n name: z.string(),\n slug: z.string(),\n status: z.literal('active'),\n defaultLocale: z.string(),\n supportedLocales: z.array(z.string()),\n defaultCurrencyCode: z.string(),\n settings: z.record(z.string(), z.unknown()),\n }),\n effectiveLocale: z.string(),\n requestedLocale: z.string().nullable(),\n supportedLocales: z.array(z.string()),\n currencyCode: z.string(),\n buyer: z.object({\n isAuthenticated: z.boolean(),\n taxMode: ecommercePriceDisplayModeSchema,\n displayName: z.string().nullable(),\n companyName: z.string().nullable(),\n allowPurchaseOnAccount: z.boolean(),\n }),\n})\n\nconst storefrontContextBadRequestSchema = z.union([storefrontInvalidQueryErrorSchema, storefrontErrorSchema])\n\nconst storefrontContextGetDoc: OpenApiMethodDoc = {\n summary: 'Resolve the storefront context for the current host',\n description:\n 'Public, unauthenticated. Resolves the store from the Host header (or `storeSlug` when OM_ECOMMERCE_DEV_STORE_SLUG=true) and the optional portal session, and returns a safe projection of the store and buyer context. Anonymous responses are `public, max-age=60`; authenticated responses are `private, no-store`. Rate limited per IP and store.',\n tags: [storefrontContextTag],\n query: ecommerceStorefrontContextQuerySchema,\n responses: [\n { status: 200, description: 'Storefront context resolved.', schema: storefrontContextResponseSchema },\n ],\n errors: [\n {\n status: 400,\n description: 'Unknown, repeated or malformed query parameters, or storeSlug used while the development flag is off',\n schema: storefrontContextBadRequestSchema,\n },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontContextTag,\n summary: 'Public storefront context',\n methods: {\n GET: storefrontContextGetDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -68,7 +68,7 @@ const storefrontProductGetDoc = {
|
|
|
68
68
|
{ status: 404, description: "No store serves this host or path, or the product is not visible to this buyer", schema: storefrontErrorSchema },
|
|
69
69
|
{ status: 410, description: "Store archived", schema: storefrontErrorSchema },
|
|
70
70
|
{ status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
|
|
71
|
-
{ status: 503, description: "Store misconfigured (no default channel binding)", schema: storefrontErrorSchema }
|
|
71
|
+
{ status: 503, description: "Store misconfigured (no usable default channel binding)", schema: storefrontErrorSchema }
|
|
72
72
|
]
|
|
73
73
|
};
|
|
74
74
|
const openApi = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/ecommerce/api/storefront/products/%5BidOrHandle%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n ecommerceStorefrontProductDetailQuerySchema,\n ecommerceStorefrontProductParamsSchema,\n} from '../../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../../lib/storeContextService'\nimport { cachedGetStorefrontProductDetail } from '../../../../lib/storefrontProductCache'\nimport { parseStorefrontProductDetailQuery } from '../../../../lib/storefrontQuery'\nimport {\n storefrontErrorResponse,\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../../storefrontRouteSupport'\nimport { storefrontProductDetailResponseSchema } from '../openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/products/[idOrHandle]',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=60'\nconst PRODUCT_NOT_FOUND = { error: 'product_not_found' } as const\n\ntype ProductRouteParams = { idOrHandle?: string }\n\ntype ProductRouteContext = { params?: ProductRouteParams | Promise<ProductRouteParams> }\n\nexport async function GET(req: Request, routeContext: ProductRouteContext = {}) {\n try {\n const query = parseStorefrontProductDetailQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'productDetail')\n if (rateLimited) return rateLimited\n const params = ecommerceStorefrontProductParamsSchema.safeParse((await routeContext.params) ?? {})\n if (!params.success) return storefrontErrorResponse(404, { ...PRODUCT_NOT_FOUND })\n const detail = await cachedGetStorefrontProductDetail(container, context, params.data.idOrHandle, {\n variantId: query.variantId ?? null,\n locale: query.locale ?? null,\n })\n if (!detail) return storefrontErrorResponse(404, { ...PRODUCT_NOT_FOUND })\n return NextResponse.json(detail, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront product detail failed',\n code: 'ecommerce.storefront_product_detail_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontProductTag = 'Ecommerce'\n\nconst storefrontProductGetDoc: OpenApiMethodDoc = {\n summary: 'Get one storefront product for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns the product by UUID or handle with buyer-resolved prices per variant. A product that does not exist, is inactive, deleted, of another tenant or outside the buyer\\'s effective assortment answers the same 404 `{ \"error\": \"product_not_found\" }`. `variantId` preselects a variant; top-level `priceTiers` are the selected variant\\'s tiers. Anonymous responses are `public, max-age=60`; authenticated responses are `private, no-store`. Cached server-side for 60 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontProductTag],\n pathParams: ecommerceStorefrontProductParamsSchema,\n query: ecommerceStorefrontProductDetailQuerySchema,\n responses: [\n { status: 200, description: 'The product as this buyer sees it.', schema: storefrontProductDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path, or the product is not visible to this buyer', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontProductTag,\n summary: 'Public storefront product detail',\n methods: {\n GET: storefrontProductGetDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kCAAkC;AAE3C,SAAS,wCAAwC;AACjD,SAAS,yCAAyC;AAClD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6CAA6C;AAE/C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAChC,MAAM,oBAAoB,EAAE,OAAO,oBAAoB;AAMvD,eAAsB,IAAI,KAAc,eAAoC,CAAC,GAAG;AAC9E,MAAI;AACF,UAAM,QAAQ,kCAAkC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC7E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,eAAe;AAC7F,UAAI,YAAa,QAAO;AACxB,YAAM,SAAS,uCAAuC,UAAW,MAAM,aAAa,UAAW,CAAC,CAAC;AACjG,UAAI,CAAC,OAAO,QAAS,QAAO,wBAAwB,KAAK,EAAE,GAAG,kBAAkB,CAAC;AACjF,YAAM,SAAS,MAAM,iCAAiC,WAAW,SAAS,OAAO,KAAK,YAAY;AAAA,QAChG,WAAW,MAAM,aAAa;AAAA,QAC9B,QAAQ,MAAM,UAAU;AAAA,MAC1B,CAAC;AACD,UAAI,CAAC,OAAQ,QAAO,wBAAwB,KAAK,EAAE,GAAG,kBAAkB,CAAC;AACzE,aAAO,aAAa,KAAK,QAAQ,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IAC1G,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,uBAAuB;AAE7B,MAAM,0BAA4C;AAAA,EAChD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,oBAAoB;AAAA,EAC3B,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,sCAAsC,QAAQ,sCAAsC;AAAA,EAClH;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,kFAAkF,QAAQ,sBAAsB;AAAA,IAC5I,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n ecommerceStorefrontProductDetailQuerySchema,\n ecommerceStorefrontProductParamsSchema,\n} from '../../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../../lib/storeContextService'\nimport { cachedGetStorefrontProductDetail } from '../../../../lib/storefrontProductCache'\nimport { parseStorefrontProductDetailQuery } from '../../../../lib/storefrontQuery'\nimport {\n storefrontErrorResponse,\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../../storefrontRouteSupport'\nimport { storefrontProductDetailResponseSchema } from '../openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/products/[idOrHandle]',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=60'\nconst PRODUCT_NOT_FOUND = { error: 'product_not_found' } as const\n\ntype ProductRouteParams = { idOrHandle?: string }\n\ntype ProductRouteContext = { params?: ProductRouteParams | Promise<ProductRouteParams> }\n\nexport async function GET(req: Request, routeContext: ProductRouteContext = {}) {\n try {\n const query = parseStorefrontProductDetailQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'productDetail')\n if (rateLimited) return rateLimited\n const params = ecommerceStorefrontProductParamsSchema.safeParse((await routeContext.params) ?? {})\n if (!params.success) return storefrontErrorResponse(404, { ...PRODUCT_NOT_FOUND })\n const detail = await cachedGetStorefrontProductDetail(container, context, params.data.idOrHandle, {\n variantId: query.variantId ?? null,\n locale: query.locale ?? null,\n })\n if (!detail) return storefrontErrorResponse(404, { ...PRODUCT_NOT_FOUND })\n return NextResponse.json(detail, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront product detail failed',\n code: 'ecommerce.storefront_product_detail_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontProductTag = 'Ecommerce'\n\nconst storefrontProductGetDoc: OpenApiMethodDoc = {\n summary: 'Get one storefront product for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns the product by UUID or handle with buyer-resolved prices per variant. A product that does not exist, is inactive, deleted, of another tenant or outside the buyer\\'s effective assortment answers the same 404 `{ \"error\": \"product_not_found\" }`. `variantId` preselects a variant; top-level `priceTiers` are the selected variant\\'s tiers. Anonymous responses are `public, max-age=60`; authenticated responses are `private, no-store`. Cached server-side for 60 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontProductTag],\n pathParams: ecommerceStorefrontProductParamsSchema,\n query: ecommerceStorefrontProductDetailQuerySchema,\n responses: [\n { status: 200, description: 'The product as this buyer sees it.', schema: storefrontProductDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path, or the product is not visible to this buyer', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontProductTag,\n summary: 'Public storefront product detail',\n methods: {\n GET: storefrontProductGetDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kCAAkC;AAE3C,SAAS,wCAAwC;AACjD,SAAS,yCAAyC;AAClD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6CAA6C;AAE/C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAChC,MAAM,oBAAoB,EAAE,OAAO,oBAAoB;AAMvD,eAAsB,IAAI,KAAc,eAAoC,CAAC,GAAG;AAC9E,MAAI;AACF,UAAM,QAAQ,kCAAkC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC7E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,eAAe;AAC7F,UAAI,YAAa,QAAO;AACxB,YAAM,SAAS,uCAAuC,UAAW,MAAM,aAAa,UAAW,CAAC,CAAC;AACjG,UAAI,CAAC,OAAO,QAAS,QAAO,wBAAwB,KAAK,EAAE,GAAG,kBAAkB,CAAC;AACjF,YAAM,SAAS,MAAM,iCAAiC,WAAW,SAAS,OAAO,KAAK,YAAY;AAAA,QAChG,WAAW,MAAM,aAAa;AAAA,QAC9B,QAAQ,MAAM,UAAU;AAAA,MAC1B,CAAC;AACD,UAAI,CAAC,OAAQ,QAAO,wBAAwB,KAAK,EAAE,GAAG,kBAAkB,CAAC;AACzE,aAAO,aAAa,KAAK,QAAQ,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IAC1G,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,uBAAuB;AAE7B,MAAM,0BAA4C;AAAA,EAChD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,oBAAoB;AAAA,EAC3B,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,sCAAsC,QAAQ,sCAAsC;AAAA,EAClH;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,kFAAkF,QAAQ,sBAAsB;AAAA,IAC5I,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,2DAA2D,QAAQ,sBAAsB;AAAA,EACvH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -59,7 +59,7 @@ const storefrontProductsGetDoc = {
|
|
|
59
59
|
{ status: 404, description: "No store serves this host or path", schema: storefrontErrorSchema },
|
|
60
60
|
{ status: 410, description: "Store archived", schema: storefrontErrorSchema },
|
|
61
61
|
{ status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
|
|
62
|
-
{ status: 503, description: "Store misconfigured (no default channel binding)", schema: storefrontErrorSchema }
|
|
62
|
+
{ status: 503, description: "Store misconfigured (no usable default channel binding)", schema: storefrontErrorSchema }
|
|
63
63
|
]
|
|
64
64
|
};
|
|
65
65
|
const openApi = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/ecommerce/api/storefront/products/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontProductListQuerySchema } from '../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport { cachedListStorefrontProducts } from '../../../lib/storefrontProductCache'\nimport { parseStorefrontProductListQuery } from '../../../lib/storefrontQuery'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../storefrontRouteSupport'\nimport { storefrontProductListResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/products',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=30, stale-while-revalidate=30'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontProductListQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'products')\n if (rateLimited) return rateLimited\n const body = await cachedListStorefrontProducts(container, context, query)\n const headers = storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL)\n if (body.sortApproximate) headers['X-Sort-Approximate'] = 'true'\n if (body.sortUnavailable) headers['X-Sort-Unavailable'] = 'true'\n return NextResponse.json(body, { headers })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront product listing failed',\n code: 'ecommerce.storefront_products_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontProductsTag = 'Ecommerce'\n\nconst storefrontProductsGetDoc: OpenApiMethodDoc = {\n summary: 'List storefront products for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then lists the products of the buyer\\'s effective assortment with buyer-resolved prices. Unknown, repeated or malformed parameters are rejected with 400. Options use bracket notation (`options[color]=red,blue`). `availability=` is page-scoped: `total` counts the pre-availability set and a page may be short. Facets count each dimension against every other active filter (cross-exclusion); category, tag, option and product-type counts are shared per assortment scope (and empty past 10 000 products in the search universe), `priceRange` spans the buyer\\'s own prices over the filtered set without the price filter, and `availability` counts the returned page only. `X-Sort-Approximate: true` marks a price order past the sort cap; `X-Sort-Unavailable: true` marks a declined price sort. Anonymous responses are `public, max-age=30, stale-while-revalidate=30`; authenticated responses are `private, no-store`. Cached server-side for 30 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontProductsTag],\n query: ecommerceStorefrontProductListQuerySchema,\n responses: [\n { status: 200, description: 'Products of the buyer\\'s effective assortment.', schema: storefrontProductListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontProductsTag,\n summary: 'Public storefront product listing',\n methods: {\n GET: storefrontProductsGetDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,iDAAiD;AAC1D,SAAS,kCAAkC;AAE3C,SAAS,oCAAoC;AAC7C,SAAS,uCAAuC;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,gCAAgC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC3E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,UAAU;AACxF,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,6BAA6B,WAAW,SAAS,KAAK;AACzE,YAAM,UAAU,yBAAyB,SAAS,uBAAuB;AACzE,UAAI,KAAK,gBAAiB,SAAQ,oBAAoB,IAAI;AAC1D,UAAI,KAAK,gBAAiB,SAAQ,oBAAoB,IAAI;AAC1D,aAAO,aAAa,KAAK,MAAM,EAAE,QAAQ,CAAC;AAAA,IAC5C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,wBAAwB;AAE9B,MAAM,2BAA6C;AAAA,EACjD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,qBAAqB;AAAA,EAC5B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,iDAAkD,QAAQ,oCAAoC;AAAA,EAC5H;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontProductListQuerySchema } from '../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../lib/storeContextService'\nimport { cachedListStorefrontProducts } from '../../../lib/storefrontProductCache'\nimport { parseStorefrontProductListQuery } from '../../../lib/storefrontQuery'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../storefrontRouteSupport'\nimport { storefrontProductListResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/products',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=30, stale-while-revalidate=30'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontProductListQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'products')\n if (rateLimited) return rateLimited\n const body = await cachedListStorefrontProducts(container, context, query)\n const headers = storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL)\n if (body.sortApproximate) headers['X-Sort-Approximate'] = 'true'\n if (body.sortUnavailable) headers['X-Sort-Unavailable'] = 'true'\n return NextResponse.json(body, { headers })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront product listing failed',\n code: 'ecommerce.storefront_products_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontProductsTag = 'Ecommerce'\n\nconst storefrontProductsGetDoc: OpenApiMethodDoc = {\n summary: 'List storefront products for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then lists the products of the buyer\\'s effective assortment with buyer-resolved prices. Unknown, repeated or malformed parameters are rejected with 400. Options use bracket notation (`options[color]=red,blue`). `availability=` is page-scoped: `total` counts the pre-availability set and a page may be short. Facets count each dimension against every other active filter (cross-exclusion); category, tag, option and product-type counts are shared per assortment scope (and empty past 10 000 products in the search universe), `priceRange` spans the buyer\\'s own prices over the filtered set without the price filter, and `availability` counts the returned page only. `X-Sort-Approximate: true` marks a price order past the sort cap; `X-Sort-Unavailable: true` marks a declined price sort. Anonymous responses are `public, max-age=30, stale-while-revalidate=30`; authenticated responses are `private, no-store`. Cached server-side for 30 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontProductsTag],\n query: ecommerceStorefrontProductListQuerySchema,\n responses: [\n { status: 200, description: 'Products of the buyer\\'s effective assortment.', schema: storefrontProductListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontProductsTag,\n summary: 'Public storefront product listing',\n methods: {\n GET: storefrontProductsGetDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,iDAAiD;AAC1D,SAAS,kCAAkC;AAE3C,SAAS,oCAAoC;AAC7C,SAAS,uCAAuC;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,gCAAgC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC3E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,UAAU;AACxF,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,6BAA6B,WAAW,SAAS,KAAK;AACzE,YAAM,UAAU,yBAAyB,SAAS,uBAAuB;AACzE,UAAI,KAAK,gBAAiB,SAAQ,oBAAoB,IAAI;AAC1D,UAAI,KAAK,gBAAiB,SAAQ,oBAAoB,IAAI;AAC1D,aAAO,aAAa,KAAK,MAAM,EAAE,QAAQ,CAAC;AAAA,IAC5C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,wBAAwB;AAE9B,MAAM,2BAA6C;AAAA,EACjD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,qBAAqB;AAAA,EAC5B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,iDAAkD,QAAQ,oCAAoC;AAAA,EAC5H;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mDAAmD,QAAQ,kCAAkC;AAAA,IACzH,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,2DAA2D,QAAQ,sBAAsB;AAAA,EACvH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -56,7 +56,7 @@ const storefrontSearchSuggestGetDoc = {
|
|
|
56
56
|
{ status: 404, description: "No store serves this host or path", schema: storefrontErrorSchema },
|
|
57
57
|
{ status: 410, description: "Store archived", schema: storefrontErrorSchema },
|
|
58
58
|
{ status: 429, description: "Too many requests", schema: rateLimitErrorSchema },
|
|
59
|
-
{ status: 503, description: "Store misconfigured (no default channel binding)", schema: storefrontErrorSchema }
|
|
59
|
+
{ status: 503, description: "Store misconfigured (no usable default channel binding)", schema: storefrontErrorSchema }
|
|
60
60
|
]
|
|
61
61
|
};
|
|
62
62
|
const openApi = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/ecommerce/api/storefront/search/suggest/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontSearchSuggestQuerySchema } from '../../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../../lib/storeContextService'\nimport { parseStorefrontSearchSuggestQuery } from '../../../../lib/storefrontQuery'\nimport { cachedSuggestStorefrontSearch } from '../../../../lib/storefrontSearchCache'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../../storefrontRouteSupport'\nimport { storefrontSearchSuggestResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/search/suggest',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=30, stale-while-revalidate=30'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontSearchSuggestQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'searchSuggest')\n if (rateLimited) return rateLimited\n const body = await cachedSuggestStorefrontSearch(container, context, query)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront search suggest failed',\n code: 'ecommerce.storefront_search_suggest_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontSearchTag = 'Ecommerce'\n\nconst storefrontSearchSuggestGetDoc: OpenApiMethodDoc = {\n summary: 'Typeahead suggestions for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns up to `limit` products of the buyer\\'s effective assortment matching `q`, with the buyer\\'s formatted price, and up to `limit` visible categories whose localized name matches `q`. Products are ranked by the `tokens` or `pgvector` search strategy with the assortment scope inside the ranking query, or by the escaped `ILIKE` match when neither is available; the payload is identical either way. A `q` shorter than 2 characters returns empty arrays, never an error. Products whose availability policy hides them when out of stock are omitted. No facets are computed. Anonymous responses are `public, max-age=30, stale-while-revalidate=30`; authenticated responses are `private, no-store`. Cached server-side for 30 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontSearchTag],\n query: ecommerceStorefrontSearchSuggestQuerySchema,\n responses: [\n { status: 200, description: 'Products and categories matching the query.', schema: storefrontSearchSuggestResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing `q`, or unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontSearchTag,\n summary: 'Public storefront search suggestions',\n methods: {\n GET: storefrontSearchSuggestGetDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,mDAAmD;AAC5D,SAAS,kCAAkC;AAE3C,SAAS,yCAAyC;AAClD,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6CAA6C;AAE/C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,kCAAkC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC7E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,eAAe;AAC7F,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,8BAA8B,WAAW,SAAS,KAAK;AAC1E,aAAO,aAAa,KAAK,MAAM,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IACxG,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,sBAAsB;AAE5B,MAAM,gCAAkD;AAAA,EACtD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,mBAAmB;AAAA,EAC1B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,+CAA+C,QAAQ,sCAAsC;AAAA,EAC3H;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mEAAmE,QAAQ,kCAAkC;AAAA,IACzI,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { ecommerceStorefrontSearchSuggestQuerySchema } from '../../../../data/validators'\nimport { enforceStorefrontRateLimit } from '../../../../lib/storefrontRateLimit'\nimport type { StoreContextService } from '../../../../lib/storeContextService'\nimport { parseStorefrontSearchSuggestQuery } from '../../../../lib/storefrontQuery'\nimport { cachedSuggestStorefrontSearch } from '../../../../lib/storefrontSearchCache'\nimport {\n storefrontErrorSchema,\n storefrontInvalidQueryErrorSchema,\n storefrontRouteErrorResponse,\n storefrontSuccessHeaders,\n runInStoreCacheTenant,\n} from '../../storefrontRouteSupport'\nimport { storefrontSearchSuggestResponseSchema } from './openapiSchemas'\n\nexport const metadata = {\n path: '/ecommerce/storefront/search/suggest',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ANONYMOUS_CACHE_CONTROL = 'public, max-age=30, stale-while-revalidate=30'\n\nexport async function GET(req: Request) {\n try {\n const query = parseStorefrontSearchSuggestQuery(new URL(req.url).searchParams)\n const container = await createRequestContainer()\n const service = container.resolve('storeContextService') as StoreContextService\n const context = await service.resolve(req, { pathname: query.path ?? '/' })\n return await runInStoreCacheTenant(context, async () => {\n const rateLimited = await enforceStorefrontRateLimit(container, req, context, 'searchSuggest')\n if (rateLimited) return rateLimited\n const body = await cachedSuggestStorefrontSearch(container, context, query)\n return NextResponse.json(body, { headers: storefrontSuccessHeaders(context, ANONYMOUS_CACHE_CONTROL) })\n })\n } catch (error) {\n return storefrontRouteErrorResponse(error, {\n message: 'Storefront search suggest failed',\n code: 'ecommerce.storefront_search_suggest_failed',\n })\n }\n}\n\nexport default GET\n\nconst storefrontSearchTag = 'Ecommerce'\n\nconst storefrontSearchSuggestGetDoc: OpenApiMethodDoc = {\n summary: 'Typeahead suggestions for the current buyer',\n description:\n 'Public. Resolves the store from the Host header and the optional portal session, then returns up to `limit` products of the buyer\\'s effective assortment matching `q`, with the buyer\\'s formatted price, and up to `limit` visible categories whose localized name matches `q`. Products are ranked by the `tokens` or `pgvector` search strategy with the assortment scope inside the ranking query, or by the escaped `ILIKE` match when neither is available; the payload is identical either way. A `q` shorter than 2 characters returns empty arrays, never an error. Products whose availability policy hides them when out of stock are omitted. No facets are computed. Anonymous responses are `public, max-age=30, stale-while-revalidate=30`; authenticated responses are `private, no-store`. Cached server-side for 30 s per buyer context digest. Rate limited per IP and store.',\n tags: [storefrontSearchTag],\n query: ecommerceStorefrontSearchSuggestQuerySchema,\n responses: [\n { status: 200, description: 'Products and categories matching the query.', schema: storefrontSearchSuggestResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing `q`, or unknown, repeated or malformed query parameters', schema: storefrontInvalidQueryErrorSchema },\n { status: 401, description: 'Portal session invalid or issued for another tenant or organization', schema: storefrontErrorSchema },\n { status: 403, description: 'Draft store (only when OM_ECOMMERCE_DEV_STORE_SLUG=true)', schema: storefrontErrorSchema },\n { status: 404, description: 'No store serves this host or path', schema: storefrontErrorSchema },\n { status: 410, description: 'Store archived', schema: storefrontErrorSchema },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n { status: 503, description: 'Store misconfigured (no usable default channel binding)', schema: storefrontErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: storefrontSearchTag,\n summary: 'Public storefront search suggestions',\n methods: {\n GET: storefrontSearchSuggestGetDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B;AACrC,SAAS,mDAAmD;AAC5D,SAAS,kCAAkC;AAE3C,SAAS,yCAAyC;AAClD,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6CAA6C;AAE/C,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,EACf;AACF;AAEA,MAAM,0BAA0B;AAEhC,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,kCAAkC,IAAI,IAAI,IAAI,GAAG,EAAE,YAAY;AAC7E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,qBAAqB;AACvD,UAAM,UAAU,MAAM,QAAQ,QAAQ,KAAK,EAAE,UAAU,MAAM,QAAQ,IAAI,CAAC;AAC1E,WAAO,MAAM,sBAAsB,SAAS,YAAY;AACtD,YAAM,cAAc,MAAM,2BAA2B,WAAW,KAAK,SAAS,eAAe;AAC7F,UAAI,YAAa,QAAO;AACxB,YAAM,OAAO,MAAM,8BAA8B,WAAW,SAAS,KAAK;AAC1E,aAAO,aAAa,KAAK,MAAM,EAAE,SAAS,yBAAyB,SAAS,uBAAuB,EAAE,CAAC;AAAA,IACxG,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,6BAA6B,OAAO;AAAA,MACzC,SAAS;AAAA,MACT,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gBAAQ;AAEf,MAAM,sBAAsB;AAE5B,MAAM,gCAAkD;AAAA,EACtD,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,mBAAmB;AAAA,EAC1B,OAAO;AAAA,EACP,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,+CAA+C,QAAQ,sCAAsC;AAAA,EAC3H;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,mEAAmE,QAAQ,kCAAkC;AAAA,IACzI,EAAE,QAAQ,KAAK,aAAa,uEAAuE,QAAQ,sBAAsB;AAAA,IACjI,EAAE,QAAQ,KAAK,aAAa,4DAA4D,QAAQ,sBAAsB;AAAA,IACtH,EAAE,QAAQ,KAAK,aAAa,qCAAqC,QAAQ,sBAAsB;AAAA,IAC/F,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,sBAAsB;AAAA,IAC5E,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,qBAAqB;AAAA,IAC9E,EAAE,QAAQ,KAAK,aAAa,2DAA2D,QAAQ,sBAAsB;AAAA,EACvH;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,EACP;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ecommerceCustomerUserTag,
|
|
11
11
|
ecommerceDomainMappingTag,
|
|
12
12
|
ecommerceDomainTag,
|
|
13
|
+
ecommerceSalesChannelTag,
|
|
13
14
|
ecommerceStoreTag,
|
|
14
15
|
ecommerceUngroupedBuyerTag,
|
|
15
16
|
invalidateEcommerceCacheTags,
|
|
@@ -46,6 +47,7 @@ const storeDomainBindingEventTags = (payload) => [
|
|
|
46
47
|
)
|
|
47
48
|
];
|
|
48
49
|
const storeChannelBindingEventTags = (payload) => tagsFrom([readPayloadString(payload, "storeId"), readPayloadString(payload, "previousStoreId")], ecommerceStoreTag);
|
|
50
|
+
const salesChannelEventTags = (payload) => tagsFrom([readPayloadString(payload, "id")], ecommerceSalesChannelTag);
|
|
49
51
|
const domainMappingEventTags = (payload) => [
|
|
50
52
|
...tagsFrom(
|
|
51
53
|
[readPayloadString(payload, "id"), readPayloadString(payload, "replacedDomainId")],
|
|
@@ -179,6 +181,7 @@ export {
|
|
|
179
181
|
invalidateForPriceEvent,
|
|
180
182
|
invalidateForProductEvent,
|
|
181
183
|
invalidateForVariantEvent,
|
|
184
|
+
salesChannelEventTags,
|
|
182
185
|
storeChannelBindingEventTags,
|
|
183
186
|
storeDomainBindingEventTags,
|
|
184
187
|
storeEventTags
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ecommerce/lib/cacheInvalidation.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport {\n catalogCategoryTag,\n catalogPriceTag,\n catalogProductTag,\n catalogProductsTag,\n ecommerceCustomerGroupTag,\n ecommerceCustomerTag,\n ecommerceCustomerUserTag,\n ecommerceDomainMappingTag,\n ecommerceDomainTag,\n ecommerceStoreTag,\n ecommerceUngroupedBuyerTag,\n invalidateEcommerceCacheTags,\n storefrontAvailabilityTag,\n} from './cacheKeys'\nimport {\n asPayloadRecord,\n eventAction,\n readEventScope,\n readPayloadString,\n tryResolveService,\n type EcommerceSubscriberContext,\n} from './subscriberSupport'\n\n/**\n * Event \u2192 cache-tag mapping for the storefront caches (SPEC-029 \u00A78 table). Every eviction goes\n * through `invalidateEcommerceCacheTags`, which clears the tenant-agnostic resolution cache and the\n * emitting tenant's buyer/storefront caches.\n */\n\nconst logger = createLogger('ecommerce').child({ component: 'cache-invalidation' })\n\ntype TagDeriver = (payload: Record<string, unknown>, action: string | null, tenantId: string | null) => string[]\n\nconst STORE_CACHE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted', 'branding_updated'])\nconst CUSTOMER_GROUP_CACHE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted'])\nconst CUSTOMER_GROUP_DEFAULT_CANDIDATE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated'])\nconst CATALOG_CRUD_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted'])\nconst CUSTOMER_USER_CACHE_ACTIONS: ReadonlySet<string> = new Set(['updated', 'deleted'])\n\nfunction tagsFrom(values: Array<string | null>, toTag: (value: string) => string): string[] {\n return values.filter((value): value is string => value !== null).map(toTag)\n}\n\nexport const storeEventTags: TagDeriver = (payload, action) => {\n if (action && !STORE_CACHE_ACTIONS.has(action)) return []\n return tagsFrom([readPayloadString(payload, 'id')], ecommerceStoreTag)\n}\n\nexport const storeDomainBindingEventTags: TagDeriver = (payload) => [\n ...tagsFrom(\n [readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')],\n ecommerceStoreTag,\n ),\n ...tagsFrom(\n [readPayloadString(payload, 'domainMappingId'), readPayloadString(payload, 'previousDomainMappingId')],\n ecommerceDomainMappingTag,\n ),\n]\n\nexport const storeChannelBindingEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')], ecommerceStoreTag)\n\nexport const domainMappingEventTags: TagDeriver = (payload) => [\n ...tagsFrom(\n [readPayloadString(payload, 'id'), readPayloadString(payload, 'replacedDomainId')],\n ecommerceDomainMappingTag,\n ),\n ...tagsFrom(\n [readPayloadString(payload, 'hostname'), readPayloadString(payload, 'replacedHostname')],\n ecommerceDomainTag,\n ),\n]\n\n/**\n * `customer_accounts.user.updated|deleted`: the user's buyer contexts, so a deactivated or deleted\n * portal user stops resolving to their cached identity (and contract prices) before the TTL ends.\n */\nexport const customerUserEventTags: TagDeriver = (payload, action) => {\n if (action && !CUSTOMER_USER_CACHE_ACTIONS.has(action)) return []\n return tagsFrom([readPayloadString(payload, 'id')], ecommerceCustomerUserTag)\n}\n\nexport const customerGroupMembershipEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'customerId')], ecommerceCustomerTag)\n\n/**\n * `customer_groups.group.*`: the group's buyer contexts. The payload carries no `isDefault`, so a\n * create or update also evicts the tenant's ungrouped buyer contexts, which would fall back to the\n * group if it became the tenant default; a replaced default is announced as its own update.\n */\nexport const customerGroupEventTags: TagDeriver = (payload, action, tenantId) => {\n if (action && !CUSTOMER_GROUP_CACHE_ACTIONS.has(action)) return []\n const tags = action === 'created' ? [] : tagsFrom([readPayloadString(payload, 'id')], ecommerceCustomerGroupTag)\n if (tenantId && (!action || CUSTOMER_GROUP_DEFAULT_CANDIDATE_ACTIONS.has(action))) {\n tags.push(ecommerceUngroupedBuyerTag(tenantId))\n }\n return tags\n}\n\nexport const customerGroupTermsEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'groupId')], ecommerceCustomerGroupTag)\n\nexport async function invalidateForEvent(\n payload: unknown,\n ctx: EcommerceSubscriberContext,\n deriveTags: TagDeriver,\n): Promise<number> {\n const record = asPayloadRecord(payload)\n const { tenantId } = readEventScope(record, ctx)\n const tags = deriveTags(record, eventAction(ctx.eventName), tenantId)\n if (tags.length === 0) return 0\n return invalidateEcommerceCacheTags(ctx, { tenantId, tags })\n}\n\ntype CatalogLookupDatabase = {\n catalog_product_variant_prices: {\n id: string\n tenant_id: string\n organization_id: string\n customer_id: string | null\n product_id: string | null\n variant_id: string | null\n }\n catalog_product_variants: {\n id: string\n tenant_id: string\n organization_id: string\n product_id: string\n }\n availability_policies: {\n id: string\n tenant_id: string\n organization_id: string\n product_id: string | null\n }\n}\n\ntype PriceOwner = { customerId: string | null; productId: string | null }\n\ntype LookupScope = { tenantId: string; organizationId: string | null }\n\nasync function readPriceOwner(em: EntityManager, priceId: string, scope: LookupScope): Promise<PriceOwner | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('catalog_product_variant_prices as price')\n .leftJoin('catalog_product_variants as variant', 'variant.id', 'price.variant_id')\n .select(['price.customer_id as customerId', 'price.product_id as productId', 'variant.product_id as variantProductId'])\n .where('price.id', '=', priceId)\n .where('price.tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('price.organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n if (!row) return null\n return { customerId: row.customerId ?? null, productId: row.productId ?? row.variantProductId ?? null }\n}\n\nasync function readVariantProductId(em: EntityManager, variantId: string, scope: LookupScope): Promise<string | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('catalog_product_variants')\n .select('product_id')\n .where('id', '=', variantId)\n .where('tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n return row?.product_id ?? null\n}\n\nasync function readPolicyProductId(em: EntityManager, policyId: string, scope: LookupScope): Promise<string | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('availability_policies')\n .select('product_id')\n .where('id', '=', policyId)\n .where('tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n return row?.product_id ?? null\n}\n\n/**\n * Reads the row an id-only catalog/availability payload points at. A missing `em`, a missing row\n * and a failed lookup all answer `undefined`, so the caller falls back to its broader tag.\n */\nasync function lookupForEvent<T>(\n ctx: EcommerceSubscriberContext,\n id: string,\n scope: LookupScope,\n lookup: (em: EntityManager, id: string, scope: LookupScope) => Promise<T | null>,\n failureCode: string,\n): Promise<T | undefined> {\n const em = tryResolveService<EntityManager>(ctx, 'em')\n if (!em) return undefined\n try {\n return (await lookup(em, id, scope)) ?? undefined\n } catch (error) {\n logger.warn('Storefront cache lookup failed; falling back to broader tags', { id, failureCode, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: failureCode,\n attributes: { id },\n })\n return undefined\n }\n}\n\ntype ScopedEvent = { id: string; action: string | null; scope: LookupScope }\n\nfunction readScopedEvent(payload: unknown, ctx: EcommerceSubscriberContext): ScopedEvent | null {\n const record = asPayloadRecord(payload)\n const id = readPayloadString(record, 'id')\n const scope = readEventScope(record, ctx)\n if (!id || !scope.tenantId) return null\n return { id, action: eventAction(ctx.eventName), scope: { tenantId: scope.tenantId, organizationId: scope.organizationId } }\n}\n\nfunction isCatalogCrudAction(action: string | null): boolean {\n return action === null || CATALOG_CRUD_ACTIONS.has(action)\n}\n\n/**\n * `catalog.price.*` (\u00A78 buyer context, Storefront Public API \u00A79): evicts the product's\n * `catalog-price` tag and every listing of the tenant, plus the owning customer's buyer context\n * when the row is customer-specific (`customerOverlayId`). The payload carries ids only, so the row\n * is read back; a deleted row is gone, so a delete evicts the listings only and the product detail\n * and buyer context fall back to their TTLs (catalog declares no price-kind events either).\n */\nexport async function invalidateForPriceEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const owner =\n event.action === 'deleted'\n ? undefined\n : await lookupForEvent(ctx, event.id, event.scope, readPriceOwner, 'ecommerce.price_cache_invalidation_failed')\n const tags = [catalogProductsTag(event.scope.tenantId)]\n if (owner?.productId) tags.push(catalogPriceTag(owner.productId))\n if (owner?.customerId) tags.push(ecommerceCustomerTag(owner.customerId))\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n\n/** `catalog.product.created|updated|deleted`: the product's detail entries and every listing of the tenant. */\nexport async function invalidateForProductEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n return invalidateEcommerceCacheTags(ctx, {\n tenantId: event.scope.tenantId,\n tags: [catalogProductTag(event.id), catalogProductsTag(event.scope.tenantId)],\n })\n}\n\n/**\n * `catalog.variant.*`: the variant's product (read back \u2014 the payload carries ids only) and every\n * listing of the tenant. A deleted variant row is gone, so its product detail falls back to its TTL.\n */\nexport async function invalidateForVariantEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const productId =\n event.action === 'deleted'\n ? undefined\n : await lookupForEvent(ctx, event.id, event.scope, readVariantProductId, 'ecommerce.variant_cache_invalidation_failed')\n const tags = [catalogProductsTag(event.scope.tenantId)]\n if (productId) tags.push(catalogProductTag(productId))\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n\n/** `catalog.category.*`: details showing the category and every listing (category names, filters, assortment). */\nexport async function invalidateForCategoryEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n return invalidateEcommerceCacheTags(ctx, {\n tenantId: event.scope.tenantId,\n tags: [catalogCategoryTag(event.id), catalogProductsTag(event.scope.tenantId)],\n })\n}\n\n/**\n * `availability.policy.*`: a policy tied to a product evicts that product and every listing; a\n * store- or tenant-wide policy (or one that cannot be read back) evicts every storefront product\n * entry of the tenant. Policies are soft-deleted, so a delete still reads back its product.\n */\nexport async function invalidateForAvailabilityPolicyEvent(\n payload: unknown,\n ctx: EcommerceSubscriberContext,\n): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const productId = await lookupForEvent(\n ctx,\n event.id,\n event.scope,\n readPolicyProductId,\n 'ecommerce.availability_policy_cache_invalidation_failed',\n )\n const tags = productId\n ? [catalogProductTag(productId), catalogProductsTag(event.scope.tenantId)]\n : [storefrontAvailabilityTag(event.scope.tenantId)]\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport {\n catalogCategoryTag,\n catalogPriceTag,\n catalogProductTag,\n catalogProductsTag,\n ecommerceCustomerGroupTag,\n ecommerceCustomerTag,\n ecommerceCustomerUserTag,\n ecommerceDomainMappingTag,\n ecommerceDomainTag,\n ecommerceSalesChannelTag,\n ecommerceStoreTag,\n ecommerceUngroupedBuyerTag,\n invalidateEcommerceCacheTags,\n storefrontAvailabilityTag,\n} from './cacheKeys'\nimport {\n asPayloadRecord,\n eventAction,\n readEventScope,\n readPayloadString,\n tryResolveService,\n type EcommerceSubscriberContext,\n} from './subscriberSupport'\n\n/**\n * Event \u2192 cache-tag mapping for the storefront caches (SPEC-029 \u00A78 table). Every eviction goes\n * through `invalidateEcommerceCacheTags`, which clears the tenant-agnostic resolution cache and the\n * emitting tenant's buyer/storefront caches.\n */\n\nconst logger = createLogger('ecommerce').child({ component: 'cache-invalidation' })\n\ntype TagDeriver = (payload: Record<string, unknown>, action: string | null, tenantId: string | null) => string[]\n\nconst STORE_CACHE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted', 'branding_updated'])\nconst CUSTOMER_GROUP_CACHE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted'])\nconst CUSTOMER_GROUP_DEFAULT_CANDIDATE_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated'])\nconst CATALOG_CRUD_ACTIONS: ReadonlySet<string> = new Set(['created', 'updated', 'deleted'])\nconst CUSTOMER_USER_CACHE_ACTIONS: ReadonlySet<string> = new Set(['updated', 'deleted'])\n\nfunction tagsFrom(values: Array<string | null>, toTag: (value: string) => string): string[] {\n return values.filter((value): value is string => value !== null).map(toTag)\n}\n\nexport const storeEventTags: TagDeriver = (payload, action) => {\n if (action && !STORE_CACHE_ACTIONS.has(action)) return []\n return tagsFrom([readPayloadString(payload, 'id')], ecommerceStoreTag)\n}\n\nexport const storeDomainBindingEventTags: TagDeriver = (payload) => [\n ...tagsFrom(\n [readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')],\n ecommerceStoreTag,\n ),\n ...tagsFrom(\n [readPayloadString(payload, 'domainMappingId'), readPayloadString(payload, 'previousDomainMappingId')],\n ecommerceDomainMappingTag,\n ),\n]\n\nexport const storeChannelBindingEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'storeId'), readPayloadString(payload, 'previousStoreId')], ecommerceStoreTag)\n\nexport const salesChannelEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'id')], ecommerceSalesChannelTag)\n\nexport const domainMappingEventTags: TagDeriver = (payload) => [\n ...tagsFrom(\n [readPayloadString(payload, 'id'), readPayloadString(payload, 'replacedDomainId')],\n ecommerceDomainMappingTag,\n ),\n ...tagsFrom(\n [readPayloadString(payload, 'hostname'), readPayloadString(payload, 'replacedHostname')],\n ecommerceDomainTag,\n ),\n]\n\n/**\n * `customer_accounts.user.updated|deleted`: the user's buyer contexts, so a deactivated or deleted\n * portal user stops resolving to their cached identity (and contract prices) before the TTL ends.\n */\nexport const customerUserEventTags: TagDeriver = (payload, action) => {\n if (action && !CUSTOMER_USER_CACHE_ACTIONS.has(action)) return []\n return tagsFrom([readPayloadString(payload, 'id')], ecommerceCustomerUserTag)\n}\n\nexport const customerGroupMembershipEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'customerId')], ecommerceCustomerTag)\n\n/**\n * `customer_groups.group.*`: the group's buyer contexts. The payload carries no `isDefault`, so a\n * create or update also evicts the tenant's ungrouped buyer contexts, which would fall back to the\n * group if it became the tenant default; a replaced default is announced as its own update.\n */\nexport const customerGroupEventTags: TagDeriver = (payload, action, tenantId) => {\n if (action && !CUSTOMER_GROUP_CACHE_ACTIONS.has(action)) return []\n const tags = action === 'created' ? [] : tagsFrom([readPayloadString(payload, 'id')], ecommerceCustomerGroupTag)\n if (tenantId && (!action || CUSTOMER_GROUP_DEFAULT_CANDIDATE_ACTIONS.has(action))) {\n tags.push(ecommerceUngroupedBuyerTag(tenantId))\n }\n return tags\n}\n\nexport const customerGroupTermsEventTags: TagDeriver = (payload) =>\n tagsFrom([readPayloadString(payload, 'groupId')], ecommerceCustomerGroupTag)\n\nexport async function invalidateForEvent(\n payload: unknown,\n ctx: EcommerceSubscriberContext,\n deriveTags: TagDeriver,\n): Promise<number> {\n const record = asPayloadRecord(payload)\n const { tenantId } = readEventScope(record, ctx)\n const tags = deriveTags(record, eventAction(ctx.eventName), tenantId)\n if (tags.length === 0) return 0\n return invalidateEcommerceCacheTags(ctx, { tenantId, tags })\n}\n\ntype CatalogLookupDatabase = {\n catalog_product_variant_prices: {\n id: string\n tenant_id: string\n organization_id: string\n customer_id: string | null\n product_id: string | null\n variant_id: string | null\n }\n catalog_product_variants: {\n id: string\n tenant_id: string\n organization_id: string\n product_id: string\n }\n availability_policies: {\n id: string\n tenant_id: string\n organization_id: string\n product_id: string | null\n }\n}\n\ntype PriceOwner = { customerId: string | null; productId: string | null }\n\ntype LookupScope = { tenantId: string; organizationId: string | null }\n\nasync function readPriceOwner(em: EntityManager, priceId: string, scope: LookupScope): Promise<PriceOwner | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('catalog_product_variant_prices as price')\n .leftJoin('catalog_product_variants as variant', 'variant.id', 'price.variant_id')\n .select(['price.customer_id as customerId', 'price.product_id as productId', 'variant.product_id as variantProductId'])\n .where('price.id', '=', priceId)\n .where('price.tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('price.organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n if (!row) return null\n return { customerId: row.customerId ?? null, productId: row.productId ?? row.variantProductId ?? null }\n}\n\nasync function readVariantProductId(em: EntityManager, variantId: string, scope: LookupScope): Promise<string | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('catalog_product_variants')\n .select('product_id')\n .where('id', '=', variantId)\n .where('tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n return row?.product_id ?? null\n}\n\nasync function readPolicyProductId(em: EntityManager, policyId: string, scope: LookupScope): Promise<string | null> {\n let query = em\n .getKysely<CatalogLookupDatabase>()\n .selectFrom('availability_policies')\n .select('product_id')\n .where('id', '=', policyId)\n .where('tenant_id', '=', scope.tenantId)\n if (scope.organizationId) query = query.where('organization_id', '=', scope.organizationId)\n const row = await query.executeTakeFirst()\n return row?.product_id ?? null\n}\n\n/**\n * Reads the row an id-only catalog/availability payload points at. A missing `em`, a missing row\n * and a failed lookup all answer `undefined`, so the caller falls back to its broader tag.\n */\nasync function lookupForEvent<T>(\n ctx: EcommerceSubscriberContext,\n id: string,\n scope: LookupScope,\n lookup: (em: EntityManager, id: string, scope: LookupScope) => Promise<T | null>,\n failureCode: string,\n): Promise<T | undefined> {\n const em = tryResolveService<EntityManager>(ctx, 'em')\n if (!em) return undefined\n try {\n return (await lookup(em, id, scope)) ?? undefined\n } catch (error) {\n logger.warn('Storefront cache lookup failed; falling back to broader tags', { id, failureCode, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: failureCode,\n attributes: { id },\n })\n return undefined\n }\n}\n\ntype ScopedEvent = { id: string; action: string | null; scope: LookupScope }\n\nfunction readScopedEvent(payload: unknown, ctx: EcommerceSubscriberContext): ScopedEvent | null {\n const record = asPayloadRecord(payload)\n const id = readPayloadString(record, 'id')\n const scope = readEventScope(record, ctx)\n if (!id || !scope.tenantId) return null\n return { id, action: eventAction(ctx.eventName), scope: { tenantId: scope.tenantId, organizationId: scope.organizationId } }\n}\n\nfunction isCatalogCrudAction(action: string | null): boolean {\n return action === null || CATALOG_CRUD_ACTIONS.has(action)\n}\n\n/**\n * `catalog.price.*` (\u00A78 buyer context, Storefront Public API \u00A79): evicts the product's\n * `catalog-price` tag and every listing of the tenant, plus the owning customer's buyer context\n * when the row is customer-specific (`customerOverlayId`). The payload carries ids only, so the row\n * is read back; a deleted row is gone, so a delete evicts the listings only and the product detail\n * and buyer context fall back to their TTLs (catalog declares no price-kind events either).\n */\nexport async function invalidateForPriceEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const owner =\n event.action === 'deleted'\n ? undefined\n : await lookupForEvent(ctx, event.id, event.scope, readPriceOwner, 'ecommerce.price_cache_invalidation_failed')\n const tags = [catalogProductsTag(event.scope.tenantId)]\n if (owner?.productId) tags.push(catalogPriceTag(owner.productId))\n if (owner?.customerId) tags.push(ecommerceCustomerTag(owner.customerId))\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n\n/** `catalog.product.created|updated|deleted`: the product's detail entries and every listing of the tenant. */\nexport async function invalidateForProductEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n return invalidateEcommerceCacheTags(ctx, {\n tenantId: event.scope.tenantId,\n tags: [catalogProductTag(event.id), catalogProductsTag(event.scope.tenantId)],\n })\n}\n\n/**\n * `catalog.variant.*`: the variant's product (read back \u2014 the payload carries ids only) and every\n * listing of the tenant. A deleted variant row is gone, so its product detail falls back to its TTL.\n */\nexport async function invalidateForVariantEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const productId =\n event.action === 'deleted'\n ? undefined\n : await lookupForEvent(ctx, event.id, event.scope, readVariantProductId, 'ecommerce.variant_cache_invalidation_failed')\n const tags = [catalogProductsTag(event.scope.tenantId)]\n if (productId) tags.push(catalogProductTag(productId))\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n\n/** `catalog.category.*`: details showing the category and every listing (category names, filters, assortment). */\nexport async function invalidateForCategoryEvent(payload: unknown, ctx: EcommerceSubscriberContext): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n return invalidateEcommerceCacheTags(ctx, {\n tenantId: event.scope.tenantId,\n tags: [catalogCategoryTag(event.id), catalogProductsTag(event.scope.tenantId)],\n })\n}\n\n/**\n * `availability.policy.*`: a policy tied to a product evicts that product and every listing; a\n * store- or tenant-wide policy (or one that cannot be read back) evicts every storefront product\n * entry of the tenant. Policies are soft-deleted, so a delete still reads back its product.\n */\nexport async function invalidateForAvailabilityPolicyEvent(\n payload: unknown,\n ctx: EcommerceSubscriberContext,\n): Promise<number> {\n const event = readScopedEvent(payload, ctx)\n if (!event || !isCatalogCrudAction(event.action)) return 0\n const productId = await lookupForEvent(\n ctx,\n event.id,\n event.scope,\n readPolicyProductId,\n 'ecommerce.availability_policy_cache_invalidation_failed',\n )\n const tags = productId\n ? [catalogProductTag(productId), catalogProductsTag(event.scope.tenantId)]\n : [storefrontAvailabilityTag(event.scope.tenantId)]\n return invalidateEcommerceCacheTags(ctx, { tenantId: event.scope.tenantId, tags })\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC;AAAA,EACE;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,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAQP,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,qBAAqB,CAAC;AAIlF,MAAM,sBAA2C,oBAAI,IAAI,CAAC,WAAW,WAAW,WAAW,kBAAkB,CAAC;AAC9G,MAAM,+BAAoD,oBAAI,IAAI,CAAC,WAAW,WAAW,SAAS,CAAC;AACnG,MAAM,2CAAgE,oBAAI,IAAI,CAAC,WAAW,SAAS,CAAC;AACpG,MAAM,uBAA4C,oBAAI,IAAI,CAAC,WAAW,WAAW,SAAS,CAAC;AAC3F,MAAM,8BAAmD,oBAAI,IAAI,CAAC,WAAW,SAAS,CAAC;AAEvF,SAAS,SAAS,QAA8B,OAA4C;AAC1F,SAAO,OAAO,OAAO,CAAC,UAA2B,UAAU,IAAI,EAAE,IAAI,KAAK;AAC5E;AAEO,MAAM,iBAA6B,CAAC,SAAS,WAAW;AAC7D,MAAI,UAAU,CAAC,oBAAoB,IAAI,MAAM,EAAG,QAAO,CAAC;AACxD,SAAO,SAAS,CAAC,kBAAkB,SAAS,IAAI,CAAC,GAAG,iBAAiB;AACvE;AAEO,MAAM,8BAA0C,CAAC,YAAY;AAAA,EAClE,GAAG;AAAA,IACD,CAAC,kBAAkB,SAAS,SAAS,GAAG,kBAAkB,SAAS,iBAAiB,CAAC;AAAA,IACrF;AAAA,EACF;AAAA,EACA,GAAG;AAAA,IACD,CAAC,kBAAkB,SAAS,iBAAiB,GAAG,kBAAkB,SAAS,yBAAyB,CAAC;AAAA,IACrG;AAAA,EACF;AACF;AAEO,MAAM,+BAA2C,CAAC,YACvD,SAAS,CAAC,kBAAkB,SAAS,SAAS,GAAG,kBAAkB,SAAS,iBAAiB,CAAC,GAAG,iBAAiB;AAE7G,MAAM,wBAAoC,CAAC,YAChD,SAAS,CAAC,kBAAkB,SAAS,IAAI,CAAC,GAAG,wBAAwB;AAEhE,MAAM,yBAAqC,CAAC,YAAY;AAAA,EAC7D,GAAG;AAAA,IACD,CAAC,kBAAkB,SAAS,IAAI,GAAG,kBAAkB,SAAS,kBAAkB,CAAC;AAAA,IACjF;AAAA,EACF;AAAA,EACA,GAAG;AAAA,IACD,CAAC,kBAAkB,SAAS,UAAU,GAAG,kBAAkB,SAAS,kBAAkB,CAAC;AAAA,IACvF;AAAA,EACF;AACF;AAMO,MAAM,wBAAoC,CAAC,SAAS,WAAW;AACpE,MAAI,UAAU,CAAC,4BAA4B,IAAI,MAAM,EAAG,QAAO,CAAC;AAChE,SAAO,SAAS,CAAC,kBAAkB,SAAS,IAAI,CAAC,GAAG,wBAAwB;AAC9E;AAEO,MAAM,mCAA+C,CAAC,YAC3D,SAAS,CAAC,kBAAkB,SAAS,YAAY,CAAC,GAAG,oBAAoB;AAOpE,MAAM,yBAAqC,CAAC,SAAS,QAAQ,aAAa;AAC/E,MAAI,UAAU,CAAC,6BAA6B,IAAI,MAAM,EAAG,QAAO,CAAC;AACjE,QAAM,OAAO,WAAW,YAAY,CAAC,IAAI,SAAS,CAAC,kBAAkB,SAAS,IAAI,CAAC,GAAG,yBAAyB;AAC/G,MAAI,aAAa,CAAC,UAAU,yCAAyC,IAAI,MAAM,IAAI;AACjF,SAAK,KAAK,2BAA2B,QAAQ,CAAC;AAAA,EAChD;AACA,SAAO;AACT;AAEO,MAAM,8BAA0C,CAAC,YACtD,SAAS,CAAC,kBAAkB,SAAS,SAAS,CAAC,GAAG,yBAAyB;AAE7E,eAAsB,mBACpB,SACA,KACA,YACiB;AACjB,QAAM,SAAS,gBAAgB,OAAO;AACtC,QAAM,EAAE,SAAS,IAAI,eAAe,QAAQ,GAAG;AAC/C,QAAM,OAAO,WAAW,QAAQ,YAAY,IAAI,SAAS,GAAG,QAAQ;AACpE,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,SAAO,6BAA6B,KAAK,EAAE,UAAU,KAAK,CAAC;AAC7D;AA6BA,eAAe,eAAe,IAAmB,SAAiB,OAAgD;AAChH,MAAI,QAAQ,GACT,UAAiC,EACjC,WAAW,yCAAyC,EACpD,SAAS,uCAAuC,cAAc,kBAAkB,EAChF,OAAO,CAAC,mCAAmC,iCAAiC,wCAAwC,CAAC,EACrH,MAAM,YAAY,KAAK,OAAO,EAC9B,MAAM,mBAAmB,KAAK,MAAM,QAAQ;AAC/C,MAAI,MAAM,eAAgB,SAAQ,MAAM,MAAM,yBAAyB,KAAK,MAAM,cAAc;AAChG,QAAM,MAAM,MAAM,MAAM,iBAAiB;AACzC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,EAAE,YAAY,IAAI,cAAc,MAAM,WAAW,IAAI,aAAa,IAAI,oBAAoB,KAAK;AACxG;AAEA,eAAe,qBAAqB,IAAmB,WAAmB,OAA4C;AACpH,MAAI,QAAQ,GACT,UAAiC,EACjC,WAAW,0BAA0B,EACrC,OAAO,YAAY,EACnB,MAAM,MAAM,KAAK,SAAS,EAC1B,MAAM,aAAa,KAAK,MAAM,QAAQ;AACzC,MAAI,MAAM,eAAgB,SAAQ,MAAM,MAAM,mBAAmB,KAAK,MAAM,cAAc;AAC1F,QAAM,MAAM,MAAM,MAAM,iBAAiB;AACzC,SAAO,KAAK,cAAc;AAC5B;AAEA,eAAe,oBAAoB,IAAmB,UAAkB,OAA4C;AAClH,MAAI,QAAQ,GACT,UAAiC,EACjC,WAAW,uBAAuB,EAClC,OAAO,YAAY,EACnB,MAAM,MAAM,KAAK,QAAQ,EACzB,MAAM,aAAa,KAAK,MAAM,QAAQ;AACzC,MAAI,MAAM,eAAgB,SAAQ,MAAM,MAAM,mBAAmB,KAAK,MAAM,cAAc;AAC1F,QAAM,MAAM,MAAM,MAAM,iBAAiB;AACzC,SAAO,KAAK,cAAc;AAC5B;AAMA,eAAe,eACb,KACA,IACA,OACA,QACA,aACwB;AACxB,QAAM,KAAK,kBAAiC,KAAK,IAAI;AACrD,MAAI,CAAC,GAAI,QAAO;AAChB,MAAI;AACF,WAAQ,MAAM,OAAO,IAAI,IAAI,KAAK,KAAM;AAAA,EAC1C,SAAS,OAAO;AACd,WAAO,KAAK,gEAAgE,EAAE,IAAI,aAAa,KAAK,MAAM,CAAC;AAC3G,wBAAoB,GAAG,YAAY,OAAO;AAAA,MACxC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,YAAY,EAAE,GAAG;AAAA,IACnB,CAAC;AACD,WAAO;AAAA,EACT;AACF;AAIA,SAAS,gBAAgB,SAAkB,KAAqD;AAC9F,QAAM,SAAS,gBAAgB,OAAO;AACtC,QAAM,KAAK,kBAAkB,QAAQ,IAAI;AACzC,QAAM,QAAQ,eAAe,QAAQ,GAAG;AACxC,MAAI,CAAC,MAAM,CAAC,MAAM,SAAU,QAAO;AACnC,SAAO,EAAE,IAAI,QAAQ,YAAY,IAAI,SAAS,GAAG,OAAO,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe,EAAE;AAC7H;AAEA,SAAS,oBAAoB,QAAgC;AAC3D,SAAO,WAAW,QAAQ,qBAAqB,IAAI,MAAM;AAC3D;AASA,eAAsB,wBAAwB,SAAkB,KAAkD;AAChH,QAAM,QAAQ,gBAAgB,SAAS,GAAG;AAC1C,MAAI,CAAC,SAAS,CAAC,oBAAoB,MAAM,MAAM,EAAG,QAAO;AACzD,QAAM,QACJ,MAAM,WAAW,YACb,SACA,MAAM,eAAe,KAAK,MAAM,IAAI,MAAM,OAAO,gBAAgB,2CAA2C;AAClH,QAAM,OAAO,CAAC,mBAAmB,MAAM,MAAM,QAAQ,CAAC;AACtD,MAAI,OAAO,UAAW,MAAK,KAAK,gBAAgB,MAAM,SAAS,CAAC;AAChE,MAAI,OAAO,WAAY,MAAK,KAAK,qBAAqB,MAAM,UAAU,CAAC;AACvE,SAAO,6BAA6B,KAAK,EAAE,UAAU,MAAM,MAAM,UAAU,KAAK,CAAC;AACnF;AAGA,eAAsB,0BAA0B,SAAkB,KAAkD;AAClH,QAAM,QAAQ,gBAAgB,SAAS,GAAG;AAC1C,MAAI,CAAC,SAAS,CAAC,oBAAoB,MAAM,MAAM,EAAG,QAAO;AACzD,SAAO,6BAA6B,KAAK;AAAA,IACvC,UAAU,MAAM,MAAM;AAAA,IACtB,MAAM,CAAC,kBAAkB,MAAM,EAAE,GAAG,mBAAmB,MAAM,MAAM,QAAQ,CAAC;AAAA,EAC9E,CAAC;AACH;AAMA,eAAsB,0BAA0B,SAAkB,KAAkD;AAClH,QAAM,QAAQ,gBAAgB,SAAS,GAAG;AAC1C,MAAI,CAAC,SAAS,CAAC,oBAAoB,MAAM,MAAM,EAAG,QAAO;AACzD,QAAM,YACJ,MAAM,WAAW,YACb,SACA,MAAM,eAAe,KAAK,MAAM,IAAI,MAAM,OAAO,sBAAsB,6CAA6C;AAC1H,QAAM,OAAO,CAAC,mBAAmB,MAAM,MAAM,QAAQ,CAAC;AACtD,MAAI,UAAW,MAAK,KAAK,kBAAkB,SAAS,CAAC;AACrD,SAAO,6BAA6B,KAAK,EAAE,UAAU,MAAM,MAAM,UAAU,KAAK,CAAC;AACnF;AAGA,eAAsB,2BAA2B,SAAkB,KAAkD;AACnH,QAAM,QAAQ,gBAAgB,SAAS,GAAG;AAC1C,MAAI,CAAC,SAAS,CAAC,oBAAoB,MAAM,MAAM,EAAG,QAAO;AACzD,SAAO,6BAA6B,KAAK;AAAA,IACvC,UAAU,MAAM,MAAM;AAAA,IACtB,MAAM,CAAC,mBAAmB,MAAM,EAAE,GAAG,mBAAmB,MAAM,MAAM,QAAQ,CAAC;AAAA,EAC/E,CAAC;AACH;AAOA,eAAsB,qCACpB,SACA,KACiB;AACjB,QAAM,QAAQ,gBAAgB,SAAS,GAAG;AAC1C,MAAI,CAAC,SAAS,CAAC,oBAAoB,MAAM,MAAM,EAAG,QAAO;AACzD,QAAM,YAAY,MAAM;AAAA,IACtB;AAAA,IACA,MAAM;AAAA,IACN,MAAM;AAAA,IACN;AAAA,IACA;AAAA,EACF;AACA,QAAM,OAAO,YACT,CAAC,kBAAkB,SAAS,GAAG,mBAAmB,MAAM,MAAM,QAAQ,CAAC,IACvE,CAAC,0BAA0B,MAAM,MAAM,QAAQ,CAAC;AACpD,SAAO,6BAA6B,KAAK,EAAE,UAAU,MAAM,MAAM,UAAU,KAAK,CAAC;AACnF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -69,6 +69,9 @@ function ecommerceDomainMappingTag(domainMappingId) {
|
|
|
69
69
|
function ecommerceDomainTag(hostname) {
|
|
70
70
|
return `ecommerce-domain:${hostname}`;
|
|
71
71
|
}
|
|
72
|
+
function ecommerceSalesChannelTag(salesChannelId) {
|
|
73
|
+
return `ecommerce-sales-channel:${salesChannelId}`;
|
|
74
|
+
}
|
|
72
75
|
function ecommerceCustomerTag(customerId) {
|
|
73
76
|
return `customer:${customerId}`;
|
|
74
77
|
}
|
|
@@ -226,6 +229,7 @@ export {
|
|
|
226
229
|
ecommerceDomainMappingTag,
|
|
227
230
|
ecommerceDomainTag,
|
|
228
231
|
ecommerceResolutionCache,
|
|
232
|
+
ecommerceSalesChannelTag,
|
|
229
233
|
ecommerceStoreTag,
|
|
230
234
|
ecommerceUngroupedBuyerTag,
|
|
231
235
|
invalidateEcommerceCacheTags,
|