/** * همگام‌سازی `data/specialties.json` با دیتابیس، در زمان build. * * node scripts/sync-specialties.mjs * * فایل قبلاً یک اسنپ‌شات دستی بود و عقب می‌ماند: تخصصی که ادمین می‌ساخت در سایت * نبود، پس صفحه‌اش ۴۰۴ می‌شد، در سایت‌مپ نمی‌آمد، و breadcrumb صفحهٔ پزشک به آن * لینک نمی‌داد. نُه مصرف‌کننده از همین فایل می‌خوانند، از `app/sitemap.js` تا * `helper/filterList`، پس منبع باید یکی و تازه بماند. * * فایل عمداً حذف نشد و runtime fetch جایش نیامد: `sitemap` و صفحات تخصص باید * ایستا بمانند و به دسترس‌بودن API در زمان درخواست گره نخورند. * * فقط به `build` وصل است، نه `dev` — توسعهٔ آفلاین نباید به API نیاز داشته باشد. * * بک‌اند محلی گواهی self-signed دارد، پس اجرای محلی مثل اسکریپت `dev` نیاز دارد: * * NODE_TLS_REJECT_UNAUTHORIZED=0 npm run build * * این متغیر عمداً داخل `prebuild` ست نشده تا build تولیدی اعتبارسنجی TLS را از * دست ندهد. */ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const OUT = join(ROOT, 'data/specialties.json'); /** * `.env` را Next می‌خواند، نه یک اسکریپت خام node. اینجا فقط برای اجرای محلی * خوانده می‌شود و متغیرهای واقعیِ محیط را بازنویسی نمی‌کند — در CI فایل نیست و * مقدار از پلتفرم می‌آید. */ function loadDotEnv() { const file = join(ROOT, '.env'); if (!existsSync(file)) return; for (const line of readFileSync(file, 'utf8').split('\n')) { const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/); if (!match) continue; const [, key, raw] = match; if (process.env[key] === undefined) { process.env[key] = raw.replace(/^["']|["']$/g, ''); } } } loadDotEnv(); const API_URL = process.env.NEXT_PUBLIC_API_URL; if (!API_URL) { throw new Error('sync-specialties: NEXT_PUBLIC_API_URL is not set'); } // `GET /api/v1/specialties` عمومی است و توکن نمی‌خواهد؛ بدون پارامتر `parent_id` // همهٔ تخصص‌های فعال را می‌دهد، ریشه‌ها و فرزندان با هم. پاسخ دولایه است. const res = await fetch(`${API_URL}/api/v1/specialties`); if (!res.ok) { throw new Error(`sync-specialties: HTTP ${res.status} from ${API_URL}`); } const items = (await res.json())?.data?.data; // لیست خالی یعنی چیزی غلط است — پاسخ عوض شده، دیتابیس خالی است، یا پشت پراکسی // نشسته‌ایم. بازنویسی با آن، سایت‌مپ را آب می‌کند و صفحات تخصص را از ایندکس // می‌اندازد؛ شکستِ build از آن به‌مراتب ارزان‌تر است. if (!Array.isArray(items) || items.length === 0) { throw new Error('sync-specialties: empty list; refusing to overwrite data/specialties.json'); } // ترتیب پایدار بر اساس id تا diff فایل نویزی نشود و مرور تغییرات ممکن بماند. const sorted = [...items].sort((a, b) => a.id - b.id); const before = JSON.parse(readFileSync(OUT, 'utf8')); writeFileSync(OUT, `${JSON.stringify(sorted, null, 2)}\n`, 'utf8'); console.log(`sync-specialties: ${before.length} → ${sorted.length} specialties from ${API_URL}`);