Add Hamkar default appearance snapshot seed script and builder support.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-1
@@ -23,7 +23,8 @@
|
|||||||
"prisma:deploy": "prisma migrate deploy",
|
"prisma:deploy": "prisma migrate deploy",
|
||||||
"prisma:seed": "ts-node --transpile-only prisma/seed.ts",
|
"prisma:seed": "ts-node --transpile-only prisma/seed.ts",
|
||||||
"prisma:import-users": "ts-node --transpile-only prisma/import-users.ts",
|
"prisma:import-users": "ts-node --transpile-only prisma/import-users.ts",
|
||||||
"prisma:backfill-appearance": "ts-node --transpile-only prisma/backfill-appearance-from-registry.ts"
|
"prisma:backfill-appearance": "ts-node --transpile-only prisma/backfill-appearance-from-registry.ts",
|
||||||
|
"prisma:seed-hamkar-defaults": "ts-node --transpile-only prisma/seed-hamkar-default-snapshots.ts"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "ts-node --transpile-only prisma/seed.ts"
|
"seed": "ts-node --transpile-only prisma/seed.ts"
|
||||||
|
|||||||
@@ -40,8 +40,21 @@ export function buildHamkarUiFromBrand(input: {
|
|||||||
price_font_size?: string
|
price_font_size?: string
|
||||||
orderUI?: string
|
orderUI?: string
|
||||||
}
|
}
|
||||||
|
defaultSnapshot?: {
|
||||||
|
registryMode?: string | null
|
||||||
|
brand?: {
|
||||||
|
primaryColor?: string | null
|
||||||
|
secondaryColor?: string | null
|
||||||
|
backgroundColor?: string | null
|
||||||
|
}
|
||||||
|
settings?: {
|
||||||
|
pricing_box_theme?: string
|
||||||
|
price_font_size?: string
|
||||||
|
orderUI?: string
|
||||||
|
}
|
||||||
|
} | null
|
||||||
}) {
|
}) {
|
||||||
return {
|
const base = {
|
||||||
kind: 'hamkar' as const,
|
kind: 'hamkar' as const,
|
||||||
settingsManaged: Boolean(input.settingsManaged),
|
settingsManaged: Boolean(input.settingsManaged),
|
||||||
registryMode: input.registryMode || 'dynamicMode',
|
registryMode: input.registryMode || 'dynamicMode',
|
||||||
@@ -57,6 +70,29 @@ export function buildHamkarUiFromBrand(input: {
|
|||||||
orderUI: String(input.settings?.orderUI || 'old'),
|
orderUI: String(input.settings?.orderUI || 'old'),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!input.defaultSnapshot) return base
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
defaultSnapshot: {
|
||||||
|
registryMode: input.defaultSnapshot.registryMode || base.registryMode,
|
||||||
|
brand: {
|
||||||
|
primaryColor: input.defaultSnapshot.brand?.primaryColor ?? null,
|
||||||
|
secondaryColor: input.defaultSnapshot.brand?.secondaryColor ?? null,
|
||||||
|
backgroundColor: input.defaultSnapshot.brand?.backgroundColor ?? null,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
pricing_box_theme: String(
|
||||||
|
input.defaultSnapshot.settings?.pricing_box_theme || base.settings.pricing_box_theme,
|
||||||
|
),
|
||||||
|
price_font_size: String(
|
||||||
|
input.defaultSnapshot.settings?.price_font_size || base.settings.price_font_size,
|
||||||
|
),
|
||||||
|
orderUI: String(input.defaultSnapshot.settings?.orderUI || base.settings.orderUI),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildKhanegiUiFromColors(input: {
|
export function buildKhanegiUiFromColors(input: {
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
settingsManaged: Boolean(existing?.settingsManaged),
|
||||||
|
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();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user