# 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.