/**
* @module video
*/
const videoRepo = require("../../repositories/video");
const videoPhaseRepo = require("../../repositories/video/phase");
const log = require("../../util/log");
const filtering = require("../filtering");
const cache = require("../cache");
const _ = require('lodash');
/**
* Fetch similar video records by provided video and video-phase
* records. If the provided video has multiple phases, one is picked
* to avoid duplicate results.
* Returns an array of video records with a default maximum size of 5.
* @param {Object} video
* @param {Number} [maximum=5] default is 5
* @return {Array} similar video records
*/
const fetchSimilarVideoRecords = async (video, maximum) => {
let findRecords = [];
if (cache.has("videos")) {
const cachedVal = cache.get("videos");
log.info("Video %s: Fetching similar videos from CACHE", video.id);
const matching = [];
cachedVal.forEach(e => {
if (
(e.capabilityId === video.capabilityId) &&
(e.categoryId === video.categoryId) &&
(e.competencyId === video.competencyId) &&
(e.phases[0] === video.phases[0])
) {
matching.push(e);
}
});
findRecords = matching;
} else {
log.info("Video %s: Fetching similar videos from DB", video.id);
const findWithPhaseResult = await videoRepo.findByFilters({
capabilityId: video.capabilityId,
categoryId: video.categoryId,
competencyId: video.competencyId,
// if a video has multiple phases, one of them is picked for search
phaseId: video.phases[0],
});
findRecords = findWithPhaseResult.rows;
}
// TODO: fetch more
const filteredRecords = findRecords.filter(e => e.id !== video.id);
log.info("Video %s: Found %s similar video(s), returning %s",
video.id, filteredRecords.length,
((filteredRecords.length > 5) ? (maximum ? maximum : 5) : filteredRecords.length));
// return maximum 5 by default
return filteredRecords.slice(0, (maximum ? maximum : 5));
};
/**
* Fetches all info related to video and resolves
* all identifiers to values in the database.
* Returns resolved video object.
* @param {Number} videoId
* @return {Object} video
*/
const fetchAndResolveVideo = async (videoId) => {
log.info("Video %s: Fetching all info", videoId);
if (cache.has(`video-${videoId}`)) {
log.info("Video %s: Fetched all info from CACHE", videoId);
return cache.get(`video-${videoId}`);
} else {
throw new Error(`No video found by id '${videoId}'`);
}
};
/**
* Fetch videos based on input filters.
* Fetches from cache if possible
* @param {Object} filters
* @return {Array} matching video objects
*/
const fetchByFilters = async (filters) => {
if (cache.has("videos")) {
const cachedVal = cache.get("videos");
const matching = [];
const safeKey = _.escapeRegExp(filters.keyword);
const regex = RegExp(safeKey ? safeKey : '', 'i');
cachedVal.forEach(e => {
if (
(
!filters.capability ||
parseInt(filters.capability, 10) === -1 ||
e.capabilityId === parseInt(filters.capability, 10)
) &&
(
!filters.category ||
parseInt(filters.category, 10) === -1 ||
e.categoryId === parseInt(filters.category, 10)
) &&
(
!filters.competency ||
parseInt(filters.competency, 10) === -1 ||
e.competencyId === parseInt(filters.competency, 10)
) &&
(
!filters.phase ||
parseInt(filters.phase, 10) === -1 ||
e.phases.includes(parseInt(filters.phase, 10))
) &&
(
regex.test(e.title)
)
) {
matching.push(e);
}
});
log.info("Fetched %s videos with %s filters from CACHE", matching.length, JSON.stringify(filters));
return matching;
} else {
const findResult = await videoRepo.findByFiltersAndKeywordJoint({
filters: {
capabilityId: filters.capability ? parseInt(filters.capability, 10) : -1,
categoryId: filters.category ? parseInt(filters.category, 10) : -1,
competencyId: filters.competency ? parseInt(filters.competency, 10) : -1,
phaseId: filters.phase ? parseInt(filters.phase, 10) : -1,
},
keyword: filters.keyword ? filters.keyword : '',
});
log.info("Fetched %s videos with %s filters from DB", findResult.rows.length, JSON.stringify(filters));
return findResult.rows;
}
};
/**
* Fetch call videos.
* Fetches from cache if possible, otherwise
* fetches from database and caches values
* for future use.
*/
const fetchAll = async () => {
if (cache.has("videos")) {
return cache.get("videos");
} else {
const videos = await fetchByFilters({});
cache.set("videos", videos);
videos.forEach(e => {
cache.set(`video-${e.id}`, e);
});
return (videos);
}
};
/**
* Fetch all videos that have unique titles.
* Fetches all videos then filters and sorts
* by the titles.
* @return {Array} videos
*/
const fetchAllWithUniqueTitles = async () => {
const allVideos = await fetchAll();
return filtering.filterAndSortByTitle(allVideos);
};
/**
* Add a new video: insert new video object to the database,
* and immediately attempt to update the cache with the new object.
* The video object is only cached if it can successfully be updated
* from the LinkedIn Learning API.
* @param {Object} video
*/
const addNewVideo = async (video) => {
const insertionResult = await videoRepo.insert({
title: video.title,
hyperlink: video.hyperlink,
capabilityId: parseInt(video.capability, 10),
categoryId: parseInt(video.category, 10),
competencyId: parseInt(video.competency, 10),
urn: video.urn,
});
const videoId = insertionResult.rows[0].id;
if (Array.isArray(video.phases)) {
for await (const phase of video.phases) {
await videoPhaseRepo.insert({
videoId,
phaseId: parseInt(phase, 10),
});
}
} else {
await videoPhaseRepo.insert({
videoId,
phaseId: parseInt(video.phases, 10),
});
}
log.info("Video %d: Successfully inserted new record to database", videoId);
log.info("Video %d: Caching new video...", videoId);
const findResult = await videoRepo.findByIdWithFullInfo(videoId);
const temp = findResult.rows[0];
cache.set(`video-${videoId}`, temp);
await cache.updateFromAPI(`video-${videoId}`);
log.info("Video %d: Cache updated with new video.", videoId);
return videoId;
};
/**
* Update a video: update a stored video object in the database,
* and immediately attempt to update the cache with the object.
* The video object is only cached if it can successfully be updated
* from the LinkedIn Learning API.
* @param {Object} video
*/
const updateVideo = async (video) => {
const videoId = parseInt(video.id, 10);
await videoRepo.update({
title: video.title,
hyperlink: video.hyperlink,
capabilityId: parseInt(video.capability, 10),
categoryId: parseInt(video.category, 10),
competencyId: parseInt(video.competency, 10),
urn: video.urn,
id: videoId,
});
await videoPhaseRepo.removeByVideoId(videoId);
if (Array.isArray(video.phases)) {
for await (const phase of video.phases) {
await videoPhaseRepo.insert({
videoId,
phaseId: parseInt(phase, 10),
});
}
} else {
await videoPhaseRepo.insert({
videoId,
phaseId: parseInt(video.phases, 10),
});
}
log.info("Video %d: Successfully updated record in database", videoId);
log.info("Video %d: Caching updated video...", videoId);
const findResult = await videoRepo.findByIdWithFullInfo(videoId);
const temp = findResult.rows[0];
const videosArray = cache.get("videos");
const index = videosArray.findIndex((e) => {
return e.id === videoId;
});
videosArray[index] = temp;
cache.set(`video-${videoId}`, temp);
cache.set("videos", videosArray);
log.info("Video %d: Cache updated with updated video.", videoId);
};
/**
* Delete a video from the database and efficiently remove it
* from the cache so full flushing of cache can be avoided.
* @param {Number} id
*/
const deleteVideo = async (id) => {
log.info("Video %d: Deleting video with all details...", id);
await videoRepo.removeById(id);
await videoPhaseRepo.removeByVideoId(id);
log.info("Video %d: Deleting video from cache...", id);
cache.del(`video-${id}`);
const videosArray = cache.get("videos");
const index = videosArray.findIndex((e) => {
return e.id === parseInt(id, 10);
});
videosArray.splice(index, 1);
cache.set("videos", videosArray);
log.info("Video %d: Deleted video from cache...", id);
log.info("Video %d: Deleted video with all details...", id);
};
module.exports = {
fetchSimilarVideoRecords,
fetchAndResolveVideo,
fetchByFilters,
fetchAll,
fetchAllWithUniqueTitles,
addNewVideo,
updateVideo,
deleteVideo,
};