New features: - HLS video encoding/streaming (hls routes, hls-encoder) and a media transcripts pipeline (transcripts route + media-transcript queries) - AI assistant chat vs. run model: agent-course-generation worker/queue, run-monitor pane, chat-changes card, content ask-ai bar - Per-theme course-page previews across org landing-page themes (bold, classic, corporate, editorial, etc.) with a shared course token/component set - View-as-student mode and login-link / link-invite invite flows Refactor: - Extract packages/core consolidating config (env, storage, dashboard-url), services and utils previously duplicated under apps/api/src/config - Remove legacy agent service modules (chat-tools, chat-context, context-window, document, fetch-url, lesson-content) - Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding - Add format script to tenant-router so turbo run format covers it
172 lines
4.8 KiB
TypeScript
172 lines
4.8 KiB
TypeScript
import { AppError, ErrorCodes } from '@cio/utils/errors';
|
|
import { sanitizeOptionalHtml } from '../utils/sanitize-html';
|
|
import type { TLessonLanguage, TLocale, TNewLessonLanguage } from '@cio/db/types';
|
|
import {
|
|
deleteLessonLanguage,
|
|
getLessonLanguageByLessonIdAndLocale,
|
|
getLessonLanguagesByLessonId,
|
|
updateLessonLanguage,
|
|
upsertLessonLanguage
|
|
} from '@cio/db/queries/lesson/language';
|
|
|
|
import { getLessonById } from '@cio/db/queries/lesson/lesson';
|
|
import { touchCourseUpdatedAt } from '@cio/db/queries/course';
|
|
|
|
/**
|
|
* Gets all language translations for a lesson
|
|
* @param lessonId - The lesson ID
|
|
* @returns Array of lesson languages
|
|
*/
|
|
export async function listLessonLanguages(lessonId: string): Promise<TLessonLanguage[]> {
|
|
try {
|
|
const languages = await getLessonLanguagesByLessonId(lessonId);
|
|
return languages;
|
|
} catch (error) {
|
|
throw new AppError(
|
|
error instanceof Error ? error.message : 'Failed to fetch lesson languages',
|
|
ErrorCodes.LESSON_LANGUAGE_FETCH_FAILED,
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a single lesson language by locale
|
|
* @param lessonId - The lesson ID
|
|
* @param locale - The locale
|
|
* @returns Lesson language or null if not found
|
|
*/
|
|
export async function getLessonLanguage(lessonId: string, locale: TLocale): Promise<TLessonLanguage | null> {
|
|
try {
|
|
const language = await getLessonLanguageByLessonIdAndLocale(lessonId, locale);
|
|
return language;
|
|
} catch (error) {
|
|
throw new AppError(
|
|
error instanceof Error ? error.message : 'Failed to fetch lesson language',
|
|
ErrorCodes.LESSON_LANGUAGE_FETCH_FAILED,
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates or updates a lesson language translation
|
|
* @param lessonId - The lesson ID
|
|
* @param data - Lesson language data
|
|
* @returns Created or updated lesson language
|
|
*/
|
|
export async function upsertLessonLanguageService(
|
|
lessonId: string,
|
|
data: Omit<TNewLessonLanguage, 'lessonId'>
|
|
): Promise<TLessonLanguage> {
|
|
try {
|
|
// Verify lesson exists
|
|
const lesson = await getLessonById(lessonId);
|
|
if (!lesson) {
|
|
throw new AppError('Lesson not found', ErrorCodes.LESSON_NOT_FOUND, 404);
|
|
}
|
|
|
|
const language = await upsertLessonLanguage({
|
|
...data,
|
|
content: sanitizeOptionalHtml(data.content),
|
|
lessonId
|
|
});
|
|
|
|
if (lesson.courseId) {
|
|
await touchCourseUpdatedAt(lesson.courseId);
|
|
}
|
|
|
|
return language;
|
|
} catch (error) {
|
|
if (error instanceof AppError) {
|
|
throw error;
|
|
}
|
|
throw new AppError(
|
|
error instanceof Error ? error.message : 'Failed to create or update lesson language',
|
|
ErrorCodes.LESSON_LANGUAGE_CREATE_FAILED,
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates a lesson language translation
|
|
* @param lessonId - The lesson ID
|
|
* @param locale - The locale
|
|
* @param data - Lesson language update data
|
|
* @returns Updated lesson language
|
|
*/
|
|
export async function updateLessonLanguageService(
|
|
lessonId: string,
|
|
locale: TLocale,
|
|
data: Partial<TNewLessonLanguage>
|
|
): Promise<TLessonLanguage> {
|
|
try {
|
|
// Verify lesson exists
|
|
const lesson = await getLessonById(lessonId);
|
|
if (!lesson) {
|
|
throw new AppError('Lesson not found', ErrorCodes.LESSON_NOT_FOUND, 404);
|
|
}
|
|
|
|
const existing = await getLessonLanguageByLessonIdAndLocale(lessonId, locale);
|
|
if (!existing) {
|
|
throw new AppError('Lesson language not found', ErrorCodes.LESSON_LANGUAGE_NOT_FOUND, 404);
|
|
}
|
|
|
|
const language = await updateLessonLanguage(lessonId, locale, {
|
|
...data,
|
|
content: sanitizeOptionalHtml(data.content)
|
|
});
|
|
|
|
if (lesson.courseId) {
|
|
await touchCourseUpdatedAt(lesson.courseId);
|
|
}
|
|
|
|
return language;
|
|
} catch (error) {
|
|
if (error instanceof AppError) {
|
|
throw error;
|
|
}
|
|
throw new AppError(
|
|
error instanceof Error ? error.message : 'Failed to update lesson language',
|
|
ErrorCodes.LESSON_LANGUAGE_UPDATE_FAILED,
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function deleteLessonLanguageService(lessonId: string, locale: TLocale): Promise<TLessonLanguage> {
|
|
try {
|
|
const lesson = await getLessonById(lessonId);
|
|
if (!lesson) {
|
|
throw new AppError('Lesson not found', ErrorCodes.LESSON_NOT_FOUND, 404);
|
|
}
|
|
|
|
const existing = await getLessonLanguageByLessonIdAndLocale(lessonId, locale);
|
|
if (!existing) {
|
|
throw new AppError('Lesson language not found', ErrorCodes.LESSON_LANGUAGE_NOT_FOUND, 404);
|
|
}
|
|
|
|
const deleted = await deleteLessonLanguage(lessonId, locale);
|
|
if (!deleted) {
|
|
throw new AppError('Lesson language not found', ErrorCodes.LESSON_LANGUAGE_NOT_FOUND, 404);
|
|
}
|
|
|
|
if (lesson.courseId) {
|
|
await touchCourseUpdatedAt(lesson.courseId);
|
|
}
|
|
|
|
return deleted;
|
|
} catch (error) {
|
|
if (error instanceof AppError) {
|
|
throw error;
|
|
}
|
|
|
|
throw new AppError(
|
|
error instanceof Error ? error.message : 'Failed to delete lesson language',
|
|
ErrorCodes.LESSON_LANGUAGE_DELETE_FAILED,
|
|
500
|
|
);
|
|
}
|
|
}
|