289 lines
8.8 KiB
Markdown
289 lines
8.8 KiB
Markdown
# Milestone 2: Backend & DB — Menu Images Field Addition Strategy
|
|
## Explorer 1 Analysis Report
|
|
|
|
This report outlines the precise implementation strategy for adding optional menu images fields (`rootImage` on `Menu`, and `columnImage` / `hoverImage` on `MenuItem`) across the codebase.
|
|
|
|
---
|
|
|
|
### 1. Prisma Schema Changes (`apps/backend/prisma/schema.prisma`)
|
|
|
|
We will add the new optional image fields to the `Menu` and `MenuItem` models.
|
|
|
|
#### Model `Menu` Updates (around Line 638)
|
|
```prisma
|
|
model Menu {
|
|
id Int @id @default(autoincrement())
|
|
name String @unique // e.g., "header", "footer"
|
|
payload Json?
|
|
rootImage String? // <-- Added optional root image for the menu
|
|
isActive Boolean @default(true)
|
|
items MenuItem[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
```
|
|
|
|
#### Model `MenuItem` Updates (around Line 648)
|
|
```prisma
|
|
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? // <-- Added optional column image URL
|
|
hoverImage String? // <-- Added optional hover image URL
|
|
translations MenuItemTranslation[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
```
|
|
|
|
#### Migration Commands Required
|
|
1. Run the migration to generate and apply the schema changes in the database development environment:
|
|
```bash
|
|
cd apps/backend && bunx prisma migrate dev --name add_menu_image_fields
|
|
```
|
|
*Note: Using `bunx` matches the workspace runner environment (as specified in package.json dev scripts).*
|
|
2. Prisma client will be automatically generated upon a successful migration. If not, generate manually using:
|
|
```bash
|
|
cd apps/backend && bunx prisma generate
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Shared Types Updates (`packages/types/index.ts`)
|
|
|
|
Modify the interfaces to expose the newly added fields.
|
|
|
|
#### Interface `MenuItem` (around Line 195)
|
|
```typescript
|
|
export interface MenuItem {
|
|
id: number
|
|
label: string
|
|
url: string
|
|
order: number
|
|
columnImage?: string | null // <-- Added
|
|
hoverImage?: string | null // <-- Added
|
|
translations?: any[]
|
|
}
|
|
```
|
|
|
|
#### Interface `Menu` (around Line 203)
|
|
```typescript
|
|
export interface Menu {
|
|
id: number
|
|
name: string
|
|
items: MenuItem[]
|
|
rootImage?: string | null // <-- Added
|
|
payload?: any
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3. Backend Route Updates (`apps/backend/src/routes/menus.ts`)
|
|
|
|
Update the Elysia endpoints to handle, validate, save, and return these new fields.
|
|
|
|
#### 1) `POST /` Endpoint
|
|
* **Body Destructuring & Destructuring Cast (around Line 34):**
|
|
```typescript
|
|
const { name, rootImage, items } = body as {
|
|
name: string;
|
|
rootImage?: string;
|
|
items?: { label: string; url: string; order: number; columnImage?: string; hoverImage?: string }[]
|
|
};
|
|
```
|
|
* **Database Creation Payload (around Line 35):**
|
|
```typescript
|
|
const menu = await prisma.menu.create({
|
|
data: {
|
|
name,
|
|
rootImage,
|
|
items: items ? {
|
|
create: items
|
|
} : undefined
|
|
},
|
|
include: { items: true }
|
|
});
|
|
```
|
|
* **Validation Schema (around Line 50):**
|
|
```typescript
|
|
body: t.Object({
|
|
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(),
|
|
columnImage: t.Optional(t.Nullable(t.String())),
|
|
hoverImage: t.Optional(t.Nullable(t.String()))
|
|
})))
|
|
})
|
|
```
|
|
|
|
#### 2) `PUT /:name` Endpoint
|
|
* **Body Destructuring & Type Casting (around Line 62):**
|
|
```typescript
|
|
const { items, payload, isActive, rootImage } = body as {
|
|
payload?: any;
|
|
isActive?: boolean;
|
|
rootImage?: string | null;
|
|
items: {
|
|
id?: number;
|
|
label: string;
|
|
url: string;
|
|
order: number;
|
|
columnImage?: string | null;
|
|
hoverImage?: string | null;
|
|
translations?: { langCode: string; label: string }[]
|
|
}[]
|
|
};
|
|
```
|
|
* **Map `rootImage` to `updateData` (around Line 69):**
|
|
```typescript
|
|
const updateData: any = {};
|
|
if (payload !== undefined) updateData.payload = payload;
|
|
if (isActive !== undefined) updateData.isActive = isActive;
|
|
if (rootImage !== undefined) updateData.rootImage = rootImage;
|
|
```
|
|
* **Transaction - Item Update (around Line 90):**
|
|
```typescript
|
|
await tx.menuItem.update({
|
|
where: { id: item.id },
|
|
data: {
|
|
label: item.label,
|
|
url: item.url,
|
|
order: item.order,
|
|
columnImage: item.columnImage !== undefined ? item.columnImage : undefined,
|
|
hoverImage: item.hoverImage !== undefined ? item.hoverImage : undefined
|
|
}
|
|
});
|
|
```
|
|
* **Transaction - Item Create (around Line 105):**
|
|
```typescript
|
|
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 ? {
|
|
create: item.translations.map(t => ({
|
|
langCode: t.langCode,
|
|
label: t.label
|
|
}))
|
|
} : undefined
|
|
}
|
|
});
|
|
```
|
|
* **Validation Schema (around Line 133):**
|
|
```typescript
|
|
body: t.Object({
|
|
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({
|
|
langCode: t.String(),
|
|
label: t.String()
|
|
})))
|
|
}))
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
### 4. MCP Server Tools Updates
|
|
|
|
#### 1) Tool Input Validation (`apps/mcp-server/menu-tools.ts` around Line 181)
|
|
Add input check in `validateMenuItems`:
|
|
```typescript
|
|
if (item.columnImage !== undefined && item.columnImage !== null && typeof item.columnImage !== "string") {
|
|
issues.push(`items[${index}].columnImage must be a string when provided`);
|
|
}
|
|
if (item.hoverImage !== undefined && item.hoverImage !== null && typeof item.hoverImage !== "string") {
|
|
issues.push(`items[${index}].hoverImage must be a string when provided`);
|
|
}
|
|
```
|
|
|
|
#### 2) Tool Schema and Handler (`apps/mcp-server/menu-toolkit.ts`)
|
|
* **`upsert_menu` Input Schema:**
|
|
Add `rootImage` property to top-level properties:
|
|
```json
|
|
"rootImage": {
|
|
"type": "string",
|
|
"description": "Optional root image URL for the menu"
|
|
}
|
|
```
|
|
Add `columnImage` and `hoverImage` to item properties:
|
|
```json
|
|
"columnImage": {
|
|
"type": "string",
|
|
"description": "Optional column image URL for the menu item"
|
|
},
|
|
"hoverImage": {
|
|
"type": "string",
|
|
"description": "Optional hover image URL for the menu item"
|
|
}
|
|
```
|
|
* **`handleMenuTool` logic for `upsert_menu`:**
|
|
Check and pass `rootImage` in the body payload (around Line 227):
|
|
```typescript
|
|
if (params.rootImage !== undefined) {
|
|
body.rootImage = params.rootImage;
|
|
} else if (existingMenu?.rootImage !== undefined) {
|
|
body.rootImage = existingMenu.rootImage;
|
|
}
|
|
```
|
|
Update dryRun output preview (around Line 253):
|
|
```typescript
|
|
preview: {
|
|
name: params.name,
|
|
itemCount: items.length,
|
|
hasPayload: !!body.payload,
|
|
isActive: body.isActive,
|
|
rootImage: body.rootImage
|
|
}
|
|
```
|
|
Update actual successful response payload to expose `rootImage` (around Line 273):
|
|
```typescript
|
|
text: JSON.stringify(
|
|
{
|
|
success: true,
|
|
operation: "upsert_menu",
|
|
name: res.data.name,
|
|
isActive: res.data.isActive,
|
|
rootImage: res.data.rootImage,
|
|
itemCount: res.data.items?.length || 0,
|
|
hasPayload: !!res.data.payload
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
### 5. Potential Risks & Mitigation
|
|
|
|
1. **Migration Conflicts:**
|
|
* *Risk:* Running migrations in environments where database instances are out of sync can lead to conflicts.
|
|
* *Mitigation:* Ensure other developers pull changes and run `bunx prisma migrate dev` before starting work.
|
|
2. **Backward Compatibility:**
|
|
* *Risk:* Existing menus or components that do not expect nullable/optional fields might break if type checks are strictly checking properties.
|
|
* *Mitigation:* Use `String?` / optional properties which evaluate to `null` or `undefined`. Ensure frontend consumption handles `null` values gracefully (e.g. using optional chaining `menu.rootImage` and fallback components).
|
|
3. **Monorepo Dependency Syncing:**
|
|
* *Risk:* If `@homaroma/types` is updated but workspace dependencies aren't built or re-linked, typescript compilation might fail on clients.
|
|
* *Mitigation:* Run workspace-wide build or type checks (`npm run build` or `bunx tsc`) after updating the package.
|