@memberjunction/actions-bizapps-lms
Version:
LMS system integration actions for MemberJunction
270 lines • 11.9 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { RegisterClass } from '@memberjunction/global';
import { LearnWorldsBaseAction } from '../learnworlds-base.action.js';
import { BaseAction } from '@memberjunction/actions';
/**
* Action to retrieve all course enrollments for a specific user
*/
let GetUserEnrollmentsAction = class GetUserEnrollmentsAction extends LearnWorldsBaseAction {
/**
* Metadata about this action
*/
get Description() {
return 'Retrieves all course enrollments for a specific LearnWorlds user with detailed progress information';
}
/**
* Typed public method for direct (non-framework) callers.
* Sets company context, fetches enrollments, and returns a strongly-typed result.
* Throws on error.
*/
async GetUserEnrollments(params, contextUser) {
this.SetCompanyContext(params.CompanyID);
const { UserID: userId, Status: status, IncludeExpired: includeExpired = false, IncludeCourseDetails: includeCourseDetails = true, SortBy: sortBy = 'enrolled_at', SortOrder: sortOrder = 'desc', MaxResults: maxResults = 100, } = params;
if (!userId) {
throw new Error('UserID is required');
}
this.validatePathSegment(userId, 'UserID');
const queryParams = this.buildEnrollmentQueryParams(status, includeExpired, sortBy, sortOrder, maxResults);
const enrollmentsData = await this.makeLearnWorldsPaginatedRequest(`users/${userId}/courses`, queryParams, contextUser, maxResults);
const enrollments = enrollmentsData || [];
const formattedEnrollments = await this.formatEnrollments(enrollments, includeCourseDetails, contextUser);
const summary = this.calculateEnrollmentSummary(userId, formattedEnrollments);
return {
Enrollments: formattedEnrollments,
TotalCount: formattedEnrollments.length,
Summary: summary,
};
}
/**
* Framework entry-point – thin wrapper around the typed public method.
*/
async InternalRunAction(params) {
const { Params, ContextUser } = params;
this.params = Params;
try {
const typedParams = this.extractGetUserEnrollmentsParams(Params);
const result = await this.GetUserEnrollments(typedParams, ContextUser);
this.setOutputParam(Params, 'Enrollments', result.Enrollments);
this.setOutputParam(Params, 'TotalCount', result.TotalCount);
this.setOutputParam(Params, 'Summary', result.Summary);
return this.buildSuccessResult(`Retrieved ${result.TotalCount} enrollments for user`, Params);
}
catch (error) {
const msg = error instanceof Error ? error.message : 'Unknown error occurred';
return this.buildErrorResult('ERROR', `Error retrieving enrollments: ${msg}`, Params);
}
}
/**
* Extract typed params from framework ActionParam[]
*/
extractGetUserEnrollmentsParams(params) {
return {
CompanyID: this.getRequiredStringParam(params, 'CompanyID'),
UserID: this.getRequiredStringParam(params, 'UserID'),
Status: this.getOptionalStringParam(params, 'Status'),
IncludeExpired: this.getOptionalBooleanParam(params, 'IncludeExpired', false),
IncludeCourseDetails: this.getOptionalBooleanParam(params, 'IncludeCourseDetails', true),
SortBy: this.getOptionalStringParam(params, 'SortBy') || 'enrolled_at',
SortOrder: (this.getOptionalStringParam(params, 'SortOrder') || 'desc'),
MaxResults: this.getOptionalNumberParam(params, 'MaxResults', LearnWorldsBaseAction.LW_MAX_PAGE_SIZE),
};
}
/**
* Build query parameters for the enrollments API request
*/
buildEnrollmentQueryParams(status, includeExpired, sortBy, sortOrder, maxResults) {
const queryParams = {
limit: Math.min(maxResults, LearnWorldsBaseAction.LW_MAX_PAGE_SIZE),
sort: sortBy,
order: sortOrder,
};
if (status) {
queryParams.status = status;
}
if (!includeExpired) {
queryParams.hide_expired = true;
}
return queryParams;
}
/**
* Format all raw enrollments into typed FormattedEnrollment[].
* When course details are requested, fetches all unique courses in parallel
* and attaches results from a lookup map.
*/
async formatEnrollments(enrollments, includeCourseDetails, contextUser) {
// Build a course details map in one batch to avoid per-item API calls
const courseDetailsMap = includeCourseDetails
? await this.fetchCourseDetailsBatch(enrollments, contextUser)
: new Map();
return enrollments.map((enrollment) => {
const formatted = this.formatSingleEnrollment(enrollment);
if (includeCourseDetails && enrollment.course_id) {
formatted.course = courseDetailsMap.get(enrollment.course_id);
}
return formatted;
});
}
/**
* Fetch course details for all unique course IDs with controlled concurrency and return a lookup map.
*/
async fetchCourseDetailsBatch(enrollments, contextUser) {
const uniqueCourseIds = [...new Set(enrollments.map((e) => e.course_id).filter((id) => !!id))];
const results = await this.processInBatches(uniqueCourseIds, (courseId) => {
this.validatePathSegment(courseId, 'CourseID');
return this.fetchCourseDetailsForEnrollment(courseId, contextUser);
});
const map = new Map();
uniqueCourseIds.forEach((courseId, index) => {
map.set(courseId, results[index]);
});
return map;
}
/**
* Format a single raw enrollment to a FormattedEnrollment
*/
formatSingleEnrollment(enrollment) {
return {
id: enrollment.id || '',
courseId: enrollment.course_id || '',
enrolledAt: enrollment.enrolled_at || enrollment.created_at,
startsAt: enrollment.starts_at,
expiresAt: enrollment.expires_at,
status: enrollment.status || 'active',
progress: {
percentage: enrollment.progress_percentage || 0,
completedUnits: enrollment.completed_units || 0,
totalUnits: enrollment.total_units || 0,
completedLessons: enrollment.completed_lessons || 0,
totalLessons: enrollment.total_lessons || 0,
lastAccessedAt: enrollment.last_accessed_at,
totalTimeSpent: enrollment.total_time_spent || 0,
totalTimeSpentText: this.formatDuration(enrollment.total_time_spent || 0),
},
grade: enrollment.grade,
certificateEligible: enrollment.certificate_eligible || false,
certificateIssuedAt: enrollment.certificate_issued_at,
completedAt: enrollment.completed_at,
};
}
/**
* Fetch course details for a given enrollment's course ID
*/
async fetchCourseDetailsForEnrollment(courseId, contextUser) {
try {
const course = await this.makeLearnWorldsRequest(`courses/${courseId}`, 'GET', undefined, contextUser);
return {
id: course.id || courseId,
title: course.title || '',
description: course.short_description || course.description,
imageUrl: course.image_url,
level: course.level,
duration: course.duration,
durationText: this.formatDuration(course.duration || 0),
instructorName: course.instructor_name,
certificateEnabled: course.certificate_enabled || false,
};
}
catch (error) {
console.warn(`Failed to get course details for ${courseId}:`, error);
return undefined;
}
}
/**
* Calculate summary statistics from formatted enrollments
*/
calculateEnrollmentSummary(userId, formattedEnrollments) {
const totalEnrollments = formattedEnrollments.length;
const activeEnrollments = formattedEnrollments.filter((e) => e.status === 'active').length;
const completedEnrollments = formattedEnrollments.filter((e) => e.progress.percentage >= 100 || e.completedAt).length;
const expiredEnrollments = formattedEnrollments.filter((e) => e.status === 'expired').length;
const averageProgress = totalEnrollments > 0 ? formattedEnrollments.reduce((sum, e) => sum + e.progress.percentage, 0) / totalEnrollments : 0;
const totalTimeSpent = formattedEnrollments.reduce((sum, e) => sum + (e.progress.totalTimeSpent || 0), 0);
const certificatesEarned = formattedEnrollments.filter((e) => e.certificateIssuedAt).length;
return {
userId,
totalEnrollments,
activeEnrollments,
completedEnrollments,
expiredEnrollments,
inProgressEnrollments: Math.max(0, totalEnrollments - completedEnrollments - expiredEnrollments),
averageProgressPercentage: Math.round(averageProgress * 100) / 100,
totalTimeSpent,
totalTimeSpentText: this.formatDuration(totalTimeSpent),
certificatesEarned,
enrollmentsByStatus: {
active: activeEnrollments,
completed: completedEnrollments,
expired: expiredEnrollments,
},
};
}
/**
* Define the parameters this action expects
*/
get Params() {
const baseParams = this.getCommonLMSParams();
const specificParams = [
{
Name: 'UserID',
Type: 'Input',
Value: null,
},
{
Name: 'Status',
Type: 'Input',
Value: null,
},
{
Name: 'IncludeExpired',
Type: 'Input',
Value: false,
},
{
Name: 'IncludeCourseDetails',
Type: 'Input',
Value: true,
},
{
Name: 'SortBy',
Type: 'Input',
Value: 'enrolled_at',
},
{
Name: 'SortOrder',
Type: 'Input',
Value: 'desc',
},
{
Name: 'MaxResults',
Type: 'Input',
Value: 100,
},
{
Name: 'Enrollments',
Type: 'Output',
Value: null,
},
{
Name: 'TotalCount',
Type: 'Output',
Value: null,
},
{
Name: 'Summary',
Type: 'Output',
Value: null,
},
];
return [...baseParams, ...specificParams];
}
};
GetUserEnrollmentsAction = __decorate([
RegisterClass(BaseAction, 'GetUserEnrollmentsAction')
], GetUserEnrollmentsAction);
export { GetUserEnrollmentsAction };
//# sourceMappingURL=get-user-enrollments.action.js.map