Source: course/index.js

  1. /**
  2. * @module course
  3. */
  4. const courseRepo = require("../../repositories/course");
  5. const coursePhaseRepo = require("../../repositories/course/phase");
  6. const log = require("../../util/log");
  7. const filtering = require("../filtering");
  8. const cache = require("../cache");
  9. const _ = require('lodash');
  10. /**
  11. * Fetch similar course records by provided course and course-phase
  12. * records. If the provided course has multiple phases, one is picked
  13. * to avoid duplicate results.
  14. * Returns an array of course records with a default maximum size of 5.
  15. * @param {Object} course
  16. * @param {Number} [maximum=5] default is 5
  17. * @return {Array} similar course records
  18. */
  19. const fetchSimilarCourseRecords = async (course, maximum) => {
  20. let findRecords = [];
  21. if (cache.has("courses")) {
  22. const cachedVal = cache.get("courses");
  23. log.info("Course %s: Fetching similar courses from CACHE", course.id);
  24. const matching = [];
  25. cachedVal.forEach(e => {
  26. if (
  27. (e.capabilityId === course.capabilityId) &&
  28. (e.categoryId === course.categoryId) &&
  29. (e.competencyId === course.competencyId) &&
  30. (e.phases[0] === course.phases[0])
  31. ) {
  32. matching.push(e);
  33. }
  34. });
  35. findRecords = matching;
  36. } else {
  37. log.info("Course %s: Fetching similar courses from DB", course.id);
  38. const findWithPhaseResult = await courseRepo.findByFilters({
  39. capabilityId: course.capabilityId,
  40. categoryId: course.categoryId,
  41. competencyId: course.competencyId,
  42. // if a course has multiple phases, one of them is picked for search
  43. phaseId: course.phases[0],
  44. });
  45. findRecords = findWithPhaseResult.rows;
  46. }
  47. // TODO: fetch more
  48. const filteredRecords = findRecords.filter(e => e.id !== course.id);
  49. log.info("Course %s: Found %s similar course(s), returning %s",
  50. course.id, filteredRecords.length,
  51. ((filteredRecords.length > 5) ? (maximum ? maximum : 5) : filteredRecords.length));
  52. // return maximum 5 by default
  53. return filteredRecords.slice(0, (maximum ? maximum : 5));
  54. };
  55. /**
  56. * Fetches all info related to course and resolves
  57. * all identifiers to values in the database.
  58. * Returns resolved course object.
  59. * @param {Number} courseId
  60. * @return {Object} course
  61. */
  62. const fetchAndResolveCourse = async (courseId) => {
  63. log.info("Course %s: Fetching all info", courseId);
  64. if (cache.has(`course-${courseId}`)) {
  65. log.info("Course %s: Fetched all info from CACHE", courseId);
  66. return cache.get(`course-${courseId}`);
  67. } else {
  68. throw new Error(`No course found by id '${courseId}'`);
  69. }
  70. };
  71. /**
  72. * Fetch courses based on input filters.
  73. * Fetches from cache if possible
  74. * @param {Object} filters
  75. * @return {Array} matching course objects
  76. */
  77. const fetchByFilters = async (filters) => {
  78. if (cache.has("courses")) {
  79. const cachedVal = cache.get("courses");
  80. const matching = [];
  81. const safeKey = _.escapeRegExp(filters.keyword);
  82. const regex = RegExp(safeKey ? safeKey : '', 'i');
  83. cachedVal.forEach(e => {
  84. if (
  85. (
  86. !filters.capability ||
  87. parseInt(filters.capability, 10) === -1 ||
  88. e.capabilityId === parseInt(filters.capability, 10)
  89. ) &&
  90. (
  91. !filters.category ||
  92. parseInt(filters.category, 10) === -1 ||
  93. e.categoryId === parseInt(filters.category, 10)
  94. ) &&
  95. (
  96. !filters.competency ||
  97. parseInt(filters.competency, 10) === -1 ||
  98. e.competencyId === parseInt(filters.competency, 10)
  99. ) &&
  100. (
  101. !filters.phase ||
  102. parseInt(filters.phase, 10) === -1 ||
  103. e.phases.includes(parseInt(filters.phase, 10))
  104. ) &&
  105. (
  106. regex.test(e.title)
  107. )
  108. ) {
  109. matching.push(e);
  110. }
  111. });
  112. log.info("Fetched %s courses with %s filters from CACHE", matching.length, JSON.stringify(filters));
  113. return matching;
  114. } else {
  115. const findResult = await courseRepo.findByFiltersAndKeywordJoint({
  116. filters: {
  117. capabilityId: filters.capability ? parseInt(filters.capability, 10) : -1,
  118. categoryId: filters.category ? parseInt(filters.category, 10) : -1,
  119. competencyId: filters.competency ? parseInt(filters.competency, 10) : -1,
  120. phaseId: filters.phase ? parseInt(filters.phase, 10) : -1,
  121. },
  122. keyword: filters.keyword ? filters.keyword : '',
  123. });
  124. log.info("Fetched %s courses with %s filters from DB", findResult.rows.length, JSON.stringify(filters));
  125. return findResult.rows;
  126. }
  127. };
  128. /**
  129. * Fetch call courses.
  130. * Fetches from cache if possible, otherwise
  131. * fetches from database and caches values
  132. * for future use.
  133. */
  134. const fetchAll = async () => {
  135. if (cache.has("courses")) {
  136. return cache.get("courses");
  137. } else {
  138. const courses = await fetchByFilters({});
  139. cache.set("courses", courses);
  140. courses.forEach(e => {
  141. cache.set(`course-${e.id}`, e);
  142. });
  143. return (courses);
  144. }
  145. };
  146. /**
  147. * Fetch all courses that have unique titles.
  148. * Fetches all courses then filters and sorts
  149. * by the titles.
  150. * @return {Array} courses
  151. */
  152. const fetchAllWithUniqueTitles = async () => {
  153. const allCourses = await fetchAll();
  154. return filtering.filterAndSortByTitle(allCourses);
  155. };
  156. /**
  157. * Add a new course: insert new course object to the database,
  158. * and immediately attempt to update the cache with the new object.
  159. * The course object is only cached if it can successfully be updated
  160. * from the LinkedIn Learning API.
  161. * @param {Object} course
  162. */
  163. const addNewCourse = async (course) => {
  164. const insertionResult = await courseRepo.insert({
  165. title: course.title,
  166. hyperlink: course.hyperlink,
  167. capabilityId: parseInt(course.capability, 10),
  168. categoryId: parseInt(course.category, 10),
  169. competencyId: parseInt(course.competency, 10),
  170. urn: course.urn,
  171. });
  172. const courseId = insertionResult.rows[0].id;
  173. if (Array.isArray(course.phases)) {
  174. for await (const phase of course.phases) {
  175. await coursePhaseRepo.insert({
  176. courseId,
  177. phaseId: parseInt(phase, 10),
  178. });
  179. }
  180. } else {
  181. await coursePhaseRepo.insert({
  182. courseId,
  183. phaseId: parseInt(course.phases, 10),
  184. });
  185. }
  186. log.info("Course %d: Successfully inserted new record to database", courseId);
  187. log.info("Course %d: Caching new course...", courseId);
  188. const findResult = await courseRepo.findByIdWithFullInfo(courseId);
  189. const temp = findResult.rows[0];
  190. cache.set(`course-${courseId}`, temp);
  191. await cache.updateFromAPI(`course-${courseId}`);
  192. log.info("Course %d: Cache updated with new course.", courseId);
  193. return courseId;
  194. };
  195. /**
  196. * Update a course: update a stored course object in the database,
  197. * and immediately attempt to update the cache with the object.
  198. * The course object is only cached if it can successfully be updated
  199. * from the LinkedIn Learning API.
  200. * @param {Object} course
  201. */
  202. const updateCourse = async (course) => {
  203. const courseId = parseInt(course.id, 10);
  204. await courseRepo.update({
  205. title: course.title,
  206. hyperlink: course.hyperlink,
  207. capabilityId: parseInt(course.capability, 10),
  208. categoryId: parseInt(course.category, 10),
  209. competencyId: parseInt(course.competency, 10),
  210. urn: course.urn,
  211. id: courseId,
  212. });
  213. await coursePhaseRepo.removeByCourseId(courseId);
  214. if (Array.isArray(course.phases)) {
  215. for await (const phase of course.phases) {
  216. await coursePhaseRepo.insert({
  217. courseId,
  218. phaseId: parseInt(phase, 10),
  219. });
  220. }
  221. } else {
  222. await coursePhaseRepo.insert({
  223. courseId,
  224. phaseId: parseInt(course.phases, 10),
  225. });
  226. }
  227. log.info("Course %d: Successfully updated record in database", courseId);
  228. log.info("Course %d: Caching updated course...", courseId);
  229. const findResult = await courseRepo.findByIdWithFullInfo(courseId);
  230. const temp = findResult.rows[0];
  231. const coursesArray = cache.get("courses");
  232. const index = coursesArray.findIndex((e) => {
  233. return e.id === courseId;
  234. });
  235. coursesArray[index] = temp;
  236. cache.set(`course-${courseId}`, temp);
  237. cache.set("courses", coursesArray);
  238. log.info("Course %d: Cache updated with updated course.", courseId);
  239. };
  240. /**
  241. * Delete a course from the database and efficiently remove it
  242. * from the cache so full flushing of cache can be avoided.
  243. * @param {Number} id
  244. */
  245. const deleteCourse = async (id) => {
  246. log.info("Course %d: Deleting course with all details...", id);
  247. await courseRepo.removeById(id);
  248. await coursePhaseRepo.removeByCourseId(id);
  249. log.info("Course %d: Deleting course from cache...", id);
  250. cache.del(`course-${id}`);
  251. const coursesArray = cache.get("courses");
  252. const index = coursesArray.findIndex((e) => {
  253. return e.id === parseInt(id, 10);
  254. });
  255. coursesArray.splice(index, 1);
  256. cache.set("courses", coursesArray);
  257. log.info("Course %d: Deleted course from cache...", id);
  258. log.info("Course %d: Deleted course with all details...", id);
  259. };
  260. module.exports = {
  261. fetchSimilarCourseRecords,
  262. fetchAndResolveCourse,
  263. fetchByFilters,
  264. fetchAll,
  265. fetchAllWithUniqueTitles,
  266. addNewCourse,
  267. updateCourse,
  268. deleteCourse,
  269. };