diff --git a/server/server.js b/server/server.js index 4f3655a..007a412 100644 --- a/server/server.js +++ b/server/server.js @@ -673,6 +673,111 @@ app.get('/api/health', async (req, res) => { }); }); +// ─── MIGRATIONS ───────────────────────────────────────────────── + +// One-time migration: convert Translations with post_id → Artefacts (copy type) +// Safe to run multiple times — skips posts that already have copy Artefacts. +app.post('/api/admin/migrate-translations-to-artefacts', requireAuth, async (req, res) => { + if (req.session.userRole !== 'superadmin') return res.status(403).json({ error: 'Superadmin only' }); + const dryRun = req.body.dry_run !== false; // default true for safety + const report = { dry_run: dryRun, skipped: [], migrated: [], errors: [] }; + + try { + // All translations linked to a post (caption or body copy) + const translations = await nocodb.list('Translations', { + where: `(post_id,gt,0)`, limit: 2000, + }); + + for (const t of translations) { + if (!t.post_id) continue; + const copyType = t.copy_type || 'body'; + + // Skip if an Artefact for this post+copy_type already exists + const existing = await nocodb.list('Artefacts', { + where: `(post_id,eq,${t.post_id})~and(type,eq,copy)~and(copy_type,eq,${copyType})`, + limit: 1, + }); + if (existing.length > 0) { + report.skipped.push({ translation_id: t.Id, post_id: t.post_id, copy_type: copyType, reason: 'artefact already exists' }); + continue; + } + + if (dryRun) { + report.migrated.push({ translation_id: t.Id, post_id: t.post_id, copy_type: copyType, title: t.title, dry_run: true }); + continue; + } + + try { + // 1. Create Artefact + const artefact = await nocodb.create('Artefacts', { + title: t.title || (copyType === 'caption' ? 'Caption' : 'Body Copy'), + type: 'copy', + copy_type: copyType, + status: t.status || 'draft', + post_id: t.post_id, + brand_id: t.brand_id || null, + approver_ids: t.approver_ids || null, + approval_token: t.approval_token || null, + approved_by_name: t.approved_by_name || null, + approved_at: t.approved_at || null, + feedback: t.feedback || null, + created_by_user_id: t.created_by_user_id || null, + }); + + // 2. Create first ArtefactVersion + const version = await nocodb.create('ArtefactVersions', { + artefact_id: artefact.Id, + version_number: 1, + status: t.status || 'draft', + created_by_user_id: t.created_by_user_id || null, + }); + + // 3. Migrate source content as a version text entry + const textsToCreate = []; + if (t.source_content && t.source_language) { + textsToCreate.push({ + version_id: version.Id, + language_code: t.source_language, + language_label: t.source_language, + content: t.source_content, + status: 'draft', + }); + } + + // 4. Migrate TranslationTexts as additional language entries + const translationTexts = await nocodb.list('TranslationTexts', { + where: `(translation_id,eq,${t.Id})`, limit: 50, + }); + for (const tt of translationTexts) { + if (tt.language_code === t.source_language) continue; // already added above + textsToCreate.push({ + version_id: version.Id, + language_code: tt.language_code, + language_label: tt.language_label || tt.language_code, + content: tt.content || '', + status: tt.status || 'draft', + }); + } + + if (textsToCreate.length > 0) { + await nocodb.bulkCreate('ArtefactVersionTexts', textsToCreate); + } + + // 5. Set current_version on Artefact + await nocodb.update('Artefacts', artefact.Id, { current_version: 1 }); + + report.migrated.push({ translation_id: t.Id, artefact_id: artefact.Id, post_id: t.post_id, copy_type: copyType, title: artefact.title, texts: textsToCreate.length }); + } catch (err) { + report.errors.push({ translation_id: t.Id, post_id: t.post_id, error: err.message }); + } + } + + res.json({ ...report, summary: { total: translations.length, migrated: report.migrated.length, skipped: report.skipped.length, errors: report.errors.length } }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // ─── EMAIL TEST ───────────────────────────────────────────────── app.post('/api/admin/test-email', requireAuth, async (req, res) => {