'use strict' const _ = require('lodash') const { Streams, StreamAcl, knex } = require('@/modules/core/dbSchema') const { getStream, getFavoritedStreams, getFavoritedStreamsCount, setStreamFavorited, canUserFavoriteStream, deleteStream: deleteStreamFromDb, updateStream: updateStreamInDb, revokeStreamPermissions, grantStreamPermissions } = require('@/modules/core/repositories/streams') const { UnauthorizedError, InvalidArgumentError } = require('@/modules/shared/errors') const { dbLogger } = require('@/logging/logging') const { createStreamReturnRecord } = require('@/modules/core/services/streams/management') const { isResourceAllowed } = require('@/modules/core/helpers/token') const { TokenResourceIdentifierType } = require('@/modules/core/graph/generated/graphql') /** * NOTE: Stop adding stuff to this service, create specialized service modules instead for various domains * relating to streams. Otherwise we're not only breaking the single responsibility principle, but also * increasing the chances of circular dependencies (which often cause actual errors) since everything relies * on this service. */ module.exports = { /** * @deprecated Use createStreamReturnRecord() * @param {import('@/modules/core/graph/generated/graphql').StreamCreateInput & {ownerId: string}} param0 * @returns {Promise} */ async createStream(params) { const { id } = await createStreamReturnRecord(params, { createActivity: false }) return id }, getStream, /** * @deprecated Use updateStreamAndNotify or use the repository function directly * @param {import('@/modules/core/graph/generated/graphql').StreamUpdateInput} update */ async updateStream(update) { const updatedStream = await updateStreamInDb(update) return updatedStream?.id || null }, setStreamFavorited, /** * @deprecated Use repository method directly */ async grantPermissionsStream({ streamId, userId, role }) { return await grantStreamPermissions({ streamId, userId, role }) }, /** * @deprecated Use repository method directly */ async revokePermissionsStream({ streamId, userId }) { return await revokeStreamPermissions({ streamId, userId }) }, /** * @deprecated Use deleteStreamAndNotify or use the repository function directly */ async deleteStream({ streamId }) { dbLogger.info('Deleting stream %s', streamId) return await deleteStreamFromDb(streamId) }, async getStreams({ cursor, limit, orderBy, visibility, searchQuery, streamIdWhitelist }) { const query = knex.select().from('streams') const countQuery = Streams.knex() if (searchQuery) { const whereFunc = function () { this.where('streams.name', 'ILIKE', `%${searchQuery}%`).orWhere( 'streams.description', 'ILIKE', `%${searchQuery}%` ) } query.where(whereFunc) countQuery.where(whereFunc) } if (visibility && visibility !== 'all') { if (!['private', 'public'].includes(visibility)) throw new Error('Stream visibility should be either private, public or all') const isPublic = visibility === 'public' const publicFunc = function () { this.where({ isPublic }) } query.andWhere(publicFunc) countQuery.andWhere(publicFunc) } if (streamIdWhitelist?.length) { query.whereIn('id', streamIdWhitelist) countQuery.whereIn('id', streamIdWhitelist) } const [res] = await countQuery.count() const count = parseInt(res.count) if (!count) return { streams: [], totalCount: 0 } orderBy = orderBy || 'updatedAt,desc' const [columnName, order] = orderBy.split(',') if (cursor) query.where(columnName, order === 'desc' ? '<' : '>', cursor) const rows = await query.orderBy(`${columnName}`, order).limit(limit) const cursorDate = rows.length ? rows.slice(-1)[0][columnName] : null return { streams: rows, totalCount: count, cursorDate } }, async getStreamUsers({ streamId }) { const query = StreamAcl.knex() .columns({ role: 'stream_acl.role' }, 'id', 'name', 'company', 'avatar') .select() .where({ resourceId: streamId }) .rightJoin('users', { 'users.id': 'stream_acl.userId' }) .select('stream_acl.role', 'name', 'id', 'company', 'avatar') .orderBy('stream_acl.role') return await query }, /** * Favorite or unfavorite a stream * @param {Object} p * @param {string} p.userId * @param {string} p.streamId * @param {boolean} [p.favorited] Whether to favorite or unfavorite (true by default) * @param {import('@/modules/core/helpers/token').ContextResourceAccessRules} [p.userResourceAccessRules] Resource access rules (if any) for the user doing the favoriting * @returns {Promise} Updated stream */ async favoriteStream({ userId, streamId, favorited, userResourceAccessRules }) { // Check if user has access to stream const canFavorite = await canUserFavoriteStream({ userId, streamId }) const hasResourceAccess = isResourceAllowed({ resourceId: streamId, resourceAccessRules: userResourceAccessRules, resourceType: TokenResourceIdentifierType.Project }) if (!canFavorite || !hasResourceAccess) { throw new UnauthorizedError("User doesn't have access to the specified stream", { info: { userId, streamId } }) } // Favorite/unfavorite the stream await setStreamFavorited({ streamId, userId, favorited }) // Get updated stream info return await getStream({ streamId, userId }) }, /** * Get user favorited streams & metadata * @param {Object} p * @param {string} p.userId * @param {number} [p.limit] Defaults to 25 * @param {string} [p.cursor] Optionally specify date after which to look for favorites * @param {string[] | undefined} [p.streamIdWhitelist] Optionally specify a list of stream IDs to filter by * @returns */ async getFavoriteStreamsCollection({ userId, limit, cursor, streamIdWhitelist }) { limit = _.clamp(limit || 25, 1, 25) // Get total count of favorited streams const totalCount = await getFavoritedStreamsCount(userId, streamIdWhitelist) // Get paginated streams const { cursor: finalCursor, streams } = await getFavoritedStreams({ userId, cursor, limit, streamIdWhitelist }) return { totalCount, cursor: finalCursor, items: streams } }, /** * Get active user stream favorite date (using dataloader) * @param {Object} p * @param {import('@/modules/shared/index').GraphQLContext} p.ctx * @param {string} p.streamId * @param {Promise} */ async getActiveUserStreamFavoriteDate({ ctx, streamId }) { if (!ctx.userId) { return null } if (!streamId) { throw new InvalidArgumentError('Invalid stream ID') } return ( (await ctx.loaders.streams.getUserFavoriteData.load(streamId))?.createdAt || null ) }, /** * Get stream favorites count (using dataloader) * @param {Object} p * @param {import('@/modules/shared/index').GraphQLContext} p.ctx * @param {string} p.streamId * @returns {Promise} */ async getStreamFavoritesCount({ ctx, streamId }) { if (!streamId) { throw new InvalidArgumentError('Invalid stream ID') } return (await ctx.loaders.streams.getFavoritesCount.load(streamId)) || 0 }, /** * @param {Object} p * @param {import('@/modules/shared/index').GraphQLContext} p.ctx * @param {string} p.userId * @returns {Promise} */ async getOwnedFavoritesCount({ ctx, userId }) { if (!userId) { throw new InvalidArgumentError('Invalid user ID') } return (await ctx.loaders.streams.getOwnedFavoritesCount.load(userId)) || 0 } }