Initial commit

Next.js + Express event management app for Hope Family Church.
This commit is contained in:
2026-07-23 15:26:47 +02:00
commit 3d381944d2
246 changed files with 57565 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
const fs = require('fs');
const path = require('path');
/**
* Checks whether the Prisma client has the EventAttachment model and the table is queryable.
* @param {import('@prisma/client').PrismaClient} prisma
*/
async function canUseEventAttachment(prisma) {
const hasModel = !!(prisma && prisma.eventAttachment && typeof prisma.eventAttachment.findMany === 'function');
if (!hasModel) return { ok: false, reason: 'NO_MODEL' };
try {
await prisma.eventAttachment.findFirst({});
return { ok: true };
} catch (e) {
return { ok: false, reason: 'QUERY_ERROR', error: String(e?.message || e) };
}
}
/**
* Reads all manifest files for event attachments and inserts missing rows into DB.
* It is idempotent: it will skip entries that already exist by id. If id lookup fails,
* it tries to avoid duplicates using a composite check (eventId + filename + size).
*
* @param {import('@prisma/client').PrismaClient} prisma
* @param {{ dryRun?: boolean, removeManifestAfterImport?: boolean }} [options]
* @returns {Promise<{processedFiles:number, imported:number, skipped:number, errors:Array<{file:string,error:string}>, details:Array<{file:string, imported:number, skipped:number}>}>}
*/
async function syncManifestsToDb(prisma, options = {}) {
const { dryRun = false, removeManifestAfterImport = false } = options;
const baseDir = path.join(__dirname, '..', '..', 'public', 'uploads', 'event-files');
const result = { processedFiles: 0, imported: 0, skipped: 0, errors: [], details: [] };
const check = await canUseEventAttachment(prisma);
if (!check.ok) {
return { ...result, errors: [{ file: '*', error: `Attachments model not usable (${check.reason})${check.error ? ': ' + check.error : ''}` }] };
}
if (!fs.existsSync(baseDir)) return result;
const files = fs.readdirSync(baseDir).filter(f => f.endsWith('.attachments.json'));
for (const file of files) {
const manifestPath = path.join(baseDir, file);
result.processedFiles += 1;
let list = [];
try {
const raw = fs.readFileSync(manifestPath, 'utf-8');
list = JSON.parse(raw) || [];
} catch (e) {
result.errors.push({ file, error: 'Failed to parse JSON: ' + String(e?.message || e) });
continue;
}
let imported = 0;
let skipped = 0;
for (const entry of list) {
try {
// Check existing by id first
const existsById = entry?.id ? await prisma.eventAttachment.findUnique({ where: { id: entry.id } }) : null;
if (existsById) { skipped++; continue; }
// Check using composite heuristic to avoid duplicates
const maybeExisting = await prisma.eventAttachment.findFirst({
where: {
eventId: entry.eventId,
filename: entry.filename,
size: typeof entry.size === 'number' ? entry.size : undefined,
}
});
if (maybeExisting) { skipped++; continue; }
if (!dryRun) {
await prisma.eventAttachment.create({
data: {
id: entry.id || undefined,
eventId: entry.eventId,
originalName: entry.originalName || entry.filename || 'file',
filename: entry.filename,
mimeType: entry.mimeType || 'application/octet-stream',
size: typeof entry.size === 'number' ? entry.size : 0,
url: entry.url,
createdAt: entry.createdAt ? new Date(entry.createdAt) : undefined,
}
});
}
imported++;
} catch (e) {
result.errors.push({ file, error: 'Insert failed: ' + String(e?.message || e) });
}
}
result.imported += imported;
result.skipped += skipped;
result.details.push({ file, imported, skipped });
// Optionally remove manifest if all entries are in DB now
if (!dryRun && removeManifestAfterImport && imported > 0) {
try {
fs.unlinkSync(manifestPath);
} catch {}
}
}
return result;
}
module.exports = { syncManifestsToDb, canUseEventAttachment };