Files
klz-cables.com/scripts/merge-locale-duplicates.ts
2026-02-26 01:32:22 +01:00

261 lines
9.4 KiB
TypeScript

/**
* merge-locale-duplicates.ts
*
* Merges duplicate DE/EN documents into single Payload localized documents.
*
* Problem: Before native localization, DE and EN were stored as separate rows.
* Now each should be one document with locale-specific data in the *_locales tables.
*
* Strategy:
* 1. Products: Match by slug → Keep DE row as canonical, copy EN data, delete EN row
* 2. Posts: Match by slug → Same strategy
* 3. Pages: Match by slug map (impressum↔legal-notice, blog↔blog, etc.) → Same strategy
*/
import pg from 'pg';
const { Pool } = pg;
const DB_URL =
process.env.DATABASE_URI ||
process.env.POSTGRES_URI ||
`postgresql://payload:120in09oenaoinsd9iaidon@127.0.0.1:54322/payload`;
const pool = new Pool({ connectionString: DB_URL });
async function q<T = any>(query: string, values: unknown[] = []): Promise<T[]> {
const result = await pool.query(query, values);
return result.rows as T[];
}
async function mergeProducts() {
console.log('\n── PRODUCTS ───────────────────────────────────────');
const pairs = await q<{ de_id: number; en_id: number; slug: string }>(`
SELECT
de.id as de_id,
en.id as en_id,
de_loc.slug as slug
FROM products de
JOIN products_locales de_loc ON de_loc._parent_id = de.id AND de_loc._locale = 'de'
JOIN products_locales en_loc ON en_loc.slug = de_loc.slug AND en_loc._locale = 'en'
JOIN products en ON en.id = en_loc._parent_id
WHERE de.id != en.id
`);
console.log(`Found ${pairs.length} DE/EN product pairs to merge`);
for (const { de_id, en_id, slug } of pairs) {
console.log(` Merging: ${slug} (DE id=${de_id} ← EN id=${en_id})`);
const [enData] = await q(`
SELECT * FROM products_locales WHERE _parent_id = $1 AND _locale = 'en'
`, [en_id]);
if (enData) {
await q(`
INSERT INTO products_locales (title, description, application, content, _locale, _parent_id)
VALUES ($1, $2, $3, $4, 'en', $5)
ON CONFLICT (_locale, _parent_id) DO UPDATE
SET title = EXCLUDED.title,
description = EXCLUDED.description,
application = EXCLUDED.application,
content = EXCLUDED.content
`, [enData.title, enData.description, enData.application, enData.content, de_id]);
}
// Move categories from EN to DE if DE has none
await q(`
UPDATE products_categories SET _parent_id = $1
WHERE _parent_id = $2
AND NOT EXISTS (SELECT 1 FROM products_categories WHERE _parent_id = $1)
`, [de_id, en_id]);
// Move images (rels) from EN to DE if DE has none
await q(`
UPDATE products_rels SET parent = $1
WHERE parent = $2
AND NOT EXISTS (SELECT 1 FROM products_rels WHERE parent = $1)
`, [de_id, en_id]);
// Copy featuredImage if DE is missing one
await q(`
UPDATE products SET featured_image_id = (
SELECT featured_image_id FROM products WHERE id = $2
)
WHERE id = $1 AND featured_image_id IS NULL
`, [de_id, en_id]);
// Delete EN locale entry and EN product row
await q(`DELETE FROM products_locales WHERE _parent_id = $1`, [en_id]);
await q(`DELETE FROM _products_v WHERE parent = $1`, [en_id]);
await q(`DELETE FROM products WHERE id = $1`, [en_id]);
console.log(`${slug}`);
}
const [{ count }] = await q(`SELECT count(*) FROM products`);
console.log(`Products remaining: ${count}`);
}
async function mergePosts() {
console.log('\n── POSTS ──────────────────────────────────────────');
const pairs = await q<{ de_id: number; en_id: number; slug: string }>(`
SELECT
de.id as de_id,
en.id as en_id,
de_loc.slug as slug
FROM posts de
JOIN posts_locales de_loc ON de_loc._parent_id = de.id AND de_loc._locale = 'de'
JOIN posts_locales en_loc ON en_loc.slug = de_loc.slug AND en_loc._locale = 'en'
JOIN posts en ON en.id = en_loc._parent_id
WHERE de.id != en.id
`);
console.log(`Found ${pairs.length} DE/EN post pairs to merge`);
for (const { de_id, en_id, slug } of pairs) {
console.log(` Merging post: ${slug} (DE id=${de_id} ← EN id=${en_id})`);
const [enData] = await q(`
SELECT * FROM posts_locales WHERE _parent_id = $1 AND _locale = 'en'
`, [en_id]);
if (enData) {
await q(`
INSERT INTO posts_locales (title, slug, excerpt, category, content, _locale, _parent_id)
VALUES ($1, $2, $3, $4, $5, 'en', $6)
ON CONFLICT (_locale, _parent_id) DO UPDATE
SET title = EXCLUDED.title, slug = EXCLUDED.slug,
excerpt = EXCLUDED.excerpt, category = EXCLUDED.category,
content = EXCLUDED.content
`, [enData.title, enData.slug, enData.excerpt, enData.category, enData.content, de_id]);
}
// Copy featuredImage/date from EN if DE is missing
await q(`
UPDATE posts SET
featured_image_id = COALESCE(featured_image_id, (SELECT featured_image_id FROM posts WHERE id = $2)),
date = COALESCE(date, (SELECT date FROM posts WHERE id = $2))
WHERE id = $1
`, [de_id, en_id]);
await q(`DELETE FROM posts_locales WHERE _parent_id = $1`, [en_id]);
await q(`DELETE FROM _posts_v WHERE parent = $1`, [en_id]);
await q(`DELETE FROM posts WHERE id = $1`, [en_id]);
console.log(`${slug}`);
}
const [{ count }] = await q(`SELECT count(*) FROM posts`);
console.log(`Posts remaining: ${count}`);
}
// DE slug → EN slug mapping for pages
const PAGE_SLUG_MAP: Record<string, string> = {
impressum: 'legal-notice',
datenschutz: 'privacy-policy',
agbs: 'terms',
kontakt: 'contact',
produkte: 'products',
blog: 'blog',
team: 'team',
start: 'start',
danke: 'thanks',
};
async function mergePages() {
console.log('\n── PAGES ──────────────────────────────────────────');
for (const [deSlug, enSlug] of Object.entries(PAGE_SLUG_MAP)) {
const [dePage] = await q<{ id: number }>(`
SELECT p.id FROM pages p
JOIN pages_locales pl ON pl._parent_id = p.id AND pl._locale = 'de' AND pl.slug = $1
LIMIT 1
`, [deSlug]);
const [enPage] = await q<{ id: number }>(`
SELECT p.id FROM pages p
JOIN pages_locales pl ON pl._parent_id = p.id AND pl._locale = 'en' AND pl.slug = $1
LIMIT 1
`, [enSlug]);
if (!dePage && !enPage) {
console.log(` ⚠ No page found for ${deSlug}/${enSlug} — skipping`);
continue;
}
if (!dePage) {
console.log(` ⚠ No DE page for "${deSlug}" — EN-only page id=${enPage!.id} kept`);
continue;
}
if (!enPage) {
console.log(` ⚠ No EN page for "${enSlug}" — DE-only page id=${dePage.id} kept`);
continue;
}
if (dePage.id === enPage.id) {
console.log(`${deSlug}/${enSlug} already merged (id=${dePage.id})`);
continue;
}
console.log(` Merging: ${deSlug}${enSlug} (DE id=${dePage.id} ← EN id=${enPage.id})`);
const [enData] = await q(`
SELECT * FROM pages_locales WHERE _parent_id = $1 AND _locale = 'en'
`, [enPage.id]);
if (enData) {
await q(`
INSERT INTO pages_locales (title, slug, excerpt, content, _locale, _parent_id)
VALUES ($1, $2, $3, $4, 'en', $5)
ON CONFLICT (_locale, _parent_id) DO UPDATE
SET title = EXCLUDED.title, slug = EXCLUDED.slug,
excerpt = EXCLUDED.excerpt, content = EXCLUDED.content
`, [enData.title, enData.slug, enData.excerpt, enData.content, dePage.id]);
}
// Copy featuredImage/layout from EN if DE is missing
await q(`
UPDATE pages SET
featured_image_id = COALESCE(featured_image_id, (SELECT featured_image_id FROM pages WHERE id = $2)),
layout = COALESCE(layout, (SELECT layout FROM pages WHERE id = $2))
WHERE id = $1
`, [dePage.id, enPage.id]);
await q(`DELETE FROM pages_locales WHERE _parent_id = $1`, [enPage.id]);
await q(`DELETE FROM _pages_v WHERE parent = $1`, [enPage.id]);
await q(`DELETE FROM pages WHERE id = $1`, [enPage.id]);
console.log(`${deSlug}/${enSlug}`);
}
const [{ count }] = await q(`SELECT count(*) FROM pages`);
console.log(`Pages remaining: ${count}`);
}
async function main() {
console.log('🔀 Merging duplicate locale documents into native Payload localization...');
try {
await mergeProducts();
await mergePosts();
await mergePages();
console.log('\n── Final pages state ──────────────────────────────');
const pages = await q(`
SELECT p.id, pl._locale, pl.slug, pl.title FROM pages p
JOIN pages_locales pl ON pl._parent_id = p.id
ORDER BY p.id, pl._locale
`);
pages.forEach((r) => console.log(` [id=${r.id}] ${r._locale}: ${r.slug}${r.title}`));
console.log('\n✅ Done!');
} finally {
await pool.end();
}
}
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});