@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,105 @@
|
|
|
1
|
+
// Per-kind target validation for Megamenu items — feature 015 / T012.
|
|
2
|
+
//
|
|
3
|
+
// The Zod boundary in `packages/contracts/src/megamenu.ts` enforces the
|
|
4
|
+
// shape rules (URL schemes, button variants, asset kinds). Cross-module
|
|
5
|
+
// existence + scope checks (Category exists, CMS Page / Block exists in
|
|
6
|
+
// scope, Asset matches an expected kind) live here so the unit test can
|
|
7
|
+
// stub them without booting a server.
|
|
8
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
9
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
10
|
+
const externalUrlRe = /^(?:https?:\/\/|tel:|mailto:)/i;
|
|
11
|
+
/**
|
|
12
|
+
* Validates a single MegamenuItem against the cross-module ports. Throws
|
|
13
|
+
* `HttpError` on the first failure so callers (`MegamenuItemService.setTree`)
|
|
14
|
+
* can re-raise unchanged.
|
|
15
|
+
*
|
|
16
|
+
* The four checks used to arrive as `TargetValidatorDeps`, an interface this
|
|
17
|
+
* module exported for a composition root to build closures against
|
|
18
|
+
* (`specs/110-instance-repository/` T118c). They are their owners' published read
|
|
19
|
+
* ports now, resolved by `backend/index.ts` and declared in this module's
|
|
20
|
+
* manifest.
|
|
21
|
+
*
|
|
22
|
+
* **`_channelIds` is threaded and read by nothing, which is what the roots' SQL
|
|
23
|
+
* did.** `setTree` computes the union of the menu's bound channels and passes it
|
|
24
|
+
* here; neither root's closure ever looked at it, so `MEGAMENU_TARGET_OUT_OF_SCOPE`
|
|
25
|
+
* has only ever meant *the target does not exist*. That is a documented v1
|
|
26
|
+
* narrowing — `megamenu-item-service.ts` says so at the call site, and the scope
|
|
27
|
+
* refusal is US3's — and the parameter is kept so US3 lands inside this function
|
|
28
|
+
* rather than re-plumbing its caller. Naming it `_` is the only change: the
|
|
29
|
+
* ignoring used to happen two files away in a root nobody reads.
|
|
30
|
+
*/
|
|
31
|
+
export async function validateTarget(item, _channelIds, ports) {
|
|
32
|
+
// Defence in depth — Zod has already filtered URL schemes at the
|
|
33
|
+
// boundary, but the validator double-checks any free URL on a kind
|
|
34
|
+
// where we expect one.
|
|
35
|
+
if (item.kind === 'external-link') {
|
|
36
|
+
if (!externalUrlRe.test(item.target.url)) {
|
|
37
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `External link URL "${item.target.url}" must use http://, https://, tel:, or mailto:.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Optional icon on link kinds: must reference an image asset.
|
|
41
|
+
if (item.kind === 'category-link' ||
|
|
42
|
+
item.kind === 'cms-page-link' ||
|
|
43
|
+
item.kind === 'external-link') {
|
|
44
|
+
const iconAssetId = item.target.iconAssetId;
|
|
45
|
+
if (iconAssetId) {
|
|
46
|
+
const icon = await ports.assets.findById(iconAssetId);
|
|
47
|
+
if (icon?.kind !== 'image') {
|
|
48
|
+
throw new HttpError(400, ERROR_CODES.MEGAMENU_ASSET_KIND_MISMATCH, `Icon asset ${iconAssetId} is not an image.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
switch (item.kind) {
|
|
53
|
+
case 'category-link': {
|
|
54
|
+
// No `liveOnly` and no `isActive` test: the roots' `select 1 from
|
|
55
|
+
// categories where id = ?` accepted a deactivated or soft-deleted
|
|
56
|
+
// category, and an admin pointing a menu item at one is not making an
|
|
57
|
+
// error the storefront cannot answer — `resolveCategoryUrl` drops the
|
|
58
|
+
// item at render time.
|
|
59
|
+
const ok = (await ports.categories.findById(item.target.categoryId)) !== null;
|
|
60
|
+
if (!ok) {
|
|
61
|
+
throw new HttpError(400, ERROR_CODES.MEGAMENU_TARGET_OUT_OF_SCOPE, `Category ${item.target.categoryId} is not in scope of the megamenu's bindings.`);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
case 'cms-page-link': {
|
|
66
|
+
const ok = (await ports.pages.findById(item.target.pageId)) !== null;
|
|
67
|
+
if (!ok) {
|
|
68
|
+
throw new HttpError(400, ERROR_CODES.MEGAMENU_TARGET_OUT_OF_SCOPE, `CMS Page ${item.target.pageId} is not in scope of the megamenu's bindings.`);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
case 'cms-block-embed': {
|
|
73
|
+
// `findById` and not `findLocalizedById`: existence, `active` included,
|
|
74
|
+
// is the question — an operator may be pointing a menu item at a block
|
|
75
|
+
// they have deactivated while they edit it, and the roots' SQL accepted
|
|
76
|
+
// one. The storefront refuses it later, where a shopper is the one asking.
|
|
77
|
+
const ok = (await ports.blocks.findById(item.target.blockId)) !== null;
|
|
78
|
+
if (!ok) {
|
|
79
|
+
throw new HttpError(400, ERROR_CODES.MEGAMENU_TARGET_OUT_OF_SCOPE, `CMS Block ${item.target.blockId} is not in scope of the megamenu's bindings.`);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
case 'asset': {
|
|
84
|
+
if ('assetId' in item.target) {
|
|
85
|
+
const asset = await ports.assets.findById(item.target.assetId);
|
|
86
|
+
if (asset?.kind !== item.target.kind) {
|
|
87
|
+
throw new HttpError(400, ERROR_CODES.MEGAMENU_ASSET_KIND_MISMATCH, `Asset ${item.target.assetId} is not a ${item.target.kind}.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Free-URL asset items require a server-side import into the
|
|
92
|
+
// Assets Library (T068). The Library does not yet expose
|
|
93
|
+
// `importFromUrl`; until it does, refuse free URLs at the
|
|
94
|
+
// validator with a clear error so admins know to upload first.
|
|
95
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, 'Free-URL asset items are not yet supported. Upload the asset to the Assets Library first and reference it by id.');
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
case 'external-link':
|
|
100
|
+
case 'button':
|
|
101
|
+
// No cross-module check beyond the icon already covered above.
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=target-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target-validator.js","sourceRoot":"","sources":["../../../src/backend/services/target-validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Megamenu module — feature 015.
|
|
3
|
+
*
|
|
4
|
+
* Owns the megamenu (multi-column dropdown navigation) configuration.
|
|
5
|
+
* Manifest backfilled alongside feature 020.
|
|
6
|
+
*/
|
|
7
|
+
export declare const manifest: {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
dependencies: string[];
|
|
12
|
+
description?: string | undefined;
|
|
13
|
+
acknowledgedDependencies?: {
|
|
14
|
+
moduleId: string;
|
|
15
|
+
port: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
}[] | undefined;
|
|
18
|
+
nonBindingDependencies?: {
|
|
19
|
+
moduleId: string;
|
|
20
|
+
name: string;
|
|
21
|
+
kind: "contributes-to" | "degrades-without" | "refuses-without";
|
|
22
|
+
reason: string;
|
|
23
|
+
whenAbsent?: string | undefined;
|
|
24
|
+
}[] | undefined;
|
|
25
|
+
activation?: {
|
|
26
|
+
settingCode: string;
|
|
27
|
+
default: boolean;
|
|
28
|
+
} | {
|
|
29
|
+
nonDeactivatable: true;
|
|
30
|
+
reason: string;
|
|
31
|
+
} | undefined;
|
|
32
|
+
settings?: {
|
|
33
|
+
moduleCode: string;
|
|
34
|
+
groups: {
|
|
35
|
+
code: string;
|
|
36
|
+
name: string;
|
|
37
|
+
salesChannelCodes?: string[] | undefined;
|
|
38
|
+
isSystemProtected?: boolean | undefined;
|
|
39
|
+
}[];
|
|
40
|
+
settings: {
|
|
41
|
+
code: string;
|
|
42
|
+
name: string;
|
|
43
|
+
valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
|
|
44
|
+
defaultValue: unknown;
|
|
45
|
+
description?: string | undefined;
|
|
46
|
+
groupCode?: string | undefined;
|
|
47
|
+
previousDefaultValues?: unknown[] | undefined;
|
|
48
|
+
salesChannelCodes?: string[] | undefined;
|
|
49
|
+
enumOptions?: string[] | undefined;
|
|
50
|
+
configurationType?: string | undefined;
|
|
51
|
+
hidden?: boolean | undefined;
|
|
52
|
+
}[];
|
|
53
|
+
} | undefined;
|
|
54
|
+
i18n?: {
|
|
55
|
+
bundlesDir: string;
|
|
56
|
+
} | undefined;
|
|
57
|
+
docs?: false | {
|
|
58
|
+
dir: string;
|
|
59
|
+
} | undefined;
|
|
60
|
+
demo?: false | {
|
|
61
|
+
summary: string;
|
|
62
|
+
seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
|
|
63
|
+
reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
|
|
64
|
+
after?: readonly string[] | undefined;
|
|
65
|
+
package?: string | undefined;
|
|
66
|
+
} | undefined;
|
|
67
|
+
actions?: {
|
|
68
|
+
id: string;
|
|
69
|
+
labelKey: string;
|
|
70
|
+
icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
|
|
71
|
+
targetRoute: string;
|
|
72
|
+
keywords: string[];
|
|
73
|
+
weight: number;
|
|
74
|
+
descriptionKey?: string | undefined;
|
|
75
|
+
requiredPermission?: string | undefined;
|
|
76
|
+
}[] | undefined;
|
|
77
|
+
permissions?: {
|
|
78
|
+
code: string;
|
|
79
|
+
label: string;
|
|
80
|
+
module?: string | undefined;
|
|
81
|
+
description?: string | undefined;
|
|
82
|
+
requires?: string[] | undefined;
|
|
83
|
+
}[] | undefined;
|
|
84
|
+
transactionalEmails?: {
|
|
85
|
+
code: string;
|
|
86
|
+
name: string;
|
|
87
|
+
variables: {
|
|
88
|
+
key: string;
|
|
89
|
+
label: string;
|
|
90
|
+
sampleValue?: string | undefined;
|
|
91
|
+
description?: string | undefined;
|
|
92
|
+
}[];
|
|
93
|
+
description?: string | undefined;
|
|
94
|
+
group?: string | undefined;
|
|
95
|
+
}[] | undefined;
|
|
96
|
+
capabilities?: string[] | undefined;
|
|
97
|
+
exclusiveCapabilities?: {
|
|
98
|
+
key: string;
|
|
99
|
+
errorCode: string;
|
|
100
|
+
}[] | undefined;
|
|
101
|
+
errorCodes?: {
|
|
102
|
+
code: string;
|
|
103
|
+
tokens?: string[] | undefined;
|
|
104
|
+
}[] | undefined;
|
|
105
|
+
blocks?: {
|
|
106
|
+
name: string;
|
|
107
|
+
labelKey: string;
|
|
108
|
+
category: string;
|
|
109
|
+
contexts: ("cms" | "invoice" | "email" | "newsletter")[];
|
|
110
|
+
fields: Record<string, {
|
|
111
|
+
type: "number" | "object" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "uuid" | "richtext";
|
|
112
|
+
label?: string | undefined;
|
|
113
|
+
required?: boolean | undefined;
|
|
114
|
+
options?: {
|
|
115
|
+
label: string;
|
|
116
|
+
value: string | number;
|
|
117
|
+
}[] | undefined;
|
|
118
|
+
refKind?: string | undefined;
|
|
119
|
+
}>;
|
|
120
|
+
descriptionKey?: string | undefined;
|
|
121
|
+
defaultProps?: Record<string, unknown> | undefined;
|
|
122
|
+
responsiveFields?: string[] | undefined;
|
|
123
|
+
previewIcon?: string | undefined;
|
|
124
|
+
weight?: number | undefined;
|
|
125
|
+
}[] | undefined;
|
|
126
|
+
blockCategories?: {
|
|
127
|
+
key: string;
|
|
128
|
+
titleKey: string;
|
|
129
|
+
contexts: ("cms" | "invoice" | "email" | "newsletter")[];
|
|
130
|
+
weight?: number | undefined;
|
|
131
|
+
visible?: boolean | undefined;
|
|
132
|
+
}[] | undefined;
|
|
133
|
+
env?: {
|
|
134
|
+
name: string;
|
|
135
|
+
describes: {
|
|
136
|
+
en: string;
|
|
137
|
+
pl: string;
|
|
138
|
+
};
|
|
139
|
+
requirement: {
|
|
140
|
+
kind: "required";
|
|
141
|
+
} | {
|
|
142
|
+
kind: "requiredWhen";
|
|
143
|
+
input: string;
|
|
144
|
+
equals: string;
|
|
145
|
+
} | {
|
|
146
|
+
kind: "optional";
|
|
147
|
+
without: {
|
|
148
|
+
en: string;
|
|
149
|
+
pl: string;
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
secret: boolean;
|
|
153
|
+
generable: boolean;
|
|
154
|
+
owner: {
|
|
155
|
+
kind: "platform";
|
|
156
|
+
} | {
|
|
157
|
+
kind: "application";
|
|
158
|
+
application: "admin" | "backend" | "storefront";
|
|
159
|
+
} | {
|
|
160
|
+
kind: "module";
|
|
161
|
+
moduleId: string;
|
|
162
|
+
};
|
|
163
|
+
consumers: ("admin" | "backend" | "storefront")[];
|
|
164
|
+
addressOf: "admin" | "backend" | "storefront" | null;
|
|
165
|
+
}[] | undefined;
|
|
166
|
+
};
|
|
167
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkKnB,CAAC"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { defineModuleManifest } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* Megamenu module — feature 015.
|
|
4
|
+
*
|
|
5
|
+
* Owns the megamenu (multi-column dropdown navigation) configuration.
|
|
6
|
+
* Manifest backfilled alongside feature 020.
|
|
7
|
+
*/
|
|
8
|
+
export const manifest = defineModuleManifest({
|
|
9
|
+
id: 'megamenu',
|
|
10
|
+
name: 'Megamenu',
|
|
11
|
+
description: 'Multi-column dropdown navigation builder.',
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
// `auth` owns the `requireAdmin` port the admin routes are gated by — feature
|
|
14
|
+
// 072 made it required rather than defaulting to a permissive no-op.
|
|
15
|
+
// `dictionaries` owns the validator the menu service checks languages with.
|
|
16
|
+
// `assets_library` owns the reference registry this module contributes its
|
|
17
|
+
// asset-and-icon scan to (T143a); `cms` owns the one it contributes the
|
|
18
|
+
// page/block scan to. Both edges existed as composition-root
|
|
19
|
+
// cross-registrations, which is to say nowhere an operator could see them.
|
|
20
|
+
// `languages` owns `languageReferenceRegistry`, the registry this module
|
|
21
|
+
// contributes its "which languages do bindings carry" descriptor to (feature
|
|
22
|
+
// 077, D-87). `languages` used to ask the question itself, with a
|
|
23
|
+
// `count(*) from "megamenu_bindings"` naming this module's table.
|
|
24
|
+
//
|
|
25
|
+
// `catalog` is new with `specs/110-instance-repository/` T118c, and it is the
|
|
26
|
+
// one edge this module did not already have. A menu item may target a
|
|
27
|
+
// category, so validating it and resolving its storefront URL are reads of
|
|
28
|
+
// `catalog`'s table — issued by a composition root in raw SQL until T118c, in
|
|
29
|
+
// a file that is nobody's declared dependency, so the edge existed nowhere an
|
|
30
|
+
// operator or a check could see it. It is `catalogCategoryReadPort` now, and
|
|
31
|
+
// the `demo` block below records what that changes about its own argument.
|
|
32
|
+
//
|
|
33
|
+
// The bind costs no operator a control, and that is derived rather than
|
|
34
|
+
// waived: `catalog` declares itself non-deactivatable, so this is not an edge
|
|
35
|
+
// into a switchable module and it owes no `nonBindingDependencies` entry. The
|
|
36
|
+
// other two owners T118c's ports belong to — `cms` and `assets_library` — were
|
|
37
|
+
// both already declared here, for the reference registries above, so the drain
|
|
38
|
+
// adds one line to this array and no judgement beyond it.
|
|
39
|
+
dependencies: [
|
|
40
|
+
'assets_library',
|
|
41
|
+
'catalog',
|
|
42
|
+
'cms',
|
|
43
|
+
'languages',
|
|
44
|
+
'sales_channels',
|
|
45
|
+
'auth',
|
|
46
|
+
'dictionaries',
|
|
47
|
+
],
|
|
48
|
+
settings: {
|
|
49
|
+
moduleCode: 'megamenu',
|
|
50
|
+
groups: [{ code: 'megamenu', name: 'Megamenu' }],
|
|
51
|
+
settings: [
|
|
52
|
+
{
|
|
53
|
+
// Feature 073 — the operator's activation control. Platform-wide.
|
|
54
|
+
code: 'megamenu.enabled',
|
|
55
|
+
name: 'Megamenu enabled',
|
|
56
|
+
description: 'Switches the megamenu admin screens, their API and the storefront navigation they drive on or off. Nothing is dropped: menus, items and channel bindings stay in the database and reappear exactly as configured when you switch it back on.',
|
|
57
|
+
groupCode: 'megamenu',
|
|
58
|
+
valueType: 'boolean',
|
|
59
|
+
defaultValue: true,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* The ten error codes this module owns — feature 090, Phase 3.
|
|
65
|
+
*
|
|
66
|
+
* **This list is the incumbent prefix chain's answer, copied from
|
|
67
|
+
* `backend/test/fixtures/error-code-routing/chain-answers.ts`, not a
|
|
68
|
+
* judgement.** The migration is answer-preserving over all 289 codes with no
|
|
69
|
+
* exception list (`specs/090-module-owned-error-codes/contracts/error-code-declaration.md`
|
|
70
|
+
* §6.2), so nothing here was added or dropped because it looked misplaced;
|
|
71
|
+
* re-routing is §6.5's and out of scope.
|
|
72
|
+
*
|
|
73
|
+
* **Two codes a reader would attribute elsewhere, in both directions.**
|
|
74
|
+
* `MEGAMENU_LANGUAGE_NOT_IN_CHANNEL_SCOPE` reads as `sales_channels`' — it
|
|
75
|
+
* is raised while checking a language against a Sales Channel's configured
|
|
76
|
+
* language set — and is ours: the chain routes it on the `MEGAMENU_` prefix
|
|
77
|
+
* and `megamenu-service.ts` is what raises it. Going the other way, the three
|
|
78
|
+
* codes this module raises and does **not** own are `NOT_FOUND`,
|
|
79
|
+
* `VALIDATION_FAILED` and `VERSION_CONFLICT`, all the platform's; the first
|
|
80
|
+
* of those is raised for a *Sales Channel* that does not exist, so a reader
|
|
81
|
+
* tracing that refusal would look in `sales_channels` and find it in `core`.
|
|
82
|
+
*
|
|
83
|
+
* **Two of the ten are raised by nothing, and neither is a missing rule.**
|
|
84
|
+
* Both spellings were searched over `packages` and `backend/src` —
|
|
85
|
+
* `ERROR_CODES.<CODE>` and the bare quoted literal — because a module need
|
|
86
|
+
* not spell its codes the first way (runbook step 2, T12).
|
|
87
|
+
* `MEGAMENU_DEPTH_EXCEEDED` is built, enforced and covered by
|
|
88
|
+
* `test/contract/megamenu/admin-items-depth.contract.test.ts`, and it reaches
|
|
89
|
+
* the client — as a `meta.warnings` entry on a **200**, because FR-011 makes
|
|
90
|
+
* depth a non-blocking soft warning. It is never thrown, so the envelope can
|
|
91
|
+
* never render the `errors.MEGAMENU_DEPTH_EXCEEDED` sentence written for it
|
|
92
|
+
* in both languages. `MEGAMENU_REFERENCED` is built too: this module
|
|
93
|
+
* contributes reference descriptors to `assets_library`'s and `cms`' delete
|
|
94
|
+
* scans (`asset-references.ts`, `cms-references.ts`), the delete *is* refused
|
|
95
|
+
* and the holder list *does* name the menu — but the refusal is answered
|
|
96
|
+
* under the deleting module's code, `ASSET_REFERENCED` or `CMS_REFERENCED`,
|
|
97
|
+
* so this one never travels either.
|
|
98
|
+
* `specs/015-megamenu/contracts/megamenu-reference-scan.contract.md`
|
|
99
|
+
* still specifies `409 MEGAMENU_REFERENCED`; the contract and the
|
|
100
|
+
* implementation disagree, and that is a product finding rather than this
|
|
101
|
+
* merge request's to repair.
|
|
102
|
+
*
|
|
103
|
+
* **No `tokens`, derived rather than assumed.** The envelope's `refusalToken`
|
|
104
|
+
* (`packages/platform/src/http/error-envelope.ts`) reads exactly one member
|
|
105
|
+
* of `details` — `code`, and only when `details` is an object. Every raise of
|
|
106
|
+
* one of these codes was enumerated over `packages` and `backend/src` rather
|
|
107
|
+
* than over this package alone (runbook §5): all sixteen pass three
|
|
108
|
+
* arguments to `HttpError` and no fourth at all, so none can carry a token.
|
|
109
|
+
* The bundle agrees from the other direction — ten `errors.<CODE>` keys and
|
|
110
|
+
* not one `errors.<CODE>.<token>`.
|
|
111
|
+
*/
|
|
112
|
+
errorCodes: [
|
|
113
|
+
{ code: 'MEGAMENU_ASSET_KIND_MISMATCH' },
|
|
114
|
+
{ code: 'MEGAMENU_BINDING_ALREADY_EXISTS' },
|
|
115
|
+
{ code: 'MEGAMENU_BINDING_NOT_FOUND' },
|
|
116
|
+
{ code: 'MEGAMENU_DEPTH_EXCEEDED' },
|
|
117
|
+
{ code: 'MEGAMENU_EMPTY_TREE' },
|
|
118
|
+
{ code: 'MEGAMENU_HAS_ACTIVE_BINDINGS' },
|
|
119
|
+
{ code: 'MEGAMENU_LANGUAGE_NOT_IN_CHANNEL_SCOPE' },
|
|
120
|
+
{ code: 'MEGAMENU_NOT_FOUND' },
|
|
121
|
+
{ code: 'MEGAMENU_REFERENCED' },
|
|
122
|
+
{ code: 'MEGAMENU_TARGET_OUT_OF_SCOPE' },
|
|
123
|
+
],
|
|
124
|
+
i18n: { bundlesDir: 'i18n' },
|
|
125
|
+
docs: { dir: 'docs' },
|
|
126
|
+
permissions: [
|
|
127
|
+
{ code: 'megamenu.read', label: 'View megamenu configuration' },
|
|
128
|
+
{ code: 'megamenu.write', label: 'Edit megamenu configuration' },
|
|
129
|
+
],
|
|
130
|
+
actions: [
|
|
131
|
+
{
|
|
132
|
+
id: 'edit-megamenu',
|
|
133
|
+
labelKey: 'actions.editMegamenu.label',
|
|
134
|
+
descriptionKey: 'actions.editMegamenu.description',
|
|
135
|
+
icon: 'Menu',
|
|
136
|
+
targetRoute: '/megamenu',
|
|
137
|
+
requiredPermission: 'megamenu.write',
|
|
138
|
+
keywords: ['menu', 'edit', 'navigation', 'edytuj', 'nawigacja'],
|
|
139
|
+
weight: 240,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
// Feature 073 — the operator's activation control. Platform-wide. A platform
|
|
143
|
+
// without a megamenu is a smaller platform, not a broken one: the menus,
|
|
144
|
+
// their items and their channel bindings stay in the database and reappear
|
|
145
|
+
// exactly as configured when it is switched back on.
|
|
146
|
+
/**
|
|
147
|
+
* Nothing to demonstrate of its own (feature 113, T225 — contract §1.2).
|
|
148
|
+
*
|
|
149
|
+
* The demo shop does have a megamenu, and it is not this module's demo data:
|
|
150
|
+
* it mirrors `catalog`'s demo category tree and binds to the demo sales
|
|
151
|
+
* channels, so it is three modules' rows in one step and belongs to whoever
|
|
152
|
+
* owns the instance (§5.1). It is step 1 of `backend/src/seeds/
|
|
153
|
+
* demo-composition.ts`.
|
|
154
|
+
*
|
|
155
|
+
* **This block used to add *"and this module does not declare `catalog`"* as
|
|
156
|
+
* §2.3's measured case.** It declares it since
|
|
157
|
+
* `specs/110-instance-repository/` T118c, for a reason that has nothing to do
|
|
158
|
+
* with demo data: a menu item may target a category, so validating one is a
|
|
159
|
+
* read of `catalogCategoryReadPort`. The clause is retired rather than
|
|
160
|
+
* updated because it was the weaker half of the argument all along — the rule
|
|
161
|
+
* is that `demo` may not become a way of *acquiring* a dependency, and it
|
|
162
|
+
* holds identically now that the dependency is here on its own merits.
|
|
163
|
+
*
|
|
164
|
+
* `false` rather than absent, because the two are different states: this is
|
|
165
|
+
* a decision that the module owes nothing, not a module nobody has looked at.
|
|
166
|
+
*/
|
|
167
|
+
demo: false,
|
|
168
|
+
activation: { settingCode: 'megamenu.enabled', default: true },
|
|
169
|
+
});
|
|
170
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,WAAW,EAAE,2CAA2C;IACxD,OAAO,EAAE,OAAO;IAChB,8EAA8E;IAC9E,qEAAqE;IACrE,4EAA4E;IAC5E,2EAA2E;IAC3E,wEAAwE;IACxE,6DAA6D;IAC7D,2EAA2E;IAC3E,yEAAyE;IACzE,6EAA6E;IAC7E,kEAAkE;IAClE,kEAAkE;IAClE,EAAE;IACF,8EAA8E;IAC9E,sEAAsE;IACtE,2EAA2E;IAC3E,8EAA8E;IAC9E,8EAA8E;IAC9E,6EAA6E;IAC7E,2EAA2E;IAC3E,EAAE;IACF,wEAAwE;IACxE,8EAA8E;IAC9E,8EAA8E;IAC9E,+EAA+E;IAC/E,+EAA+E;IAC/E,0DAA0D;IAC1D,YAAY,EAAE;QACZ,gBAAgB;QAChB,SAAS;QACT,KAAK;QACL,WAAW;QACX,gBAAgB;QAChB,MAAM;QACN,cAAc;KACf;IACD,QAAQ,EAAE;QACR,UAAU,EAAE,UAAU;QACtB,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;QAChD,QAAQ,EAAE;YACR;gBACE,kEAAkE;gBAClE,IAAI,EAAE,kBAAkB;gBACxB,IAAI,EAAE,kBAAkB;gBACxB,WAAW,EACT,8OAA8O;gBAChP,SAAS,EAAE,UAAU;gBACrB,SAAS,EAAE,SAAS;gBACpB,YAAY,EAAE,IAAI;aACnB;SACF;KACF;IACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgDG;IACH,UAAU,EAAE;QACV,EAAE,IAAI,EAAE,8BAA8B,EAAE;QACxC,EAAE,IAAI,EAAE,iCAAiC,EAAE;QAC3C,EAAE,IAAI,EAAE,4BAA4B,EAAE;QACtC,EAAE,IAAI,EAAE,yBAAyB,EAAE;QACnC,EAAE,IAAI,EAAE,qBAAqB,EAAE;QAC/B,EAAE,IAAI,EAAE,8BAA8B,EAAE;QACxC,EAAE,IAAI,EAAE,wCAAwC,EAAE;QAClD,EAAE,IAAI,EAAE,oBAAoB,EAAE;QAC9B,EAAE,IAAI,EAAE,qBAAqB,EAAE;QAC/B,EAAE,IAAI,EAAE,8BAA8B,EAAE;KACzC;IACD,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB,WAAW,EAAE;QACX,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,6BAA6B,EAAE;QAC/D,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,6BAA6B,EAAE;KACjE;IACD,OAAO,EAAE;QACP;YACE,EAAE,EAAE,eAAe;YACnB,QAAQ,EAAE,4BAA4B;YACtC,cAAc,EAAE,kCAAkC;YAClD,IAAI,EAAE,MAAM;YACZ,WAAW,EAAE,WAAW;YACxB,kBAAkB,EAAE,gBAAgB;YACpC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,CAAC;YAC/D,MAAM,EAAE,GAAG;SACZ;KACF;IACD,6EAA6E;IAC7E,yEAAyE;IACzE,2EAA2E;IAC3E,qDAAqD;IACrD;;;;;;;;;;;;;;;;;;;;QAoBI;IACJ,IAAI,EAAE,KAAK;IACX,UAAU,EAAE,EAAE,WAAW,EAAE,kBAAkB,EAAE,OAAO,EAAE,IAAI,EAAE;CAC/D,CAAC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* Megamenu module — feature 015 / data-model.md.
|
|
4
|
+
*
|
|
5
|
+
* Schema changes (one atomic migration):
|
|
6
|
+
* - new tables: `megamenus`, `megamenu_items`, `megamenu_bindings`.
|
|
7
|
+
* - GIN index on `megamenu_items.target` (jsonb_path_ops) so the
|
|
8
|
+
* cross-module reference scan stays fast.
|
|
9
|
+
* - btree index on `megamenu_items(megamenu_id, parent_id, position)`
|
|
10
|
+
* drives the per-tree fetch.
|
|
11
|
+
* - **partial unique index** on
|
|
12
|
+
* `megamenu_bindings(sales_channel_id, language) WHERE active = true`
|
|
13
|
+
* enforces "exactly one active megamenu per (channel, language)"
|
|
14
|
+
* (FR-008 + research § R3) at the DB level.
|
|
15
|
+
*
|
|
16
|
+
* No seed rows; no data backfill — first-time admins create the first
|
|
17
|
+
* configuration through the admin UI.
|
|
18
|
+
*/
|
|
19
|
+
export declare class Migration20260505T193836MegamenuInit extends Migration {
|
|
20
|
+
up(): Promise<void>;
|
|
21
|
+
down(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=20260505T193836_megamenu_init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260505T193836_megamenu_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260505T193836_megamenu_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,oCAAqC,SAAQ,SAAS;IAClD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IA4EnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAKrC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* Megamenu module — feature 015 / data-model.md.
|
|
4
|
+
*
|
|
5
|
+
* Schema changes (one atomic migration):
|
|
6
|
+
* - new tables: `megamenus`, `megamenu_items`, `megamenu_bindings`.
|
|
7
|
+
* - GIN index on `megamenu_items.target` (jsonb_path_ops) so the
|
|
8
|
+
* cross-module reference scan stays fast.
|
|
9
|
+
* - btree index on `megamenu_items(megamenu_id, parent_id, position)`
|
|
10
|
+
* drives the per-tree fetch.
|
|
11
|
+
* - **partial unique index** on
|
|
12
|
+
* `megamenu_bindings(sales_channel_id, language) WHERE active = true`
|
|
13
|
+
* enforces "exactly one active megamenu per (channel, language)"
|
|
14
|
+
* (FR-008 + research § R3) at the DB level.
|
|
15
|
+
*
|
|
16
|
+
* No seed rows; no data backfill — first-time admins create the first
|
|
17
|
+
* configuration through the admin UI.
|
|
18
|
+
*/
|
|
19
|
+
export class Migration20260505T193836MegamenuInit extends Migration {
|
|
20
|
+
async up() {
|
|
21
|
+
// ────────────────────────────────────────────────────────────────────
|
|
22
|
+
// 1) megamenus
|
|
23
|
+
// ────────────────────────────────────────────────────────────────────
|
|
24
|
+
this.addSql(`
|
|
25
|
+
create table "megamenus" (
|
|
26
|
+
"id" uuid not null,
|
|
27
|
+
"name" varchar(200) not null,
|
|
28
|
+
"description" text null,
|
|
29
|
+
"version" int not null default 1,
|
|
30
|
+
"created_at" timestamptz not null,
|
|
31
|
+
"updated_at" timestamptz not null,
|
|
32
|
+
constraint "megamenus_pkey" primary key ("id")
|
|
33
|
+
);
|
|
34
|
+
`);
|
|
35
|
+
// ────────────────────────────────────────────────────────────────────
|
|
36
|
+
// 2) megamenu_items — adjacency-list tree
|
|
37
|
+
// ────────────────────────────────────────────────────────────────────
|
|
38
|
+
this.addSql(`
|
|
39
|
+
create table "megamenu_items" (
|
|
40
|
+
"id" uuid not null,
|
|
41
|
+
"megamenu_id" uuid not null,
|
|
42
|
+
"parent_id" uuid null,
|
|
43
|
+
"position" int not null default 0,
|
|
44
|
+
"kind" varchar(32) not null,
|
|
45
|
+
"labels" jsonb not null default '{}'::jsonb,
|
|
46
|
+
"descriptions" jsonb null,
|
|
47
|
+
"target" jsonb not null default '{}'::jsonb,
|
|
48
|
+
"created_at" timestamptz not null,
|
|
49
|
+
"updated_at" timestamptz not null,
|
|
50
|
+
constraint "megamenu_items_pkey" primary key ("id"),
|
|
51
|
+
constraint "megamenu_items_megamenu_fk" foreign key ("megamenu_id")
|
|
52
|
+
references "megamenus" ("id") on delete cascade,
|
|
53
|
+
constraint "megamenu_items_parent_fk" foreign key ("parent_id")
|
|
54
|
+
references "megamenu_items" ("id") on delete cascade
|
|
55
|
+
);
|
|
56
|
+
`);
|
|
57
|
+
this.addSql('create index "idx_megamenu_items_tree" on "megamenu_items" ("megamenu_id", "parent_id", "position");');
|
|
58
|
+
this.addSql('create index "idx_megamenu_items_target_refs" on "megamenu_items" using gin ("target" jsonb_path_ops);');
|
|
59
|
+
// ────────────────────────────────────────────────────────────────────
|
|
60
|
+
// 3) megamenu_bindings
|
|
61
|
+
// ────────────────────────────────────────────────────────────────────
|
|
62
|
+
this.addSql(`
|
|
63
|
+
create table "megamenu_bindings" (
|
|
64
|
+
"megamenu_id" uuid not null,
|
|
65
|
+
"sales_channel_id" uuid not null,
|
|
66
|
+
"language" varchar(8) not null,
|
|
67
|
+
"active" boolean not null default false,
|
|
68
|
+
"version" int not null default 1,
|
|
69
|
+
"created_at" timestamptz not null,
|
|
70
|
+
"updated_at" timestamptz not null,
|
|
71
|
+
constraint "megamenu_bindings_pkey"
|
|
72
|
+
primary key ("megamenu_id", "sales_channel_id", "language"),
|
|
73
|
+
constraint "megamenu_bindings_megamenu_fk" foreign key ("megamenu_id")
|
|
74
|
+
references "megamenus" ("id") on delete cascade,
|
|
75
|
+
constraint "megamenu_bindings_channel_fk" foreign key ("sales_channel_id")
|
|
76
|
+
references "sales_channels" ("id") on delete cascade
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
// Partial unique index (FR-008 / R3) — exactly one active megamenu
|
|
80
|
+
// per (sales_channel_id, language).
|
|
81
|
+
this.addSql('create unique index "idx_megamenu_bindings_active_uniq" on "megamenu_bindings" ("sales_channel_id", "language") where "active" = true;');
|
|
82
|
+
// Lookup index for the storefront resolver.
|
|
83
|
+
this.addSql('create index "idx_megamenu_bindings_channel_lang" on "megamenu_bindings" ("sales_channel_id", "language");');
|
|
84
|
+
}
|
|
85
|
+
async down() {
|
|
86
|
+
this.addSql('drop table if exists "megamenu_bindings" cascade;');
|
|
87
|
+
this.addSql('drop table if exists "megamenu_items" cascade;');
|
|
88
|
+
this.addSql('drop table if exists "megamenus" cascade;');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=20260505T193836_megamenu_init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260505T193836_megamenu_init.js","sourceRoot":"","sources":["../../src/migrations/20260505T193836_megamenu_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,oCAAqC,SAAQ,SAAS;IACxD,KAAK,CAAC,EAAE;QACf,uEAAuE;QACvE,eAAe;QACf,uEAAuE;QACvE,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;KAUX,CAAC,CAAC;QAEH,uEAAuE;QACvE,0CAA0C;QAC1C,uEAAuE;QACvE,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;KAkBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,sGAAsG,CACvG,CAAC;QACF,IAAI,CAAC,MAAM,CACT,wGAAwG,CACzG,CAAC;QAEF,uEAAuE;QACvE,uBAAuB;QACvB,uEAAuE;QACvE,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;KAgBX,CAAC,CAAC;QACH,mEAAmE;QACnE,oCAAoC;QACpC,IAAI,CAAC,MAAM,CACT,wIAAwI,CACzI,CAAC;QACF,4CAA4C;QAC5C,IAAI,CAAC,MAAM,CACT,4GAA4G,CAC7G,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,mDAAmD,CAAC,CAAC;QACjE,IAAI,CAAC,MAAM,CAAC,gDAAgD,CAAC,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC,2CAA2C,CAAC,CAAC;IAC3D,CAAC;CACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `./migrations` subpath — every migration class this module owns, as one
|
|
3
|
+
* ordered `migrations` array.
|
|
4
|
+
*
|
|
5
|
+
* The array is what the platform reads when this module is **installed**:
|
|
6
|
+
* `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
|
|
7
|
+
* the package outright when it is absent (D-168).
|
|
8
|
+
*
|
|
9
|
+
* Listed in ascending timestamp, which orders **this module's own** migrations
|
|
10
|
+
* and nothing else (feature 081). Where the block sits relative to every other
|
|
11
|
+
* module's is decided by the manifest `dependencies` graph.
|
|
12
|
+
*
|
|
13
|
+
* The **named** exports stay, and the asymmetry with `./backend` — which
|
|
14
|
+
* publishes an array and no entity class by name (D-168) — is deliberate.
|
|
15
|
+
* `db/migrations-registry.generated.ts` imports each class by name from this
|
|
16
|
+
* specifier, and a migration class name is contract in a way an entity class
|
|
17
|
+
* name is not: `mikro_orm_migrations` persists it, so it is a string every
|
|
18
|
+
* already-migrated database holds.
|
|
19
|
+
*
|
|
20
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
21
|
+
* not run: `migration:pending` reports nothing pending and the first symptom
|
|
22
|
+
* is a query against a table nobody created.
|
|
23
|
+
*/
|
|
24
|
+
import { Migration20260505T193836MegamenuInit } from './20260505T193836_megamenu_init.js';
|
|
25
|
+
export declare const migrations: (typeof Migration20260505T193836MegamenuInit)[];
|
|
26
|
+
export { Migration20260505T193836MegamenuInit, };
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,oCAAoC,EAAE,MAAM,oCAAoC,CAAC;AAE1F,eAAO,MAAM,UAAU,iDAEtB,CAAC;AAEF,OAAO,EACL,oCAAoC,GACrC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `./migrations` subpath — every migration class this module owns, as one
|
|
3
|
+
* ordered `migrations` array.
|
|
4
|
+
*
|
|
5
|
+
* The array is what the platform reads when this module is **installed**:
|
|
6
|
+
* `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
|
|
7
|
+
* the package outright when it is absent (D-168).
|
|
8
|
+
*
|
|
9
|
+
* Listed in ascending timestamp, which orders **this module's own** migrations
|
|
10
|
+
* and nothing else (feature 081). Where the block sits relative to every other
|
|
11
|
+
* module's is decided by the manifest `dependencies` graph.
|
|
12
|
+
*
|
|
13
|
+
* The **named** exports stay, and the asymmetry with `./backend` — which
|
|
14
|
+
* publishes an array and no entity class by name (D-168) — is deliberate.
|
|
15
|
+
* `db/migrations-registry.generated.ts` imports each class by name from this
|
|
16
|
+
* specifier, and a migration class name is contract in a way an entity class
|
|
17
|
+
* name is not: `mikro_orm_migrations` persists it, so it is a string every
|
|
18
|
+
* already-migrated database holds.
|
|
19
|
+
*
|
|
20
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
21
|
+
* not run: `migration:pending` reports nothing pending and the first symptom
|
|
22
|
+
* is a query against a table nobody created.
|
|
23
|
+
*/
|
|
24
|
+
import { Migration20260505T193836MegamenuInit } from './20260505T193836_megamenu_init.js';
|
|
25
|
+
export const migrations = [
|
|
26
|
+
Migration20260505T193836MegamenuInit,
|
|
27
|
+
];
|
|
28
|
+
export { Migration20260505T193836MegamenuInit, };
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,oCAAoC,EAAE,MAAM,oCAAoC,CAAC;AAE1F,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,oCAAoC;CACrC,CAAC;AAEF,OAAO,EACL,oCAAoC,GACrC,CAAC"}
|