apps/recallassess/recallassess-api/src/api/admin/course-module/course-module.service.ts
BNestBaseModuleService
Methods |
|
constructor(prisma: BNestPrismaService, systemLogService: SystemLogService)
|
|||||||||
|
Parameters :
|
| Async add | ||||||
add(data: any)
|
||||||
|
Override add method
Parameters :
Returns :
unknown
|
| Async delete | ||||||
delete(id: number)
|
||||||
|
Override delete method
Parameters :
Returns :
Promise<void>
|
| Private Async getCourseEnrollmentCountForModule | ||||||
getCourseEnrollmentCountForModule(courseModuleId: number)
|
||||||
|
Get course enrollment count from LearningGroupParticipant for the course that contains this module. Resolves course_id via CourseModulePage, then counts learningGroupParticipants (same as course enrollment_count logic).
Parameters :
Returns :
Promise<number>
|
| Async getDetail | ||||||
getDetail(id: number)
|
||||||
|
Override getDetail to add course_enrollment_count (enrollments in the course that contains this module).
Parameters :
Returns :
Promise<DetailResponseDataInterface<any>>
|
| Async getModulesByCourse |
getModulesByCourse(courseId: number, excludeFromBat: unknown)
|
|
Course modules relevant to a course: linked via CourseModulePage, plus any module already used on AssessmentQuestion for this course (covers courses without pages seeded yet).
Returns :
Promise<any[]>
|
| Async save |
save(id: number, data: any)
|
|
Override save method
Returns :
Promise<SaveResponseDataInterface<any>>
|
import { SystemLogService } from "@api/shared/services";
import { buildListResponseData } from "@bish-nest/core";
import { BNestBaseModuleService } from "@bish-nest/core/data/module-service/base-module.service";
import { PaginationOptions } from "@bish-nest/core/data/pagination/pagination-options.interface";
import { DetailResponseDataInterface } from "@bish-nest/core/interfaces/detail-response-data.interface";
import { ListResponseDataInterface } from "@bish-nest/core/interfaces/list-response-data.interface";
import { SaveResponseDataInterface } from "@bish-nest/core/interfaces/save-response-data.interface";
import { BNestPrismaService } from "@bish-nest/core/services";
import { Injectable, NotFoundException } from "@nestjs/common";
import { SystemLogEntityType } from "@prisma/client";
import { plainToInstance } from "class-transformer";
import { cloneDeep } from "lodash";
@Injectable()
export class CourseModuleService extends BNestBaseModuleService {
constructor(
protected prisma: BNestPrismaService,
private readonly systemLogService: SystemLogService,
) {
super();
}
/**
* Override getDetail to add course_enrollment_count (enrollments in the course that contains this module).
*/
async getDetail(id: number): Promise<DetailResponseDataInterface<any>> {
const detailData = await this.moduleMethods.getDetailData(id);
const courseEnrollmentCount = await this.getCourseEnrollmentCountForModule(id);
const detailWithCount = {
...detailData,
course_enrollment_count: courseEnrollmentCount,
course_has_enrollments: courseEnrollmentCount > 0,
};
const moduleCurrentCfg = this.gVars.moduleCurrentCfg;
const transformedData = plainToInstance(moduleCurrentCfg.detailDto, detailWithCount, {
excludeExtraneousValues: true,
});
return this.moduleMethods.getReturnDataForDetail(transformedData);
}
/**
* Get course enrollment count from LearningGroupParticipant for the course that contains this module.
* Resolves course_id via CourseModulePage, then counts learningGroupParticipants (same as course enrollment_count logic).
*/
private async getCourseEnrollmentCountForModule(courseModuleId: number): Promise<number> {
const page = await this.prisma.client.courseModulePage.findFirst({
where: { course_module_id: courseModuleId },
select: { course_id: true },
});
const courseId = page?.course_id;
if (courseId == null) return 0;
return this.prisma.client.learningGroupParticipant.count({
where: { course_id: courseId },
});
}
/**
* Override add method
*/
async add(data: any) {
const repo = this.commonMethods.getRepo(this.gVars.moduleCurrentCfg.repoName);
let dataAdd = plainToInstance(this.gVars.moduleCurrentCfg.addDto, data);
// Use scalar audit columns to avoid relation create errors
dataAdd = await this.dbUtilService.addCreatedById(dataAdd);
const newRow = await repo.create({ data: dataAdd });
const addResponse = this.moduleMethods.getReturnDataForAdd(newRow);
// Log the creation
await this.systemLogService.logInsert(
SystemLogEntityType.COURSE_MODULE,
newRow.id,
newRow as Record<string, unknown>,
);
return addResponse;
}
/**
* Override save method
*/
async save(id: number, data: any): Promise<SaveResponseDataInterface<any>> {
// Get old data before update
const oldCourseModule = await this.prisma.client.courseModule.findUnique({
where: { id },
});
if (!oldCourseModule) {
throw new NotFoundException(`Course module with ID ${id} not found`);
}
const repo = this.commonMethods.getRepo(this.gVars.moduleCurrentCfg.repoName);
let dataSave = plainToInstance(this.gVars.moduleCurrentCfg.saveDto, data);
// Use scalar audit columns to avoid relation update errors
dataSave = await this.dbUtilService.addUpdatedById(dataSave);
await repo.update({ where: { id }, data: dataSave });
// Get updated data directly from database for logging
const updatedCourseModule = await this.prisma.client.courseModule.findUnique({
where: { id },
});
if (!updatedCourseModule) {
throw new NotFoundException(`Course module with ID ${id} not found after update`);
}
// Calculate changed fields and log the update
const changedFields = SystemLogService.calculateChangedFields(
oldCourseModule as Record<string, unknown>,
updatedCourseModule as Record<string, unknown>,
);
await this.systemLogService.logUpdate(
SystemLogEntityType.COURSE_MODULE,
id,
oldCourseModule as Record<string, unknown>,
updatedCourseModule as Record<string, unknown>,
changedFields,
);
return await this.getDetail(id);
}
/**
* Override delete method
*/
async delete(id: number): Promise<void> {
// Get course module data before deletion
const courseModule = await this.prisma.client.courseModule.findUnique({
where: { id },
});
if (!courseModule) {
throw new NotFoundException(`Course module with ID ${id} not found`);
}
// Call parent delete method
await super.delete(id);
// Log the deletion
await this.systemLogService.logDelete(
SystemLogEntityType.COURSE_MODULE,
id,
courseModule as Record<string, unknown>,
);
}
/**
* Override getList so that advanced search by course_id is applied via relation
* (CourseModule has no course_id column; filter via courseModulePages.some.course_id).
*/
async getList(paginationOptions: PaginationOptions): Promise<ListResponseDataInterface<any>> {
const modifiedPagination = cloneDeep(paginationOptions) as PaginationOptions & { where: any };
// Default list sort to sort_order asc so display order matches intended sequence
const orderBy = (modifiedPagination as any).orderBy;
if (!orderBy || orderBy === "created_at") {
(modifiedPagination as any).orderBy = "sort_order";
(modifiedPagination as any).sortOrder = "asc";
}
const whereRaw = (modifiedPagination as any).where;
const courseIdEntry =
whereRaw && typeof whereRaw === "object" && whereRaw["course_id"]
? (whereRaw["course_id"] as { operator?: string; value?: string })
: null;
if (modifiedPagination.where && courseIdEntry) {
delete modifiedPagination.where["course_id"];
}
let whereCondition = this.moduleMethods.getWhereCondition(modifiedPagination);
if (courseIdEntry && courseIdEntry.value !== undefined && courseIdEntry.value !== "") {
const courseIdNum = Number.parseInt(String(courseIdEntry.value), 10);
if (!Number.isNaN(courseIdNum)) {
const relationFilter = {
courseModulePages: {
some: { course_id: courseIdNum },
},
};
whereCondition = whereCondition || { AND: [] };
const and = Array.isArray(whereCondition.AND) ? whereCondition.AND : [whereCondition];
(whereCondition as any).AND = [...and, relationFilter];
}
}
const findParams = this.moduleMethods.getFindParams(modifiedPagination);
const countParams: Record<string, unknown> = {};
if (whereCondition) {
findParams["where"] = whereCondition;
countParams["where"] = whereCondition;
}
const moduleCurrentCfg = this.gVars.moduleCurrentCfg;
const [rawData, totalCount] = await this.moduleMethods.getListData(
moduleCurrentCfg.repoName,
findParams,
countParams,
);
const { page, limit } = paginationOptions;
let data: any[] = rawData;
if (moduleCurrentCfg.listDto) {
data = rawData.map((row: any) =>
plainToInstance(moduleCurrentCfg.listDto, row, { excludeExtraneousValues: true }),
);
}
return buildListResponseData(data, page || 1, limit || 10, totalCount);
}
/**
* Course modules relevant to a course: linked via CourseModulePage, plus any module
* already used on AssessmentQuestion for this course (covers courses without pages seeded yet).
*/
async getModulesByCourse(courseId: number, excludeFromBat = false): Promise<any[]> {
const pages = await this.prisma.client.courseModulePage.findMany({
where: { course_id: courseId },
select: { course_module_id: true },
distinct: ["course_module_id"],
});
const moduleIdsFromPages = pages.map((page) => page.course_module_id);
const fromAssessmentQuestions = await this.prisma.client.assessmentQuestion.findMany({
where: {
course_id: courseId,
course_module_id: { not: null },
},
select: { course_module_id: true },
distinct: ["course_module_id"],
});
const moduleIdsFromQuestions = fromAssessmentQuestions
.map((q) => q.course_module_id)
.filter((id): id is number => id != null);
const moduleIds = [...new Set([...moduleIdsFromPages, ...moduleIdsFromQuestions])];
if (moduleIds.length === 0) {
return [];
}
const modules = await this.prisma.client.courseModule.findMany({
where: {
id: { in: moduleIds },
exclude_from_bat: excludeFromBat ? false : undefined,
},
orderBy: { sort_order: "asc" },
});
return modules;
}
}