feat: implement automation feature
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
import { AppError, ErrorCodes } from '@api/utils/errors';
|
||||
import { ContentType } from '@cio/utils/constants';
|
||||
import { db } from '@cio/db/drizzle';
|
||||
import { db, type DbOrTxClient } from '@cio/db/drizzle';
|
||||
import { sql, inArray } from 'drizzle-orm';
|
||||
import { lesson, exercise } from '@cio/db/schema';
|
||||
import { courseSection, lesson, exercise } from '@cio/db/schema';
|
||||
import { getCourseContentItems, type CourseContentItemRow } from '@cio/db/queries/course/content';
|
||||
import { deleteLesson } from '@cio/db/queries/lesson';
|
||||
import { deleteCourseSection, getCourseSectionById } from '@cio/db/queries/course';
|
||||
import { deleteCourseSection, getCourseSectionById, getCourseSectionsByCourseId } from '@cio/db/queries/course';
|
||||
import { deleteExercise } from '@cio/db/queries/exercise/exercise';
|
||||
import type { TCourseContentDelete, TCourseContentUpdate } from '@cio/utils/validation/course';
|
||||
import type { TCourseContentDelete, TCourseContentReorder, TCourseContentUpdate } from '@cio/utils/validation/course';
|
||||
|
||||
type CourseContentReorderResult = {
|
||||
courseId: string;
|
||||
updatedSections: number;
|
||||
updatedItems: number;
|
||||
updatedLessons: number;
|
||||
updatedExercises: number;
|
||||
};
|
||||
|
||||
function buildUpdatePayload(item: TCourseContentUpdate['items'][number]) {
|
||||
const payload: Record<string, unknown> = {};
|
||||
@@ -64,122 +72,175 @@ async function assertCourseContentItems(courseId: string, items: TCourseContentU
|
||||
});
|
||||
}
|
||||
|
||||
async function assertCourseSections(courseId: string, sections: NonNullable<TCourseContentReorder['sections']>) {
|
||||
const existingSections = await getCourseSectionsByCourseId(courseId);
|
||||
const sectionIds = new Set(existingSections.map((section) => section.id));
|
||||
|
||||
sections.forEach((section) => {
|
||||
if (!sectionIds.has(section.id)) {
|
||||
throw new AppError('Course section not found', ErrorCodes.COURSE_SECTION_NOT_FOUND, 404);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function applyCourseContentUpdates(tx: DbOrTxClient, items: TCourseContentUpdate['items']) {
|
||||
const updates = items
|
||||
.map((item) => ({
|
||||
item,
|
||||
payload: buildUpdatePayload(item)
|
||||
}))
|
||||
.filter(({ payload }) => Object.keys(payload).length > 0);
|
||||
|
||||
if (updates.length === 0) {
|
||||
return {
|
||||
updatedItems: 0,
|
||||
updatedLessons: 0,
|
||||
updatedExercises: 0
|
||||
};
|
||||
}
|
||||
|
||||
const updatedAt = new Date().toISOString();
|
||||
|
||||
const lessonUpdates = updates.filter(({ item }) => item.type === ContentType.Lesson);
|
||||
const exerciseUpdates = updates.filter(({ item }) => item.type === ContentType.Exercise);
|
||||
|
||||
if (lessonUpdates.length > 0) {
|
||||
const lessonIds = lessonUpdates.map(({ item }) => item.id);
|
||||
const lessonOrderUpdates = lessonUpdates
|
||||
.filter(({ payload }) => payload.order !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.order }));
|
||||
const lessonSectionUpdates = lessonUpdates
|
||||
.filter(({ payload }) => payload.sectionId !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.sectionId }));
|
||||
const lessonUnlockUpdates = lessonUpdates
|
||||
.filter(({ payload }) => payload.isUnlocked !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.isUnlocked }));
|
||||
|
||||
const lessonSet: Record<string, unknown> = {
|
||||
updatedAt
|
||||
};
|
||||
|
||||
if (lessonOrderUpdates.length > 0) {
|
||||
lessonSet.order = sql`case ${lesson.id} ${sql.join(
|
||||
lessonOrderUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${lesson.order} end`;
|
||||
}
|
||||
|
||||
if (lessonSectionUpdates.length > 0) {
|
||||
lessonSet.sectionId = sql`case ${lesson.id} ${sql.join(
|
||||
lessonSectionUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${lesson.sectionId} end`;
|
||||
}
|
||||
|
||||
if (lessonUnlockUpdates.length > 0) {
|
||||
lessonSet.isUnlocked = sql`case ${lesson.id} ${sql.join(
|
||||
lessonUnlockUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${lesson.isUnlocked} end`;
|
||||
}
|
||||
|
||||
const updatedLessons = await tx
|
||||
.update(lesson)
|
||||
.set(lessonSet)
|
||||
.where(inArray(lesson.id, lessonIds))
|
||||
.returning({ id: lesson.id });
|
||||
|
||||
if (updatedLessons.length !== lessonIds.length) {
|
||||
throw new AppError('Lesson not found', ErrorCodes.LESSON_NOT_FOUND, 404);
|
||||
}
|
||||
}
|
||||
|
||||
if (exerciseUpdates.length > 0) {
|
||||
const exerciseIds = exerciseUpdates.map(({ item }) => item.id);
|
||||
const exerciseOrderUpdates = exerciseUpdates
|
||||
.filter(({ payload }) => payload.order !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.order }));
|
||||
const exerciseSectionUpdates = exerciseUpdates
|
||||
.filter(({ payload }) => payload.sectionId !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.sectionId }));
|
||||
const exerciseUnlockUpdates = exerciseUpdates
|
||||
.filter(({ payload }) => payload.isUnlocked !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.isUnlocked }));
|
||||
|
||||
const exerciseSet: Record<string, unknown> = {
|
||||
updatedAt
|
||||
};
|
||||
|
||||
if (exerciseOrderUpdates.length > 0) {
|
||||
exerciseSet.order = sql`case ${exercise.id} ${sql.join(
|
||||
exerciseOrderUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${exercise.order} end`;
|
||||
}
|
||||
|
||||
if (exerciseSectionUpdates.length > 0) {
|
||||
exerciseSet.sectionId = sql`case ${exercise.id} ${sql.join(
|
||||
exerciseSectionUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${exercise.sectionId} end`;
|
||||
}
|
||||
|
||||
if (exerciseUnlockUpdates.length > 0) {
|
||||
exerciseSet.isUnlocked = sql`case ${exercise.id} ${sql.join(
|
||||
exerciseUnlockUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${exercise.isUnlocked} end`;
|
||||
}
|
||||
|
||||
const updatedExercises = await tx
|
||||
.update(exercise)
|
||||
.set(exerciseSet)
|
||||
.where(inArray(exercise.id, exerciseIds))
|
||||
.returning({ id: exercise.id });
|
||||
|
||||
if (updatedExercises.length !== exerciseIds.length) {
|
||||
throw new AppError('Exercise not found', ErrorCodes.EXERCISE_NOT_FOUND, 404);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
updatedItems: updates.length,
|
||||
updatedLessons: lessonUpdates.length,
|
||||
updatedExercises: exerciseUpdates.length
|
||||
};
|
||||
}
|
||||
|
||||
async function applySectionOrderUpdates(tx: DbOrTxClient, sections: NonNullable<TCourseContentReorder['sections']>) {
|
||||
if (sections.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sectionIds = sections.map((section) => section.id);
|
||||
const updatedAt = new Date().toISOString();
|
||||
const sectionSet: Record<string, unknown> = {
|
||||
updatedAt,
|
||||
order: sql`case ${courseSection.id} ${sql.join(
|
||||
sections.map((section) => sql`when ${section.id} then ${section.order}`),
|
||||
sql` `
|
||||
)} else ${courseSection.order} end`
|
||||
};
|
||||
|
||||
const updatedSections = await tx
|
||||
.update(courseSection)
|
||||
.set(sectionSet)
|
||||
.where(inArray(courseSection.id, sectionIds))
|
||||
.returning({ id: courseSection.id });
|
||||
|
||||
if (updatedSections.length !== sectionIds.length) {
|
||||
throw new AppError('Course section not found', ErrorCodes.COURSE_SECTION_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
return sections.length;
|
||||
}
|
||||
|
||||
export async function updateCourseContent(courseId: string, items: TCourseContentUpdate['items']) {
|
||||
try {
|
||||
await assertCourseContentItems(courseId, items);
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
const updates = items
|
||||
.map((item) => ({
|
||||
item,
|
||||
payload: buildUpdatePayload(item)
|
||||
}))
|
||||
.filter(({ payload }) => Object.keys(payload).length > 0);
|
||||
|
||||
if (updates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedAt = new Date().toISOString();
|
||||
|
||||
const lessonUpdates = updates.filter(({ item }) => item.type === ContentType.Lesson);
|
||||
const exerciseUpdates = updates.filter(({ item }) => item.type === ContentType.Exercise);
|
||||
|
||||
if (lessonUpdates.length > 0) {
|
||||
const lessonIds = lessonUpdates.map(({ item }) => item.id);
|
||||
const lessonOrderUpdates = lessonUpdates
|
||||
.filter(({ payload }) => payload.order !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.order }));
|
||||
const lessonSectionUpdates = lessonUpdates
|
||||
.filter(({ payload }) => payload.sectionId !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.sectionId }));
|
||||
const lessonUnlockUpdates = lessonUpdates
|
||||
.filter(({ payload }) => payload.isUnlocked !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.isUnlocked }));
|
||||
|
||||
const lessonSet: Record<string, unknown> = {
|
||||
updatedAt
|
||||
};
|
||||
|
||||
if (lessonOrderUpdates.length > 0) {
|
||||
lessonSet.order = sql`case ${lesson.id} ${sql.join(
|
||||
lessonOrderUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${lesson.order} end`;
|
||||
}
|
||||
|
||||
if (lessonSectionUpdates.length > 0) {
|
||||
lessonSet.sectionId = sql`case ${lesson.id} ${sql.join(
|
||||
lessonSectionUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${lesson.sectionId} end`;
|
||||
}
|
||||
|
||||
if (lessonUnlockUpdates.length > 0) {
|
||||
lessonSet.isUnlocked = sql`case ${lesson.id} ${sql.join(
|
||||
lessonUnlockUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${lesson.isUnlocked} end`;
|
||||
}
|
||||
|
||||
const updatedLessons = await tx
|
||||
.update(lesson)
|
||||
.set(lessonSet)
|
||||
.where(inArray(lesson.id, lessonIds))
|
||||
.returning({ id: lesson.id });
|
||||
|
||||
if (updatedLessons.length !== lessonIds.length) {
|
||||
throw new AppError('Lesson not found', ErrorCodes.LESSON_NOT_FOUND, 404);
|
||||
}
|
||||
}
|
||||
|
||||
if (exerciseUpdates.length > 0) {
|
||||
const exerciseIds = exerciseUpdates.map(({ item }) => item.id);
|
||||
const exerciseOrderUpdates = exerciseUpdates
|
||||
.filter(({ payload }) => payload.order !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.order }));
|
||||
const exerciseSectionUpdates = exerciseUpdates
|
||||
.filter(({ payload }) => payload.sectionId !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.sectionId }));
|
||||
const exerciseUnlockUpdates = exerciseUpdates
|
||||
.filter(({ payload }) => payload.isUnlocked !== undefined)
|
||||
.map(({ item, payload }) => ({ id: item.id, value: payload.isUnlocked }));
|
||||
|
||||
const exerciseSet: Record<string, unknown> = {
|
||||
updatedAt
|
||||
};
|
||||
|
||||
if (exerciseOrderUpdates.length > 0) {
|
||||
exerciseSet.order = sql`case ${exercise.id} ${sql.join(
|
||||
exerciseOrderUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${exercise.order} end`;
|
||||
}
|
||||
|
||||
if (exerciseSectionUpdates.length > 0) {
|
||||
exerciseSet.sectionId = sql`case ${exercise.id} ${sql.join(
|
||||
exerciseSectionUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${exercise.sectionId} end`;
|
||||
}
|
||||
|
||||
if (exerciseUnlockUpdates.length > 0) {
|
||||
exerciseSet.isUnlocked = sql`case ${exercise.id} ${sql.join(
|
||||
exerciseUnlockUpdates.map((update) => sql`when ${update.id} then ${update.value}`),
|
||||
sql` `
|
||||
)} else ${exercise.isUnlocked} end`;
|
||||
}
|
||||
|
||||
const updatedExercises = await tx
|
||||
.update(exercise)
|
||||
.set(exerciseSet)
|
||||
.where(inArray(exercise.id, exerciseIds))
|
||||
.returning({ id: exercise.id });
|
||||
|
||||
if (updatedExercises.length !== exerciseIds.length) {
|
||||
throw new AppError('Exercise not found', ErrorCodes.EXERCISE_NOT_FOUND, 404);
|
||||
}
|
||||
}
|
||||
await applyCourseContentUpdates(tx, items);
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
@@ -194,6 +255,48 @@ export async function updateCourseContent(courseId: string, items: TCourseConten
|
||||
}
|
||||
}
|
||||
|
||||
export async function reorderCourseContent(
|
||||
courseId: string,
|
||||
payload: TCourseContentReorder
|
||||
): Promise<CourseContentReorderResult> {
|
||||
try {
|
||||
if (payload.sections?.length) {
|
||||
await assertCourseSections(courseId, payload.sections);
|
||||
}
|
||||
|
||||
if (payload.items?.length) {
|
||||
await assertCourseContentItems(courseId, payload.items);
|
||||
}
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const updatedSections = payload.sections?.length ? await applySectionOrderUpdates(tx, payload.sections) : 0;
|
||||
const itemResult = payload.items?.length
|
||||
? await applyCourseContentUpdates(tx, payload.items)
|
||||
: {
|
||||
updatedItems: 0,
|
||||
updatedLessons: 0,
|
||||
updatedExercises: 0
|
||||
};
|
||||
|
||||
return {
|
||||
courseId,
|
||||
updatedSections,
|
||||
...itemResult
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AppError(
|
||||
error instanceof Error ? error.message : 'Failed to reorder course content',
|
||||
ErrorCodes.INTERNAL_ERROR,
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCourseContent(courseId: string, payload: TCourseContentDelete) {
|
||||
try {
|
||||
if (payload.sectionId) {
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import * as schema from '@cio/db/schema';
|
||||
|
||||
import { AppError, ErrorCodes } from '@api/utils/errors';
|
||||
import type { TCourse, TOrganization } from '@db/types';
|
||||
import type { TCourseLandingPageMetadataUpdate, TCourseLandingPageUpdate } from '@cio/utils/validation/course';
|
||||
|
||||
import { db } from '@cio/db/drizzle';
|
||||
import { env } from '@api/config/env';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { generateSlug } from '@cio/utils/functions';
|
||||
import { getCourseById } from '@cio/db/queries/course';
|
||||
import { getCourseOrganizationId } from '@cio/db/queries/tag';
|
||||
import { getDashboardBaseUrl } from '@api/config/dashboard-url';
|
||||
import { getOrganizationById } from '@cio/db/queries/organization';
|
||||
import { updateCourse } from '@api/services/course/course';
|
||||
|
||||
type CourseLandingPageUpdateResult = {
|
||||
course: TCourse;
|
||||
courseUrl: string;
|
||||
bannerImageUrl: string | null;
|
||||
};
|
||||
|
||||
async function generateUniqueCourseSlug(baseSlug: string): Promise<string> {
|
||||
let suffix = 0;
|
||||
let candidate = baseSlug;
|
||||
|
||||
while (true) {
|
||||
const existing = await db
|
||||
.select({ id: schema.course.id })
|
||||
.from(schema.course)
|
||||
.where(eq(schema.course.slug, candidate))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
suffix += 1;
|
||||
candidate = `${baseSlug}-${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureCourseSlug(courseId: string, title: string | null | undefined): Promise<string> {
|
||||
const [course] = await getCourseById(courseId);
|
||||
if (course?.slug) {
|
||||
return course.slug;
|
||||
}
|
||||
|
||||
const baseSlug = generateSlug(title, { fallback: 'course' });
|
||||
const uniqueSlug = await generateUniqueCourseSlug(baseSlug);
|
||||
await updateCourse(courseId, { slug: uniqueSlug });
|
||||
return uniqueSlug;
|
||||
}
|
||||
|
||||
function buildCourseBaseUrl(organization: TOrganization) {
|
||||
if (organization.customDomain && organization.isCustomDomainVerified) {
|
||||
return `https://${organization.customDomain}`;
|
||||
}
|
||||
|
||||
return getDashboardBaseUrl(organization.siteName ?? undefined);
|
||||
}
|
||||
|
||||
async function resolveCourseUrl(organizationId: string, courseId: string, title: string) {
|
||||
const organization = await getOrganizationById(organizationId);
|
||||
if (!organization) {
|
||||
throw new AppError('Organization not found', ErrorCodes.ORG_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
const slug = await ensureCourseSlug(courseId, title);
|
||||
return `${buildCourseBaseUrl(organization)}/course/${encodeURIComponent(slug)}`;
|
||||
}
|
||||
|
||||
async function resolveUnsplashBannerImage(courseTitle: string, query?: string) {
|
||||
if (!env.UNSPLASH_API_KEY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const searchQuery = (query?.trim() || courseTitle.trim() || 'education').slice(0, 120);
|
||||
const response = await fetch(
|
||||
`https://api.unsplash.com/search/photos?page=1&per_page=15&auto=format&fit=crop&w=2970&q=80&client_id=${env.UNSPLASH_API_KEY}&query=${encodeURIComponent(searchQuery)}`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
results?: Array<{
|
||||
urls?: {
|
||||
regular?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
const photos = data.results ?? [];
|
||||
if (photos.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const photo = photos[Math.floor(Math.random() * photos.length)];
|
||||
return photo?.urls?.regular ?? null;
|
||||
}
|
||||
|
||||
async function resolveLandingPageImage(
|
||||
courseTitle: string,
|
||||
payload: Pick<TCourseLandingPageUpdate, 'generateImage' | 'imageQuery' | 'imageUrl'>
|
||||
) {
|
||||
if (payload.imageUrl) {
|
||||
return payload.imageUrl;
|
||||
}
|
||||
|
||||
if (!payload.generateImage && !payload.imageQuery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await resolveUnsplashBannerImage(courseTitle, payload.imageQuery);
|
||||
} catch (error) {
|
||||
console.error('resolveLandingPageImage error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function mergeLandingPageMetadata(
|
||||
existing: TCourse['metadata'] | null | undefined,
|
||||
update: TCourseLandingPageMetadataUpdate | undefined
|
||||
): TCourse['metadata'] | undefined {
|
||||
if (!update) {
|
||||
return existing ?? undefined;
|
||||
}
|
||||
|
||||
const current: NonNullable<TCourse['metadata']> = {
|
||||
goals: '',
|
||||
description: '',
|
||||
requirements: '',
|
||||
allowNewStudent: true,
|
||||
...(existing ?? {})
|
||||
};
|
||||
|
||||
return {
|
||||
...current,
|
||||
...update,
|
||||
reward: update.reward
|
||||
? {
|
||||
show: update.reward.show ?? current.reward?.show ?? false,
|
||||
description: update.reward.description ?? current.reward?.description ?? ''
|
||||
}
|
||||
: current.reward,
|
||||
instructor: update.instructor
|
||||
? {
|
||||
name: update.instructor.name ?? current.instructor?.name ?? '',
|
||||
role: update.instructor.role ?? current.instructor?.role ?? '',
|
||||
coursesNo: update.instructor.coursesNo ?? current.instructor?.coursesNo ?? 0,
|
||||
description: update.instructor.description ?? current.instructor?.description ?? '',
|
||||
imgUrl: update.instructor.imgUrl ?? current.instructor?.imgUrl ?? ''
|
||||
}
|
||||
: current.instructor,
|
||||
certificate: update.certificate
|
||||
? {
|
||||
templateUrl: update.certificate.templateUrl ?? current.certificate?.templateUrl ?? ''
|
||||
}
|
||||
: current.certificate,
|
||||
sectionDisplay: update.sectionDisplay
|
||||
? { ...(current.sectionDisplay ?? {}), ...update.sectionDisplay }
|
||||
: current.sectionDisplay,
|
||||
reviews: update.reviews ?? current.reviews,
|
||||
lessonTabsOrder: update.lessonTabsOrder ?? current.lessonTabsOrder
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeReviews(reviews: NonNullable<TCourse['metadata']>['reviews'] | undefined) {
|
||||
return reviews?.map((review, index) => ({
|
||||
id: review.id ?? Date.now() + index,
|
||||
hide: review.hide ?? false,
|
||||
name: review.name,
|
||||
avatar_url: review.avatar_url,
|
||||
rating: review.rating,
|
||||
created_at: review.created_at ?? Date.now(),
|
||||
description: review.description
|
||||
}));
|
||||
}
|
||||
|
||||
export async function updateCourseLandingPageService(
|
||||
courseId: string,
|
||||
payload: TCourseLandingPageUpdate
|
||||
): Promise<CourseLandingPageUpdateResult> {
|
||||
const [existingCourse] = await getCourseById(courseId);
|
||||
if (!existingCourse) {
|
||||
throw new AppError('Course not found', ErrorCodes.COURSE_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
const nextTitle = payload.title ?? existingCourse.title;
|
||||
const metadata = mergeLandingPageMetadata(existingCourse.metadata ?? undefined, payload.metadata);
|
||||
const imageUrl = await resolveLandingPageImage(nextTitle, payload);
|
||||
|
||||
const updatedCourse = await updateCourse(courseId, {
|
||||
title: payload.title,
|
||||
description: payload.description,
|
||||
overview: payload.overview,
|
||||
cost: payload.cost,
|
||||
currency: payload.currency,
|
||||
metadata: metadata
|
||||
? {
|
||||
...metadata,
|
||||
reviews: normalizeReviews(metadata.reviews)
|
||||
}
|
||||
: undefined,
|
||||
...(imageUrl ? { logo: imageUrl, bannerImage: imageUrl } : {})
|
||||
});
|
||||
|
||||
const organizationId = await getCourseOrganizationId(courseId);
|
||||
if (!organizationId) {
|
||||
throw new AppError('Course organization not found', ErrorCodes.COURSE_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
const courseUrl = await resolveCourseUrl(organizationId, courseId, updatedCourse.title);
|
||||
|
||||
return {
|
||||
course: updatedCourse,
|
||||
courseUrl,
|
||||
bannerImageUrl: updatedCourse.logo || imageUrl || null
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user