Files
api/prisma/seed-hamkar-default-snapshots.ts
T
MahdiandCursor 11887bc5ed feat(get-user): expose Hamkar menu icon brand colors
Emit menu_icon_color and menu_icon_active_color when brandManaged so apps can theme nav icons separately.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 18:06:24 +03:30

239 lines
7.3 KiB
TypeScript

/**
* Seed Hamkar defaultSnapshot from users.json (original registry look).
* Optionally restore current appearance to that snapshot (--restore).
*
* Usage:
* npx ts-node --transpile-only prisma/seed-hamkar-default-snapshots.ts
* npx ts-node --transpile-only prisma/seed-hamkar-default-snapshots.ts --restore
* npx ts-node --transpile-only prisma/seed-hamkar-default-snapshots.ts --restore --domain=montpar.ir
*/
import * as fs from 'fs';
import * as path from 'path';
import { PrismaClient, ProductType } from '@prisma/client';
import { buildHamkarUiFromBrand } from './lib/appearance-builders';
const prisma = new PrismaClient();
type SourceUser = {
id: number;
name?: string | null;
name_fa?: string | null;
domain?: string | null;
domains?: string | string[] | null;
api_origin?: string | null;
mode?: string | null;
theme?: string | null;
color?: string | null;
secondary_color?: string | null;
background_color?: string | null;
deleted_at?: string | null;
};
function parseArgs() {
const restore = process.argv.includes('--restore');
const domainArg = process.argv.find((a) => a.startsWith('--domain='));
const domain = domainArg ? domainArg.slice('--domain='.length).trim().toLowerCase() : null;
return { restore, domain };
}
function parseDomains(raw: SourceUser['domains']): string[] {
if (!raw) return [];
try {
const value = typeof raw === 'string' ? JSON.parse(raw) : raw;
if (!Array.isArray(value)) return [];
return value
.map((item) => String(item).trim().toLowerCase())
.filter(Boolean)
.map((item) => item.replace(/^https?:\/\//, '').split('/')[0]);
} catch {
return [];
}
}
function primaryDomain(row: SourceUser): string | null {
const fromField = row.domain?.trim().toLowerCase();
if (fromField) return fromField.replace(/^https?:\/\//, '').split('/')[0];
const fromOrigin = row.api_origin?.trim().toLowerCase();
if (fromOrigin) return fromOrigin.replace(/^https?:\/\//, '').split('/')[0];
const domains = parseDomains(row.domains);
return domains[0] || null;
}
function emptyToNull(v: string | null | undefined) {
const s = String(v ?? '').trim();
if (!s || s === 'null' || s === 'undefined') return null;
return s;
}
async function main() {
const { restore, domain: onlyDomain } = parseArgs();
const file = path.join(__dirname, 'data', 'users.json');
if (!fs.existsSync(file)) {
throw new Error(`Missing ${file}`);
}
const rows = JSON.parse(fs.readFileSync(file, 'utf8')) as SourceUser[];
const byDomain = new Map<string, SourceUser>();
for (const row of rows) {
if (row.deleted_at) continue;
const d = primaryDomain(row);
if (!d) continue;
if (!byDomain.has(d)) byDomain.set(d, row);
for (const extra of parseDomains(row.domains)) {
if (!byDomain.has(extra)) byDomain.set(extra, row);
}
}
const tenants = await prisma.tenant.findMany({
where: { product: ProductType.hamkar },
});
const filtered = onlyDomain
? tenants.filter((tenant) => {
const keys = [
tenant.domain?.toLowerCase(),
...((Array.isArray(tenant.domains) ? tenant.domains : []) as string[]).map((d) =>
String(d).toLowerCase(),
),
];
return keys.includes(onlyDomain);
})
: tenants;
let updated = 0;
let restored = 0;
let skipped = 0;
for (const tenant of filtered) {
const domainKeys = [
tenant.domain?.toLowerCase(),
...((Array.isArray(tenant.domains) ? tenant.domains : []) as string[]).map((d) =>
String(d).toLowerCase(),
),
].filter(Boolean) as string[];
const source =
domainKeys.map((d) => byDomain.get(d)).find(Boolean) ||
null;
const noteMode =
tenant.notes?.match(/registryMode:([^\s|]+)/)?.[1] ||
tenant.notes?.match(/(?:^|\|)\s*mode:([^\s|]+)/)?.[1] ||
null;
const registryMode =
(source?.mode && String(source.mode).trim()) ||
(source?.theme && String(source.theme).trim()) ||
noteMode ||
'dynamicMode';
const brandPrimary = emptyToNull(source?.color) ?? emptyToNull(tenant.primaryColor);
const brandSecondary =
emptyToNull(source?.secondary_color) ?? emptyToNull(tenant.secondaryColor);
const brandBackground =
emptyToNull(source?.background_color) ?? emptyToNull(tenant.backgroundColor);
const existing =
tenant.appUiConfig &&
typeof tenant.appUiConfig === 'object' &&
(tenant.appUiConfig as any).kind === 'hamkar'
? (tenant.appUiConfig as Record<string, any>)
: null;
const defaultSnapshot = {
registryMode,
brand: {
primaryColor: brandPrimary,
secondaryColor: brandSecondary,
backgroundColor: brandBackground,
},
settings: {
pricing_box_theme: String(existing?.settings?.pricing_box_theme || '1'),
price_font_size: String(existing?.settings?.price_font_size || 'بزرگ'),
orderUI: String(existing?.settings?.orderUI || 'old'),
},
};
// Never overwrite an existing defaultSnapshot (immutable factory look)
const keepSnapshot =
existing?.defaultSnapshot && typeof existing.defaultSnapshot === 'object'
? existing.defaultSnapshot
: defaultSnapshot;
const nextUi = buildHamkarUiFromBrand({
primaryColor: restore
? keepSnapshot.brand?.primaryColor
: existing?.brand?.primaryColor ?? brandPrimary,
secondaryColor: restore
? keepSnapshot.brand?.secondaryColor
: existing?.brand?.secondaryColor ?? brandSecondary,
backgroundColor: restore
? keepSnapshot.brand?.backgroundColor
: existing?.brand?.backgroundColor ?? brandBackground,
menuIconColor: restore
? keepSnapshot.brand?.menuIconColor
: existing?.brand?.menuIconColor ?? null,
menuIconActiveColor: restore
? keepSnapshot.brand?.menuIconActiveColor
: existing?.brand?.menuIconActiveColor ?? null,
settingsManaged: Boolean(existing?.settingsManaged),
brandManaged: Boolean(existing?.brandManaged),
registryMode: restore
? keepSnapshot.registryMode
: existing?.registryMode || registryMode,
settings: restore ? keepSnapshot.settings : existing?.settings,
});
const appUiConfig = {
...nextUi,
defaultSnapshot: keepSnapshot,
settings: nextUi.settings,
};
const unchanged =
JSON.stringify(existing) === JSON.stringify(appUiConfig) && !restore;
if (unchanged) {
skipped += 1;
continue;
}
await prisma.tenant.update({
where: { id: tenant.id },
data: {
appUiConfig: appUiConfig as any,
...(restore
? {
primaryColor: keepSnapshot.brand?.primaryColor ?? null,
secondaryColor: keepSnapshot.brand?.secondaryColor ?? null,
backgroundColor: keepSnapshot.brand?.backgroundColor ?? null,
}
: {}),
},
});
updated += 1;
if (restore) restored += 1;
console.log(
`${restore ? 'restored' : 'seeded'}: ${tenant.domain} → default=${keepSnapshot.registryMode}`,
);
}
console.log({
tenants: filtered.length,
updated,
restored,
skipped,
restore,
domain: onlyDomain,
});
}
main()
.catch((err) => {
console.error(err);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});