Files
rootnode-academy/platform/api/src/programs.js
T
ced4568 895e16fea4 feat(programs): first-class program model — init_/compile_/root_ ladder, seeded root-kubernetes-foundations
- migrations/019: programs table (slug PK, matching assessment_definitions/cohorts
  convention), family (init/compile/root) and delivery_mode (self_paced/cohort/hybrid) as real
  CHECK-constrained enums, versioned like assessment_definitions so an edit to
  curriculum_path/delivery_mode never silently changes what an existing enrollment means. Seeds
  exactly one program wrapping the existing RootNode/Kubernetes curriculum and capstone — no
  new curriculum content invented.
- programs.js: list/get/create/update, with automatic version bump only when a
  snapshot-relevant field actually changes.
2026-08-25 12:07:34 -05:00

52 lines
2.6 KiB
JavaScript

// Phase 5B §2 — programs: first-class learning products (distinct from curriculum JSON, which
// stays just the content a program points at). CRUD here mirrors assessments.js's definition
// pattern (a slug primary key, a plain incrementing version, active/inactive status) rather
// than inventing a new convention.
async function listPrograms(pool, { activeOnly } = {}) {
const { rows } = await pool.query(
`SELECT p.*, count(e.enrollment_id)::int AS enrollment_count
FROM programs p LEFT JOIN enrollments e ON e.program_id = p.program_id
${activeOnly ? "WHERE p.status = 'active'" : ''}
GROUP BY p.program_id ORDER BY p.family, p.name`
);
return rows;
}
async function getProgram(pool, programId) {
const { rows } = await pool.query(`SELECT * FROM programs WHERE program_id = $1`, [programId]);
return rows[0] || null;
}
async function createProgram(pool, { programId, name, description, family, deliveryMode, curriculumPath, completionAssessmentId, estimatedDurationHours, createdBy }) {
const { rows } = await pool.query(
`INSERT INTO programs (program_id, name, description, family, delivery_mode, curriculum_path, completion_assessment_id, estimated_duration_hours, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`,
[programId, name, description || null, family, deliveryMode || 'self_paced', curriculumPath || 'rootnode-path', completionAssessmentId || null, estimatedDurationHours || null, createdBy || null]
);
return rows[0];
}
// Bumps version whenever a field an enrollment snapshots (curriculum_path, delivery_mode)
// actually changes — matches assessment_definitions' "edit later never changes what an
// already-drawn session is graded against" principle. Editing only cosmetic fields (name,
// description, estimated_duration_hours) does not bump the version.
async function updateProgram(pool, programId, fields) {
const current = await getProgram(pool, programId);
if (!current) return null;
const snapshotChanged = (fields.curriculum_path && fields.curriculum_path !== current.curriculum_path)
|| (fields.delivery_mode && fields.delivery_mode !== current.delivery_mode);
const map = { ...fields };
if (snapshotChanged) map.version = current.version + 1;
const cols = Object.keys(map);
if (!cols.length) return current;
const setClause = cols.map((c, i) => `${c} = $${i + 2}`).join(', ');
const { rows } = await pool.query(
`UPDATE programs SET ${setClause}, updated_at = now() WHERE program_id = $1 RETURNING *`,
[programId, ...cols.map((c) => map[c])]
);
return rows[0];
}
module.exports = { listPrograms, getProgram, createProgram, updateProgram };