const assert = require('node:assert/strict'); const fs = require('node:fs'); require('dotenv').config({ quiet: true }); const { PrismaClient } = require('@prisma/client'); const url = new URL(process.env.DATABASE_URL); url.hostname = '192.168.0.20'; url.port = '15437'; url.pathname = '/lms'; url.searchParams.set('connect_timeout', '5'); const db = new PrismaClient({ datasources: { db: { url: url.toString() } } }); const apply = process.argv.includes('--apply'); const include = { weeks: { orderBy: { week: 'asc' }, include: { modules: { orderBy: { sortOrder: 'asc' } }, topics: { orderBy: { sortOrder: 'asc' } }, practices: { orderBy: { sortOrder: 'asc' } }, references: { orderBy: { id: 'asc' } }, } }, environmentProfiles: { orderBy: { id: 'asc' } }, maps: { orderBy: { id: 'asc' } }, weekMaps: { orderBy: { id: 'asc' } }, instructorCurriculumWeekOverrides: { orderBy: { id: 'asc' } }, }; const snapshot = client => client.course.findMany({ orderBy: { id: 'asc' }, include }); function data(row) { const { id, createdAt, updatedAt, ...rest } = row; return rest; } async function main() { const before = await snapshot(db); const base = before.find(c => c.slug === 'futuredrive-autonomy-basic'); assert(base); const source = base.weeks.find(w => w.week === 3); assert.equal(source?.title, '원격 개발'); assert.equal(source.topics.length, 2); assert.equal(source.modules.length, 11); const profiles = base.environmentProfiles.filter(p => p.week === 3); assert.equal(profiles.length, 2); const targets = before.filter(c => c.id !== base.id && c.slug !== 'autonomy-studio-lms-guide' && !c.weeks.some(w => w.week === 3)); for (const c of targets) { assert(!c.environmentProfiles.some(p => p.week === 3), `${c.title}: existing profile`); assert(!c.weekMaps.some(p => p.week === 3), `${c.title}: existing map assignments`); assert(!c.instructorCurriculumWeekOverrides.some(p => p.week === 3), `${c.title}: existing override`); } if (apply) { fs.mkdirSync('backups', { recursive: true }); const file = `backups/courses-before-remote-week-${Date.now()}.json`; fs.writeFileSync(file, JSON.stringify(before, null, 2), { mode: 0o600, flag: 'wx' }); console.log('Backup:', file); } const summary = []; try { await db.$transaction(async tx => { assert.deepEqual(await snapshot(tx), before); for (const c of targets) { const { modules, topics, practices, references, ...weekData } = data(source); const week = await tx.courseWeek.create({ data: { ...weekData, courseId: c.id } }); for (const topic of topics) await tx.courseWeekTopic.create({ data: { ...data(topic), weekId: week.id } }); for (const practice of practices) await tx.courseWeekPractice.create({ data: { ...data(practice), weekId: week.id } }); for (const reference of references) await tx.courseWeekReference.create({ data: { ...data(reference), weekId: week.id } }); const ids = new Map(); for (const module of modules) { const created = await tx.courseWeekModule.create({ data: { ...data(module), weekId: week.id } }); ids.set(module.id, created.id); } for (const profile of profiles) { const config = structuredClone(profile.config); config.moduleSet.modules = config.moduleSet.modules.map(m => { assert(ids.has(m.id), 'Cross-week module reference'); return { ...m, id: ids.get(m.id) }; }); await tx.courseEnvironmentProfile.create({ data: { ...data(profile), courseId: c.id, config } }); } for (const assignment of base.weekMaps.filter(m => m.week === 3)) { const systemMap = ['nav2-turtlebot3-world', 'nav2-turtlebot3-house'].includes(assignment.mapKey); if (!systemMap && !c.maps.some(m => m.key === assignment.mapKey)) { const map = base.maps.find(m => m.key === assignment.mapKey); assert(map, 'Missing source map'); await tx.courseMap.create({ data: { ...data(map), courseId: c.id } }); } await tx.courseWeekMap.create({ data: { ...data(assignment), courseId: c.id } }); } summary.push({ course: c.title, week: 3, weekId: week.id, modules: ids.size }); } const after = await snapshot(tx); for (const old of before) { const current = after.find(c => c.id === old.id); if (!targets.some(c => c.id === old.id)) { assert.deepEqual(current, old); continue; } assert.deepEqual(current.weeks.filter(w => w.week !== 3), old.weeks); assert.deepEqual(current.environmentProfiles.filter(p => p.week !== 3), old.environmentProfiles); assert.deepEqual(current.weekMaps.filter(m => m.week !== 3), old.weekMaps); assert.deepEqual(current.instructorCurriculumWeekOverrides, old.instructorCurriculumWeekOverrides); for (const map of old.maps) assert.deepEqual(current.maps.find(m => m.id === map.id), map); const added = current.weeks.find(w => w.week === 3); assert.equal(added.topics.length, 2); assert.equal(added.modules.length, 11); const envs = current.environmentProfiles.filter(p => p.week === 3); assert.equal(envs.length, 2); for (const p of envs) { assert.equal(p.config.moduleSet.modules.length, 9); assert(p.config.moduleSet.modules.every(m => added.modules.some(n => n.id === m.id))); } } if (!apply) throw new Error('DRY_RUN_ROLLBACK'); }, { isolationLevel: 'Serializable', timeout: 30000 }); } catch (e) { if (e.message !== 'DRY_RUN_ROLLBACK') throw e; } console.log(JSON.stringify({ applied: apply, courses: summary }, null, 2)); } main().catch(e => { console.error(e.message); process.exitCode = 1; }).finally(() => db.$disconnect());