import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite' import { defaultNewsletterFormData, type NewsletterInterestCopy } from '@/spa/newsletterForm' type MigrationDB = MigrateUpArgs['db'] const ident = (value: string) => `\`${value.replace(/`/g, '``')}\`` const literal = (value: string) => `'${value.replace(/'/g, "''")}'` const snake = (value: string) => value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) const scalarFields = [ 'eyebrow', 'heading', 'body', 'formEyebrow', 'formTitle', 'formBody', 'emailLabel', 'emailPlaceholder', 'submitLabel', 'privacy', 'validationRequired', 'validationInvalid', 'successHeading', 'successBody', 'footnote', 'footnoteStrong', ] as const const sharedScalarFields = [...scalarFields, 'submissionError'] as const const pagePrefixes = [ 'home_application_newsletter_phase_evaluation', 'home_application_newsletter_phase_completed', 'participation_application_form_newsletter_phase_evaluation', 'participation_application_form_newsletter_phase_completed', ] as const const legacyBenefitTables = [ 'home_news_eval_bens', 'home_news_done_bens', 'part_news_eval_bens', 'part_news_done_bens', '_home_news_eval_bens_v', '_home_news_done_bens_v', '_part_news_eval_bens_v', '_part_news_done_bens_v', ] as const async function tableExists(db: MigrationDB, tableName: string) { const rows = (await db.all( sql.raw(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`), )) as Array<{ name: string }> return rows.length > 0 } async function columnExists(db: MigrationDB, tableName: string, columnName: string) { if (!(await tableExists(db, tableName))) return false const columns = (await db.all(sql.raw(`PRAGMA table_info(${ident(tableName)})`))) as Array<{ name: string }> return columns.some((column) => column.name === columnName) } async function addColumn(db: MigrationDB, tableName: string, columnName: string, definition = 'text') { if (await columnExists(db, tableName, columnName)) return await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} ADD ${ident(columnName)} ${definition};`)) } async function dropColumn(db: MigrationDB, tableName: string, columnName: string) { if (!(await columnExists(db, tableName, columnName))) return await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} DROP COLUMN ${ident(columnName)};`)) } function globalColumn(phase: 'evaluation' | 'completed', field: (typeof sharedScalarFields)[number]) { return `${phase}_${snake(field)}` } function pageColumn(prefix: (typeof pagePrefixes)[number], field: (typeof scalarFields)[number]) { return `${prefix}_${snake(field)}` } function globalTableColumns() { const definitions: string[] = ['`id` integer PRIMARY KEY NOT NULL'] for (const phase of ['evaluation', 'completed'] as const) { const defaults = defaultNewsletterFormData[phase] for (const field of sharedScalarFields) { definitions.push(`${ident(globalColumn(phase, field))} text DEFAULT ${literal(defaults[field])}`) } } definitions.push('`updated_at` text', '`created_at` text') return definitions.join(',\n') } async function createBenefitTable(db: MigrationDB, tableName: string, parentTable: string, idType: 'text' | 'integer') { const versionUUID = parentTable === '_pages_v' ? ',\n `_uuid` text' : '' await db.run(sql.raw(` CREATE TABLE IF NOT EXISTS ${ident(tableName)} ( \`_order\` integer NOT NULL, \`_parent_id\` integer NOT NULL, \`id\` ${idType} PRIMARY KEY NOT NULL, \`num\` text, \`label\` text, \`desc\` text${versionUUID}, FOREIGN KEY (\`_parent_id\`) REFERENCES ${ident(parentTable)}(\`id\`) ON UPDATE no action ON DELETE cascade ); `)) await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_order_idx`)} ON ${ident(tableName)} (\`_order\`);`)) await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_parent_id_idx`)} ON ${ident(tableName)} (\`_parent_id\`);`)) } async function seedGlobalScalarsFromHome(db: MigrationDB) { await db.run(sql.raw(` INSERT OR IGNORE INTO \`newsletter_form\` (\`id\`, \`updated_at\`, \`created_at\`) VALUES (1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); `)) if (!(await columnExists(db, 'pages', pageColumn(pagePrefixes[0], 'eyebrow')))) return const assignments: string[] = [] for (const phase of ['evaluation', 'completed'] as const) { const prefix = phase === 'evaluation' ? pagePrefixes[0] : pagePrefixes[1] for (const field of scalarFields) { const target = globalColumn(phase, field) const source = pageColumn(prefix, field) assignments.push( `${ident(target)} = COALESCE((SELECT ${ident(source)} FROM \`pages\` WHERE \`spa_path\` = '/' OR \`slug\` IN ('startseite', 'home') ORDER BY CASE WHEN \`spa_path\` = '/' THEN 0 ELSE 1 END LIMIT 1), ${ident(target)})`, ) } } await db.run(sql.raw(`UPDATE \`newsletter_form\` SET ${assignments.join(',\n')} WHERE \`id\` = 1;`)) } async function seedBenefits(db: MigrationDB, phase: 'evaluation' | 'completed') { const target = phase === 'evaluation' ? 'newsletter_eval_bens' : 'newsletter_done_bens' const source = phase === 'evaluation' ? 'home_news_eval_bens' : 'home_news_done_bens' const existingCount = (await db.all(sql.raw(`SELECT COUNT(*) AS count FROM ${ident(target)} WHERE \`_parent_id\` = 1;`))) as Array<{ count: number }> if (Number(existingCount[0]?.count || 0) > 0) return if (await tableExists(db, source)) { await db.run(sql.raw(` INSERT OR IGNORE INTO ${ident(target)} (\`_order\`, \`_parent_id\`, \`id\`, \`num\`, \`label\`, \`desc\`) SELECT benefits.\`_order\`, 1, lower(hex(randomblob(16))), benefits.\`num\`, benefits.\`label\`, benefits.\`desc\` FROM ${ident(source)} benefits INNER JOIN \`pages\` page ON page.\`id\` = benefits.\`_parent_id\` WHERE page.\`spa_path\` = '/' OR page.\`slug\` IN ('startseite', 'home') ORDER BY benefits.\`_order\`; `)) } const count = (await db.all(sql.raw(`SELECT COUNT(*) AS count FROM ${ident(target)} WHERE \`_parent_id\` = 1;`))) as Array<{ count: number }> if (Number(count[0]?.count || 0) > 0) return for (const [index, benefit] of defaultNewsletterFormData[phase].benefits.entries()) { await db.run(sql.raw(` INSERT INTO ${ident(target)} (\`_order\`, \`_parent_id\`, \`id\`, \`num\`, \`label\`, \`desc\`) VALUES (${index + 1}, 1, lower(hex(randomblob(16))), ${literal(benefit.num)}, ${literal(benefit.label)}, ${literal(benefit.desc)}); `)) } } async function removeLegacyPageFields(db: MigrationDB) { for (const prefix of pagePrefixes) { for (const field of scalarFields) { await dropColumn(db, 'pages', pageColumn(prefix, field)) await dropColumn(db, '_pages_v', `version_${pageColumn(prefix, field)}`) } } for (const tableName of legacyBenefitTables) { await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident(tableName)};`)) } } async function restoreLegacyPageFields(db: MigrationDB) { for (const prefix of pagePrefixes) { for (const field of scalarFields) { await addColumn(db, 'pages', pageColumn(prefix, field)) await addColumn(db, '_pages_v', `version_${pageColumn(prefix, field)}`) } } await createBenefitTable(db, 'home_news_eval_bens', 'pages', 'text') await createBenefitTable(db, 'home_news_done_bens', 'pages', 'text') await createBenefitTable(db, 'part_news_eval_bens', 'pages', 'text') await createBenefitTable(db, 'part_news_done_bens', 'pages', 'text') await createBenefitTable(db, '_home_news_eval_bens_v', '_pages_v', 'integer') await createBenefitTable(db, '_home_news_done_bens_v', '_pages_v', 'integer') await createBenefitTable(db, '_part_news_eval_bens_v', '_pages_v', 'integer') await createBenefitTable(db, '_part_news_done_bens_v', '_pages_v', 'integer') if (!(await tableExists(db, 'newsletter_form'))) return for (const [pageKind, pageFilter] of [ ['home', "`spa_path` = '/' OR `slug` IN ('startseite', 'home')"], ['participation', "`spa_path` = '/teilnahme' OR `slug` IN ('teilnahme', 'participation')"], ] as const) { const assignments: string[] = [] for (const phase of ['evaluation', 'completed'] as const) { const prefix = pageKind === 'home' ? phase === 'evaluation' ? pagePrefixes[0] : pagePrefixes[1] : phase === 'evaluation' ? pagePrefixes[2] : pagePrefixes[3] for (const field of scalarFields) { assignments.push(`${ident(pageColumn(prefix, field))} = (SELECT ${ident(globalColumn(phase, field))} FROM \`newsletter_form\` WHERE \`id\` = 1)`) } } await db.run(sql.raw(`UPDATE \`pages\` SET ${assignments.join(',\n')} WHERE ${pageFilter};`)) } for (const [target, source, filter] of [ ['home_news_eval_bens', 'newsletter_eval_bens', "page.`spa_path` = '/' OR page.`slug` IN ('startseite', 'home')"], ['home_news_done_bens', 'newsletter_done_bens', "page.`spa_path` = '/' OR page.`slug` IN ('startseite', 'home')"], ['part_news_eval_bens', 'newsletter_eval_bens', "page.`spa_path` = '/teilnahme' OR page.`slug` IN ('teilnahme', 'participation')"], ['part_news_done_bens', 'newsletter_done_bens', "page.`spa_path` = '/teilnahme' OR page.`slug` IN ('teilnahme', 'participation')"], ] as const) { await db.run(sql.raw(` INSERT INTO ${ident(target)} (\`_order\`, \`_parent_id\`, \`id\`, \`num\`, \`label\`, \`desc\`) SELECT benefit.\`_order\`, page.\`id\`, lower(hex(randomblob(16))), benefit.\`num\`, benefit.\`label\`, benefit.\`desc\` FROM ${ident(source)} benefit CROSS JOIN \`pages\` page WHERE ${filter}; `)) } } async function rebuildLockedDocumentRelations(db: MigrationDB, includeNewsletter: boolean) { const tableName = 'payload_locked_documents_rels' if (!(await tableExists(db, tableName))) return const newsletterColumn = includeNewsletter ? ',\n `newsletter_subscriptions_id` integer' : '' const newsletterForeignKey = includeNewsletter ? ',\n FOREIGN KEY (`newsletter_subscriptions_id`) REFERENCES `newsletter_subscriptions`(`id`) ON UPDATE no action ON DELETE cascade' : '' const existingNewsletterColumn = await columnExists(db, tableName, 'newsletter_subscriptions_id') const commonColumns = [ 'id', 'order', 'parent_id', 'path', 'pages_id', 'preistraeger_id', 'jury_mitglieder_id', 'partners_id', 'posts_id', 'events_id', ...(includeNewsletter && existingNewsletterColumn ? ['newsletter_subscriptions_id'] : []), 'media_id', 'users_id', 'payload_folders_id', ] await db.run(sql.raw('DROP TABLE IF EXISTS `__new_payload_locked_documents_rels`;')) await db.run(sql.raw(` CREATE TABLE \`__new_payload_locked_documents_rels\` ( \`id\` integer PRIMARY KEY NOT NULL, \`order\` integer, \`parent_id\` integer NOT NULL, \`path\` text NOT NULL, \`pages_id\` integer, \`preistraeger_id\` integer, \`jury_mitglieder_id\` integer, \`partners_id\` integer, \`posts_id\` integer, \`events_id\` integer${newsletterColumn}, \`media_id\` integer, \`users_id\` integer, \`payload_folders_id\` integer, FOREIGN KEY (\`parent_id\`) REFERENCES \`payload_locked_documents\`(\`id\`) ON UPDATE no action ON DELETE cascade, FOREIGN KEY (\`pages_id\`) REFERENCES \`pages\`(\`id\`) ON UPDATE no action ON DELETE cascade, FOREIGN KEY (\`preistraeger_id\`) REFERENCES \`preistraeger\`(\`id\`) ON UPDATE no action ON DELETE cascade, FOREIGN KEY (\`jury_mitglieder_id\`) REFERENCES \`jury_mitglieder\`(\`id\`) ON UPDATE no action ON DELETE cascade, FOREIGN KEY (\`partners_id\`) REFERENCES \`partners\`(\`id\`) ON UPDATE no action ON DELETE cascade, FOREIGN KEY (\`posts_id\`) REFERENCES \`posts\`(\`id\`) ON UPDATE no action ON DELETE cascade, FOREIGN KEY (\`events_id\`) REFERENCES \`events\`(\`id\`) ON UPDATE no action ON DELETE cascade${newsletterForeignKey}, FOREIGN KEY (\`media_id\`) REFERENCES \`media\`(\`id\`) ON UPDATE no action ON DELETE cascade, FOREIGN KEY (\`users_id\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE cascade, FOREIGN KEY (\`payload_folders_id\`) REFERENCES \`payload_folders\`(\`id\`) ON UPDATE no action ON DELETE cascade ); `)) const copiedColumns = commonColumns.map(ident).join(', ') await db.run(sql.raw(`INSERT INTO \`__new_payload_locked_documents_rels\` (${copiedColumns}) SELECT ${copiedColumns} FROM ${ident(tableName)};`)) await db.run(sql.raw(`DROP TABLE ${ident(tableName)};`)) await db.run(sql.raw(`ALTER TABLE \`__new_payload_locked_documents_rels\` RENAME TO ${ident(tableName)};`)) for (const [suffix, column] of [ ['order', 'order'], ['parent', 'parent_id'], ['path', 'path'], ['pages_id', 'pages_id'], ['preistraeger_id', 'preistraeger_id'], ['jury_mitglieder_id', 'jury_mitglieder_id'], ['partners_id', 'partners_id'], ['posts_id', 'posts_id'], ['events_id', 'events_id'], ...(includeNewsletter ? [['newsletter_subscriptions_i', 'newsletter_subscriptions_id']] : []), ['media_id', 'media_id'], ['users_id', 'users_id'], ['payload_folders_id', 'payload_folders_id'], ] as string[][]) { await db.run(sql.raw(`CREATE INDEX ${ident(`${tableName}_${suffix}_idx`)} ON ${ident(tableName)} (${ident(column)});`)) } } export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise { await db.run(sql.raw(`CREATE TABLE IF NOT EXISTS \`newsletter_form\` (${globalTableColumns()});`)) await createBenefitTable(db, 'newsletter_eval_bens', 'newsletter_form', 'text') await createBenefitTable(db, 'newsletter_done_bens', 'newsletter_form', 'text') await seedGlobalScalarsFromHome(db) await seedBenefits(db, 'evaluation') await seedBenefits(db, 'completed') await db.run(sql.raw(` CREATE TABLE IF NOT EXISTS \`newsletter_subscriptions\` ( \`id\` integer PRIMARY KEY NOT NULL, \`email\` text NOT NULL, \`source\` text NOT NULL, \`phase\` text NOT NULL, \`last_submitted_at\` text NOT NULL, \`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, \`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL ); `)) await db.run(sql.raw('CREATE UNIQUE INDEX IF NOT EXISTS `newsletter_subscriptions_email_idx` ON `newsletter_subscriptions` (`email`);')) await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `newsletter_subscriptions_updated_at_idx` ON `newsletter_subscriptions` (`updated_at`);')) await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `newsletter_subscriptions_created_at_idx` ON `newsletter_subscriptions` (`created_at`);')) await rebuildLockedDocumentRelations(db, true) await removeLegacyPageFields(db) } export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise { await restoreLegacyPageFields(db) await rebuildLockedDocumentRelations(db, false) await db.run(sql.raw('DROP TABLE IF EXISTS `newsletter_subscriptions`;')) await db.run(sql.raw('DROP TABLE IF EXISTS `newsletter_eval_bens`;')) await db.run(sql.raw('DROP TABLE IF EXISTS `newsletter_done_bens`;')) await db.run(sql.raw('DROP TABLE IF EXISTS `newsletter_form`;')) }