Files
2026-06-28 21:34:58 -04:00

11 KiB

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 --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:
    bun x prisma migrate dev --name add_menu_images
    
  2. Production/Deployment Environments: Apply the migrations safely and regenerate the client with:
    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 --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 --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 --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 --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.