Files
lms-be/scripts/copy-remote-development-week.cjs

108 lines
5.7 KiB
JavaScript

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());