feat: export a import kampani a cenoviek pre prenos na novy server

- npm run db:export: kampane a sablony cenoviek do supabase/data/ (+ obrazky z uloziska)
- npm run db:import [-- --dry-run]: nahra data s povodnymi ID, prepise odkazy
  na nove ulozisko, da sa spustit opakovane
- aktualny export: 7 kampani, 1 sablona cenovky

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-19 11:00:35 +02:00
1 parent 5d1900740c
commit 5f6a39debe
7 files changed
+4832 -4

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
// Export kampaní (templates) a šablón cenoviek (badges) zo Supabase do supabase/data/
// pre prenos na nový server. Stiahne aj obrázky z úložiska newsletter-assets, na ktoré
// dáta odkazujú a nie sú medzi statickými obrázkami v supabase/assets/.
// Spustenie: npm run db:export (číta .env.local pôvodného projektu)
import { createClient } from '@supabase/supabase-js';
import { mkdir, writeFile, access } from 'node:fs/promises';
import path from 'node:path';
const BUCKET = 'newsletter-assets';
const TABLES = ['templates', 'badges'];
const DATA_DIR = path.join(process.cwd(), 'supabase', 'data');
const FILES_DIR = path.join(DATA_DIR, 'files');
const ASSETS_DIR = path.join(process.cwd(), 'supabase', 'assets');
const url = (process.env.NEXT_PUBLIC_SUPABASE_URL || '').replace(/\/$/, '');
const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!url || !key) {
console.error('Chýba NEXT_PUBLIC_SUPABASE_URL alebo kľúč (SUPABASE_SERVICE_ROLE_KEY / NEXT_PUBLIC_SUPABASE_ANON_KEY) v .env.local');
process.exit(1);
}
const supabase = createClient(url, key);
const storageBase = `${url}/storage/v1/object/public/${BUCKET}/`;
await mkdir(FILES_DIR, { recursive: true });
const referenced = new Set();
for (const table of TABLES) {
const { data, error } = await supabase.from(table).select('*').order('created_at', { ascending: true });
if (error) {
console.error(`✗ ${table}: ${error.message}`);
process.exit(1);
}
await writeFile(path.join(DATA_DIR, `${table}.json`), JSON.stringify(data, null, 2) + '\n');
console.log(`✓ ${table}: ${data.length} záznamov`);
// Odkazy na súbory v úložisku (cesta v rámci bucketu)
const json = JSON.stringify(data);
let i = 0;
while ((i = json.indexOf(storageBase, i)) !== -1) {
const start = i + storageBase.length;
let end = start;
while (end < json.length && !`"'()\\ `.includes(json[end])) end++;
referenced.add(decodeURIComponent(json.slice(start, end)));
i = end;
}
}
const exists = (p) => access(p).then(() => true, () => false);
let downloaded = 0;
for (const filePath of referenced) {
// Statické obrázky šablóny sú už v supabase/assets a nahrajú sa cez db:upload-assets
if (!filePath.includes('/') && (await exists(path.join(ASSETS_DIR, filePath)))) continue;
const res = await fetch(storageBase + filePath.split('/').map(encodeURIComponent).join('/'));
if (!res.ok) {
console.warn(`! ${filePath}: HTTP ${res.status} (preskočené)`);
continue;
}
const target = path.join(FILES_DIR, filePath);
await mkdir(path.dirname(target), { recursive: true });
await writeFile(target, Buffer.from(await res.arrayBuffer()));
downloaded++;
}
await writeFile(
path.join(DATA_DIR, 'meta.json'),
JSON.stringify({ exportedAt: new Date().toISOString(), sourceStorageBase: storageBase }, null, 2) + '\n'
);
console.log(`✓ obrázky: ${referenced.size} odkazov, ${downloaded} stiahnutých do supabase/data/files/`);
console.log('\nHotovo. Commitnite priečinok supabase/data/ a na novom serveri spustite npm run db:import.');
+79
View File
@@ -0,0 +1,79 @@
// Import kampaní a šablón cenoviek zo supabase/data/ (vytvorené cez npm run db:export) do nového Supabase.
// - nahrá obrázky zo supabase/data/files/ do úložiska newsletter-assets
// - odkazy na pôvodné úložisko v dátach prepíše na úložisko nového projektu
// - záznamy vloží s pôvodnými ID; existujúce záznamy s rovnakým ID prepíše (dá sa spustiť opakovane)
// Predpoklad: spustený supabase/schema.sql. Spustenie: npm run db:import
// Skúška bez zápisu: npm run db:import -- --dry-run
import { createClient } from '@supabase/supabase-js';
import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
const BUCKET = 'newsletter-assets';
const TABLES = ['templates', 'badges'];
const DATA_DIR = path.join(process.cwd(), 'supabase', 'data');
const FILES_DIR = path.join(DATA_DIR, 'files');
const CONTENT_TYPES = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp' };
const url = (process.env.NEXT_PUBLIC_SUPABASE_URL || '').replace(/\/$/, '');
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!url || !serviceKey) {
console.error('Chýba NEXT_PUBLIC_SUPABASE_URL alebo SUPABASE_SERVICE_ROLE_KEY v .env.local');
process.exit(1);
}
const dryRun = process.argv.includes('--dry-run');
const supabase = createClient(url, serviceKey);
if (dryRun) console.log('Skúšobný režim (--dry-run): nič sa nezapíše.');
const meta = JSON.parse(await readFile(path.join(DATA_DIR, 'meta.json'), 'utf8'));
const newStorageBase = `${url}/storage/v1/object/public/${BUCKET}/`;
// 1. Obrázky
async function listFiles(dir, prefix = '') {
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
const out = [];
for (const e of entries) {
const rel = prefix ? `${prefix}/${e.name}` : e.name;
if (e.isDirectory()) out.push(...(await listFiles(path.join(dir, e.name), rel)));
else out.push(rel);
}
return out;
}
const files = await listFiles(FILES_DIR);
let failed = 0;
for (const rel of files) {
if (dryRun) continue;
const { error } = await supabase.storage.from(BUCKET).upload(rel, await readFile(path.join(FILES_DIR, rel)), {
contentType: CONTENT_TYPES[path.extname(rel).toLowerCase()] || 'application/octet-stream',
upsert: true
});
if (error) {
failed++;
console.error(`✗ súbor ${rel}: ${error.message}`);
}
}
console.log(`${dryRun ? '•' : '✓'} obrázky: ${files.length - failed}/${files.length} ${dryRun ? 'na nahratie' : 'nahratých'}`);
// 2. Dáta (s prepisom odkazov na nové úložisko)
for (const table of TABLES) {
const raw = await readFile(path.join(DATA_DIR, `${table}.json`), 'utf8');
const rewritten = raw.split(meta.sourceStorageBase).length - 1;
const rows = JSON.parse(raw.split(meta.sourceStorageBase).join(newStorageBase));
if (dryRun) {
console.log(`• ${table}: ${rows.length} záznamov, ${meta.sourceStorageBase === newStorageBase ? 0 : rewritten} prepísaných odkazov na úložisko`);
continue;
}
for (let i = 0; i < rows.length; i += 50) {
const { error } = await supabase.from(table).upsert(rows.slice(i, i + 50), { onConflict: 'id' });
if (error) {
console.error(`✗ ${table}: ${error.message}`);
process.exit(1);
}
}
console.log(`✓ ${table}: ${rows.length} záznamov`);
}
if (meta.sourceStorageBase !== newStorageBase) {
console.log(`\nOdkazy ${meta.sourceStorageBase} → ${newStorageBase}`);
}
console.log('Hotovo.');
if (failed) process.exit(1);