- Added new newsletter subscription functionality to the participation page. - Created a new `NewsletterForm` component to handle user subscriptions. - Introduced `NewsletterSubscriptions` collection to manage subscriptions in the database. - Developed migration script to set up necessary database tables and fields for the newsletter feature. - Refactored participation content to remove deprecated newsletter phase data and integrate new structure. - Updated the `Participation` page to utilize the new newsletter form and handle different application phases.
344 lines
15 KiB
TypeScript
344 lines
15 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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`;'))
|
|
}
|