x
This commit is contained in:
1 parent
3965f4f4da
commit
70877882b0
82 files changed
+4353
-186
No files matched your search
@@ -0,0 +1,39 @@
|
||||
# BRIEFING — 2026-06-29T01:20:34Z
|
||||
|
||||
## Mission
|
||||
Analyze codebase and formulate implementation strategy for updating Menu/MenuItem schema, shared types, backend routes, and MCP tools with new image fields.
|
||||
|
||||
## 🔒 My Identity
|
||||
- Archetype: Explorer
|
||||
- Roles: Read-only investigator
|
||||
- Working directory: /home/ti/git/homaroma/.agents/explorer_m2_3
|
||||
- Original parent: 8e727cf6-d863-41ec-9bd2-f8dead86db2a
|
||||
- Milestone: Milestone 2 (Backend & DB)
|
||||
|
||||
## 🔒 Key Constraints
|
||||
- Read-only investigation — do NOT implement
|
||||
- Operational code-only mode: NO external web searches, curl, wget, etc.
|
||||
- No editing files outside of own directory.
|
||||
|
||||
## Current Parent
|
||||
- Conversation ID: 8e727cf6-d863-41ec-9bd2-f8dead86db2a
|
||||
- Updated: 2026-06-29T01:21:36Z
|
||||
|
||||
## Investigation State
|
||||
- **Explored paths**: `apps/backend/prisma/schema.prisma`, `packages/types/index.ts`, `apps/backend/src/routes/menus.ts`, `apps/mcp-server/menu-tools.ts`, `apps/mcp-server/menu-toolkit.ts`
|
||||
- **Key findings**:
|
||||
- `Menu` and `MenuItem` models mapped in Prisma database, requiring `rootImage` (Menu) and `columnImage`/`hoverImage` (MenuItem) nullable columns.
|
||||
- Shared TypeScript interfaces in `packages/types/index.ts` must expose the same fields.
|
||||
- Elysia API routes in `menus.ts` require TypeBox validation updates and database payload mapping for POST/PUT.
|
||||
- MCP Server tools require validation changes in `menu-tools.ts` and tool schema definitions in `menu-toolkit.ts`.
|
||||
- **Unexplored areas**: None, task is complete.
|
||||
|
||||
## Key Decisions Made
|
||||
- Chose `t.Optional(t.Nullable(t.String()))` for Elysia API payload validation to preserve backward compatibility.
|
||||
- Determined that database migrations must be run via `bunx prisma migrate dev` locally and `bunx prisma migrate deploy` in production.
|
||||
- Kept the seed script (`seed.cjs`) unchanged as the new fields are optional.
|
||||
|
||||
## Artifact Index
|
||||
- /home/ti/git/homaroma/.agents/explorer_m2_3/analysis.md — Main analysis and implementation strategy report
|
||||
- /home/ti/git/homaroma/.agents/explorer_m2_3/handoff.md — Final handoff report
|
||||
- /home/ti/git/homaroma/.agents/explorer_m2_3/progress.md — Liveness progress heartbeat
|
||||
@@ -0,0 +1,475 @@
|
||||
# Implementation Strategy for Menu Image Fields
|
||||
|
||||
This report outlines the technical analysis and implementation strategy for adding new image fields to the `Menu` and `MenuItem` models in the backend database, shared types, backend API routes, and MCP tools.
|
||||
|
||||
---
|
||||
|
||||
## Summary of Findings
|
||||
|
||||
- **Database Model**: In `apps/backend/prisma/schema.prisma`, `Menu` holds a flat list of `MenuItem` objects via a relation, and a hierarchical JSON `payload`. To support the layout requirements, `Menu` requires `rootImage` (optional `String?`), while `MenuItem` requires `columnImage` and `hoverImage` (both optional `String?`).
|
||||
- **TypeScript Types**: `packages/types/index.ts` exposes interface types `Menu` and `MenuItem` directly from source files, so adding the optional image properties there is straightforward.
|
||||
- **Backend Routes**: `apps/backend/src/routes/menus.ts` defines Elysia routes. The `POST /` and `PUT /:name` endpoints require updating their TypeBox validation schemas (`t.Optional(t.Nullable(t.String()))`) and mapping the new fields in Prisma transactions. `GET` endpoints automatically return these new database fields.
|
||||
- **MCP Server Tools**: `apps/mcp-server/menu-tools.ts` validates `MenuItem` lists via `validateMenuItems` and requires checks for the new image fields. `apps/mcp-server/menu-toolkit.ts` exposes `upsert_menu` and requires updating its input schema properties and input payload mapping.
|
||||
|
||||
---
|
||||
|
||||
## 1. Database Schema & Migration Strategy
|
||||
|
||||
### File: `apps/backend/prisma/schema.prisma`
|
||||
|
||||
#### Existing Code (Lines 638–658)
|
||||
```prisma
|
||||
model Menu {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique // e.g., "header", "footer"
|
||||
payload Json?
|
||||
isActive Boolean @default(true)
|
||||
items MenuItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
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)
|
||||
translations MenuItemTranslation[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
#### Proposed Changes
|
||||
Add `rootImage` to the `Menu` model, and `columnImage`/`hoverImage` to the `MenuItem` model:
|
||||
|
||||
```prisma
|
||||
model Menu {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique // e.g., "header", "footer"
|
||||
payload Json?
|
||||
isActive Boolean @default(true)
|
||||
rootImage String?
|
||||
items MenuItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
### Migration Commands
|
||||
1. **Development Environment**: To generate and apply the SQL migration locally, run:
|
||||
```bash
|
||||
bunx prisma migrate dev --name add_menu_image_fields
|
||||
```
|
||||
*Warning*: Never use `prisma db push` in this project as it bypasses migrations and will lead to local/production schema drift.
|
||||
2. **Production/CI Environment**: To apply the created migration to the production database during deployment, run:
|
||||
```bash
|
||||
bunx prisma migrate deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Shared Type Declarations
|
||||
|
||||
### File: `packages/types/index.ts`
|
||||
|
||||
#### Existing Code (Lines 195–208)
|
||||
```typescript
|
||||
export interface MenuItem {
|
||||
id: number
|
||||
label: string
|
||||
url: string
|
||||
order: number
|
||||
translations?: any[]
|
||||
}
|
||||
|
||||
export interface Menu {
|
||||
id: number
|
||||
name: string
|
||||
items: MenuItem[]
|
||||
payload?: any
|
||||
}
|
||||
```
|
||||
|
||||
#### Proposed Changes
|
||||
Update interfaces to expose the optional properties:
|
||||
|
||||
```typescript
|
||||
export interface MenuItem {
|
||||
id: number
|
||||
label: string
|
||||
url: string
|
||||
order: number
|
||||
columnImage?: string | null
|
||||
hoverImage?: string | null
|
||||
translations?: any[]
|
||||
}
|
||||
|
||||
export interface Menu {
|
||||
id: number
|
||||
name: string
|
||||
rootImage?: string | null
|
||||
items: MenuItem[]
|
||||
payload?: any
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend Route Changes
|
||||
|
||||
### File: `apps/backend/src/routes/menus.ts`
|
||||
|
||||
Both `POST /` and `PUT /:name` routes must be modified.
|
||||
|
||||
#### A. Route `POST /` (Lines 32–59)
|
||||
|
||||
##### Before
|
||||
```typescript
|
||||
.post("/", async ({ body, set }) => {
|
||||
try {
|
||||
const { name, items } = body as { name: string; items?: { label: string; url: string; order: number }[] };
|
||||
const menu = await prisma.menu.create({
|
||||
data: {
|
||||
name,
|
||||
items: items ? {
|
||||
create: items
|
||||
} : undefined
|
||||
},
|
||||
include: { items: true }
|
||||
});
|
||||
return menu;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
set.status = 500;
|
||||
return { error: "Failed to create menu" };
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
items: t.Optional(t.Array(t.Object({
|
||||
label: t.String(),
|
||||
url: t.String(),
|
||||
order: t.Number()
|
||||
})))
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
##### After (Proposed)
|
||||
```typescript
|
||||
.post("/", async ({ body, set }) => {
|
||||
try {
|
||||
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
|
||||
} : undefined
|
||||
},
|
||||
include: { items: true }
|
||||
});
|
||||
return menu;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
set.status = 500;
|
||||
return { error: "Failed to create menu" };
|
||||
}
|
||||
}, {
|
||||
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()))
|
||||
})))
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### B. Route `PUT /:name` (Lines 60–147)
|
||||
|
||||
##### Before
|
||||
```typescript
|
||||
.put("/:name", async ({ params: { name }, body, set }) => {
|
||||
try {
|
||||
const { items, payload, isActive } = body as {
|
||||
payload?: any;
|
||||
isActive?: boolean;
|
||||
items: { id?: number; label: string; url: string; order: number; 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 (!menu) {
|
||||
menu = await prisma.menu.create({ data: { name, ...updateData } });
|
||||
} else if (Object.keys(updateData).length > 0) {
|
||||
menu = await prisma.menu.update({ where: { name }, data: updateData });
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx: any) => {
|
||||
const existingItems = await tx.menuItem.findMany({ where: { menuId: menu!.id } });
|
||||
const existingItemIds = existingItems.map((i: any) => i.id);
|
||||
const payloadItemIds = items.map(i => i.id).filter(id => id != null);
|
||||
const itemsToDelete = existingItemIds.filter((id: number) => !payloadItemIds.includes(id));
|
||||
|
||||
if (itemsToDelete.length > 0) {
|
||||
await tx.menuItem.deleteMany({ where: { id: { in: itemsToDelete } } });
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
if (item.id && existingItemIds.includes(item.id)) {
|
||||
await tx.menuItem.update({
|
||||
where: { id: item.id },
|
||||
data: { label: item.label, url: item.url, order: item.order }
|
||||
});
|
||||
if (item.translations?.length) {
|
||||
for (const t of item.translations) {
|
||||
await tx.menuItemTranslation.upsert({
|
||||
where: { menuItemId_langCode: { menuItemId: item.id, langCode: t.langCode } },
|
||||
update: { label: t.label },
|
||||
create: { menuItemId: item.id, langCode: t.langCode, label: t.label }
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await tx.menuItem.create({
|
||||
data: {
|
||||
menuId: menu!.id,
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
order: item.order,
|
||||
translations: item.translations?.length ? {
|
||||
create: item.translations.map(t => ({
|
||||
langCode: t.langCode,
|
||||
label: t.label
|
||||
}))
|
||||
} : undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// ... (retrieval and return logic)
|
||||
}, {
|
||||
body: t.Object({
|
||||
payload: t.Optional(t.Any()),
|
||||
isActive: t.Optional(t.Boolean()),
|
||||
items: t.Array(t.Object({
|
||||
id: t.Optional(t.Number()),
|
||||
label: t.String(),
|
||||
url: t.String(),
|
||||
order: t.Number(),
|
||||
translations: t.Optional(t.Array(t.Object({
|
||||
langCode: t.String(),
|
||||
label: t.String()
|
||||
})))
|
||||
}))
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
##### After (Proposed)
|
||||
```typescript
|
||||
.put("/:name", async ({ params: { name }, body, set }) => {
|
||||
try {
|
||||
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 }[]
|
||||
}[]
|
||||
};
|
||||
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 } });
|
||||
} else if (Object.keys(updateData).length > 0) {
|
||||
menu = await prisma.menu.update({ where: { name }, data: updateData });
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx: any) => {
|
||||
const existingItems = await tx.menuItem.findMany({ where: { menuId: menu!.id } });
|
||||
const existingItemIds = existingItems.map((i: any) => i.id);
|
||||
const payloadItemIds = items.map(i => i.id).filter(id => id != null);
|
||||
const itemsToDelete = existingItemIds.filter((id: number) => !payloadItemIds.includes(id));
|
||||
|
||||
if (itemsToDelete.length > 0) {
|
||||
await tx.menuItem.deleteMany({ where: { id: { in: itemsToDelete } } });
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
if (item.id && existingItemIds.includes(item.id)) {
|
||||
await tx.menuItem.update({
|
||||
where: { id: item.id },
|
||||
data: {
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
order: item.order,
|
||||
columnImage: item.columnImage,
|
||||
hoverImage: item.hoverImage
|
||||
}
|
||||
});
|
||||
if (item.translations?.length) {
|
||||
for (const t of item.translations) {
|
||||
await tx.menuItemTranslation.upsert({
|
||||
where: { menuItemId_langCode: { menuItemId: item.id, langCode: t.langCode } },
|
||||
update: { label: t.label },
|
||||
create: { menuItemId: item.id, langCode: t.langCode, label: t.label }
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// ... (retrieval and return logic)
|
||||
}, {
|
||||
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 Adaptations
|
||||
|
||||
### A. File: `apps/mcp-server/menu-tools.ts`
|
||||
|
||||
The `validateMenuItems` helper function needs to allow validating `columnImage` and `hoverImage` when present inside items.
|
||||
|
||||
#### Proposed Change
|
||||
Add validation logic inside the `items.forEach` block:
|
||||
```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`);
|
||||
}
|
||||
```
|
||||
|
||||
### B. File: `apps/mcp-server/menu-toolkit.ts`
|
||||
|
||||
The `upsert_menu` MCP tool schema and handler must be updated to accept and forward the new fields.
|
||||
|
||||
#### Proposed Changes
|
||||
1. **Schema Update (`MENU_TOOL_DEFINITIONS`)**:
|
||||
Under `upsert_menu.inputSchema.properties`, add `rootImage`:
|
||||
```typescript
|
||||
rootImage: { type: "string", description: "Optional image URL for the menu root (optional, nullable)" },
|
||||
```
|
||||
Under `upsert_menu.inputSchema.properties.items.items.properties`, add `columnImage` and `hoverImage`:
|
||||
```typescript
|
||||
columnImage: { type: "string", description: "Optional column image URL for this menu item" },
|
||||
hoverImage: { type: "string", description: "Optional hover image URL for this menu item" },
|
||||
```
|
||||
|
||||
2. **Handler Update (`handleMenuTool`)**:
|
||||
In `case "upsert_menu":`, map `rootImage` into the PUT request payload `body`:
|
||||
```typescript
|
||||
if (params.rootImage !== undefined) {
|
||||
body.rootImage = params.rootImage;
|
||||
} else if (existingMenu?.rootImage !== undefined) {
|
||||
body.rootImage = existingMenu.rootImage;
|
||||
}
|
||||
```
|
||||
Also update the dryRun response preview block to display the rootImage state:
|
||||
```typescript
|
||||
preview: {
|
||||
name: params.name,
|
||||
itemCount: items.length,
|
||||
hasPayload: !!body.payload,
|
||||
isActive: body.isActive,
|
||||
rootImage: body.rootImage // Added preview
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Potential Risks & Mitigation Plans
|
||||
|
||||
1. **Local and Production Environment Schema Mismatch**:
|
||||
- **Risk**: Directly deploying backend code without applying schema changes can lead to runtime crashes due to undefined columns.
|
||||
- **Mitigation**: Schema migrations (`prisma migrate dev`) must be run and verified in local/staging before merging changes to main. In production, migrations must run (`prisma migrate deploy`) prior to starting the new server container instances.
|
||||
2. **Nullable Fields on Client Side**:
|
||||
- **Risk**: Since the new image fields are optional (`String?`), they default to `null` for all existing menus. Client applications (SPA, website storefront, admin dashboard) must render layouts gracefully when these fields are missing.
|
||||
- **Mitigation**: Code on client sides must fallback to default placeholders (like mock assets or empty spaces) when the image URLs are null or empty.
|
||||
3. **Elysia TypeScript Transpilation Sync**:
|
||||
- **Risk**: The backend routes are transpiled into `.js` and `.d.ts` inside the same source directory (`apps/backend/src/routes/`). Discrepancies between `.ts` source edits and build outputs could cause runtime type mismatch or route resolution bugs.
|
||||
- **Mitigation**: A clean backend rebuild (e.g. compiling with TSC/Bun compiler) should be executed right after changes are made to ensure source code and build assets are in perfect alignment.
|
||||
4. **Seed Execution Safety**:
|
||||
- **Risk**: Database seeding in `seed.cjs` upserts menus and menu items. If database constraints are strict, omitting the new columns might fail.
|
||||
- **Mitigation**: In Prisma, optional columns without default values automatically default to `null` and are not required during `create`/`upsert` calls. Thus, `seed.cjs` will not break, but documenting this compatibility is critical.
|
||||
@@ -0,0 +1,171 @@
|
||||
# Handoff Report — Explorer 3, Milestone 2
|
||||
|
||||
## 1. Observation
|
||||
|
||||
Direct observations from codebase inspection:
|
||||
|
||||
1. **Prisma Models**:
|
||||
File: `apps/backend/prisma/schema.prisma`, lines 638–658:
|
||||
```prisma
|
||||
model Menu {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique // e.g., "header", "footer"
|
||||
payload Json?
|
||||
isActive Boolean @default(true)
|
||||
items MenuItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
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)
|
||||
translations MenuItemTranslation[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
2. **TypeScript Interfaces**:
|
||||
File: `packages/types/index.ts`, lines 195–208:
|
||||
```typescript
|
||||
export interface MenuItem {
|
||||
id: number
|
||||
label: string
|
||||
url: string
|
||||
order: number
|
||||
translations?: any[]
|
||||
}
|
||||
|
||||
export interface Menu {
|
||||
id: number
|
||||
name: string
|
||||
items: MenuItem[]
|
||||
payload?: any
|
||||
}
|
||||
```
|
||||
|
||||
3. **Elysia Backend Routes**:
|
||||
File: `apps/backend/src/routes/menus.ts`, lines 34–58:
|
||||
```typescript
|
||||
const { name, items } = body as { name: string; items?: { label: string; url: string; order: number }[] };
|
||||
...
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
items: t.Optional(t.Array(t.Object({
|
||||
label: t.String(),
|
||||
url: t.String(),
|
||||
order: t.Number()
|
||||
})))
|
||||
})
|
||||
```
|
||||
And lines 62–66:
|
||||
```typescript
|
||||
const { items, payload, isActive } = body as {
|
||||
payload?: any;
|
||||
isActive?: boolean;
|
||||
items: { id?: number; label: string; url: string; order: number; translations?: { langCode: string; label: string }[] }[]
|
||||
};
|
||||
```
|
||||
|
||||
4. **MCP Validation Helper**:
|
||||
File: `apps/mcp-server/menu-tools.ts`, lines 158–170:
|
||||
```typescript
|
||||
function validateMenuItems(items: any) {
|
||||
const issues: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!Array.isArray(items)) {
|
||||
issues.push("items must be an array");
|
||||
return { issues, warnings };
|
||||
}
|
||||
|
||||
items.forEach((item, index) => {
|
||||
if (!isPlainObject(item)) {
|
||||
issues.push(`items[${index}] must be a plain object`);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
5. **MCP Schema**:
|
||||
File: `apps/mcp-server/menu-toolkit.ts`, lines 34–74, the definition of `upsert_menu`:
|
||||
```typescript
|
||||
{
|
||||
name: "upsert_menu",
|
||||
description: "Update or create a complete menu by replacing its items. Optional payload/isActive support covers tree-based menus and menu state. Uses proper diff & sync for translations.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Name of the menu (e.g. main, footer-explore)" },
|
||||
dryRun: { type: "boolean", description: "Preview the mutation without saving it" },
|
||||
payload: {
|
||||
oneOf: [{ type: "object" }, { type: "array" }],
|
||||
description: "Optional menu payload for tree-based menus such as main"
|
||||
},
|
||||
isActive: { type: "boolean", description: "Optional active state for the menu" },
|
||||
items: {
|
||||
type: "array",
|
||||
description: "Array of menu items",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "number", description: "ID of existing item (optional)" },
|
||||
label: { type: "string", description: "Label of the menu item" },
|
||||
url: { type: "string", description: "URL of the menu item" },
|
||||
order: { type: "number", description: "Order sequence" },
|
||||
translations: {
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Logic Chain
|
||||
|
||||
1. **Database Schema**: To store `rootImage` for Menu, and `columnImage`/`hoverImage` for MenuItem, we must add nullable string fields (`String?`) to these models in `schema.prisma`.
|
||||
2. **TypeScript Declarations**: The TypeScript project imports standard types from `@homaroma/types` (`packages/types/index.ts`). These interfaces must match the database schemas by adding `rootImage?: string | null`, `columnImage?: string | null`, and `hoverImage?: string | null`.
|
||||
3. **API Validation**: The backend validates POST/PUT requests using Elysia TypeBox (`t.Object`). Adding the new fields as optional nullable strings (`t.Optional(t.Nullable(t.String()))`) enables the backend to validate incoming values correctly while maintaining backward compatibility.
|
||||
4. **API Mapping**: The database update transactions inside `menus.ts` map the validated payload to Prisma database queries. We must update the `create` and `update` queries to assign `rootImage` for Menu, and `columnImage`/`hoverImage` for MenuItem.
|
||||
5. **MCP Schema & Helper**: The MCP toolkit contains utility validation for items before sending PUT requests. Adding validation rules in `validateMenuItems` prevents MCP validation blocks. Updating `MENU_TOOL_DEFINITIONS` exposes the new input parameters to AI clients interacting with `upsert_menu`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Caveats
|
||||
|
||||
- **Seeding Script**: The seed file (`apps/backend/prisma/seed.cjs`) upserts default core menus. It does not contain image values, but since the new fields are fully optional, no updates are strictly required there.
|
||||
- **Frontend Fallbacks**: Client code (SPA and Website) must handle null values safely. The actual layout rendering logic (DesktopMegaMenu) is a separate frontend concern that must be updated dynamically to read these fields.
|
||||
- **Transpiled Route Artifacts**: There are transpiled `.js` and `.d.ts` files inside the routes folder, meaning a clean build step is required after modifications to typescript source files.
|
||||
|
||||
---
|
||||
|
||||
## 4. Conclusion
|
||||
|
||||
The strategy outlines a safe, fully backward-compatible database, route, and MCP tool update. The exact changes required in the four files have been mapped and documented in detail in `analysis.md`. The next step is to delegate the implementation tasks to the Implementer agent.
|
||||
|
||||
---
|
||||
|
||||
## 5. Verification Method
|
||||
|
||||
To independently verify the final implementation:
|
||||
|
||||
1. **Schema Check**:
|
||||
Confirm that the prisma client generates successfully by running:
|
||||
```bash
|
||||
bunx prisma generate
|
||||
```
|
||||
2. **Database Migration Status**:
|
||||
Run the following command to ensure migrations are applied cleanly without conflicts:
|
||||
```bash
|
||||
bunx prisma migrate status
|
||||
```
|
||||
3. **Route Integration & Tests**:
|
||||
Execute the backend build script to verify there are no syntax or type mismatch errors:
|
||||
```bash
|
||||
npm run build --workspace apps/backend
|
||||
```
|
||||
Execute the workspace test command:
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
## 2026-06-29T01:20:34Z
|
||||
|
||||
You are Explorer 3 for Milestone 2: Backend & DB.
|
||||
Your working directory is /home/ti/git/homaroma/.agents/explorer_m2_3.
|
||||
Please analyze the codebase and formulate a precise implementation strategy for updating:
|
||||
1. Prisma schema (`apps/backend/prisma/schema.prisma`): Add rootImage (String?, optional) to Menu, and columnImage (String?, optional) and hoverImage (String?, optional) to MenuItem. Explain what migration commands are needed.
|
||||
2. Shared types (`packages/types/index.ts`): Expose the new fields.
|
||||
3. Backend route (`apps/backend/src/routes/menus.ts`): Modify the endpoints (GET, POST, etc.) to handle, validate, save, and return these fields.
|
||||
4. MCP Server tools (`apps/mcp-server/menu-tools.ts` and `apps/mcp-server/menu-toolkit.ts`): Check if any MCP tools need updating to support these new fields (both inputs and return values).
|
||||
|
||||
Read these files, verify their structure, check zod validators or Prisma types, and write your report to /home/ti/git/homaroma/.agents/explorer_m2_3/analysis.md. Summarize your findings, the exact file changes needed (with lines if possible), and any potential risks. Do not implement.
|
||||
Once done, send a message back with your findings and the path to your analysis.md.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Progress — 2026-06-29T01:20:34Z
|
||||
|
||||
Last visited: 2026-06-29T01:21:36Z
|
||||
|
||||
- [x] Initialized ORIGINAL_REQUEST.md
|
||||
- [x] Created BRIEFING.md
|
||||
- [x] Read backend-mcp-lead SKILL.md
|
||||
- [x] Explore Prisma schema (`apps/backend/prisma/schema.prisma`)
|
||||
- [x] Explore shared types (`packages/types/index.ts`)
|
||||
- [x] Explore backend routes (`apps/backend/src/routes/menus.ts`)
|
||||
- [x] Explore MCP server tools (`apps/mcp-server/menu-tools.ts` and `apps/mcp-server/menu-toolkit.ts`)
|
||||
- [x] Formulate implementation strategy and write `analysis.md`
|
||||
- [x] Create `handoff.md` and send completion message to parent
|
||||
Reference in new issue
Block a user