Initial commit
This commit is contained in:
97
scripts/applyMigrations.js
Normal file
97
scripts/applyMigrations.js
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Lightweight migration runner for Supabase.
|
||||
* Reads SQL files in supabase/migrations (sorted) and applies any that have not been recorded.
|
||||
*
|
||||
* Env:
|
||||
* - SUPABASE_DB_URL or DATABASE_URL: postgres connection string (service role / connection string)
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const migrationsDir = path.resolve(__dirname, '..', 'supabase', 'migrations');
|
||||
const connectionString = process.env.SUPABASE_DB_URL || process.env.DATABASE_URL;
|
||||
|
||||
if (!connectionString) {
|
||||
console.error('Missing SUPABASE_DB_URL (or DATABASE_URL) environment variable. Aborting migrations.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(migrationsDir)) {
|
||||
console.error(`Migrations directory not found: ${migrationsDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pool = new Pool({ connectionString });
|
||||
|
||||
async function ensureMigrationsTable(client) {
|
||||
await client.query(`
|
||||
create table if not exists public.schema_migrations (
|
||||
id serial primary key,
|
||||
filename text not null unique,
|
||||
applied_at timestamptz not null default now()
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
async function getApplied(client) {
|
||||
const { rows } = await client.query('select filename from public.schema_migrations');
|
||||
return new Set(rows.map((r) => r.filename));
|
||||
}
|
||||
|
||||
function listMigrations() {
|
||||
return fs
|
||||
.readdirSync(migrationsDir)
|
||||
.filter((file) => file.endsWith('.sql'))
|
||||
.sort();
|
||||
}
|
||||
|
||||
async function applyMigration(client, filename) {
|
||||
const filePath = path.join(migrationsDir, filename);
|
||||
const sql = fs.readFileSync(filePath, 'utf8');
|
||||
console.log(`Applying migration: ${filename}`);
|
||||
await client.query('begin');
|
||||
try {
|
||||
await client.query(sql);
|
||||
await client.query('insert into public.schema_migrations (filename) values ($1)', [filename]);
|
||||
await client.query('commit');
|
||||
console.log(`✓ Applied ${filename}`);
|
||||
} catch (err) {
|
||||
await client.query('rollback');
|
||||
console.error(`✗ Failed ${filename}:`, err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await ensureMigrationsTable(client);
|
||||
const applied = await getApplied(client);
|
||||
const migrations = listMigrations();
|
||||
|
||||
const pending = migrations.filter((m) => !applied.has(m));
|
||||
if (pending.length === 0) {
|
||||
console.log('No pending migrations.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const migration of pending) {
|
||||
await applyMigration(client, migration);
|
||||
}
|
||||
console.log('All pending migrations applied.');
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error('Migration run failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
130
scripts/sendFinalizationReminders.js
Normal file
130
scripts/sendFinalizationReminders.js
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env node
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const connectionString = process.env.SUPABASE_DB_URL || process.env.DATABASE_URL;
|
||||
const resendApiKey = process.env.RESEND_API_KEY;
|
||||
const fromEmail = process.env.REMINDER_FROM_EMAIL;
|
||||
const appUrl = process.env.REMINDER_APP_URL || 'http://localhost:5173';
|
||||
const dryRun = process.env.REMINDER_DRY_RUN === 'true';
|
||||
|
||||
if (!connectionString) {
|
||||
console.error('Missing SUPABASE_DB_URL (or DATABASE_URL).');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!dryRun && (!resendApiKey || !fromEmail)) {
|
||||
console.error('Missing RESEND_API_KEY or REMINDER_FROM_EMAIL.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pool = new Pool({ connectionString });
|
||||
|
||||
const findPendingReminders = async (client) => {
|
||||
const { rows } = await client.query(`
|
||||
select
|
||||
a.id,
|
||||
a.name,
|
||||
a.pat_type,
|
||||
a.datum,
|
||||
a.created_at,
|
||||
u.email
|
||||
from public.assessments a
|
||||
join auth.users u on u.id = a.user_id
|
||||
where a.is_finalized = false
|
||||
and a.created_at <= now() - interval '3 days'
|
||||
and a.finalization_reminder_sent_at is null
|
||||
and coalesce(u.email, '') <> ''
|
||||
order by a.created_at asc
|
||||
`);
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
const buildEmailPayload = (row) => {
|
||||
const subject = `PAT Test noch abschließen: ${row.name || row.pat_type}`;
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; line-height: 1.5; color: #111827;">
|
||||
<h2 style="margin-bottom: 12px;">PAT Test noch abschließen</h2>
|
||||
<p>Dein offener Test wurde seit mehr als 3 Tagen nicht final abgeschlossen.</p>
|
||||
<ul>
|
||||
<li><strong>Name:</strong> ${row.name || '—'}</li>
|
||||
<li><strong>PAT Typ:</strong> ${row.pat_type || '—'}</li>
|
||||
<li><strong>Datum:</strong> ${row.datum || '—'}</li>
|
||||
</ul>
|
||||
<p>Bitte öffne PAT Stats und schließe den Test final ab.</p>
|
||||
<p>
|
||||
<a href="${appUrl}" style="display: inline-block; margin-top: 8px; padding: 10px 16px; background: #059669; color: #ffffff; text-decoration: none; border-radius: 6px;">
|
||||
PAT Stats öffnen
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return {
|
||||
from: fromEmail,
|
||||
to: [row.email],
|
||||
subject,
|
||||
html
|
||||
};
|
||||
};
|
||||
|
||||
const sendEmail = async (payload) => {
|
||||
const response = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${resendApiKey}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const message = await response.text();
|
||||
throw new Error(`Resend request failed: ${response.status} ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const markReminderSent = async (client, assessmentId) => {
|
||||
await client.query(
|
||||
`
|
||||
update public.assessments
|
||||
set finalization_reminder_sent_at = now()
|
||||
where id = $1
|
||||
and is_finalized = false
|
||||
and finalization_reminder_sent_at is null
|
||||
`,
|
||||
[assessmentId]
|
||||
);
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const rows = await findPendingReminders(client);
|
||||
|
||||
if (!rows.length) {
|
||||
console.log('No pending finalization reminders.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
if (dryRun) {
|
||||
console.log(`[dry-run] Would remind ${row.email} for assessment ${row.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await sendEmail(buildEmailPayload(row));
|
||||
await markReminderSent(client, row.id);
|
||||
console.log(`Reminder sent for assessment ${row.id} to ${row.email}`);
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
};
|
||||
|
||||
run().catch((error) => {
|
||||
console.error('Failed to send finalization reminders:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user