← All @molecule/* packages · App templates
@molecule/api-resource-courseAPI resource · resource-course · API (Node) · v1.0.1 · Apache-2.0
Course / module / module-item / enrollment resource with access helpers.
npm install @molecule/api-resource-course@molecule/api-resource-course is an API resource: the routes, validation and storage for resource-course, built on the database and auth cores so it runs on whichever providers your app has bonded.
import {
createCourse,
createEnrollment,
assertCourseStaff,
assertEnrolled,
} from '@molecule/api-resource-course'
const course = await createCourse({
org_id: orgId,
title: 'Algebra 101',
created_by: instructorId,
})
await createEnrollment({
user_id: studentId,
course_id: course.id,
role: 'student',
})
// In a handler that mutates course content:
await assertCourseStaff(req.userId, course.id)Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Course resource for molecule.dev.
Provides the course / module / module-item / enrollment data hierarchy plus
assertCourseStaff and assertEnrolled access helpers — the same checks
that previously lived inline as _lmsAccess.ts in the LMS flagship.
import {
createCourse,
createEnrollment,
assertCourseStaff,
assertEnrolled,
} from '@molecule/api-resource-course'
const course = await createCourse({
org_id: orgId,
title: 'Algebra 101',
created_by: instructorId,
})
await createEnrollment({
user_id: studentId,
course_id: course.id,
role: 'student',
})
// In a handler that mutates course content:
await assertCourseStaff(req.userId, course.id)
resource
npm install @molecule/api-resource-course @molecule/api-database @molecule/api-i18n @molecule/api-locales-resource-course @molecule/api-resource
CourseA course — the top-level container in the hierarchy.
interface Course {
/** Unique identifier (UUID). */
id: string
/** Owning organisation when multi-tenancy is enabled, otherwise the platform org id. */
org_id: string
/** Human-readable course title. */
title: string
/** Optional rich-text description of the course. */
description: string | null
/** Optional URL-friendly slug, unique within an org. */
slug: string | null
/** Course lifecycle: `draft` is invisible to students. */
status: 'draft' | 'published' | 'archived'
/** ID of the user who created the course (typically the lead instructor). */
created_by: string
/** ISO-8601 timestamp the course was created. */
created_at: string
/** ISO-8601 timestamp the course was last updated. */
updated_at: string
}
EnrollmentAn enrollment — a user's participation in a course in a given role.
interface Enrollment {
/** Unique identifier (UUID). */
id: string
/** Enrolled user id. */
user_id: string
/** Course the user is enrolled in. */
course_id: string
/** Role the user holds in this course. */
role: EnrollmentRole
/** Enrollment lifecycle status. */
status: EnrollmentStatus
/** ISO-8601 timestamp the user was enrolled. */
enrolled_at: string
/** ISO-8601 timestamp the enrollment was last updated. */
updated_at: string
}
ModuleA module — an ordered grouping of items within a course.
interface Module {
/** Unique identifier (UUID). */
id: string
/** Owning course id. */
course_id: string
/** Human-readable module title. */
title: string
/** Sort order within the course; lower comes first. */
sort_order: number
/** ISO-8601 timestamp the module was created. */
created_at: string
/** ISO-8601 timestamp the module was last updated. */
updated_at: string
}
ModuleItemA module item — an individual content block (video, reading, quiz, assignment).
The payload shape varies by kind and is stored as opaque JSON; consumers
are expected to validate it according to their domain (a quiz package, a
video package, etc.).
interface ModuleItem {
/** Unique identifier (UUID). */
id: string
/** Owning module id. */
module_id: string
/** Kind of content block. */
kind: ModuleItemKind
/** Opaque payload — shape determined by `kind`. */
payload: unknown
/** Sort order within the module; lower comes first. */
sort_order: number
/** ISO-8601 timestamp the item was created. */
created_at: string
/** ISO-8601 timestamp the item was last updated. */
updated_at: string
}
CreateCourseInputInput shape for {@link createCourse}.
type CreateCourseInput = Pick<Course, 'org_id' | 'title' | 'created_by'> &
Partial<Pick<Course, 'description' | 'slug' | 'status'>>
CreateEnrollmentInputInput shape for {@link createEnrollment}.
type CreateEnrollmentInput = Pick<Enrollment, 'user_id' | 'course_id' | 'role'> &
Partial<Pick<Enrollment, 'status'>>
CreateModuleInputInput shape for {@link createModule}.
type CreateModuleInput = Pick<Module, 'course_id' | 'title' | 'sort_order'>
CreateModuleItemInputInput shape for {@link createModuleItem}.
type CreateModuleItemInput = Pick<ModuleItem, 'module_id' | 'kind' | 'payload' | 'sort_order'>
EnrollmentRoleRoles a user may hold within a single course enrollment.
instructor and ta are course staff and may modify course content.
student and observer are read-only consumers.
type EnrollmentRole = 'student' | 'instructor' | 'ta' | 'observer'
EnrollmentStatusLifecycle states for a course enrollment.
type EnrollmentStatus = 'active' | 'invited' | 'completed' | 'dropped'
ModuleItemKindSupported kinds for content blocks within a module.
Concrete payload shapes are stored as opaque JSON keyed by the kind so the package stays decoupled from any particular media implementation.
type ModuleItemKind = 'video' | 'reading' | 'quiz' | 'assignment'
UpdateCourseInputInput shape for course updates.
type UpdateCourseInput = Partial<Pick<Course, 'title' | 'description' | 'slug' | 'status'>>
UpdateEnrollmentInputInput shape for enrollment updates.
type UpdateEnrollmentInput = Partial<Pick<Enrollment, 'role' | 'status'>>
UpdateModuleInputInput shape for module updates.
type UpdateModuleInput = Partial<Pick<Module, 'title' | 'sort_order'>>
UpdateModuleItemInputInput shape for module-item updates.
type UpdateModuleItemInput = Partial<Pick<ModuleItem, 'kind' | 'payload' | 'sort_order'>>
CourseNotFoundErrorError thrown when a course referenced by id does not exist.
Carries the requested course id and the i18n key callers should display (defaults are pre-populated for the framework-default locale).
NotCourseStaffErrorError thrown when a user is not course staff (instructor or active TA).
NotEnrolledErrorError thrown when a user is not actively enrolled in a course.
assertCourseStaff(userId, courseId)Asserts the user is course staff. Throws when the course is missing or the user does not hold a staff role.
function assertCourseStaff(userId: string, courseId: string): Promise<void>
userId — The user id.courseId — The course id.assertEnrolled(userId, courseId)Asserts the user is actively enrolled in the course (any role). Throws when the course is missing or the user has no active enrollment.
function assertEnrolled(userId: string, courseId: string): Promise<void>
userId — The user id.courseId — The course id.createCourse(input)Creates a new course.
function createCourse(input: CreateCourseInput): Promise<Course>
input — Required course fields plus optional description / slug / status.Returns: The persisted course record.
createEnrollment(input)Creates an enrollment. Idempotent — when a row already exists for the
(user_id, course_id) pair the existing record is returned unchanged.
function createEnrollment(input: CreateEnrollmentInput): Promise<Enrollment>
input — Enrollment fields.Returns: The new or existing enrollment.
createModule(input)Creates a module within a course.
function createModule(input: CreateModuleInput): Promise<Module>
input — Required module fields.Returns: The persisted module.
createModuleItem(input)Creates a module item.
function createModuleItem(input: CreateModuleItemInput): Promise<ModuleItem>
input — Required module-item fields.Returns: The persisted module item.
deleteCourse(courseId)Deletes a course by id.
function deleteCourse(courseId: string): Promise<boolean>
courseId — The course id.Returns: true if a row was deleted.
deleteEnrollment(enrollmentId)Deletes an enrollment by id.
function deleteEnrollment(enrollmentId: string): Promise<boolean>
enrollmentId — The enrollment id.Returns: true if a row was deleted.
deleteModule(moduleId)Deletes a module.
function deleteModule(moduleId: string): Promise<boolean>
moduleId — The module id.Returns: true if a row was deleted.
deleteModuleItem(itemId)Deletes a module item.
function deleteModuleItem(itemId: string): Promise<boolean>
itemId — The item id.Returns: true if a row was deleted.
getCourse(courseId)Reads a course by id.
function getCourse(courseId: string): Promise<Course | null>
courseId — The course id.Returns: The course, or null when no such course exists.
getEnrollment(userId, courseId)Reads a user's enrollment in a course (if any).
function getEnrollment(userId: string, courseId: string): Promise<Enrollment | null>
userId — The user id.courseId — The course id.Returns: The enrollment, or null when the user is not enrolled.
getModule(moduleId)Reads a module by id.
function getModule(moduleId: string): Promise<Module | null>
moduleId — The module id.Returns: The module, or null when missing.
getModuleItem(itemId)Reads a module item by id.
function getModuleItem(itemId: string): Promise<ModuleItem | null>
itemId — The item id.Returns: The module item, or null when missing.
isCourseStaff(userId, courseId)True when the user is the course's lead instructor or an active staff enrollment (instructor or TA).
function isCourseStaff(userId: string, courseId: string): Promise<boolean>
userId — The user id.courseId — The course id.Returns: Whether the user is course staff.
isEnrolled(userId, courseId)True when the user has any active enrollment in the course (any role).
function isEnrolled(userId: string, courseId: string): Promise<boolean>
userId — The user id.courseId — The course id.Returns: Whether the user is enrolled.
listCourses(orgId, options)Lists courses for an org, ordered most-recent first.
function listCourses(
orgId: string,
options?: { limit?: number; offset?: number; status?: Course['status'] },
): Promise<{ data: Course[]; total: number; limit: number; offset: number }>
orgId — The owning org id.options — Optional pagination and status filter.options.limit — Maximum number of results (default 20).options.offset — Number of results to skip (default 0).options.status — Restrict to courses with this lifecycle status.Returns: Paginated courses.
listEnrollments(courseId, options)Lists enrollments in a course, optionally filtered by role.
function listEnrollments(
courseId: string,
options?: { role?: Enrollment['role']; status?: Enrollment['status'] },
): Promise<Enrollment[]>
courseId — The course id.options — Optional role filter.options.role — Restrict to enrollments with this role.options.status — Restrict to enrollments with this lifecycle status.Returns: The enrollments.
listModuleItems(moduleId)Lists module items for a module in sort_order ascending.
function listModuleItems(moduleId: string): Promise<ModuleItem[]>
moduleId — The owning module id.Returns: The module items.
listModules(courseId)Lists modules for a course in sort_order ascending.
function listModules(courseId: string): Promise<Module[]>
courseId — The owning course id.Returns: The modules.
requireCourse(courseId)Reads a course by id, throwing {@link CourseNotFoundError} when missing.
function requireCourse(courseId: string): Promise<Course>
courseId — The course id.Returns: The course.
updateCourse(courseId, patch)Updates a course.
function updateCourse(
courseId: string,
patch: Partial<Pick<Course, 'title' | 'description' | 'slug' | 'status'>>,
): Promise<Course | null>
courseId — The course id.patch — Partial fields to update.Returns: The updated course, or null when not found.
updateEnrollment(enrollmentId, patch)Updates an enrollment.
function updateEnrollment(
enrollmentId: string,
patch: Partial<Pick<Enrollment, 'status' | 'role'>>,
): Promise<Enrollment | null>
enrollmentId — The enrollment id.patch — Partial fields to update.Returns: The updated enrollment, or null when not found.
updateModule(moduleId, patch)Updates a module.
function updateModule(
moduleId: string,
patch: Partial<Pick<Module, 'title' | 'sort_order'>>,
): Promise<Module | null>
moduleId — The module id.patch — Partial fields to update.Returns: The updated module, or null when not found.
updateModuleItem(itemId, patch)Updates a module item.
function updateModuleItem(
itemId: string,
patch: Partial<Pick<ModuleItem, 'sort_order' | 'kind' | 'payload'>>,
): Promise<ModuleItem | null>
itemId — The item id.patch — Partial fields to update.Returns: The updated module item, or null when not found.
COURSE_ENROLLMENTS_TABLEDatabase table for {@link Enrollment} records.
const COURSE_ENROLLMENTS_TABLE: 'course_enrollments'
COURSE_MODULE_ITEMS_TABLEDatabase table for {@link ModuleItem} records.
const COURSE_MODULE_ITEMS_TABLE: 'course_module_items'
COURSE_MODULES_TABLEDatabase table for {@link Module} records.
const COURSE_MODULES_TABLE: 'course_modules'
COURSES_TABLEDatabase table for {@link Course} records.
const COURSES_TABLE: 'courses'
i18nRegisteredSet to true once the locale module has been registered.
const i18nRegistered: true
Peer dependencies:
@molecule/api-database ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-locales-resource-course ^1.0.1@molecule/api-resource ^1.0.1@molecule/api-database
@molecule/api-i18n
@molecule/api-locales-resource-course
@molecule/api-resource
Migration required. src/__setup__/courses.sql ships with this package
(tables courses, course_modules, course_module_items,
course_enrollments) and must exist in the target database before use
(scaffolded apps apply it automatically; existing apps must apply it first).
Service-only package — no routes or handlers ship. You own the HTTP
surface: authenticate every endpoint and enforce access SERVER-SIDE with the
helpers — assertCourseStaff(userId, courseId) before any content mutation,
assertEnrolled(userId, courseId) before member-only reads. Client-side
gating alone is not access control.
The assert helpers THROW typed errors (CourseNotFoundError,
NotCourseStaffError, NotEnrolledError) — map them to 404/403 in your
handlers instead of letting them bubble as 500s.
Enrollments are the whole ACL. Staff = the course's created_by OR an
ACTIVE enrollment with role instructor/ta; assertEnrolled requires
status: 'active' (any role). Create enrollments with the right
role/status — there is no separate permissions table.
Persistence goes through the abstract @molecule/api-database DataStore —
wire any database bond at startup before calling these functions.
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual course/enrollment screens, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. Ground every check in the REAL model (course status draft/published/archived; enrollment role student/instructor/ta/observer and status active/invited/completed/dropped; content = modules -> module items of kind video/reading/quiz/assignment) — never a field the interface lacks:
draft
is hidden from the student catalog and cannot be enrolled in (enrolling in
a draft is rejected), while publishing it (status -> published) makes it
appear and become enrollable. An archived course likewise drops out of the
catalog.student, status active
row keyed to that (user, course); re-enrolling is idempotent (no duplicate
enrollment), and the student can now read the modules/items they could not
see before.completed (issuing a certificate if
the app models one). Progress only moves by actually completing items — a
student cannot self-mark completed without doing the lessons.dropped/invited
enrollment is not active and is treated as un-enrolled.created_by) or an
active instructor/ta may edit content, publish/unpublish, delete the
course, or view the enrollment roster: a student or observer attempting any
of these is denied 403 (assertCourseStaff -> NotCourseStaffError), and a
missing course id returns 404 (CourseNotFoundError), not a 500.Translation strings are provided by @molecule/api-locales-resource-course.