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 };