Add Khanegi default appearance snapshot seed and strip SA meta from get_user.
Mirrors Hamkar so each khanegi tenant can restore its factory ui-config without exposing defaultSnapshot publicly. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-1
@@ -24,7 +24,8 @@
|
||||
"prisma:seed": "ts-node --transpile-only prisma/seed.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:seed-hamkar-defaults": "ts-node --transpile-only prisma/seed-hamkar-default-snapshots.ts"
|
||||
"prisma:seed-hamkar-defaults": "ts-node --transpile-only prisma/seed-hamkar-default-snapshots.ts",
|
||||
"prisma:seed-khanegi-defaults": "ts-node --transpile-only prisma/seed-khanegi-default-snapshots.ts"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node --transpile-only prisma/seed.ts"
|
||||
|
||||
@@ -148,7 +148,7 @@ export function buildKhanegiUiFromColors(input: {
|
||||
onAccent: '#ffffff',
|
||||
}
|
||||
|
||||
return {
|
||||
const base = {
|
||||
schemaId: 'ui-config',
|
||||
version: '1.0.0',
|
||||
app: {
|
||||
@@ -198,4 +198,12 @@ export function buildKhanegiUiFromColors(input: {
|
||||
pages: {},
|
||||
_backfill: 'colors-from-live-registry',
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
defaultSnapshot: {
|
||||
app: JSON.parse(JSON.stringify(base.app)),
|
||||
pages: JSON.parse(JSON.stringify(base.pages || {})),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Seed Khanegi defaultSnapshot from current appUiConfig (or color-built UI).
|
||||
* Optionally restore current appearance to that snapshot (--restore).
|
||||
*
|
||||
* Usage:
|
||||
* npx ts-node --transpile-only prisma/seed-khanegi-default-snapshots.ts
|
||||
* npx ts-node --transpile-only prisma/seed-khanegi-default-snapshots.ts --restore
|
||||
* npx ts-node --transpile-only prisma/seed-khanegi-default-snapshots.ts --restore --domain=example.ir
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { PrismaClient, ProductType } from '@prisma/client';
|
||||
import { buildKhanegiUiFromColors } from './lib/appearance-builders';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
type SourceUser = {
|
||||
id: number;
|
||||
domain?: string | null;
|
||||
domains?: string | string[] | null;
|
||||
api_origin?: string | null;
|
||||
color?: string | null;
|
||||
secondary_color?: string | null;
|
||||
background_color?: string | null;
|
||||
app_ui_config?: unknown;
|
||||
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];
|
||||
return parseDomains(row.domains)[0] || null;
|
||||
}
|
||||
|
||||
function cloneSnap(ui: any) {
|
||||
return {
|
||||
app: JSON.parse(JSON.stringify(ui.app)),
|
||||
pages: JSON.parse(JSON.stringify(ui.pages || {})),
|
||||
};
|
||||
}
|
||||
|
||||
function stripMeta(ui: any) {
|
||||
if (!ui || typeof ui !== 'object') return ui;
|
||||
const { defaultSnapshot: _s, _backfill, ...rest } = ui;
|
||||
return rest;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { restore, domain: onlyDomain } = parseArgs();
|
||||
const file = path.join(__dirname, 'data', 'users.json');
|
||||
const byDomain = new Map<string, SourceUser>();
|
||||
|
||||
if (fs.existsSync(file)) {
|
||||
const rows = JSON.parse(fs.readFileSync(file, 'utf8')) as SourceUser[];
|
||||
for (const row of rows) {
|
||||
if (row.deleted_at) continue;
|
||||
const d = primaryDomain(row);
|
||||
if (d && !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.khanegi },
|
||||
});
|
||||
|
||||
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 existing =
|
||||
tenant.appUiConfig && typeof tenant.appUiConfig === 'object'
|
||||
? (tenant.appUiConfig as Record<string, any>)
|
||||
: null;
|
||||
|
||||
const fromRegistry =
|
||||
source?.app_ui_config &&
|
||||
typeof source.app_ui_config === 'object' &&
|
||||
((source.app_ui_config as any).schemaId === 'ui-config' || (source.app_ui_config as any).app)
|
||||
? (source.app_ui_config as Record<string, any>)
|
||||
: null;
|
||||
|
||||
const built =
|
||||
existing && (existing.schemaId === 'ui-config' || existing.app)
|
||||
? existing
|
||||
: buildKhanegiUiFromColors({
|
||||
primary: source?.color || tenant.primaryColor,
|
||||
secondary: source?.secondary_color || tenant.secondaryColor,
|
||||
background: source?.background_color || tenant.backgroundColor,
|
||||
existing: source?.app_ui_config,
|
||||
});
|
||||
|
||||
// Factory look: never overwrite existing snapshot; else registry JSON, else current UI
|
||||
const keepSnapshot =
|
||||
existing?.defaultSnapshot && typeof existing.defaultSnapshot === 'object'
|
||||
? existing.defaultSnapshot
|
||||
: fromRegistry
|
||||
? cloneSnap(fromRegistry)
|
||||
: cloneSnap(built);
|
||||
|
||||
const nextUi = restore
|
||||
? {
|
||||
...stripMeta(built),
|
||||
schemaId: built.schemaId || 'ui-config',
|
||||
version: built.version || '1.0.0',
|
||||
app: JSON.parse(JSON.stringify(keepSnapshot.app)),
|
||||
pages: JSON.parse(JSON.stringify(keepSnapshot.pages || {})),
|
||||
defaultSnapshot: keepSnapshot,
|
||||
}
|
||||
: {
|
||||
...stripMeta(built),
|
||||
defaultSnapshot: keepSnapshot,
|
||||
};
|
||||
|
||||
const unchanged =
|
||||
JSON.stringify(existing) === JSON.stringify(nextUi) && !restore;
|
||||
if (unchanged) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const colors = nextUi.app?.theme?.colors || {};
|
||||
await prisma.tenant.update({
|
||||
where: { id: tenant.id },
|
||||
data: {
|
||||
appUiConfig: nextUi as any,
|
||||
...(restore
|
||||
? {
|
||||
primaryColor: colors.primary ?? tenant.primaryColor,
|
||||
secondaryColor: colors.secondary ?? tenant.secondaryColor,
|
||||
backgroundColor: colors.background ?? tenant.backgroundColor,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
updated += 1;
|
||||
if (restore) restored += 1;
|
||||
console.log(`${restore ? 'restored' : 'seeded'}: ${tenant.domain}`);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -75,7 +75,8 @@ export class GetUserService {
|
||||
product: tenant.product,
|
||||
theme: themeMode,
|
||||
mode: themeMode,
|
||||
app_ui_config: tenant.product === 'khanegi' ? ui : undefined,
|
||||
app_ui_config:
|
||||
tenant.product === 'khanegi' ? this.stripKhanegiSaMeta(ui) : undefined,
|
||||
/** Laravel-compatible overlay; Hamkar merges only when present */
|
||||
ui_settings: uiSettings.length ? uiSettings : undefined,
|
||||
};
|
||||
@@ -86,6 +87,12 @@ export class GetUserService {
|
||||
};
|
||||
}
|
||||
|
||||
private stripKhanegiSaMeta(ui: Record<string, any> | null) {
|
||||
if (!ui || typeof ui !== 'object') return ui;
|
||||
const { defaultSnapshot: _snap, _backfill, ...rest } = ui;
|
||||
return rest;
|
||||
}
|
||||
|
||||
private buildHamkarUiSettings(ui: Record<string, any>): UiSetting[] {
|
||||
const settings = ui?.settings;
|
||||
if (!settings || typeof settings !== 'object') return [];
|
||||
|
||||
Reference in New Issue
Block a user