261 lines
9.4 KiB
TypeScript
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);
|
|
});
|