This commit is contained in:
Your Name committed 2026-06-28 21:34:58 -04:00
1 parent 3965f4f4da
commit 70877882b0
82 files changed
+4353 -186

No files matched your search

+285
View File
@@ -0,0 +1,285 @@
# Implementation Strategy Report — Menu & MenuItem Image Fields
This report outlines the precise implementation strategy for adding three new optional image fields to the menu system: `rootImage` for `Menu`, and `columnImage` & `hoverImage` for `MenuItem`.
---
## 1. Prisma Schema Changes (`apps/backend/prisma/schema.prisma`)
We will add the optional/nullable image fields directly to the `Menu` and `MenuItem` database models.
### Proposed Diff
```diff
diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma
--- a/apps/backend/prisma/schema.prisma
+++ b/apps/backend/prisma/schema.prisma
@@ -638,6 +638,7 @@ model Menu {
id Int @id @default(autoincrement())
name String @unique // e.g., "header", "footer"
+ rootImage String?
payload Json?
isActive Boolean @default(true)
items MenuItem[]
@@ -648,6 +649,8 @@ model MenuItem {
id Int @id @default(autoincrement())
menuId Int
menu Menu @relation(fields: [menuId], references: [id], onDelete: Cascade)
label String
url String
order Int @default(0)
+ columnImage String?
+ hoverImage String?
translations MenuItemTranslation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
```
### Required Migration Commands
1. **Development Environment:**
Run the following command inside `apps/backend/` to generate a database migration and update the Prisma client:
```bash
bun x prisma migrate dev --name add_menu_images
```
2. **Production/Deployment Environments:**
Apply the migrations safely and regenerate the client with:
```bash
bun x prisma migrate deploy
bun x prisma generate
```
---
## 2. Shared Types (`packages/types/index.ts`)
Expose these new fields in the shared interfaces `Menu` and `MenuItem` used across the backend and MCP server.
### Proposed Diff
```diff
diff --git a/packages/types/index.ts b/packages/types/index.ts
--- a/packages/types/index.ts
+++ b/packages/types/index.ts
@@ -195,6 +195,8 @@ export interface MenuItem {
label: string
url: string
order: number
+ columnImage?: string | null
+ hoverImage?: string | null
translations?: any[]
}
@@ -202,6 +204,7 @@ export interface MenuItem {
export interface Menu {
id: number
name: string
+ rootImage?: string | null
items: MenuItem[]
payload?: any
}
```
---
## 3. Backend Routes (`apps/backend/src/routes/menus.ts`)
Update the Elysia endpoints to accept, validate, process, and return the new image fields.
### Proposed Diff
```diff
diff --git a/apps/backend/src/routes/menus.ts b/apps/backend/src/routes/menus.ts
--- a/apps/backend/src/routes/menus.ts
+++ b/apps/backend/src/routes/menus.ts
@@ -32,13 +32,23 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
.post("/", async ({ body, set }) => {
try {
- const { name, items } = body as { name: string; items?: { label: string; url: string; order: number }[] };
+ const { name, rootImage, items } = body as {
+ name: string;
+ rootImage?: string | null;
+ items?: {
+ label: string;
+ url: string;
+ order: number;
+ columnImage?: string | null;
+ hoverImage?: string | null;
+ }[];
+ };
const menu = await prisma.menu.create({
data: {
name,
+ rootImage,
items: items ? {
- create: items
+ create: items.map(item => ({
+ label: item.label,
+ url: item.url,
+ order: item.order,
+ columnImage: item.columnImage,
+ hoverImage: item.hoverImage
+ }))
} : undefined
},
include: { items: true }
@@ -53,7 +63,9 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
name: t.String(),
+ rootImage: t.Optional(t.Nullable(t.String())),
items: t.Optional(t.Array(t.Object({
label: t.String(),
url: t.String(),
- order: t.Number()
+ order: t.Number(),
+ columnImage: t.Optional(t.Nullable(t.String())),
+ hoverImage: t.Optional(t.Nullable(t.String()))
})))
})
})
.put("/:name", async ({ params: { name }, body, set }) => {
try {
- const { items, payload, isActive } = body as {
+ const { items, payload, isActive, rootImage } = body as {
payload?: any;
isActive?: boolean;
+ rootImage?: string | null;
- items: { id?: number; label: string; url: string; order: number; translations?: { langCode: string; label: string }[] }[]
+ items: {
+ id?: number;
+ label: string;
+ url: string;
+ order: number;
+ columnImage?: string | null;
+ hoverImage?: string | null;
+ translations?: { langCode: string; label: string }[]
+ }[]
};
let menu = await prisma.menu.findUnique({ where: { name } });
const updateData: any = {};
if (payload !== undefined) updateData.payload = payload;
if (isActive !== undefined) updateData.isActive = isActive;
+ if (rootImage !== undefined) updateData.rootImage = rootImage;
if (!menu) {
menu = await prisma.menu.create({ data: { name, ...updateData } });
@@ -91,3 +111,5 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
await tx.menuItem.update({
where: { id: item.id },
- data: { label: item.label, url: item.url, order: item.order }
+ data: {
+ label: item.label,
+ url: item.url,
+ order: item.order,
+ columnImage: item.columnImage,
+ hoverImage: item.hoverImage
+ }
});
if (item.translations?.length) {
@@ -105,7 +127,9 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
await tx.menuItem.create({
data: {
menuId: menu!.id,
label: item.label,
url: item.url,
order: item.order,
+ columnImage: item.columnImage,
+ hoverImage: item.hoverImage,
translations: item.translations?.length ? {
@@ -134,6 +158,7 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
payload: t.Optional(t.Any()),
isActive: t.Optional(t.Boolean()),
+ rootImage: t.Optional(t.Nullable(t.String())),
items: t.Array(t.Object({
id: t.Optional(t.Number()),
label: t.String(),
url: t.String(),
order: t.Number(),
+ columnImage: t.Optional(t.Nullable(t.String())),
+ hoverImage: t.Optional(t.Nullable(t.String())),
translations: t.Optional(t.Array(t.Object({
```
---
## 4. MCP Server Tools (`apps/mcp-server/menu-tools.ts` and `apps/mcp-server/menu-toolkit.ts`)
We will update the tool validators and schemas to allow processing these fields via MCP commands.
### 4.1 Changes to `apps/mcp-server/menu-tools.ts`
Introduce validators for `columnImage` and `hoverImage` in the `validateMenuItems` helper function.
```diff
diff --git a/apps/mcp-server/menu-tools.ts b/apps/mcp-server/menu-tools.ts
--- a/apps/mcp-server/menu-tools.ts
+++ b/apps/mcp-server/menu-tools.ts
@@ -183,6 +183,12 @@ function validateMenuItems(items: any) {
if (item.order !== undefined && typeof item.order !== "number") {
issues.push(`items[${index}].order must be a number when provided`);
}
+ if (item.columnImage !== undefined && typeof item.columnImage !== "string" && item.columnImage !== null) {
+ issues.push(`items[${index}].columnImage must be a string or null when provided`);
+ }
+ if (item.hoverImage !== undefined && typeof item.hoverImage !== "string" && item.hoverImage !== null) {
+ issues.push(`items[${index}].hoverImage must be a string or null when provided`);
+ }
```
*Note: For the structural tree validation (`validateMenuNodeData`), we can also optionally validate these keys on tree nodes if they are added as part of the JSON payload.*
### 4.2 Changes to `apps/mcp-server/menu-toolkit.ts`
Update the `upsert_menu` tool definition to support the new fields in its input schema, and pass `rootImage` correctly inside the handler.
```diff
diff --git a/apps/mcp-server/menu-toolkit.ts b/apps/mcp-server/menu-toolkit.ts
--- a/apps/mcp-server/menu-toolkit.ts
+++ b/apps/mcp-server/menu-toolkit.ts
@@ -45,6 +45,7 @@ export const MENU_TOOL_DEFINITIONS = [
isActive: { type: "boolean", description: "Optional active state for the menu" },
+ rootImage: { type: ["string", "null"], description: "Optional image URL for the root menu container" },
items: {
type: "array",
description: "Array of menu items",
@@ -56,6 +57,8 @@ export const MENU_TOOL_DEFINITIONS = [
url: { type: "string", description: "URL of the menu item" },
order: { type: "number", description: "Order sequence" },
+ columnImage: { type: ["string", "null"], description: "Optional image URL for the column" },
+ hoverImage: { type: ["string", "null"], description: "Optional image URL for hover states" },
translations: {
@@ -216,6 +219,12 @@ export async function handleMenuTool(api: AxiosInstance, name: string, params: a
if (itemCheck.issues.length > 0) {
throw new Error(`Invalid menu items: ${itemCheck.issues.join("; ")}`);
}
+ if (params.rootImage !== undefined) {
+ body.rootImage = params.rootImage;
+ } else if (existingMenu?.rootImage !== undefined) {
+ body.rootImage = existingMenu.rootImage;
+ }
+
if (params.payload !== undefined) {
body.payload = params.payload;
} else if (existingMenu?.payload !== undefined) {
@@ -275,6 +284,7 @@ export async function handleMenuTool(api: AxiosInstance, name: string, params: a
name: res.data.name,
isActive: res.data.isActive,
+ rootImage: res.data.rootImage,
itemCount: res.data.items?.length || 0,
hasPayload: !!res.data.payload
```
---
## 5. Potential Risks & Mitigation
1. **Database Upgrades / Existing Records:**
- **Risk:** Existing database records will have `NULL` values for the new columns.
- **Mitigation:** Since the fields are optional/nullable in Prisma (`String?`) and shared types (`string | null`), this is safe and backwards-compatible. No database seed adjustments are strictly required to avoid crash, but seeding mock images is recommended.
2. **Frontend Type Mismatch:**
- **Risk:** Client apps (SPA/Website) define local structures for `MegaMenuPayload` which might need updating once these fields start appearing in API responses.
- **Mitigation:** The changes do not break old rendering logic as they are optional fields, but they must be exposed on the frontend interfaces in Milestone 3 to be fully utilized.
3. **Regenerating Client Libraries:**
- **Risk:** Developers or test pipelines compiling without running `prisma generate` will run into compilation errors due to type mismatches.
- **Mitigation:** Running `npm run build` or `bun run build` in the monorepo automatically triggers prisma client generation.