Skip to content

Menu Builder System - Deep Dive Documentation

Generated: 2026-02-25 Scope: Full-stack Menu Builder — backend modules, entities, frontend API/services, v2 components, legacy components Files Analyzed: 38+ (8 backend modules, 2 entities, 6 frontend API/services, 9 v2 components, 12 legacy components, + seeds, routes, helpers) Lines of Code: ~8,900+ Workflow Mode: Exhaustive Deep-Dive


Overview

The Menu Builder is a full-stack dynamic navigation management system allowing admin users to create, configure, and organize navigation menus (navbar, footer, topbar, bottombar, etc.) with nested, drag-and-drop reorderable items. Each menu item can link to internal content (blogs, pages, categories, tags, packages) or external URLs, with per-item permission controls and icon/color customization.

Purpose: CRUD management of navigation menus and their hierarchical items, with WordPress-style tree editing, drag-and-drop reordering, and multi-location assignment.

Key Responsibilities: - Menu CRUD with soft-delete, restore, permanent delete, bulk operations - MenuItem CRUD with self-referencing parent-child hierarchy (adjacency list) - Drag-and-drop reordering via @dnd-kit - Multi-content-type linking (blogs, pages, categories, tags, packages, custom URLs) - Per-item permission-based visibility control - Menu location assignment (header, footer, sidebar, topbar, bottombar, etc.)

Integration Points: Auth middleware, permission system (checkPermissionAndThrow), blog/page/category/tag entities (polymorphic postType/postId), frontend navigation rendering, Next.js server action revalidation.


Table of Contents

  1. Database Schema
  2. Backend API
  3. Frontend API Layer
  4. Frontend V2 Components (Active)
  5. Frontend Legacy Components
  6. Dead Code Inventory
  7. User Flows
  8. Data Flow
  9. Dependency Graph
  10. Known Issues
  11. Modification Guidance

Database Schema

Entity: Menu (table: menus)

# Column Type Nullable Default Unique Notes
1 id integer (auto-increment PK) NO auto YES Primary key
2 name varchar(255) NO NO No length/unique constraint. Multiple menus can share names
3 slug varchar(255) YES NULL YES Unique constraint. Used for URL-friendly identification
4 locations text (simple-json) YES NULL NO Stored as JSON string e.g. ["header","footer"]. NOT native PostgreSQL jsonb
5 createdAt timestamp NO CURRENT_TIMESTAMP NO Auto-set
6 updatedAt timestamp NO CURRENT_TIMESTAMP NO Auto-set
7 deletedAt timestamp YES NULL NO Soft delete via @DeleteDateColumn

Relations: - items@OneToMany(() => MenuItem, item => item.menu) — No cascade defined on ORM side

Entity: MenuItem (table: menu_items)

# Column Type Nullable Default Unique Notes
1 id integer (auto-increment PK) NO auto YES Primary key
2 menuId integer YES NULL NO FK → menus.id. Nullable = orphan items possible
3 title varchar(255) NO NO Display label
4 link varchar(255) NO NO URL or path. Required
5 linkType enum("dynamic","static") YES NULL NO PostgreSQL enum
6 target varchar(255) NO "_self" NO HTML anchor target
7 iconClass varchar(255) YES NULL NO CSS icon class
8 color varchar(255) YES NULL NO Color value
9 parentId integer YES NULL NO FK → self (menu_items.id). NULL = top-level
10 order integer NO 0 NO Sort order among siblings. order is a SQL reserved word
11 postType enum("blogs","pages","categories","tags","packages") YES NULL NO Polymorphic type discriminator
12 postId integer YES NULL NO References row in table indicated by postType. No FK constraint
13 parameters text YES NULL NO Free-form URL query params
14 permissions text (simple-array) YES NULL NO Comma-separated permission strings controlling visibility
15 createdAt timestamp NO CURRENT_TIMESTAMP NO Auto-set
16 updatedAt timestamp NO CURRENT_TIMESTAMP NO Auto-set
17 deletedAt timestamp YES NULL NO Soft delete

Relations: - menu@ManyToOne(() => Menu, { onDelete: "CASCADE" }) with @JoinColumn({ name: "menuId" }) - parent@ManyToOne(() => MenuItem, item => item.children, { nullable: true, onDelete: "CASCADE" }) with @JoinColumn({ name: "parentId" }) - children@OneToMany(() => MenuItem, item => item.parent)

Hierarchy Model

  • Pattern: Self-referencing adjacency list on parentId
  • Nesting depth: No limit enforced (no depth/level column)
  • Ordering: order column (integer, default 0). No unique constraint on (menuId, parentId, order) — duplicate orders possible
  • Cascade: onDelete: CASCADE on both menu and parent FKs — but only for hard deletes. Soft-delete does NOT cascade (items become orphans of soft-deleted menus)

Schema Issues

# Severity Issue
S1 Medium menuId is nullable — orphan MenuItems with no Menu are possible
S2 Medium Soft-delete on Menu does NOT cascade to items — items remain visible with menuId pointing to a soft-deleted Menu
S3 Low order is a SQL reserved word — requires quoting in raw queries
S4 Low No indexes on menuId, parentId (only implicit unique index on Menu.slug)
S5 Low simple-json for locations prevents PostgreSQL indexing/querying inside the column
S6 Low simple-array for permissions is fragile — permission strings containing commas would break
S7 Low postId/postType polymorphic association has no FK constraint — referential integrity not enforced
S8 Info GenericPage.ts has dead import of MenuItem (+ unused OneToMany, ManyToMany imports) — planned but never-implemented relation

Seed Data (Permissions)

From saas-boilerplate/src/seed/data/permission.ts, 8 permission entries exist for Menu Builder:

Permission Route Display Name Readonly
menu.view Menu Builder Management false
menu.create Menu Builder Management false
menu.update Menu Builder Management false
menu.delete Menu Builder Management false
menu-item.view Menu Builder Management false
menu-item.create Menu Builder Management false
menu-item.update Menu Builder Management false
menu-item.delete Menu Builder Management false

Note: menu.read, menu.restore, menuItem.view, menuItem.read, menu-item.restore are used in service code but NOT seeded — they will always fail checkPermissionAndThrow.

Migration Files

None. The project uses TypeORM synchronize: true — no migration files exist anywhere in saas-boilerplate/src/.


Backend API

Route Registration

From saas-boilerplate/src/app/routes/v1/index.ts: - /menusmenuRoutes (separate Router()) - /menu-itemsmenuItemRoutes (separate Router())

Both routers apply auth() + contextMiddleware globally. No hasPermission() route-level middleware — all permission checks happen in service layer via checkPermissionAndThrow().

# Method Path Validation Permission Handler Notes
1 GET / menu.view getMenuList Pagination, search on name/slug, trashOnly support
2 GET /:id menu.read getMenuById Loads items + items.children
3 POST / createMenuValidation menu.create createMenu Generates unique slug, checks name uniqueness
4 PATCH /bulk-delete deleteMenuByIds menu.delete deleteMenuByIds Soft-delete by ID array
5 PATCH /bulk-restore bulkMenuRestore menu.restore bulkMenuRestore Restore with slug conflict check
6 PATCH /:id updateMenuValidation menu.update updateMenu Updates name + locations (slug ignored!)
7 PATCH /:id/restore menu.restore restoreFromTrash Single restore
8 DELETE /:id menu.delete deleteMenu Soft-delete
9 DELETE /:id/permanent menu.delete deleteMenuPermanent Hard-delete
# Method Path Validation Permission Handler Notes
1 GET / menuItem.view getMenuItemList Cached (1-day TTL), search on title/link
2 GET /:id menuItem.read getMenuItemById Loads menu, parent, children relations
3 POST / createMenuItemValidation menu-item.create createMenuItem Bulk create (expects { items: [...] })
4 PATCH /bulk-update bulkUpdateMenuItemValidation menu-item.update updateMenuItems Transactional, validates circular refs (1-level)
5 PATCH /bulk-delete deleteMenuItemByIds menu-item.delete deleteMenuItemByIds Soft-delete by IDs
6 PATCH /bulk-restore bulkMenuItemRestore menu-item.restore bulkMenuItemRestore Bulk restore
7 PATCH /:id updateMenuItemValidation menu-item.update updateMenuItem Single update
8 PATCH /:id/restore menu-item.restore restoreFromTrash Single restore
9 DELETE /:id menu-item.delete deleteMenuItem Soft-delete
10 DELETE /:id/permanent menu-item.delete deleteMenuItemPermanent Hard-delete

Total: 19 endpoints (9 Menu + 10 MenuItem)

Validation Schemas (Zod)

Menu: - createMenuValidation: { name: string (required), slug?: string, locations?: string[] } - updateMenuValidation: Identical to create — name is required even for PATCH - deleteMenuByIds: { ids: number[].min(1) } - bulkMenuRestore: { ids: number[].min(0) } — BUG: allows ID 0 and empty array

MenuItem: - createMenuItemValidation: { items: MenuItemSchema[].min(1) } — bulk create - updateMenuItemValidation: All fields optional (proper PATCH) - bulkUpdateMenuItemValidation: { items: BulkUpdateSchema[].min(1) } — requires id, rest optional. Missing permissions field - deleteMenuItemByIds: { ids: number[].positive().min(1) } - bulkMenuItemRestore: { ids: number[].min(0) } — same min(0) bug

MenuItemSchema fields: menuId, title, link, linkType (enum), target, iconClass, color, parentId, order, permissions (string[]), postType (enum), postId, parameters

Service Layer Details

menu.service.ts (313 LOC): - Uses getQueryBuilder + BaseQueryBuilder for list queries - Uses checkPermissionAndThrow for auth in every method - Uses generateUniqueSlug for slug creation - Does NOT implement caching (dead cacheKey variable) - ILike prefix match for name uniqueness (buggy — see issues)

menuItem.service.ts (684 LOC — largest file): - Implements caching with getCache/addCache/deleteCacheByPrefix (1-day TTL) - Transactional bulk create/update with queryRunner - validateMenu(), validateParent(), validatePostIfProvided() helpers - mapMenuItemWithChildren() recursive DTO mapper - Hidden getMenuList(type) method for public navigation (NOT exposed via any route)


Frontend API Layer

Two Separate API Systems

The codebase contains two completely independent API systems that do NOT share query keys or cache:

System A: api/menu-builder/ (Old — used by legacy components)

File: api/menu-builder/index.js (676 LOC)

Hook Method Endpoint Backend Exists?
useMenuBuilders GET /menu-items/grouped-by-type NO — 404
useLabelGroups GET /menu-labels?limit=1000 NO — no module exists
useCreateMenu POST /menu-items Partial — payload mismatch
useUpdateMenu PATCH /menu-items/${id} YES
useDeleteMenu DELETE /menu-items/${id} YES
useReorderMenus PATCH /menu-items/bulk-order-update NO — route is /bulk-update
useUpsertLabelGroup POST/PUT /menu-labels or /menu-labels/${id} NO
useDeleteLabelGroup DELETE /menu-labels/${labelId} NO
useDeletePage DELETE /menu-items/${id}/pages/${pageValue} NO — fabricated sub-resource
useReorderPages PUT /menu-items/${id}/pages/reorder NO — fabricated
useReorderItems PUT /menu-items/${id}/items/reorder NO — fabricated

9 of 11 endpoints either don't exist or have payload mismatches.

Composite hook: useMenuBuilder(menuType) combines all hooks + usePages + useLabelGroups.

Issues: 3 console.log debug statements, identity-function memo, no-op labelGroupOptions.

File: api/menu-builder/menu-label.js (68 LOC) - Uses useState/useEffect pattern (not TanStack Query) — inconsistent - BUG: Double-prefixed URL: api.get(\${process.env.NEXT_PUBLIC_API_URL}/menu-labels`)apialready hasbaseURL- Calls non-existent/menu-labels` endpoint

System B: api/menu/ (V2 — used by current components)

File: api/menu/index.js (107 LOC)

Hook Method Endpoint Backend Exists?
useMenu GET /menus YES
useMenuById(id) GET /menus/${id} YES
useCreateMenu POST /menus YES
useDeleteMenu DELETE /menus/${menuId} YES
useUpdateMenu PATCH /menus/${id} YES
usePermanentDeleteMenu DELETE /menus/${menuId}/permanent YES

BUG: useMenu() passes { limits: 1000 } instead of { params: { limit: 1000 } } — query param never sent.

File: api/menu/menu-item.js (74 LOC)

Hook Method Endpoint Backend Exists?
useBulkCreateMenuItems POST /menu-items YES
useBulkUpdateMenuItems PATCH /menu-items/bulk-update YES
useDeleteMenuItem DELETE /menu-items/${itemId} YES

All v2 endpoints match the backend. Query keys: ["menu"], ["menu", id].

Hook Name Collisions

Both systems export identically-named hooks: useCreateMenu, useUpdateMenu, useDeleteMenu. Importing from both in the same file would cause collisions.

Dead Code: Service Files + useMenus Hook

Complete dead code chain with zero consumers:

lib/dummyMenuData.js
  → services/menuService.js (USE_DUMMY_DATA = true, all API calls commented out)
  → services/menuItemService.js (USE_DUMMY_DATA = true, all API calls commented out)
  → hooks/useMenus.js (wraps menuService in TanStack Query — zero imports anywhere)

Total dead code: ~334 LOC across 4 files.


Frontend V2 Components (Active)

Directory: Sass-boilerplate-frontend-v1/src/components/pages/menu-builder/ Route: /dashboard/settings/menu-builder Total LOC: ~2,500+

Component Tree

MenuBuilderPageV2 (422 LOC)
├── MenuSelector (134 LOC)
│   └── CreateMenu dialog (inline)
├── MenuSettingsPanel (156 LOC)
│   └── Location checkboxes (8 options)
├── AddItemsSection (290 LOC)
│   ├── Pages accordion (with search)
│   ├── Blogs accordion (with search)
│   ├── Categories accordion (with search)
│   ├── Tags accordion (with search)
│   └── Custom Link form (title + URL)
├── WordPressStyleMenuTree (510 LOC)
│   ├── SortableMenuItem (DnD wrapper)
│   │   └── MenuItemCard (210 LOC)
│   │       ├── Indent/Outdent buttons
│   │       ├── Edit button → EditItemDialog
│   │       └── Delete button → ConfirmationModal
│   └── DndContext + SortableContext
└── EditItemDialog (240 LOC)
    └── Form: title, link, linkType, target, iconClass, permissions

Supporting utility: lib/menuHelpers.js (125 LOC) — buildMenuTree(), flattenTree(), calculateDepth(), canIndent(), canOutdent()

Component Summaries

1. menu-builder-page-v2.jsx (422 LOC) — Main page orchestrator - State: selectedMenuId, localItems (flat array), hasUnsavedChanges, editingItem, isEditDialogOpen - Uses: useMenuById, useBulkCreateMenuItems, useBulkUpdateMenuItems, useDeleteMenuItem, usePages, useBlogs, useBlogCategories, useBlogTags - Key pattern: Batch save model — local state tracks changes, "Save Changes" button triggers bulk create/update - Splits items into newItems (no DB id) and existingItems (have DB id) for separate API calls - handleAddItems: Creates flat items with linkType: "dynamic", auto-assigns order and parentId: null - handleAddCustomLink: Creates item with linkType: "static", target: "_blank"

2. MenuSelector.jsx (134 LOC) - Fetches all menus via useMenu(), renders dropdown + "Create New Menu" button - Inline create dialog: name input → useCreateMenu mutation → auto-selects new menu - Fires onSelect(menuId) callback to parent

3. MenuSettingsPanel.jsx (156 LOC) - Shows: menu name (editable), slug (read-only), 8 location checkboxes (Header, Footer, Sidebar, Topbar, Bottom Bar, Mobile Nav, Mega Menu, Social Links) - Uses useUpdateMenu and usePermanentDeleteMenu - Permanent delete with confirmation modal

4. AddItemsSection.jsx (290 LOC) - 5 content types in accordions: Pages, Blogs, Categories, Tags, Custom Link - Each accordion fetches its own data independently - Items selected via checkboxes, "Add to Menu" button - Debounced search (300ms) per accordion - Custom link form: title + URL inputs

5. WordPressStyleMenuTree.jsx (510 LOC) - Renders flat item list with visual depth indentation (left padding per depth level) - DnD implementation: @dnd-kit/core + @dnd-kit/sortable with verticalListSortingStrategy - handleDragEnd: Moves item in flat array via arrayMove, auto-adjusts parentId based on position - Nesting: Items can be indented (make child of previous sibling) or outdented (move to parent's level) - Uses buildMenuTree only for display calculation, actual state is flat array

6. MenuItemCard.jsx (210 LOC) - Displays: title, link, linkType badge, depth indicator, drag handle - Actions: Edit (opens EditItemDialog), Delete (ConfirmationModal), Indent/Outdent arrows - Wrapped in PermissionWrapper for menu-item.update and menu-item.delete

7. SortableMenuItem.jsx (~60 LOC) - Thin DnD wrapper around MenuItemCard using useSortable hook - Provides attributes, listeners, setNodeRef, transform, transition

8. EditItemDialog.jsx (240 LOC) - Form fields: title, link, linkType (radio: dynamic/static), target (select: _self/_blank/_parent/_top), iconClass, color, permissions (multi-input) - Pre-fills from selected item - Saves via callback to parent (which tracks in localItems)

V2 User Flow

  1. Select/Create Menu: User picks existing menu from dropdown or creates new one
  2. Configure Settings: Set menu name and display locations (checkboxes)
  3. Add Items: Browse pages/blogs/categories/tags, check items, click "Add to Menu" — or enter custom link
  4. Organize: Drag-and-drop to reorder; use arrow buttons to indent/outdent for nesting
  5. Edit Items: Click edit icon to modify title, link, target, icon, permissions
  6. Delete Items: Click delete icon, confirm in modal
  7. Save: Click "Save Changes" to persist all local modifications to backend in batch

Frontend Legacy Components

Directory: Sass-boilerplate-frontend-v1/src/components/pages/menu-builder-old/ Routes: - /dashboard/settings/menu-builder-oldMenuBuilderPage - /dashboard/settings/menu-builder-old/menu-labelMenuLabelPage

Total LOC: 2,821 across 12 files

These routes are still accessible — the old pages are NOT removed, just accessible at -old paths.

Version Evolution (3 Generations)

The legacy directory contains 3 visible generations of iterative development:

Generation Files DnD Strategy Key Difference
Gen 1 menu-item-old.jsx, menu-list-old.jsx Full array reorder (client-side arrayMove) Original approach
Gen 2 menu-item-old-v2.jsx, menu-list-old-v2.jsx Position-based ({ itemId, position }) Thinner API contract
Gen 3 (Final) menu-item.jsx, menu-list.jsx Full array + optimistic updates Added PermissionWrapper, ConfirmationModal, loading states

Architecture Differences: Old vs V2

Aspect Old V2 (Current)
Data model MenuItem → LabelGroup, type field Menu → MenuItem, locations array
Grouping 4 hardcoded tabs (navbar/footer/topbar/bottombar) 8 configurable location checkboxes
Item structure Items have pages + children Items have title, link, target, icon, permissions
Add items Modal form (label + pages + type) Left panel with search across 5 content types
Nesting Nested cards-within-cards Flat list with indent/outdent arrows
Reorder DnD on 3-column card grid DnD on vertical list
Save model Immediate save per action Batch "Save Changes" button
API layer api/menu-builder/ (9 phantom endpoints) api/menu/ (all endpoints exist)
Concept LabelGroups manage grouping Menu entity manages grouping
Edit Same modal as create Dedicated EditItemDialog
Content types Pages only Pages + Blogs + Categories + Tags + Custom Links
Per-item permissions N/A Supported

Features in Old Missing from V2

  1. Label Management Page — Full CRUD with search, pagination, trash/restore
  2. Label DnD Reorderinglabel-manager.jsx
  3. Loading overlay during reorder — Blur + spinner
  4. Order number badges on cards

Features in V2 Missing from Old

  1. WordPress-style tree with indent/outdent
  2. Batch save with unsaved changes tracking
  3. Multi-content-type support (5 types vs 1)
  4. Per-item permissions
  5. Menu settings panel (name + 8 locations)
  6. Content search with debounce
  7. Icon class and color per item

Legacy Bugs (Inherited Across Generations)

  • !depth > 0 logic bug in all 3 menu-item*.jsx — works accidentally for depths 0-1 only
  • Edit2 imported but never used in all versions
  • Multiple console.log debug statements
  • useMemo and Skeleton imported but never used in menu-builder-page.jsx

Dead Code Inventory

File LOC Type Why Dead
services/menuService.js 94 Dummy data USE_DUMMY_DATA = true, all API calls commented out
services/menuItemService.js 144 Dummy data Same — zero consumers
hooks/useMenus.js 96 Query wrapper Wraps dummy service — zero consumers
lib/dummyMenuData.js ~100 Test data Only used by dead service files
api/menu-builder/menu-label.js 68 API hook Calls non-existent /menu-labels endpoint
menu-builder-old/ (12 files) 2,821 Components Superseded by v2 — still routed but functionally broken
menuItem.service.ts:getMenuList() ~45 Service method Not exposed via any route
Menu entity Not import Unused import Imported but never used
MenuItem entity IsNull import Unused import Imported but never used

Total identifiable dead code: ~3,368+ LOC


User Flows

V2: Create and Populate a Menu

1. User navigates to /dashboard/settings/menu-builder
2. MenuSelector shows "Select a menu" dropdown
3. User clicks "Create New Menu" → dialog opens
4. User types menu name → POST /menus → menu created with auto-slug
5. New menu auto-selected → MenuSettingsPanel and empty tree appear
6. User checks location boxes (e.g., Header, Footer) → PATCH /menus/:id
7. User expands "Pages" accordion in AddItemsSection
8. User searches for "About", checks "About Us" page
9. User clicks "Add to Menu" → item added to local state (unsaved)
10. User drags item to reorder → local state updated
11. User clicks indent arrow → item becomes child of item above
12. User clicks "Save Changes" → POST /menu-items (bulk create for new items)
13. Confirmation toast shown

V2: Edit and Delete Items

1. User clicks edit icon on a menu item
2. EditItemDialog opens with pre-filled data
3. User changes title, sets target="_blank", adds permissions
4. User clicks Save → local state updated (unsaved)
5. User clicks "Save Changes" → PATCH /menu-items/bulk-update
6. To delete: user clicks delete icon → ConfirmationModal
7. Confirms → DELETE /menu-items/:id (immediate, not batched)

Data Flow

V2 Data Flow

[Backend DB] → GET /menus/:id → useMenuById → menuData.items (flat)
                                              buildMenuTree() → tree structure (display only)
                                              localItems (flat state in page component)
                              ┌──────────────┬──────────────┬──────────────┐
                              ↓              ↓              ↓              ↓
                         handleAddItems  handleDrag  handleIndent  handleEdit
                              ↓              ↓              ↓              ↓
                              └──────────────┴──────────────┴──────────────┘
                                         hasUnsavedChanges = true
                                         "Save Changes" clicked
                              ┌──────────────────────────────────┐
                              ↓                                  ↓
                    newItems (no id)                 existingItems (have id)
                              ↓                                  ↓
                  POST /menu-items              PATCH /menu-items/bulk-update
                    { items: [...] }                  { items: [...] }
                              ↓                                  ↓
                              └──────────────────────────────────┘
                                    invalidateQueries(["menu", menuId])

Cache Strategy

System Query Key Invalidation Notes
V2 menus ["menu"] All menu mutations Broad invalidation (matches ["menu", id] too)
V2 menu by id ["menu", id] MenuItem mutations Targeted by menuId
Old system ["menu-builders"] Old menu mutations Completely siloed from v2
Old labels ["label-groups"] Label mutations Siloed
Backend cache menu-items:${query} deleteCacheByPrefix("menu-items:") Server-side, 1-day TTL

Dependency Graph

Backend

routes/v1/index.ts
├── MenuBuilder/Menu/menu.routes.ts
│   ├── middlewares/auth.ts
│   ├── middlewares/contextMiddleware.ts
│   ├── menu.validation.ts (Zod schemas)
│   └── menu.controller.ts
│       └── menu.service.ts
│           ├── entity/Menu.ts
│           ├── shared/getDbRepository.ts
│           ├── helper/checkPermissionAndThrow.ts
│           ├── helper/parsePaginationQuery.ts
│           ├── helper/parseBoolean.ts
│           ├── shared/getSortQuery.ts
│           ├── shared/generateUniqueSlug.ts
│           └── builder/getQueryBuilder.ts + QueryBuilder.ts
└── MenuBuilder/MenuItem/menuItem.routes.ts
    ├── middlewares/auth.ts
    ├── middlewares/contextMiddleware.ts
    ├── menuItem.validation.ts (Zod schemas)
    └── menuItem.controller.ts
        └── menuItem.service.ts
            ├── entity/MenuItem.ts
            ├── entity/Menu.ts
            ├── shared/getDbRepository.ts
            ├── helper/checkPermissionAndThrow.ts
            ├── helper/parsePaginationQuery.ts
            ├── helper/parseBoolean.ts
            ├── shared/getSortQuery.ts
            ├── shared/cache.ts (getCache, addCache, deleteCacheByPrefix)
            └── shared/validatePostTypeAndId.ts

Frontend V2

app/(dashboard)/settings/menu-builder/page.js
└── components/pages/menu-builder/menu-builder-page-v2.jsx
    ├── api/menu/index.js (useMenu, useMenuById, useCreateMenu, useUpdateMenu, usePermanentDeleteMenu)
    ├── api/menu/menu-item.js (useBulkCreateMenuItems, useBulkUpdateMenuItems, useDeleteMenuItem)
    ├── api/pages (usePages)
    ├── api/blogs (useBlogs, useBlogCategories, useBlogTags)
    ├── lib/menuHelpers.js (buildMenuTree, flattenTree, canIndent, canOutdent)
    ├── MenuSelector.jsx → api/menu/index.js (useMenu, useCreateMenu)
    ├── MenuSettingsPanel.jsx → api/menu/index.js (useUpdateMenu, usePermanentDeleteMenu)
    ├── AddItemsSection.jsx (no API, pure UI)
    ├── WordPressStyleMenuTree.jsx
    │   ├── SortableMenuItem.jsx (@dnd-kit/sortable)
    │   └── MenuItemCard.jsx
    └── EditItemDialog.jsx

Circular Dependencies

None detected.


Known Issues

Critical (3)

# Location Description
1 menuItem.service.ts Permission string format mismatch: View/Read use menuItem.view/menuItem.read (camelCase dot), CRUD uses menu-item.create/menu-item.update/menu-item.delete/menu-item.restore (kebab-case dot). Seed data has 8 entries mixing both formats — but menuItem.read and menu.read are NOT seeded, so getMenuById and getMenuItemById will ALWAYS fail permission check
2 menu.service.ts:59-61 countRootLevelItems queries wrong entity: Queries Menu table with { id, parentId: null } but Menu has no parentId column — should query MenuItem with { menuId: id, parentId: IsNull() }
3 api/menu-builder/index.js 9 of 11 frontend hooks call non-existent backend endpoints — the entire old system's API layer is broken (grouped-by-type, menu-labels, bulk-order-update, pages/reorder, items/reorder)

High (7)

# Location Description
4 menu.service.ts:93 Name uniqueness uses LIKE prefix match: ILike(\${name}%`)` — "Main" blocks "Main Menu", "Maintenance", etc. Should be exact match
5 menuItem.service.ts:510-515 Children guard is useless: deleteMenuItem checks children.length > 0 but the findOne call never loads the children relation — always undefined, guard never fires
6 api/menu/index.js useMenu() query param bug: { limits: 1000 } instead of { params: { limit: 1000 } } — no limit param actually sent, backend returns default page size
7 menu.service.ts:120-127 Menu restore drops locations: When restoring soft-deleted menu by slug match, menuData.locations is never applied — new locations silently lost
8 menu.service.ts:161-168 updateMenu ignores slug: slug accepted in validation but never written to entity
9 menu.validation.ts 5 unseeded permissions used in code: menu.read, menu.restore, menuItem.view, menuItem.read, menu-item.restore — will fail checkPermissionAndThrow for non-super-admin users
10 api/menu-builder/menu-label.js Double-prefixed URL: api.get(\${NEXT_PUBLIC_API_URL}/menu-labels`)` — axios instance already has baseURL

Medium (14)

# Location Description
11 menu.service.ts:16 Dead cacheKey variable — caching planned but never implemented for menus
12 menu.service.ts:154-159 Dead code: deletedAt check unreachable (findOne without withDeleted excludes soft-deleted)
13 menu.service.ts:182-187 Same dead code pattern in deleteMenu
14 menuItem.service.ts:622-667 getMenuList(type) method exists but NOT exposed via any route — dead/incomplete feature
15 menuItem.service.ts:27 Suspicious JSON.parse(getFromCache as unknown as string) double-conversion on cache retrieval
16 menuItem.service.ts:356-366 Circular reference detection only 1-level deep — A→B→C→A not detected
17 menuItem.validation.ts:58-75 bulkUpdateMenuItemSchema missing permissions field (present in single-update schema)
18 Schema S1 menuId nullable on MenuItem — orphan items possible
19 Schema S2 Soft-delete on Menu does NOT cascade to items — visible orphans
20 api/menu-builder/index.js 3 console.log debug statements in production code
21 api/menu/menu-item.js useBulkCreateMenuItems potential double-nesting: caller passes { items: { items: [...] } }
22 Old components !depth > 0 logic bug — works only by coincidence for depths 0-1
23 menu-form-modal.jsx PermissionWrapper wraps Cancel/Submit with menu-label.view — wrong permission
24 menu-builder-old/menu-label-page.jsx Raw API calls (api.post/patch/delete) without React Query — no loading states, no cache invalidation

Low (18)

# Location Description
25 menu.validation.ts:3-21 createMenuValidation and updateMenuValidation are exact duplicates
26 menu.validation.ts:37 bulkMenuRestore allows ID 0 and empty array
27 menuItem.validation.ts:103 Same min(0) issue for bulkMenuItemRestore
28 menu.service.ts:5 Not imported from typeorm but never used
29 menuItem.service.ts:6 IsNull imported from typeorm but never used
30 menu.service.ts:290 findByIds is deprecated in TypeORM
31 menuItem.service.ts:609 Same deprecated findByIds
32 menu.service.ts:246 restoreFromTrash returns stale entity (old deletedAt)
33 menuItem.service.ts:573 Same stale entity return
34 menu.controller.ts Inconsistent ID validation (some methods validate, some don't)
35 menuItem.controller.ts Same inconsistent ID validation
36 menu.controller.ts:93-105 Redundant Array.isArray check duplicates Zod validation
37 menu.validation.ts:13-21 updateMenuValidation requires name for PATCH (should be optional)
38 menuItem.service.ts:284 Comment says "menus" instead of "menu items"
39 Schema S3 order is SQL reserved word
40 Schema S4 No indexes on menuId, parentId
41 Dead imports Edit2, useMemo, Skeleton in various legacy components
42 Legacy handlePageReorder marked "currently not used" with console.log

Total: 42 issues (3 Critical, 7 High, 14 Medium, 18 Low)


Testing Analysis

Test Files

None. No test files exist for the Menu Builder system — backend or frontend.

Testing Gaps

  • No unit tests for menu.service.ts or menuItem.service.ts
  • No integration tests for 19 API endpoints
  • No component tests for any of the 20+ React components
  • No E2E tests for the menu builder user flow
  • The circular reference detection (1-level) has no test proving its limitation

Similar Patterns in Codebase

Pattern Menu Builder Also Used In
checkPermissionAndThrow Every service method All 31 backend modules
getQueryBuilder + BaseQueryBuilder List queries Blog, User, Product, etc.
catchAsync wrapper All controllers All controllers
softDelete + restore + permanent Menu and MenuItem Blog, User, Product, etc.
simple-json column Menu.locations Setting, AppConfig entities
@dnd-kit/sortable WordPressStyleMenuTree Blog categories, email template builder
PermissionWrapper v2 components Dashboard pages across 15+ modules
ConfirmationModal Delete confirmations All dashboard CRUD pages

Reusable Utilities Available

Utility Path Could Help With
generateUniqueSlug shared/generateUniqueSlug.ts Already used — slug generation
validatePostTypeAndId shared/validatePostTypeAndId.ts Polymorphic FK validation
buildMenuTree lib/menuHelpers.js Tree-building from flat data
CustomizeManager components/pages/dashboard/customization/ Generic CRUD management (used by old label page)

Modification Guidance

To Add a New Content Type to Menu Items

  1. Add enum value to MenuItem.postType column in entity/MenuItem.ts
  2. Add to Zod enum in menuItem.validation.ts (menuItemSchema.postType)
  3. Add validation case in shared/validatePostTypeAndId.ts
  4. Add accordion section in AddItemsSection.jsx with corresponding API hook
  5. Run TypeORM sync or create migration for the new enum value

To Fix Permission String Inconsistency (#1)

  1. Decide on ONE format: menu-item.verb (kebab-case, matches seed) or menuItem.verb (camelCase)
  2. Update menuItem.service.ts lines using checkPermissionAndThrow to use consistent format
  3. Add missing permissions to seed: menu.read, menu.restore, menu-item.restore
  4. Remove or update menuItem.view and menuItem.read to menu-item.view and menu-item.read (matching seed)
  5. Re-run seed

To Clean Up Dead Code

  1. Delete services/menuService.js, services/menuItemService.js, hooks/useMenus.js, lib/dummyMenuData.js
  2. Delete api/menu-builder/ directory (old system — all endpoints broken)
  3. Delete components/pages/menu-builder-old/ directory (12 files, 2,821 LOC)
  4. Delete route files: app/(dashboard)/settings/menu-builder-old/ and menu-label/
  5. Remove unused imports in entity/GenericPage.ts (MenuItem, OneToMany, ManyToMany)
  6. Remove unused service method menuItem.service.ts:getMenuList()
  7. Remove Not import from menu.service.ts, IsNull import from menuItem.service.ts

Testing Checklist for Changes

  • Menu CRUD: create, list, get by id, update, soft-delete, restore, permanent delete
  • MenuItem CRUD: bulk create, single update, bulk update, soft-delete, restore, permanent delete
  • Permission checks: verify all 12 permission strings work with seeded data
  • Hierarchy: create parent, create child, reorder siblings, indent, outdent
  • Nesting: verify 3+ levels deep works
  • Soft-delete cascade: delete menu, verify items status
  • Slug uniqueness: create menus with same name, verify slug differentiation
  • Drag-and-drop: reorder items, verify order persisted correctly
  • Batch save: add items, reorder, edit, then save all at once
  • Location assignment: assign menu to multiple locations, verify rendering
  • Content linking: add blog, page, category, tag, custom link items
  • Edit dialog: modify title, link, target, icon, permissions

Generated by document-project workflow (deep-dive mode) Base Documentation: docs/index.md Scan Date: 2026-02-25 Analysis Mode: Exhaustive