Replace entire repo content with code from /root/shahikitchen-google/
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
import { mkdir, readFile, writeFile, unlink } from 'fs/promises';
|
||||
import path from 'path';
|
||||
import type { MenuCategory } from '@/domain/menu/entities';
|
||||
import {
|
||||
MENU_BASELINE_VERSION_ID,
|
||||
type MenuVersionListItem,
|
||||
type MenuVersionMeta,
|
||||
type MenuVersionSnapshot,
|
||||
type MenuVersionTrigger,
|
||||
} from '@/domain/menu/versioning';
|
||||
import { menuCategories as defaultMenuCategories } from './static-menu-data';
|
||||
|
||||
const VERSIONS_DIR = path.join(process.cwd(), 'data', 'menu-versions');
|
||||
const INDEX_FILE = path.join(VERSIONS_DIR, 'index.json');
|
||||
const BASELINE_FILE = path.join(VERSIONS_DIR, 'baseline.json');
|
||||
|
||||
interface VersionIndex {
|
||||
versions: MenuVersionMeta[];
|
||||
}
|
||||
|
||||
function versionFilePath(id: string): string {
|
||||
return id === MENU_BASELINE_VERSION_ID
|
||||
? BASELINE_FILE
|
||||
: path.join(VERSIONS_DIR, `${id}.json`);
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function countMenuStats(categories: MenuCategory[]): { categoryCount: number; dishCount: number } {
|
||||
return {
|
||||
categoryCount: categories.length,
|
||||
dishCount: categories.reduce((sum, category) => sum + category.items.length, 0),
|
||||
};
|
||||
}
|
||||
|
||||
function menuFingerprint(categories: MenuCategory[]): string {
|
||||
return JSON.stringify(categories);
|
||||
}
|
||||
|
||||
function cloneCategories(categories: MenuCategory[]): MenuCategory[] {
|
||||
return JSON.parse(JSON.stringify(categories)) as MenuCategory[];
|
||||
}
|
||||
|
||||
async function ensureVersionsDir(): Promise<void> {
|
||||
await mkdir(VERSIONS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function readIndex(): Promise<VersionIndex> {
|
||||
await ensureVersionsDir();
|
||||
try {
|
||||
const raw = await readFile(INDEX_FILE, 'utf8');
|
||||
const parsed = JSON.parse(raw) as VersionIndex;
|
||||
if (!Array.isArray(parsed.versions)) return { versions: [] };
|
||||
return parsed;
|
||||
} catch {
|
||||
return { versions: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async function writeIndex(index: VersionIndex): Promise<void> {
|
||||
await ensureVersionsDir();
|
||||
await writeFile(INDEX_FILE, JSON.stringify(index, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
async function writeSnapshotFile(snapshot: MenuVersionSnapshot): Promise<void> {
|
||||
await ensureVersionsDir();
|
||||
await writeFile(versionFilePath(snapshot.id), JSON.stringify(snapshot, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
async function readSnapshotFile(id: string): Promise<MenuVersionSnapshot | null> {
|
||||
try {
|
||||
const raw = await readFile(versionFilePath(id), 'utf8');
|
||||
const parsed = JSON.parse(raw) as MenuVersionSnapshot;
|
||||
if (!isMenuCategoryArray(parsed.categories)) return null;
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createVersionId(): string {
|
||||
return `v-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
export async function ensureMenuBaseline(): Promise<MenuVersionMeta> {
|
||||
await ensureVersionsDir();
|
||||
const index = await readIndex();
|
||||
const existing = index.versions.find((version) => version.id === MENU_BASELINE_VERSION_ID);
|
||||
if (existing) return existing;
|
||||
|
||||
const categories = cloneCategories(defaultMenuCategories);
|
||||
const stats = countMenuStats(categories);
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
const baseline: MenuVersionSnapshot = {
|
||||
id: MENU_BASELINE_VERSION_ID,
|
||||
label: 'Original menu (baseline)',
|
||||
createdAt,
|
||||
trigger: 'auto',
|
||||
categories,
|
||||
isBaseline: true,
|
||||
};
|
||||
|
||||
await writeSnapshotFile(baseline);
|
||||
|
||||
const meta: MenuVersionMeta = {
|
||||
id: MENU_BASELINE_VERSION_ID,
|
||||
label: baseline.label,
|
||||
createdAt,
|
||||
trigger: 'auto',
|
||||
isBaseline: true,
|
||||
...stats,
|
||||
};
|
||||
|
||||
index.versions.push(meta);
|
||||
await writeIndex(index);
|
||||
return meta;
|
||||
}
|
||||
|
||||
export async function createMenuSnapshot(
|
||||
categories: MenuCategory[],
|
||||
label: string,
|
||||
trigger: MenuVersionTrigger = 'auto',
|
||||
): Promise<MenuVersionMeta> {
|
||||
await ensureMenuBaseline();
|
||||
const index = await readIndex();
|
||||
const id = createVersionId();
|
||||
const createdAt = new Date().toISOString();
|
||||
const stats = countMenuStats(categories);
|
||||
|
||||
const snapshot: MenuVersionSnapshot = {
|
||||
id,
|
||||
label: label.trim() || 'Menu updated',
|
||||
createdAt,
|
||||
trigger,
|
||||
categories: cloneCategories(categories),
|
||||
isBaseline: false,
|
||||
};
|
||||
|
||||
await writeSnapshotFile(snapshot);
|
||||
|
||||
const meta: MenuVersionMeta = {
|
||||
id,
|
||||
label: snapshot.label,
|
||||
createdAt,
|
||||
trigger,
|
||||
isBaseline: false,
|
||||
...stats,
|
||||
};
|
||||
|
||||
index.versions.unshift(meta);
|
||||
await writeIndex(index);
|
||||
return meta;
|
||||
}
|
||||
|
||||
export async function listMenuVersions(liveCategories: MenuCategory[]): Promise<MenuVersionListItem[]> {
|
||||
await ensureMenuBaseline();
|
||||
const index = await readIndex();
|
||||
const liveHash = menuFingerprint(liveCategories);
|
||||
|
||||
const items: MenuVersionListItem[] = [];
|
||||
|
||||
for (const meta of index.versions) {
|
||||
const snapshot = await readSnapshotFile(meta.id);
|
||||
const matchesLive = snapshot ? menuFingerprint(snapshot.categories) === liveHash : false;
|
||||
items.push({ ...meta, matchesLive });
|
||||
}
|
||||
|
||||
const baseline = items.find((item) => item.isBaseline);
|
||||
const nonBaseline = items.filter((item) => !item.isBaseline);
|
||||
nonBaseline.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
|
||||
return baseline ? [...nonBaseline, baseline] : nonBaseline;
|
||||
}
|
||||
|
||||
export async function getMenuVersionCategories(versionId: string): Promise<MenuCategory[] | null> {
|
||||
const snapshot = await readSnapshotFile(versionId);
|
||||
return snapshot ? cloneCategories(snapshot.categories) : null;
|
||||
}
|
||||
|
||||
export async function restoreMenuVersion(
|
||||
versionId: string,
|
||||
persist: (categories: MenuCategory[]) => Promise<void>,
|
||||
readLive: () => Promise<MenuCategory[]>,
|
||||
): Promise<MenuVersionMeta> {
|
||||
const snapshot = await readSnapshotFile(versionId);
|
||||
if (!snapshot) {
|
||||
throw new Error('Version not found.');
|
||||
}
|
||||
|
||||
const current = await readLive();
|
||||
await createMenuSnapshot(
|
||||
current,
|
||||
`Before rollback to "${snapshot.label}"`,
|
||||
'restore',
|
||||
);
|
||||
|
||||
await persist(snapshot.categories);
|
||||
|
||||
return {
|
||||
id: snapshot.id,
|
||||
label: snapshot.label,
|
||||
createdAt: snapshot.createdAt,
|
||||
trigger: snapshot.trigger,
|
||||
isBaseline: snapshot.isBaseline,
|
||||
...countMenuStats(snapshot.categories),
|
||||
};
|
||||
}
|
||||
|
||||
export async function resetMenuVersionHistory(categories: MenuCategory[]): Promise<MenuVersionMeta> {
|
||||
await ensureVersionsDir();
|
||||
const index = await readIndex();
|
||||
|
||||
for (const version of index.versions) {
|
||||
if (version.id === MENU_BASELINE_VERSION_ID) continue;
|
||||
try {
|
||||
await unlink(versionFilePath(version.id));
|
||||
} catch {
|
||||
// File may already be missing.
|
||||
}
|
||||
}
|
||||
|
||||
const stats = countMenuStats(categories);
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
const baseline: MenuVersionSnapshot = {
|
||||
id: MENU_BASELINE_VERSION_ID,
|
||||
label: 'Current menu (baseline)',
|
||||
createdAt,
|
||||
trigger: 'manual',
|
||||
categories: cloneCategories(categories),
|
||||
isBaseline: true,
|
||||
};
|
||||
|
||||
await writeSnapshotFile(baseline);
|
||||
|
||||
const meta: MenuVersionMeta = {
|
||||
id: MENU_BASELINE_VERSION_ID,
|
||||
label: baseline.label,
|
||||
createdAt,
|
||||
trigger: 'manual',
|
||||
isBaseline: true,
|
||||
...stats,
|
||||
};
|
||||
|
||||
index.versions = [meta];
|
||||
await writeIndex(index);
|
||||
return meta;
|
||||
}
|
||||
|
||||
export async function deleteMenuVersion(versionId: string): Promise<boolean> {
|
||||
if (versionId === MENU_BASELINE_VERSION_ID) {
|
||||
throw new Error('The baseline version cannot be deleted.');
|
||||
}
|
||||
|
||||
const index = await readIndex();
|
||||
const nextVersions = index.versions.filter((version) => version.id !== versionId);
|
||||
if (nextVersions.length === index.versions.length) return false;
|
||||
|
||||
index.versions = nextVersions;
|
||||
await writeIndex(index);
|
||||
|
||||
try {
|
||||
await unlink(versionFilePath(versionId));
|
||||
} catch {
|
||||
// File may already be missing; index update is sufficient.
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user