const CommentModel = require('../models/comment'); const AssetModel = require('../models/asset'); const SettingsService = require('./settings'); const domainlist = require('./domainlist'); const errors = require('../errors'); 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 rectifySettings(assetQuery) { return Promise.all([ SettingsService.retrieve(), assetQuery ]).then(([settings, asset]) => { // If the asset exists and has settings then return the merged object. if (asset && asset.settings) { settings.merge(asset.settings); } 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, skip, limit} = {}) { if (!value) { return AssetsService.all(skip, limit); } else { return AssetModel .find({ $text: { $search: value } }) .skip(skip) .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(skip = null, limit = null) { return AssetModel .find({}) .skip(skip) .limit(limit); } };