const CommentModel = require('../models/comment'); const AssetModel = require('../models/asset'); const SettingsService = require('./settings'); const DomainList = require('./domain_list'); const errors = require('../errors'); const merge = require('lodash/merge'); module.exports = class AssetsService { /** * Finds an asset by its id. * @param {String} id identifier of the asset (uuid). */ static findById(id) { return AssetModel.findOne({id}); } /** * Finds a asset by its url. * @param {String} url identifier of the asset (uuid). */ static findByUrl(url) { return AssetModel.findOne({url}); } /** * Retrieves the settings given an asset query and rectifies it against the * global settings. * @param {Promise} assetQuery an asset query that returns a single asset. * @return {Promise} */ static async rectifySettings(assetQuery, settings = null) { const [ globalSettings, asset, ] = await Promise.all([ settings !== null ? settings : SettingsService.retrieve(), assetQuery, ]); // If the asset exists and has settings then return the merged object. if (asset && asset.settings) { settings = merge({}, globalSettings, asset.settings); } else { settings = globalSettings; } return settings; } /** * Finds a asset by its url. * * NOTE: This function has scalability concerns regarding mongoose's decision * always write {updated_at: new Date()} on every call to findOneAndUpdate * even though the update document exactly matches the query document... In * the future this function should never update, only findOneAndCreate but this * is not possible with the mongoose driver. * * @param {String} url identifier of the asset (uuid). * @return {Promise} */ static findOrCreateByUrl(url) { // Check the URL to confirm that is in the domain whitelist return Promise.all([ DomainList.urlCheck(url), SettingsService.retrieve() ]).then(([whitelisted, settings]) => { const update = {$setOnInsert: {url}}; if (settings.autoCloseStream) { update.$setOnInsert.closedAt = new Date(Date.now() + settings.closedTimeout * 1000); } if (!whitelisted) { return Promise.reject(errors.ErrInvalidAssetURL); } else { return AssetModel.findOneAndUpdate({url}, update, { // Ensure that if it's new, we return the new object created. new: true, // Perform an upsert in the event that this doesn't exist. upsert: true, // Set the default values if not provided based on the mongoose models. setDefaultsOnInsert: true }); } }); } /** * Updates the settings for the asset. * @param {[type]} id [description] * @param {[type]} settings [description] * @return {[type]} [description] */ static overrideSettings(id, settings) { return AssetModel.findOneAndUpdate({id}, { $set: { settings } }, { new: true }); } /** * Finds assets matching keywords on the model. * @param {String} value string to search by. * @return {Promise} */ static search({value, limit, open, sortOrder, cursor} = {}) { let assets = AssetModel.find({}); if (value && value.length > 0) { assets.merge({ $text: { $search: value } }); } if (open != null) { if (open) { assets.merge({ $or: [ { closedAt: null }, { closedAt: { $gt: Date.now() } } ] }); } else { assets.merge({ closedAt: { $lt: Date.now() } }); } } if (cursor) { if (sortOrder === 'DESC') { assets.merge({ created_at: { $lt: cursor, }, }); } else { assets.merge({ created_at: { $gt: cursor, }, }); } } return assets.sort({created_at: sortOrder === 'DESC' ? -1 : 1}).limit(limit); } /** * Finds multiple assets with matching ids * @param {Array} ids an array of Strings of asset_id * @return {Promise} resolves to list of Assets */ static async findByIDs(ids) { // Find the assets. let assets = await AssetModel.find({ id: { $in: ids } }); // Return them in the right order. return ids.map((id) => assets.find((asset) => asset.id === id)); } static async updateURL(id, url) { // Try to see if an asset already exists with the given url. let asset = await AssetsService.findByUrl(url); if (asset !== null) { throw errors.ErrAssetURLAlreadyExists; } // Seems that there was no other asset with the same url, try and perform // the rename operation! An error may be thrown from this if the operation // fails. This is ok. await AssetModel.update({id}, {$set: {url}}); } static async merge(srcAssetID, dstAssetID) { // Fetch both assets. let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcAssetID, dstAssetID]); if (!srcAsset || !dstAsset) { throw errors.ErrNotFound; } // Resolve the merge operation, this invloves moving all resources attached // to the src asset to the dst asset, and then removing the src asset. // First, update all the old comments to the new asset. await CommentModel.update({ asset_id: srcAssetID }, { $set: { asset_id: dstAssetID } }, { multi: true }); // Second remove the old asset. await AssetModel.remove({ id: srcAssetID }); // That's it! } static all(limit = undefined) { return AssetModel .find({}) .limit(limit); } };