fix: normalize Khanegi newTheme ui config for userTheme schema
Map sidebar+bottom→both, products→buyProduct, ensure header.variant. Repair script + get_user normalize so colors apply instead of Zod fallback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -165,17 +165,18 @@ export function buildKhanegiUiFromColors(input: {
|
||||
borderRadius: { sm: 8, md: 12, lg: 16 },
|
||||
},
|
||||
navigation: {
|
||||
type: 'sidebar+bottom',
|
||||
// userTheme schema: bottom-navigation | sidebar | both
|
||||
type: 'both',
|
||||
sidebar: [
|
||||
{ id: 'home', label: 'داشبورد', icon: 'vuesax-linear:chart', route: 'home', order: 1, visible: true },
|
||||
{ id: 'products', label: 'معاملات', icon: 'vuesax-linear:shop', route: 'products', order: 2, visible: true },
|
||||
{ id: 'products', label: 'معاملات', icon: 'vuesax-linear:shop', route: 'buyProduct', order: 2, visible: true },
|
||||
{ id: 'transactions', label: 'تراکنشها', icon: 'vuesax-linear:receipt-item', route: 'transactions', order: 3, visible: true },
|
||||
{ id: 'wallet', label: 'کیف پول', icon: 'vuesax-linear:wallet-2', route: 'wallet', order: 4, visible: true },
|
||||
{ id: 'user', label: 'حساب کاربری', icon: 'vuesax-linear:user', route: 'user', order: 5, visible: true },
|
||||
],
|
||||
bottom: [
|
||||
{ id: 'home', label: 'خانه', icon: 'vuesax-linear:home-2', route: 'home', order: 1, visible: true },
|
||||
{ id: 'products', label: 'معامله', icon: 'vuesax-linear:shop', route: 'products', order: 2, visible: true },
|
||||
{ id: 'products', label: 'معامله', icon: 'vuesax-linear:shop', route: 'buyProduct', order: 2, visible: true },
|
||||
{ id: 'transactions', label: 'سوابق', icon: 'vuesax-linear:receipt-item', route: 'transactions', order: 3, visible: true },
|
||||
{ id: 'wallet', label: 'کیف پول', icon: 'vuesax-linear:wallet-2', route: 'wallet', order: 4, visible: true },
|
||||
{ id: 'user', label: 'پروفایل', icon: 'vuesax-linear:user', route: 'user', order: 5, visible: true },
|
||||
@@ -189,6 +190,7 @@ export function buildKhanegiUiFromColors(input: {
|
||||
},
|
||||
header: {
|
||||
visible: true,
|
||||
variant: 'default',
|
||||
showLogo: true,
|
||||
showNotifications: true,
|
||||
showProfile: true,
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Normalize stored Khanegi appUiConfig so userTheme Zod schema accepts it.
|
||||
*
|
||||
* Fixes common backfill issues:
|
||||
* navigation.type: "sidebar+bottom" → "both"
|
||||
* header.variant missing → "default"
|
||||
* route "products" → "buyProduct"
|
||||
*
|
||||
* Dry-run:
|
||||
* KHANEGI_UI_REPAIR_DRY_RUN=1 npm run prisma:repair-khanegi-ui-config
|
||||
*
|
||||
* Apply:
|
||||
* npm run prisma:repair-khanegi-ui-config
|
||||
*/
|
||||
import { Prisma, PrismaClient, ProductType } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
function env(name: string, fallback = '') {
|
||||
return String(process.env[name] ?? fallback).trim();
|
||||
}
|
||||
|
||||
function bumpAppVersion(current?: string | null): string {
|
||||
const v = String(current || '1').trim() || '1';
|
||||
if (/^\d+$/.test(v)) return String(Number(v) + 1);
|
||||
const semver = v.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (semver) return `${semver[1]}.${semver[2]}.${Number(semver[3]) + 1}`;
|
||||
return `${Date.now()}`;
|
||||
}
|
||||
|
||||
function mapNavType(value: unknown): string | undefined {
|
||||
if (typeof value !== 'string') return undefined;
|
||||
if (value === 'sidebar+bottom' || value === 'both') return 'both';
|
||||
if (value === 'bottom-navigation' || value === 'sidebar') return value;
|
||||
return value;
|
||||
}
|
||||
|
||||
function mapRoute(value: unknown): unknown {
|
||||
if (value === 'products') return 'buyProduct';
|
||||
return value;
|
||||
}
|
||||
|
||||
function mapNavItems(items: unknown): unknown {
|
||||
if (!Array.isArray(items)) return items;
|
||||
return items.map((item) => {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
|
||||
const row = { ...(item as Record<string, unknown>) };
|
||||
if ('route' in row) row.route = mapRoute(row.route);
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeUiConfig(raw: unknown): {
|
||||
next: Record<string, unknown> | null;
|
||||
changed: boolean;
|
||||
fixes: string[];
|
||||
} {
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
||||
return { next: null, changed: false, fixes: [] };
|
||||
}
|
||||
|
||||
const root = JSON.parse(JSON.stringify(raw)) as Record<string, unknown>;
|
||||
const fixes: string[] = [];
|
||||
const app =
|
||||
root.app && typeof root.app === 'object' && !Array.isArray(root.app)
|
||||
? (root.app as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
if (!app) {
|
||||
return { next: root, changed: false, fixes };
|
||||
}
|
||||
|
||||
const navigation =
|
||||
app.navigation && typeof app.navigation === 'object' && !Array.isArray(app.navigation)
|
||||
? (app.navigation as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
if (navigation) {
|
||||
const mapped = mapNavType(navigation.type);
|
||||
if (mapped && mapped !== navigation.type) {
|
||||
fixes.push(`navigation.type:${String(navigation.type)}→${mapped}`);
|
||||
navigation.type = mapped;
|
||||
}
|
||||
if ('sidebar' in navigation) {
|
||||
const before = JSON.stringify(navigation.sidebar);
|
||||
navigation.sidebar = mapNavItems(navigation.sidebar);
|
||||
if (JSON.stringify(navigation.sidebar) !== before) {
|
||||
fixes.push('navigation.sidebar.routes');
|
||||
}
|
||||
}
|
||||
if ('bottom' in navigation) {
|
||||
const before = JSON.stringify(navigation.bottom);
|
||||
navigation.bottom = mapNavItems(navigation.bottom);
|
||||
if (JSON.stringify(navigation.bottom) !== before) {
|
||||
fixes.push('navigation.bottom.routes');
|
||||
}
|
||||
}
|
||||
const referral =
|
||||
navigation.sidebarReferral &&
|
||||
typeof navigation.sidebarReferral === 'object' &&
|
||||
!Array.isArray(navigation.sidebarReferral)
|
||||
? (navigation.sidebarReferral as Record<string, unknown>)
|
||||
: null;
|
||||
if (referral && 'route' in referral) {
|
||||
const mappedRoute = mapRoute(referral.route);
|
||||
if (mappedRoute !== referral.route) {
|
||||
fixes.push('navigation.sidebarReferral.route');
|
||||
referral.route = mappedRoute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const header =
|
||||
app.header && typeof app.header === 'object' && !Array.isArray(app.header)
|
||||
? (app.header as Record<string, unknown>)
|
||||
: null;
|
||||
if (header && header.variant !== 'default') {
|
||||
fixes.push('header.variant→default');
|
||||
header.variant = 'default';
|
||||
}
|
||||
|
||||
if (app.startRoute === 'products') {
|
||||
fixes.push('startRoute:products→buyProduct');
|
||||
app.startRoute = 'buyProduct';
|
||||
}
|
||||
|
||||
// Keep factory snapshot consistent if present
|
||||
const snap =
|
||||
root.defaultSnapshot &&
|
||||
typeof root.defaultSnapshot === 'object' &&
|
||||
!Array.isArray(root.defaultSnapshot)
|
||||
? (root.defaultSnapshot as Record<string, unknown>)
|
||||
: null;
|
||||
if (snap?.app && typeof snap.app === 'object' && !Array.isArray(snap.app)) {
|
||||
const nested = normalizeUiConfig({
|
||||
schemaId: 'ui-config',
|
||||
version: '1.0.0',
|
||||
app: snap.app,
|
||||
pages: snap.pages || {},
|
||||
});
|
||||
if (nested.next?.app) {
|
||||
snap.app = nested.next.app;
|
||||
if (nested.fixes.length) fixes.push(...nested.fixes.map((f) => `snapshot.${f}`));
|
||||
}
|
||||
}
|
||||
|
||||
return { next: root, changed: fixes.length > 0, fixes };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const dryRun = env('KHANEGI_UI_REPAIR_DRY_RUN') === '1';
|
||||
const tenants = await prisma.tenant.findMany({
|
||||
where: { product: ProductType.khanegi, isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
domain: true,
|
||||
nameFa: true,
|
||||
uiVariant: true,
|
||||
appUiConfig: true,
|
||||
appVersion: true,
|
||||
},
|
||||
orderBy: { domain: 'asc' },
|
||||
});
|
||||
|
||||
let updated = 0;
|
||||
for (const t of tenants) {
|
||||
if (!t.appUiConfig) continue;
|
||||
const { next, changed, fixes } = normalizeUiConfig(t.appUiConfig);
|
||||
if (!changed || !next) continue;
|
||||
|
||||
const nextVersion = bumpAppVersion(t.appVersion);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
domain: t.domain,
|
||||
nameFa: t.nameFa,
|
||||
uiVariant: t.uiVariant,
|
||||
fixes,
|
||||
appVersion: { from: t.appVersion, to: nextVersion },
|
||||
dryRun,
|
||||
}),
|
||||
);
|
||||
|
||||
if (dryRun) continue;
|
||||
|
||||
await prisma.tenant.update({
|
||||
where: { id: t.id },
|
||||
data: {
|
||||
appUiConfig: next as Prisma.InputJsonValue,
|
||||
appVersion: nextVersion,
|
||||
},
|
||||
});
|
||||
updated += 1;
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ dryRun, updated, scanned: tenants.length, done: true }));
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user