Replace entire repo content with code from /root/shahikitchen-google/
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
import { mkdir, readFile, writeFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
import type { MenuCategory, MenuItem } from '@/domain/menu/entities';
|
||||
import type { MenuVersionTrigger } from '@/domain/menu/versioning';
|
||||
import { menuCategories as defaultMenuCategories } from './static-menu-data';
|
||||
|
||||
const MENU_DATA_DIR = path.join(process.cwd(), 'data');
|
||||
const MENU_DATA_FILE = path.join(MENU_DATA_DIR, 'menu.json');
|
||||
|
||||
let memoryCache: MenuCategory[] | null = null;
|
||||
|
||||
async function ensureMenuFile(): Promise<void> {
|
||||
await mkdir(MENU_DATA_DIR, { recursive: true });
|
||||
|
||||
try {
|
||||
await readFile(MENU_DATA_FILE, 'utf8');
|
||||
} catch {
|
||||
await writeFile(MENU_DATA_FILE, JSON.stringify(defaultMenuCategories, null, 2), 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
function isMenuCategoryArray(value: unknown): value is MenuCategory[] {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.every(
|
||||
(category) =>
|
||||
typeof category === 'object' &&
|
||||
category !== null &&
|
||||
typeof (category as MenuCategory).id === 'string' &&
|
||||
Array.isArray((category as MenuCategory).items),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function readMenuCategories(): Promise<MenuCategory[]> {
|
||||
if (memoryCache) return memoryCache;
|
||||
|
||||
await ensureMenuFile();
|
||||
const raw = await readFile(MENU_DATA_FILE, 'utf8');
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
|
||||
if (!isMenuCategoryArray(parsed)) {
|
||||
memoryCache = defaultMenuCategories;
|
||||
return memoryCache;
|
||||
}
|
||||
|
||||
memoryCache = parsed;
|
||||
return memoryCache;
|
||||
}
|
||||
|
||||
export interface WriteMenuOptions {
|
||||
snapshotLabel?: string;
|
||||
snapshotTrigger?: MenuVersionTrigger;
|
||||
skipSnapshot?: boolean;
|
||||
}
|
||||
|
||||
export async function persistMenuCategories(categories: MenuCategory[]): Promise<void> {
|
||||
await ensureMenuFile();
|
||||
await writeFile(MENU_DATA_FILE, JSON.stringify(categories, null, 2), 'utf8');
|
||||
memoryCache = categories;
|
||||
}
|
||||
|
||||
export async function writeMenuCategories(
|
||||
categories: MenuCategory[],
|
||||
options?: WriteMenuOptions,
|
||||
): Promise<void> {
|
||||
await persistMenuCategories(categories);
|
||||
|
||||
if (!options?.skipSnapshot) {
|
||||
const { createMenuSnapshot, ensureMenuBaseline } = await import('./menu-versioning');
|
||||
await ensureMenuBaseline();
|
||||
await createMenuSnapshot(
|
||||
categories,
|
||||
options?.snapshotLabel ?? 'Menu updated',
|
||||
options?.snapshotTrigger ?? 'auto',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMenuItem(
|
||||
categoryId: string,
|
||||
itemId: string,
|
||||
updates: Partial<MenuItem>,
|
||||
): Promise<MenuItem | null> {
|
||||
const categories = await readMenuCategories();
|
||||
let updatedItem: MenuItem | null = null;
|
||||
|
||||
const nextCategories = categories.map((category) => {
|
||||
if (category.id !== categoryId) return category;
|
||||
|
||||
return {
|
||||
...category,
|
||||
items: category.items.map((item) => {
|
||||
if (item.id !== itemId) return item;
|
||||
updatedItem = { ...item, ...updates, id: item.id };
|
||||
return updatedItem;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
if (!updatedItem) return null;
|
||||
|
||||
const dishName =
|
||||
nextCategories
|
||||
.find((category) => category.id === categoryId)
|
||||
?.items.find((item) => item.id === itemId)?.name ?? itemId;
|
||||
|
||||
await writeMenuCategories(nextCategories, {
|
||||
snapshotLabel: `Updated dish: ${dishName}`,
|
||||
});
|
||||
return updatedItem;
|
||||
}
|
||||
|
||||
export function slugifyMenuId(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
function allItems(categories: MenuCategory[]): MenuItem[] {
|
||||
return categories.flatMap((category) => category.items);
|
||||
}
|
||||
|
||||
function isItemIdTaken(categories: MenuCategory[], itemId: string): boolean {
|
||||
return allItems(categories).some((item) => item.id === itemId);
|
||||
}
|
||||
|
||||
function uniqueItemId(categories: MenuCategory[], baseId: string): string {
|
||||
if (!isItemIdTaken(categories, baseId)) return baseId;
|
||||
let suffix = 2;
|
||||
while (isItemIdTaken(categories, `${baseId}-${suffix}`)) suffix += 1;
|
||||
return `${baseId}-${suffix}`;
|
||||
}
|
||||
|
||||
export async function addMenuItem(categoryId: string, item: MenuItem): Promise<MenuItem | null> {
|
||||
const categories = await readMenuCategories();
|
||||
const categoryExists = categories.some((category) => category.id === categoryId);
|
||||
if (!categoryExists) return null;
|
||||
|
||||
const id = uniqueItemId(categories, slugifyMenuId(item.id || item.name));
|
||||
if (!id) return null;
|
||||
|
||||
const newItem: MenuItem = { ...item, id };
|
||||
|
||||
const nextCategories = categories.map((category) =>
|
||||
category.id === categoryId
|
||||
? { ...category, items: [...category.items, newItem] }
|
||||
: category,
|
||||
);
|
||||
|
||||
await writeMenuCategories(nextCategories, {
|
||||
snapshotLabel: `Added dish: ${newItem.name}`,
|
||||
});
|
||||
return newItem;
|
||||
}
|
||||
|
||||
export async function removeMenuItem(categoryId: string, itemId: string): Promise<boolean> {
|
||||
const categories = await readMenuCategories();
|
||||
let removed = false;
|
||||
|
||||
const nextCategories = categories.map((category) => {
|
||||
if (category.id !== categoryId) return category;
|
||||
const nextItems = category.items.filter((item) => {
|
||||
if (item.id === itemId) {
|
||||
removed = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return { ...category, items: nextItems };
|
||||
});
|
||||
|
||||
if (!removed) return false;
|
||||
|
||||
const removedName =
|
||||
categories
|
||||
.find((category) => category.id === categoryId)
|
||||
?.items.find((item) => item.id === itemId)?.name ?? itemId;
|
||||
|
||||
await writeMenuCategories(nextCategories, {
|
||||
snapshotLabel: `Removed dish: ${removedName}`,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function addMenuCategory(category: MenuCategory): Promise<MenuCategory | null> {
|
||||
const categories = await readMenuCategories();
|
||||
const id = slugifyMenuId(category.id || category.name);
|
||||
if (!id) return null;
|
||||
|
||||
if (categories.some((existing) => existing.id === id)) return null;
|
||||
|
||||
const newCategory: MenuCategory = {
|
||||
id,
|
||||
name: category.name.trim(),
|
||||
items: category.items ?? [],
|
||||
};
|
||||
|
||||
await writeMenuCategories([...categories, newCategory], {
|
||||
snapshotLabel: `Added category: ${newCategory.name}`,
|
||||
});
|
||||
return newCategory;
|
||||
}
|
||||
|
||||
export async function removeMenuCategory(categoryId: string): Promise<boolean> {
|
||||
const categories = await readMenuCategories();
|
||||
if (!categories.some((category) => category.id === categoryId)) return false;
|
||||
|
||||
const removedCategory = categories.find((category) => category.id === categoryId);
|
||||
const nextCategories = categories.filter((category) => category.id !== categoryId);
|
||||
await writeMenuCategories(nextCategories, {
|
||||
snapshotLabel: `Removed category: ${removedCategory?.name ?? categoryId}`,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function updateMenuCategory(
|
||||
categoryId: string,
|
||||
updates: Partial<Pick<MenuCategory, 'name'>>,
|
||||
): Promise<MenuCategory | null> {
|
||||
const categories = await readMenuCategories();
|
||||
let updatedCategory: MenuCategory | null = null;
|
||||
|
||||
const nextCategories = categories.map((category) => {
|
||||
if (category.id !== categoryId) return category;
|
||||
updatedCategory = {
|
||||
...category,
|
||||
...(typeof updates.name === 'string' ? { name: updates.name.trim() } : {}),
|
||||
};
|
||||
return updatedCategory;
|
||||
});
|
||||
|
||||
if (!updatedCategory) return null;
|
||||
|
||||
const categoryName =
|
||||
nextCategories.find((category) => category.id === categoryId)?.name ?? categoryId;
|
||||
|
||||
await writeMenuCategories(nextCategories, {
|
||||
snapshotLabel: `Updated category: ${categoryName}`,
|
||||
});
|
||||
return updatedCategory;
|
||||
}
|
||||
|
||||
export function clearMenuCache(): void {
|
||||
memoryCache = null;
|
||||
}
|
||||
Reference in New Issue
Block a user