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.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user