UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

197 lines (171 loc) 6.95 kB
import { ActionParam } from '@memberjunction/actions-base'; import { BaseAction } from '@memberjunction/actions'; import { RegisterClass } from '@memberjunction/global'; import { UserInfo } from '@memberjunction/core'; import { CompanyIntegrationEntity } from '@memberjunction/core-entities'; import { Metadata, RunView } from '@memberjunction/core'; /** * Base class for all LMS-related actions. * Provides common functionality and patterns for interacting with Learning Management Systems. */ @RegisterClass(BaseAction, 'BaseLMSAction') export abstract class BaseLMSAction extends BaseAction { /** * The LMS provider this action is designed for (e.g., 'LearnWorlds', 'Moodle', etc.) */ protected abstract lmsProvider: string; /** * The integration name to look up in the Integration entity */ protected abstract integrationName: string; /** * Cached company integration for the current execution */ private _companyIntegration: CompanyIntegrationEntity | null = null; /** * Common LMS parameters that many actions will need */ protected getCommonLMSParams(): ActionParam[] { return [ { Name: 'CompanyID', Type: 'Input', Value: null } ]; } /** * Gets the company integration record for the specified company and LMS */ protected async getCompanyIntegration(companyId: string, contextUser: UserInfo): Promise<CompanyIntegrationEntity> { // Check cache first if (this._companyIntegration && this._companyIntegration.CompanyID === companyId) { return this._companyIntegration; } const rv = new RunView(); const result = await rv.RunView<CompanyIntegrationEntity>({ EntityName: 'Company Integrations', ExtraFilter: `CompanyID = '${companyId}' AND Integration.Name = '${this.integrationName}'`, ResultType: 'entity_object' }, contextUser); if (!result.Success) { throw new Error(`Failed to retrieve company integration: ${result.ErrorMessage}`); } if (!result.Results || result.Results.length === 0) { throw new Error(`No ${this.integrationName} integration found for company ${companyId}. Please configure the integration first.`); } this._companyIntegration = result.Results[0]; return this._companyIntegration; } /** * Gets credentials from environment variables * Format: BIZAPPS_{PROVIDER}_{COMPANY_ID}_{CREDENTIAL_TYPE} * Example: BIZAPPS_LEARNWORLDS_12345_API_KEY */ protected getCredentialFromEnv(companyId: string, credentialType: string): string | undefined { const envKey = `BIZAPPS_${this.lmsProvider.toUpperCase().replace(/\s+/g, '_')}_${companyId}_${credentialType.toUpperCase()}`; return process.env[envKey]; } /** * Gets API credentials - first tries environment variables, then falls back to database */ protected async getAPICredentials(integration: CompanyIntegrationEntity): Promise<{ apiKey?: string; apiSecret?: string; accessToken?: string }> { const companyId = integration.CompanyID; // Try environment variables first const envApiKey = this.getCredentialFromEnv(companyId, 'API_KEY'); const envApiSecret = this.getCredentialFromEnv(companyId, 'API_SECRET'); const envAccessToken = this.getCredentialFromEnv(companyId, 'ACCESS_TOKEN'); if (envApiKey || envAccessToken) { return { apiKey: envApiKey, apiSecret: envApiSecret, accessToken: envAccessToken }; } // Fall back to database (for backwards compatibility) if (!integration.APIKey && !integration.AccessToken) { throw new Error(`No API credentials found for ${this.integrationName} integration. Please set environment variables or configure in database.`); } return { apiKey: integration.APIKey || undefined, accessToken: integration.AccessToken || undefined }; } /** * Gets the base URL for API calls */ protected async getAPIBaseURL(integration: CompanyIntegrationEntity): Promise<string> { // Check if custom URL is stored in the integration if (integration.CustomAttribute1) { return integration.CustomAttribute1; } // Return empty string - derived classes should override this return ''; } /** * Helper to get parameter value with type safety */ protected getParamValue(params: ActionParam[], paramName: string): any { const param = params.find(p => p.Name === paramName); return param?.Value; } /** * Standard date format for LMS systems (ISO 8601) */ protected formatLMSDate(date: Date): string { return date.toISOString(); } /** * Parse date from LMS format */ protected parseLMSDate(dateString: string): Date { return new Date(dateString); } /** * Calculate progress percentage */ protected calculateProgressPercentage(completed: number, total: number): number { if (total === 0) return 0; return Math.round((completed / total) * 100); } /** * Format duration in seconds to human readable format */ protected formatDuration(seconds: number): string { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; if (hours > 0) { return `${hours}h ${minutes}m ${secs}s`; } else if (minutes > 0) { return `${minutes}m ${secs}s`; } else { return `${secs}s`; } } /** * Helper to build consistent error messages for LMS operations */ protected buildLMSErrorMessage(operation: string, details: string, systemError?: any): string { let message = `LMS operation failed: ${operation}. ${details}`; if (systemError) { message += ` System error: ${systemError.message || systemError}`; } return message; } /** * Common enrollment status mapping */ protected mapEnrollmentStatus(status: string): 'active' | 'completed' | 'expired' | 'suspended' | 'unknown' { const statusMap: Record<string, 'active' | 'completed' | 'expired' | 'suspended' | 'unknown'> = { 'active': 'active', 'completed': 'completed', 'finished': 'completed', 'expired': 'expired', 'suspended': 'suspended', 'paused': 'suspended', 'inactive': 'suspended' }; return statusMap[status.toLowerCase()] || 'unknown'; } }