@endora-commerce/mod-megamenu 0.100.0
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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/admin/api/megamenu-client.d.ts +28 -0
- package/dist/admin/api/megamenu-client.d.ts.map +1 -0
- package/dist/admin/api/megamenu-client.js +43 -0
- package/dist/admin/api/megamenu-client.js.map +1 -0
- package/dist/admin/components/BindingsPanel.d.ts +10 -0
- package/dist/admin/components/BindingsPanel.d.ts.map +1 -0
- package/dist/admin/components/BindingsPanel.js +120 -0
- package/dist/admin/components/BindingsPanel.js.map +1 -0
- package/dist/admin/components/MenuItemConfigPanel.d.ts +22 -0
- package/dist/admin/components/MenuItemConfigPanel.d.ts.map +1 -0
- package/dist/admin/components/MenuItemConfigPanel.js +77 -0
- package/dist/admin/components/MenuItemConfigPanel.js.map +1 -0
- package/dist/admin/components/MenuItemTree.d.ts +13 -0
- package/dist/admin/components/MenuItemTree.d.ts.map +1 -0
- package/dist/admin/components/MenuItemTree.js +46 -0
- package/dist/admin/components/MenuItemTree.js.map +1 -0
- package/dist/admin/index.d.ts +31 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +47 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/MegamenuEditor.d.ts +4 -0
- package/dist/admin/pages/MegamenuEditor.d.ts.map +1 -0
- package/dist/admin/pages/MegamenuEditor.js +151 -0
- package/dist/admin/pages/MegamenuEditor.js.map +1 -0
- package/dist/admin/pages/MegamenuListPage.d.ts +4 -0
- package/dist/admin/pages/MegamenuListPage.d.ts.map +1 -0
- package/dist/admin/pages/MegamenuListPage.js +51 -0
- package/dist/admin/pages/MegamenuListPage.js.map +1 -0
- package/dist/backend/entities/megamenu-binding.entity.d.ts +18 -0
- package/dist/backend/entities/megamenu-binding.entity.d.ts.map +1 -0
- package/dist/backend/entities/megamenu-binding.entity.js +61 -0
- package/dist/backend/entities/megamenu-binding.entity.js.map +1 -0
- package/dist/backend/entities/megamenu-item.entity.d.ts +27 -0
- package/dist/backend/entities/megamenu-item.entity.d.ts.map +1 -0
- package/dist/backend/entities/megamenu-item.entity.js +83 -0
- package/dist/backend/entities/megamenu-item.entity.js.map +1 -0
- package/dist/backend/entities/megamenu.entity.d.ts +17 -0
- package/dist/backend/entities/megamenu.entity.d.ts.map +1 -0
- package/dist/backend/entities/megamenu.entity.js +57 -0
- package/dist/backend/entities/megamenu.entity.js.map +1 -0
- package/dist/backend/index.d.ts +116 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +147 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +10 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +50 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +6 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +26 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/asset-references.d.ts +4 -0
- package/dist/backend/services/asset-references.d.ts.map +1 -0
- package/dist/backend/services/asset-references.js +36 -0
- package/dist/backend/services/asset-references.js.map +1 -0
- package/dist/backend/services/cms-references.d.ts +6 -0
- package/dist/backend/services/cms-references.d.ts.map +1 -0
- package/dist/backend/services/cms-references.js +39 -0
- package/dist/backend/services/cms-references.js.map +1 -0
- package/dist/backend/services/cross-module-ports.d.ts +112 -0
- package/dist/backend/services/cross-module-ports.d.ts.map +1 -0
- package/dist/backend/services/cross-module-ports.js +88 -0
- package/dist/backend/services/cross-module-ports.js.map +1 -0
- package/dist/backend/services/megamenu-cache.d.ts +36 -0
- package/dist/backend/services/megamenu-cache.d.ts.map +1 -0
- package/dist/backend/services/megamenu-cache.js +70 -0
- package/dist/backend/services/megamenu-cache.js.map +1 -0
- package/dist/backend/services/megamenu-item-service.d.ts +39 -0
- package/dist/backend/services/megamenu-item-service.d.ts.map +1 -0
- package/dist/backend/services/megamenu-item-service.js +151 -0
- package/dist/backend/services/megamenu-item-service.js.map +1 -0
- package/dist/backend/services/megamenu-language-reference.d.ts +12 -0
- package/dist/backend/services/megamenu-language-reference.d.ts.map +1 -0
- package/dist/backend/services/megamenu-language-reference.js +24 -0
- package/dist/backend/services/megamenu-language-reference.js.map +1 -0
- package/dist/backend/services/megamenu-reference-registry.d.ts +24 -0
- package/dist/backend/services/megamenu-reference-registry.d.ts.map +1 -0
- package/dist/backend/services/megamenu-reference-registry.js +69 -0
- package/dist/backend/services/megamenu-reference-registry.js.map +1 -0
- package/dist/backend/services/megamenu-service.d.ts +70 -0
- package/dist/backend/services/megamenu-service.d.ts.map +1 -0
- package/dist/backend/services/megamenu-service.js +299 -0
- package/dist/backend/services/megamenu-service.js.map +1 -0
- package/dist/backend/services/storefront-resolver.d.ts +46 -0
- package/dist/backend/services/storefront-resolver.d.ts.map +1 -0
- package/dist/backend/services/storefront-resolver.js +162 -0
- package/dist/backend/services/storefront-resolver.js.map +1 -0
- package/dist/backend/services/target-validator.d.ts +24 -0
- package/dist/backend/services/target-validator.d.ts.map +1 -0
- package/dist/backend/services/target-validator.js +105 -0
- package/dist/backend/services/target-validator.js.map +1 -0
- package/dist/manifest.d.ts +167 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +170 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260505T193836_megamenu_init.d.ts +23 -0
- package/dist/migrations/20260505T193836_megamenu_init.d.ts.map +1 -0
- package/dist/migrations/20260505T193836_megamenu_init.js +91 -0
- package/dist/migrations/20260505T193836_megamenu_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/megamenu/index.md +159 -0
- package/i18n/en.json +96 -0
- package/i18n/pl.json +96 -0
- package/package.json +99 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A category target's public storefront URL, or `null` for one a shopper may not
|
|
3
|
+
* reach — which drops the item, and its children with it.
|
|
4
|
+
*
|
|
5
|
+
* `/c/<slug>` is the storefront's category route
|
|
6
|
+
* (`storefront/app/(catalog)/c/[slug]`), and a module naming it is the tree's
|
|
7
|
+
* established shape: `seo`'s sitemap builds `/c/`, `/p/` and `/<slug>` in its own
|
|
8
|
+
* sources for the same three things. The composition roots disagreed about it —
|
|
9
|
+
* the harness built `/catalog/<slug>`, which no route serves — and no test in the
|
|
10
|
+
* tree read the value, so the harness's answer was wrong for as long as it
|
|
11
|
+
* existed.
|
|
12
|
+
*
|
|
13
|
+
* **Deactivated and soft-deleted are both refusals**, which is production's
|
|
14
|
+
* `is_active = true and deleted_at is null`. `liveOnly` is the port's word for
|
|
15
|
+
* the second; `isActive` rides on the record because it is the caller's
|
|
16
|
+
* judgement, and the admin-side existence check below deliberately makes the
|
|
17
|
+
* opposite one.
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveCategoryUrl(categories, categoryId) {
|
|
20
|
+
const category = await categories.findById(categoryId, { liveOnly: true });
|
|
21
|
+
if (!category || !category.isActive || !category.slug)
|
|
22
|
+
return null;
|
|
23
|
+
return `/c/${category.slug}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A CMS page target's public storefront URL, or `null` when the page is gone.
|
|
27
|
+
*
|
|
28
|
+
* **No status or `active` narrowing**, which is what both roots did and is an
|
|
29
|
+
* asymmetry with the category case above rather than an oversight of this drain:
|
|
30
|
+
* a draft or deactivated page keeps its menu item and answers for itself when the
|
|
31
|
+
* shopper follows the link. Changing it is a product decision, and T118c moves a
|
|
32
|
+
* value rather than redesigning it.
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveCmsPageUrl(pages, pageId) {
|
|
35
|
+
const page = await pages.findById(pageId);
|
|
36
|
+
if (!page?.slug)
|
|
37
|
+
return null;
|
|
38
|
+
return `/${page.slug}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* An asset target or icon, resolved to the three fields a menu payload carries.
|
|
42
|
+
*
|
|
43
|
+
* `null` for an asset that is gone, and for one whose stored kind a menu cannot
|
|
44
|
+
* render: the library holds `pdf`, `certificate` and `other` as well, and
|
|
45
|
+
* `MegamenuAssetKind` is the narrower pair. The narrowing is the roots' own
|
|
46
|
+
* (`if (row.kind !== 'image' && row.kind !== 'video') return null`) and it is what
|
|
47
|
+
* keeps the wire shape's union honest.
|
|
48
|
+
*
|
|
49
|
+
* **No `catch` around `getAsset`, deliberately.** It throws 404 for a row that is
|
|
50
|
+
* gone, which the read above has already answered `null` for, so the only way to
|
|
51
|
+
* reach it is an asset deleted between the two reads — and swallowing it would
|
|
52
|
+
* also swallow `ModuleDisabledError`, which is the fail-open composition
|
|
53
|
+
* checklist item 7 refuses. Neither root caught here either.
|
|
54
|
+
*/
|
|
55
|
+
export async function resolveMenuAsset(ports, assetId) {
|
|
56
|
+
const record = await ports.assets.findById(assetId);
|
|
57
|
+
if (!record)
|
|
58
|
+
return null;
|
|
59
|
+
if (record.kind !== 'image' && record.kind !== 'video')
|
|
60
|
+
return null;
|
|
61
|
+
// D-223 — the URL arrives absolute; the module that built it is the only party
|
|
62
|
+
// that knows which adapter produced it and whether it is signed.
|
|
63
|
+
const detail = await ports.assetLibrary.getAsset(assetId);
|
|
64
|
+
return { url: detail.url, label: record.label, kind: record.kind };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The block a panel inlines, in the language the menu is being resolved for, or
|
|
68
|
+
* `null` when there is nothing to inline — which drops the item.
|
|
69
|
+
*
|
|
70
|
+
* `schemaVersion: 1` is **this module's wire contract**
|
|
71
|
+
* (`megamenuResolvedBlockSchema`) and not the CMS envelope's deprecated
|
|
72
|
+
* `schema_version`, which is normalized away on read. It is a constant here for
|
|
73
|
+
* the same reason it was a constant in the root closures: nothing produces a
|
|
74
|
+
* second version of this payload shape, and the field belongs to whoever owns the
|
|
75
|
+
* response.
|
|
76
|
+
*/
|
|
77
|
+
export async function resolveMenuBlock(blocks, blockId, language) {
|
|
78
|
+
const block = await blocks.findLocalizedById(blockId, language);
|
|
79
|
+
if (!block)
|
|
80
|
+
return null;
|
|
81
|
+
return {
|
|
82
|
+
id: block.id,
|
|
83
|
+
code: block.code,
|
|
84
|
+
language: block.language,
|
|
85
|
+
content: { schemaVersion: 1, data: block.data },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=cross-module-ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-module-ports.js","sourceRoot":"","sources":["../../../src/backend/services/cross-module-ports.ts"],"names":[],"mappings":"AAiEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,UAAkD,EAClD,UAAkB;IAElB,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3E,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACnE,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAwC,EACxC,MAAc;IAEd,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,CAAC,IAAI,EAAE,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7B,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAgE,EAChE,OAAe;IAEf,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAEpE,+EAA+E;IAC/E,iEAAiE;IACjE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC1D,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;AACrE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAA0C,EAC1C,OAAe,EACf,QAAgB;IAEhB,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAChE,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO;QACL,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,OAAO,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE;KAChD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis';
|
|
2
|
+
import type { ResolvedMegamenu } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* Storefront read-through cache for the Megamenu module — feature 015 / T017.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors feature 014's `CmsCache` shape exactly: same key-prefix scheme,
|
|
7
|
+
* same TTL, same SCAN-based coarse invalidation. Keys live under
|
|
8
|
+
* `megamenu:v1:<channelCode>:<language>`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MEGAMENU_CACHE_KEY_PREFIX = "megamenu:v1:";
|
|
11
|
+
export declare const MEGAMENU_CACHE_DEFAULT_TTL_SECONDS: number;
|
|
12
|
+
export interface MegamenuCacheOptions {
|
|
13
|
+
ttlSeconds?: number;
|
|
14
|
+
/** When false, get/set are short-circuit no-ops (useful for tests). */
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare class MegamenuCache {
|
|
18
|
+
private readonly redis;
|
|
19
|
+
private readonly ttlSeconds;
|
|
20
|
+
private readonly enabled;
|
|
21
|
+
constructor(redis: Redis, options?: MegamenuCacheOptions);
|
|
22
|
+
static composeKey(channelCode: string, language: string): string;
|
|
23
|
+
get(channelCode: string, language: string): Promise<ResolvedMegamenu | null>;
|
|
24
|
+
set(channelCode: string, language: string, payload: ResolvedMegamenu): Promise<void>;
|
|
25
|
+
invalidateScope(channelCode: string, language: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Coarse drop of every cache entry for one channel — used when a binding
|
|
28
|
+
* for that channel changes scope (the language is not always known to the
|
|
29
|
+
* caller without an extra DB read).
|
|
30
|
+
*/
|
|
31
|
+
invalidateChannel(channelCode: string): Promise<void>;
|
|
32
|
+
/** Used when a menu-level write happens (one menu may serve N bindings). */
|
|
33
|
+
invalidateAll(): Promise<void>;
|
|
34
|
+
private scanDelete;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=megamenu-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"megamenu-cache.d.ts","sourceRoot":"","sources":["../../../src/backend/services/megamenu-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAEnE;;;;;;GAMG;AAEH,eAAO,MAAM,yBAAyB,iBAAiB,CAAC;AACxD,eAAO,MAAM,kCAAkC,QAAS,CAAC;AAEzD,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,aAAa;IAIZ,OAAO,CAAC,QAAQ,CAAC,KAAK;IAHlC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEL,KAAK,EAAE,KAAK,EAAE,OAAO,GAAE,oBAAyB;IAK7E,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAI1D,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAY5E,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUpF,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK3E;;;;OAIG;IACG,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3D,4EAA4E;IACtE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;YAItB,UAAU;CASzB"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storefront read-through cache for the Megamenu module — feature 015 / T017.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors feature 014's `CmsCache` shape exactly: same key-prefix scheme,
|
|
5
|
+
* same TTL, same SCAN-based coarse invalidation. Keys live under
|
|
6
|
+
* `megamenu:v1:<channelCode>:<language>`.
|
|
7
|
+
*/
|
|
8
|
+
export const MEGAMENU_CACHE_KEY_PREFIX = 'megamenu:v1:';
|
|
9
|
+
export const MEGAMENU_CACHE_DEFAULT_TTL_SECONDS = 5 * 60;
|
|
10
|
+
export class MegamenuCache {
|
|
11
|
+
redis;
|
|
12
|
+
ttlSeconds;
|
|
13
|
+
enabled;
|
|
14
|
+
constructor(redis, options = {}) {
|
|
15
|
+
this.redis = redis;
|
|
16
|
+
this.ttlSeconds = options.ttlSeconds ?? MEGAMENU_CACHE_DEFAULT_TTL_SECONDS;
|
|
17
|
+
this.enabled = (options.enabled ?? true) && this.ttlSeconds > 0;
|
|
18
|
+
}
|
|
19
|
+
static composeKey(channelCode, language) {
|
|
20
|
+
return `${MEGAMENU_CACHE_KEY_PREFIX}${channelCode}:${language}`;
|
|
21
|
+
}
|
|
22
|
+
async get(channelCode, language) {
|
|
23
|
+
if (!this.enabled)
|
|
24
|
+
return null;
|
|
25
|
+
const raw = await this.redis.get(MegamenuCache.composeKey(channelCode, language));
|
|
26
|
+
if (raw === null)
|
|
27
|
+
return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
await this.redis.del(MegamenuCache.composeKey(channelCode, language));
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async set(channelCode, language, payload) {
|
|
37
|
+
if (!this.enabled)
|
|
38
|
+
return;
|
|
39
|
+
await this.redis.set(MegamenuCache.composeKey(channelCode, language), JSON.stringify(payload), 'EX', this.ttlSeconds);
|
|
40
|
+
}
|
|
41
|
+
async invalidateScope(channelCode, language) {
|
|
42
|
+
if (!this.enabled)
|
|
43
|
+
return;
|
|
44
|
+
await this.redis.del(MegamenuCache.composeKey(channelCode, language));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Coarse drop of every cache entry for one channel — used when a binding
|
|
48
|
+
* for that channel changes scope (the language is not always known to the
|
|
49
|
+
* caller without an extra DB read).
|
|
50
|
+
*/
|
|
51
|
+
async invalidateChannel(channelCode) {
|
|
52
|
+
await this.scanDelete(`${MEGAMENU_CACHE_KEY_PREFIX}${channelCode}:*`);
|
|
53
|
+
}
|
|
54
|
+
/** Used when a menu-level write happens (one menu may serve N bindings). */
|
|
55
|
+
async invalidateAll() {
|
|
56
|
+
await this.scanDelete(`${MEGAMENU_CACHE_KEY_PREFIX}*`);
|
|
57
|
+
}
|
|
58
|
+
async scanDelete(pattern) {
|
|
59
|
+
if (!this.enabled)
|
|
60
|
+
return;
|
|
61
|
+
let cursor = '0';
|
|
62
|
+
do {
|
|
63
|
+
const [next, keys] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', 200);
|
|
64
|
+
cursor = next;
|
|
65
|
+
if (keys.length > 0)
|
|
66
|
+
await this.redis.del(...keys);
|
|
67
|
+
} while (cursor !== '0');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=megamenu-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"megamenu-cache.js","sourceRoot":"","sources":["../../../src/backend/services/megamenu-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type MegamenuDetail, type PutItemsRequest } from '@endora-commerce/contracts';
|
|
3
|
+
import type { MegamenuCache } from './megamenu-cache.js';
|
|
4
|
+
import type { MegamenuService } from './megamenu-service.js';
|
|
5
|
+
import type { MegamenuCrossModulePorts } from './cross-module-ports.js';
|
|
6
|
+
export interface SetTreeWarning {
|
|
7
|
+
code: 'MEGAMENU_DEPTH_EXCEEDED';
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SetTreeResult {
|
|
11
|
+
detail: MegamenuDetail;
|
|
12
|
+
warnings: SetTreeWarning[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Tree mutations for a Megamenu — feature 015 / T026. Full-overwrite per
|
|
16
|
+
* save (per `research.md` § R4): the request supplies the desired tree
|
|
17
|
+
* and the service rewrites `megamenu_items` for the menu in one
|
|
18
|
+
* transaction. Per-kind targets are validated through the cross-module
|
|
19
|
+
* ports so the unit test can stub them.
|
|
20
|
+
*/
|
|
21
|
+
export declare class MegamenuItemService {
|
|
22
|
+
private readonly emFactory;
|
|
23
|
+
private readonly menuService;
|
|
24
|
+
private readonly ports;
|
|
25
|
+
private readonly cache?;
|
|
26
|
+
constructor(emFactory: () => EntityManager, menuService: MegamenuService, ports: MegamenuCrossModulePorts, cache?: MegamenuCache | undefined);
|
|
27
|
+
setTree(menuId: string, body: PutItemsRequest): Promise<SetTreeResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Normalises the request tree:
|
|
30
|
+
* - assigns server ids to items missing one;
|
|
31
|
+
* - validates every parentId either references an item also present in
|
|
32
|
+
* the payload or is null (top-level);
|
|
33
|
+
* - sorts siblings by their input position and rewrites position 0..N-1;
|
|
34
|
+
* - computes depth per node so the soft-warning check stays cheap.
|
|
35
|
+
*/
|
|
36
|
+
private normalise;
|
|
37
|
+
private fetchChannelIdsForMenu;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=megamenu-item-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"megamenu-item-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/megamenu-item-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,cAAc,EAEnB,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAaxE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAHN,SAAS,EAAE,MAAM,aAAa,EAC9B,WAAW,EAAE,eAAe,EAC5B,KAAK,EAAE,wBAAwB,EAC/B,KAAK,CAAC,EAAE,aAAa,YAAA;IAGlC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC;IAmE5E;;;;;;;OAOG;IACH,OAAO,CAAC,SAAS;YAwEH,sBAAsB;CAQrC"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
import { validateTarget } from './target-validator.js';
|
|
5
|
+
const SOFT_DEPTH_WARN_AT = 4;
|
|
6
|
+
/**
|
|
7
|
+
* Tree mutations for a Megamenu — feature 015 / T026. Full-overwrite per
|
|
8
|
+
* save (per `research.md` § R4): the request supplies the desired tree
|
|
9
|
+
* and the service rewrites `megamenu_items` for the menu in one
|
|
10
|
+
* transaction. Per-kind targets are validated through the cross-module
|
|
11
|
+
* ports so the unit test can stub them.
|
|
12
|
+
*/
|
|
13
|
+
export class MegamenuItemService {
|
|
14
|
+
emFactory;
|
|
15
|
+
menuService;
|
|
16
|
+
ports;
|
|
17
|
+
cache;
|
|
18
|
+
constructor(emFactory, menuService, ports, cache) {
|
|
19
|
+
this.emFactory = emFactory;
|
|
20
|
+
this.menuService = menuService;
|
|
21
|
+
this.ports = ports;
|
|
22
|
+
this.cache = cache;
|
|
23
|
+
}
|
|
24
|
+
async setTree(menuId, body) {
|
|
25
|
+
const em = this.emFactory();
|
|
26
|
+
const menuRow = await this.menuService.findRow(menuId);
|
|
27
|
+
if (!menuRow)
|
|
28
|
+
throw new HttpError(404, ERROR_CODES.MEGAMENU_NOT_FOUND, 'Megamenu not found.');
|
|
29
|
+
if (body.version !== menuRow.version) {
|
|
30
|
+
throw new HttpError(409, ERROR_CODES.VERSION_CONFLICT, 'Megamenu was updated concurrently.');
|
|
31
|
+
}
|
|
32
|
+
const normalised = this.normalise(body.items);
|
|
33
|
+
const warnings = [];
|
|
34
|
+
if (normalised.some((item) => item.depth > SOFT_DEPTH_WARN_AT)) {
|
|
35
|
+
warnings.push({
|
|
36
|
+
code: 'MEGAMENU_DEPTH_EXCEEDED',
|
|
37
|
+
message: `Tree contains items deeper than level ${SOFT_DEPTH_WARN_AT}.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// Cross-module target validation — Channel scope is the union of
|
|
41
|
+
// every binding's channel. v1 enforces existence only; full
|
|
42
|
+
// out-of-scope refusal lands in US3.
|
|
43
|
+
const channelIds = await this.fetchChannelIdsForMenu(menuId);
|
|
44
|
+
for (const item of normalised) {
|
|
45
|
+
await validateTarget(item, channelIds, this.ports);
|
|
46
|
+
}
|
|
47
|
+
await em.transactional(async (tx) => {
|
|
48
|
+
// `tx.execute`, not `tx.getConnection().execute`: a connection carries no
|
|
49
|
+
// transaction context, so this delete used to commit on its own connection
|
|
50
|
+
// the moment it ran. A re-insert the database then refused — a child item
|
|
51
|
+
// listed before its parent is enough — rolled a transaction back that had
|
|
52
|
+
// never held the delete, and the operator's whole menu was gone (issue
|
|
53
|
+
// #200). `test/integration/megamenu/write-atomicity.test.ts` is that case.
|
|
54
|
+
await tx.execute(`delete from megamenu_items where megamenu_id = ?`, [menuId]);
|
|
55
|
+
for (const item of normalised) {
|
|
56
|
+
const now = new Date();
|
|
57
|
+
await tx.execute(`insert into megamenu_items
|
|
58
|
+
(id, megamenu_id, parent_id, position, kind, labels, descriptions, target, created_at, updated_at)
|
|
59
|
+
values (?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?::jsonb, ?, ?)`, [
|
|
60
|
+
item.id,
|
|
61
|
+
menuId,
|
|
62
|
+
item.parentId,
|
|
63
|
+
item.position,
|
|
64
|
+
item.kind,
|
|
65
|
+
JSON.stringify(item.labels),
|
|
66
|
+
JSON.stringify(item.descriptions ?? null),
|
|
67
|
+
JSON.stringify(item.target),
|
|
68
|
+
now,
|
|
69
|
+
now,
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
await tx.execute(`update megamenus set version = version + 1, updated_at = now() where id = ?`, [menuId]);
|
|
73
|
+
});
|
|
74
|
+
if (this.cache)
|
|
75
|
+
await this.cache.invalidateAll();
|
|
76
|
+
const detail = await this.menuService.get(menuId);
|
|
77
|
+
return { detail, warnings };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Normalises the request tree:
|
|
81
|
+
* - assigns server ids to items missing one;
|
|
82
|
+
* - validates every parentId either references an item also present in
|
|
83
|
+
* the payload or is null (top-level);
|
|
84
|
+
* - sorts siblings by their input position and rewrites position 0..N-1;
|
|
85
|
+
* - computes depth per node so the soft-warning check stays cheap.
|
|
86
|
+
*/
|
|
87
|
+
normalise(items) {
|
|
88
|
+
const seenIds = new Set();
|
|
89
|
+
const expanded = items.map((item) => {
|
|
90
|
+
const id = item.id ?? randomUUID();
|
|
91
|
+
if (seenIds.has(id)) {
|
|
92
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `Duplicate item id "${id}".`);
|
|
93
|
+
}
|
|
94
|
+
seenIds.add(id);
|
|
95
|
+
return {
|
|
96
|
+
...item,
|
|
97
|
+
id,
|
|
98
|
+
parentId: item.parentId ?? null,
|
|
99
|
+
position: item.position,
|
|
100
|
+
depth: 0,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
// Validate parentId references.
|
|
104
|
+
for (const item of expanded) {
|
|
105
|
+
if (item.parentId !== null && !seenIds.has(item.parentId)) {
|
|
106
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `Item ${item.id} references parentId ${item.parentId} which is not in the payload.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Sort by parent + supplied position, then rewrite to 0..N-1.
|
|
110
|
+
const byParent = new Map();
|
|
111
|
+
for (const item of expanded) {
|
|
112
|
+
const list = byParent.get(item.parentId) ?? [];
|
|
113
|
+
list.push(item);
|
|
114
|
+
byParent.set(item.parentId, list);
|
|
115
|
+
}
|
|
116
|
+
for (const list of byParent.values()) {
|
|
117
|
+
list.sort((a, b) => a.position - b.position);
|
|
118
|
+
list.forEach((item, idx) => {
|
|
119
|
+
item.position = idx;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Compute depth via BFS from roots.
|
|
123
|
+
const childrenIndex = byParent;
|
|
124
|
+
const queue = [];
|
|
125
|
+
for (const root of childrenIndex.get(null) ?? []) {
|
|
126
|
+
root.depth = 1;
|
|
127
|
+
queue.push({ id: root.id, depth: 1 });
|
|
128
|
+
}
|
|
129
|
+
while (queue.length > 0) {
|
|
130
|
+
const head = queue.shift();
|
|
131
|
+
const children = childrenIndex.get(head.id) ?? [];
|
|
132
|
+
for (const child of children) {
|
|
133
|
+
child.depth = head.depth + 1;
|
|
134
|
+
queue.push({ id: child.id, depth: child.depth });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Detect cycles / orphans — every item should have its depth set.
|
|
138
|
+
for (const item of expanded) {
|
|
139
|
+
if (item.depth === 0) {
|
|
140
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `Item ${item.id} forms an orphan or cycle (no path to root).`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return expanded;
|
|
144
|
+
}
|
|
145
|
+
async fetchChannelIdsForMenu(menuId) {
|
|
146
|
+
const rows = (await this.emFactory().execute(`select distinct sales_channel_id::text as channel_id
|
|
147
|
+
from megamenu_bindings where megamenu_id = ?`, [menuId]));
|
|
148
|
+
return rows.map((r) => r.channel_id);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=megamenu-item-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"megamenu-item-service.js","sourceRoot":"","sources":["../../../src/backend/services/megamenu-item-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DictionaryReferenceRegistryPort } from '@endora-commerce/contracts';
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
3
|
+
/**
|
|
4
|
+
* Who in `megamenu` still points at a language (feature 077, D-87).
|
|
5
|
+
*
|
|
6
|
+
* `languages` used to count `megamenu_bindings` itself, naming this module's
|
|
7
|
+
* table in a string no import-level boundary check can see. A binding is
|
|
8
|
+
* per-channel and per-language, so a language the platform has dropped
|
|
9
|
+
* leaves the binding unreachable rather than merely untranslated.
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerMegamenuLanguageReferences(registry: DictionaryReferenceRegistryPort, emFactory: () => EntityManager): void;
|
|
12
|
+
//# sourceMappingURL=megamenu-language-reference.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"megamenu-language-reference.d.ts","sourceRoot":"","sources":["../../../src/backend/services/megamenu-language-reference.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,4BAA4B,CAAC;AAClF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;;GAOG;AACH,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,+BAA+B,EACzC,SAAS,EAAE,MAAM,aAAa,GAC7B,IAAI,CAoBN"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who in `megamenu` still points at a language (feature 077, D-87).
|
|
3
|
+
*
|
|
4
|
+
* `languages` used to count `megamenu_bindings` itself, naming this module's
|
|
5
|
+
* table in a string no import-level boundary check can see. A binding is
|
|
6
|
+
* per-channel and per-language, so a language the platform has dropped
|
|
7
|
+
* leaves the binding unreachable rather than merely untranslated.
|
|
8
|
+
*/
|
|
9
|
+
export function registerMegamenuLanguageReferences(registry, emFactory) {
|
|
10
|
+
registry.register({
|
|
11
|
+
ownerModuleId: 'megamenu',
|
|
12
|
+
consumer: 'megamenu',
|
|
13
|
+
tableName: 'megamenu_bindings',
|
|
14
|
+
columnName: 'language',
|
|
15
|
+
blocking: true,
|
|
16
|
+
countReferences: async (code) => {
|
|
17
|
+
const rows = (await emFactory().execute(`select count(*)::int as n from "megamenu_bindings" where "language" = ?`, [code]));
|
|
18
|
+
return rows[0]?.n ?? 0;
|
|
19
|
+
},
|
|
20
|
+
usedCodes: async () => (await emFactory().execute(`select "language" as code, count(*)::int as count
|
|
21
|
+
from "megamenu_bindings" where "language" is not null group by "language"`)),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=megamenu-language-reference.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"megamenu-language-reference.js","sourceRoot":"","sources":["../../../src/backend/services/megamenu-language-reference.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,UAAU,kCAAkC,CAChD,QAAyC,EACzC,SAA8B;IAE9B,QAAQ,CAAC,QAAQ,CAAC;QAChB,aAAa,EAAE,UAAU;QACzB,QAAQ,EAAE,UAAU;QACpB,SAAS,EAAE,mBAAmB;QAC9B,UAAU,EAAE,UAAU;QACtB,QAAQ,EAAE,IAAI;QACd,eAAe,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YAC9B,MAAM,IAAI,GAAG,CAAC,MAAM,SAAS,EAAE,CAAC,OAAO,CACrC,yEAAyE,EACzE,CAAC,IAAI,CAAC,CACP,CAAyB,CAAC;YAC3B,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QACD,SAAS,EAAE,KAAK,IAAI,EAAE,CACpB,CAAC,MAAM,SAAS,EAAE,CAAC,OAAO,CACxB;qFAC6E,CAC9E,CAA2C;KAC/C,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
export interface MegamenuItemReference {
|
|
3
|
+
menuId: string;
|
|
4
|
+
itemId: string;
|
|
5
|
+
menuName: string;
|
|
6
|
+
/** Distinguishes assetId vs iconAssetId hits in the asset scan. */
|
|
7
|
+
via?: 'assetId' | 'iconAssetId';
|
|
8
|
+
}
|
|
9
|
+
export declare class MegamenuReferenceRegistry {
|
|
10
|
+
private readonly emFactory;
|
|
11
|
+
constructor(emFactory: () => EntityManager);
|
|
12
|
+
findCategoryReferences(categoryId: string): Promise<MegamenuItemReference[]>;
|
|
13
|
+
findCmsPageReferences(pageId: string): Promise<MegamenuItemReference[]>;
|
|
14
|
+
findCmsBlockReferences(blockId: string): Promise<MegamenuItemReference[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Asset references — both `target.assetId` (asset items) and
|
|
17
|
+
* `target.iconAssetId` (link items with an icon) count. Returns one
|
|
18
|
+
* row per item; a single item that matches both scanners is rare in
|
|
19
|
+
* practice but the SQL deduplicates regardless.
|
|
20
|
+
*/
|
|
21
|
+
findAssetReferences(assetIds: string[]): Promise<MegamenuItemReference[]>;
|
|
22
|
+
private findByKindAndPath;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=megamenu-reference-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"megamenu-reference-registry.d.ts","sourceRoot":"","sources":["../../../src/backend/services/megamenu-reference-registry.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,GAAG,CAAC,EAAE,SAAS,GAAG,aAAa,CAAC;CACjC;AAED,qBAAa,yBAAyB;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAI5E,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAIvE,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAI/E;;;;;OAKG;IACG,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;YA0BjE,iBAAiB;CAqBhC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// In-process registry of references between Megamenu items and the
|
|
2
|
+
// upstream entities they target. Consulted by:
|
|
3
|
+
//
|
|
4
|
+
// * the catalog category-delete endpoint (Category → MegamenuItem)
|
|
5
|
+
// * the cms page + block delete endpoints (CmsPage / CmsBlock → MegamenuItem)
|
|
6
|
+
// * the AssetReferenceRegistry (Asset → MegamenuItem)
|
|
7
|
+
//
|
|
8
|
+
// All scans are GIN-index-served by `idx_megamenu_items_target_refs`
|
|
9
|
+
// (jsonb_path_ops) so scale is sub-millisecond at the platform's low-
|
|
10
|
+
// hundreds item count.
|
|
11
|
+
export class MegamenuReferenceRegistry {
|
|
12
|
+
emFactory;
|
|
13
|
+
constructor(emFactory) {
|
|
14
|
+
this.emFactory = emFactory;
|
|
15
|
+
}
|
|
16
|
+
async findCategoryReferences(categoryId) {
|
|
17
|
+
return this.findByKindAndPath('category-link', 'categoryId', categoryId);
|
|
18
|
+
}
|
|
19
|
+
async findCmsPageReferences(pageId) {
|
|
20
|
+
return this.findByKindAndPath('cms-page-link', 'pageId', pageId);
|
|
21
|
+
}
|
|
22
|
+
async findCmsBlockReferences(blockId) {
|
|
23
|
+
return this.findByKindAndPath('cms-block-embed', 'blockId', blockId);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Asset references — both `target.assetId` (asset items) and
|
|
27
|
+
* `target.iconAssetId` (link items with an icon) count. Returns one
|
|
28
|
+
* row per item; a single item that matches both scanners is rare in
|
|
29
|
+
* practice but the SQL deduplicates regardless.
|
|
30
|
+
*/
|
|
31
|
+
async findAssetReferences(assetIds) {
|
|
32
|
+
if (assetIds.length === 0)
|
|
33
|
+
return [];
|
|
34
|
+
const placeholders = assetIds.map(() => '?').join(', ');
|
|
35
|
+
const rows = (await this.emFactory().execute(`select mi.id::text as item_id,
|
|
36
|
+
m.id::text as menu_id,
|
|
37
|
+
m.name as menu_name,
|
|
38
|
+
case
|
|
39
|
+
when mi.target->>'assetId' in (${placeholders}) then 'assetId'
|
|
40
|
+
when mi.target->>'iconAssetId' in (${placeholders}) then 'iconAssetId'
|
|
41
|
+
else null
|
|
42
|
+
end as via
|
|
43
|
+
from megamenu_items mi
|
|
44
|
+
join megamenus m on m.id = mi.megamenu_id
|
|
45
|
+
where mi.target->>'assetId' in (${placeholders})
|
|
46
|
+
or mi.target->>'iconAssetId' in (${placeholders})`, [...assetIds, ...assetIds, ...assetIds, ...assetIds]));
|
|
47
|
+
return rows.map((row) => ({
|
|
48
|
+
itemId: row.item_id,
|
|
49
|
+
menuId: row.menu_id,
|
|
50
|
+
menuName: row.menu_name,
|
|
51
|
+
via: row.via,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
async findByKindAndPath(kind, targetField, value) {
|
|
55
|
+
const rows = (await this.emFactory().execute(`select mi.id::text as item_id,
|
|
56
|
+
m.id::text as menu_id,
|
|
57
|
+
m.name as menu_name
|
|
58
|
+
from megamenu_items mi
|
|
59
|
+
join megamenus m on m.id = mi.megamenu_id
|
|
60
|
+
where mi.kind = ?
|
|
61
|
+
and mi.target->>'${targetField}' = ?`, [kind, value]));
|
|
62
|
+
return rows.map((row) => ({
|
|
63
|
+
itemId: row.item_id,
|
|
64
|
+
menuId: row.menu_id,
|
|
65
|
+
menuName: row.menu_name,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=megamenu-reference-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"megamenu-reference-registry.js","sourceRoot":"","sources":["../../../src/backend/services/megamenu-reference-registry.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,+CAA+C;AAC/C,EAAE;AACF,qEAAqE;AACrE,gFAAgF;AAChF,wDAAwD;AACxD,EAAE;AACF,qEAAqE;AACrE,sEAAsE;AACtE,uBAAuB;AAYvB,MAAM,OAAO,yBAAyB;IACP;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,sBAAsB,CAAC,UAAkB;QAC7C,OAAO,IAAI,CAAC,iBAAiB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;IAC3E,CAAC;IAED,KAAK,CAAC,qBAAqB,CAAC,MAAc;QACxC,OAAO,IAAI,CAAC,iBAAiB,CAAC,eAAe,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,sBAAsB,CAAC,OAAe;QAC1C,OAAO,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACvE,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,mBAAmB,CAAC,QAAkB;QAC1C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACrC,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAC1C;;;;qDAI+C,YAAY;qDACZ,YAAY;;;;;8CAKnB,YAAY;8CACZ,YAAY,GAAG,EACvD,CAAC,GAAG,QAAQ,EAAE,GAAG,QAAQ,EAAE,GAAG,QAAQ,EAAE,GAAG,QAAQ,CAAC,CACrD,CAAmG,CAAC;QACrG,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACxB,MAAM,EAAE,GAAG,CAAC,OAAO;YACnB,MAAM,EAAE,GAAG,CAAC,OAAO;YACnB,QAAQ,EAAE,GAAG,CAAC,SAAS;YACvB,GAAG,EAAE,GAAG,CAAC,GAAG;SACb,CAAC,CAAC,CAAC;IACN,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAC7B,IAAY,EACZ,WAAmB,EACnB,KAAa;QAEb,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAC1C;;;;;;6BAMuB,WAAW,OAAO,EACzC,CAAC,IAAI,EAAE,KAAK,CAAC,CACd,CAAmE,CAAC;QACrE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACxB,MAAM,EAAE,GAAG,CAAC,OAAO;YACnB,MAAM,EAAE,GAAG,CAAC,OAAO;YACnB,QAAQ,EAAE,GAAG,CAAC,SAAS;SACxB,CAAC,CAAC,CAAC;IACN,CAAC;CACF"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type ActivateBindingResponse, type CreateMegamenuRequest, type DictionaryValidator, type MegamenuBinding as MegamenuBindingDto, type MegamenuDetail, type MegamenuSummary, type PatchMegamenuRequest, type ResolvedMenuItem } from '@endora-commerce/contracts';
|
|
3
|
+
import type { MegamenuCache } from './megamenu-cache.js';
|
|
4
|
+
type MegamenuRow = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description: string | null;
|
|
8
|
+
version: number;
|
|
9
|
+
created_at: Date | string;
|
|
10
|
+
updated_at: Date | string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Megamenu CRUD + binding lifecycle. The activation transaction enforces
|
|
14
|
+
* "exactly one active megamenu per (channel, language)" via the partial
|
|
15
|
+
* unique index on `megamenu_bindings(sales_channel_id, language) WHERE
|
|
16
|
+
* active = true` (R3 / FR-008).
|
|
17
|
+
*/
|
|
18
|
+
export declare class MegamenuService {
|
|
19
|
+
private readonly emFactory;
|
|
20
|
+
private readonly cache?;
|
|
21
|
+
private readonly dictionaryValidator?;
|
|
22
|
+
constructor(emFactory: () => EntityManager, cache?: MegamenuCache | undefined, dictionaryValidator?: DictionaryValidator | undefined);
|
|
23
|
+
list(): Promise<{
|
|
24
|
+
data: MegamenuSummary[];
|
|
25
|
+
nextCursor: null;
|
|
26
|
+
}>;
|
|
27
|
+
create(input: CreateMegamenuRequest): Promise<MegamenuDetail>;
|
|
28
|
+
get(id: string): Promise<MegamenuDetail>;
|
|
29
|
+
patch(id: string, input: PatchMegamenuRequest): Promise<MegamenuDetail>;
|
|
30
|
+
delete(id: string): Promise<void>;
|
|
31
|
+
listBindings(menuId: string): Promise<MegamenuBindingDto[]>;
|
|
32
|
+
addBinding(menuId: string, salesChannelId: string, language: string): Promise<MegamenuBindingDto>;
|
|
33
|
+
private validateLanguage;
|
|
34
|
+
removeBinding(menuId: string, salesChannelId: string, language: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Atomic activation swap (FR-008 / R3): deactivate the prior holder
|
|
37
|
+
* for the scope (if any) inside the same transaction, then upsert the
|
|
38
|
+
* new active row. The partial unique index refuses dual-active state
|
|
39
|
+
* even if this method's body regresses.
|
|
40
|
+
*/
|
|
41
|
+
activate(menuId: string, salesChannelId: string, language: string): Promise<ActivateBindingResponse>;
|
|
42
|
+
deactivate(menuId: string, salesChannelId: string, language: string): Promise<void>;
|
|
43
|
+
findRow(id: string, em?: EntityManager): Promise<MegamenuRow | null>;
|
|
44
|
+
private assertMenuExists;
|
|
45
|
+
private assertVersion;
|
|
46
|
+
/**
|
|
47
|
+
* The kernel's own entity, not `select … from sales_channels` (feature 075,
|
|
48
|
+
* D-87). `sales_channels` is the kernel's table since feature 072 moved the
|
|
49
|
+
* resolution machinery there, and a module relating into the kernel by ORM is
|
|
50
|
+
* the sanctioned access.
|
|
51
|
+
*
|
|
52
|
+
* Read through the caller's `em`, not through
|
|
53
|
+
* `salesChannelResolutionPort.getById`: every caller here is inside
|
|
54
|
+
* `em.transactional`, and that accessor forks its own EntityManager, so the
|
|
55
|
+
* binding checks below would validate against rows outside the transaction
|
|
56
|
+
* they are about to write into.
|
|
57
|
+
*/
|
|
58
|
+
private fetchChannel;
|
|
59
|
+
private fetchItems;
|
|
60
|
+
private fetchBindings;
|
|
61
|
+
private toSummary;
|
|
62
|
+
private toDetail;
|
|
63
|
+
private toBindingDto;
|
|
64
|
+
private toItemDto;
|
|
65
|
+
private iso;
|
|
66
|
+
}
|
|
67
|
+
/** Small helper for tests / smoke flows that need to assemble a tree. */
|
|
68
|
+
export declare function emptyResolvedItems(): ResolvedMenuItem[];
|
|
69
|
+
export {};
|
|
70
|
+
//# sourceMappingURL=megamenu-service.d.ts.map
|