Source: video/index.js

  1. /**
  2. * @module video
  3. */
  4. const videoRepo = require("../../repositories/video");
  5. const videoPhaseRepo = require("../../repositories/video/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 video records by provided video and video-phase
  12. * records. If the provided video has multiple phases, one is picked
  13. * to avoid duplicate results.
  14. * Returns an array of video records with a default maximum size of 5.
  15. * @param {Object} video
  16. * @param {Number} [maximum=5] default is 5
  17. * @return {Array} similar video records
  18. */
  19. const fetchSimilarVideoRecords = async (video, maximum) => {
  20. let findRecords = [];
  21. if (cache.has("videos")) {
  22. const cachedVal = cache.get("videos");
  23. log.info("Video %s: Fetching similar videos from CACHE", video.id);
  24. const matching = [];
  25. cachedVal.forEach(e => {
  26. if (
  27. (e.capabilityId === video.capabilityId) &&
  28. (e.categoryId === video.categoryId) &&
  29. (e.competencyId === video.competencyId) &&
  30. (e.phases[0] === video.phases[0])
  31. ) {
  32. matching.push(e);
  33. }
  34. });
  35. findRecords = matching;
  36. } else {
  37. log.info("Video %s: Fetching similar videos from DB", video.id);
  38. const findWithPhaseResult = await videoRepo.findByFilters({
  39. capabilityId: video.capabilityId,
  40. categoryId: video.categoryId,
  41. competencyId: video.competencyId,
  42. // if a video has multiple phases, one of them is picked for search
  43. phaseId: video.phases[0],
  44. });
  45. findRecords = findWithPhaseResult.rows;
  46. }
  47. // TODO: fetch more
  48. const filteredRecords = findRecords.filter(e => e.id !== video.id);
  49. log.info("Video %s: Found %s similar video(s), returning %s",
  50. video.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 video and resolves
  57. * all identifiers to values in the database.
  58. * Returns resolved video object.
  59. * @param {Number} videoId
  60. * @return {Object} video
  61. */
  62. const fetchAndResolveVideo = async (videoId) => {
  63. log.info("Video %s: Fetching all info", videoId);
  64. if (cache.has(`video-${videoId}`)) {
  65. log.info("Video %s: Fetched all info from CACHE", videoId);
  66. return cache.get(`video-${videoId}`);
  67. } else {
  68. throw new Error(`No video found by id '${videoId}'`);
  69. }
  70. };
  71. /**
  72. * Fetch videos based on input filters.
  73. * Fetches from cache if possible
  74. * @param {Object} filters
  75. * @return {Array} matching video objects
  76. */
  77. const fetchByFilters = async (filters) => {
  78. if (cache.has("videos")) {
  79. const cachedVal = cache.get("videos");
  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 videos with %s filters from CACHE", matching.length, JSON.stringify(filters));
  113. return matching;
  114. } else {
  115. const findResult = await videoRepo.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 videos with %s filters from DB", findResult.rows.length, JSON.stringify(filters));
  125. return findResult.rows;
  126. }
  127. };
  128. /**
  129. * Fetch call videos.
  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("videos")) {
  136. return cache.get("videos");
  137. } else {
  138. const videos = await fetchByFilters({});
  139. cache.set("videos", videos);
  140. videos.forEach(e => {
  141. cache.set(`video-${e.id}`, e);
  142. });
  143. return (videos);
  144. }
  145. };
  146. /**
  147. * Fetch all videos that have unique titles.
  148. * Fetches all videos then filters and sorts
  149. * by the titles.
  150. * @return {Array} videos
  151. */
  152. const fetchAllWithUniqueTitles = async () => {
  153. const allVideos = await fetchAll();
  154. return filtering.filterAndSortByTitle(allVideos);
  155. };
  156. /**
  157. * Add a new video: insert new video object to the database,
  158. * and immediately attempt to update the cache with the new object.
  159. * The video object is only cached if it can successfully be updated
  160. * from the LinkedIn Learning API.
  161. * @param {Object} video
  162. */
  163. const addNewVideo = async (video) => {
  164. const insertionResult = await videoRepo.insert({
  165. title: video.title,
  166. hyperlink: video.hyperlink,
  167. capabilityId: parseInt(video.capability, 10),
  168. categoryId: parseInt(video.category, 10),
  169. competencyId: parseInt(video.competency, 10),
  170. urn: video.urn,
  171. });
  172. const videoId = insertionResult.rows[0].id;
  173. if (Array.isArray(video.phases)) {
  174. for await (const phase of video.phases) {
  175. await videoPhaseRepo.insert({
  176. videoId,
  177. phaseId: parseInt(phase, 10),
  178. });
  179. }
  180. } else {
  181. await videoPhaseRepo.insert({
  182. videoId,
  183. phaseId: parseInt(video.phases, 10),
  184. });
  185. }
  186. log.info("Video %d: Successfully inserted new record to database", videoId);
  187. log.info("Video %d: Caching new video...", videoId);
  188. const findResult = await videoRepo.findByIdWithFullInfo(videoId);
  189. const temp = findResult.rows[0];
  190. cache.set(`video-${videoId}`, temp);
  191. await cache.updateFromAPI(`video-${videoId}`);
  192. log.info("Video %d: Cache updated with new video.", videoId);
  193. return videoId;
  194. };
  195. /**
  196. * Update a video: update a stored video object in the database,
  197. * and immediately attempt to update the cache with the object.
  198. * The video object is only cached if it can successfully be updated
  199. * from the LinkedIn Learning API.
  200. * @param {Object} video
  201. */
  202. const updateVideo = async (video) => {
  203. const videoId = parseInt(video.id, 10);
  204. await videoRepo.update({
  205. title: video.title,
  206. hyperlink: video.hyperlink,
  207. capabilityId: parseInt(video.capability, 10),
  208. categoryId: parseInt(video.category, 10),
  209. competencyId: parseInt(video.competency, 10),
  210. urn: video.urn,
  211. id: videoId,
  212. });
  213. await videoPhaseRepo.removeByVideoId(videoId);
  214. if (Array.isArray(video.phases)) {
  215. for await (const phase of video.phases) {
  216. await videoPhaseRepo.insert({
  217. videoId,
  218. phaseId: parseInt(phase, 10),
  219. });
  220. }
  221. } else {
  222. await videoPhaseRepo.insert({
  223. videoId,
  224. phaseId: parseInt(video.phases, 10),
  225. });
  226. }
  227. log.info("Video %d: Successfully updated record in database", videoId);
  228. log.info("Video %d: Caching updated video...", videoId);
  229. const findResult = await videoRepo.findByIdWithFullInfo(videoId);
  230. const temp = findResult.rows[0];
  231. const videosArray = cache.get("videos");
  232. const index = videosArray.findIndex((e) => {
  233. return e.id === videoId;
  234. });
  235. videosArray[index] = temp;
  236. cache.set(`video-${videoId}`, temp);
  237. cache.set("videos", videosArray);
  238. log.info("Video %d: Cache updated with updated video.", videoId);
  239. };
  240. /**
  241. * Delete a video 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 deleteVideo = async (id) => {
  246. log.info("Video %d: Deleting video with all details...", id);
  247. await videoRepo.removeById(id);
  248. await videoPhaseRepo.removeByVideoId(id);
  249. log.info("Video %d: Deleting video from cache...", id);
  250. cache.del(`video-${id}`);
  251. const videosArray = cache.get("videos");
  252. const index = videosArray.findIndex((e) => {
  253. return e.id === parseInt(id, 10);
  254. });
  255. videosArray.splice(index, 1);
  256. cache.set("videos", videosArray);
  257. log.info("Video %d: Deleted video from cache...", id);
  258. log.info("Video %d: Deleted video with all details...", id);
  259. };
  260. module.exports = {
  261. fetchSimilarVideoRecords,
  262. fetchAndResolveVideo,
  263. fetchByFilters,
  264. fetchAll,
  265. fetchAllWithUniqueTitles,
  266. addNewVideo,
  267. updateVideo,
  268. deleteVideo,
  269. };