RocketChat/Rocket.Chat

View on GitHub
apps/meteor/ee/server/models/raw/LivechatUnit.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import type { IOmnichannelBusinessUnit, ILivechatDepartment } from '@rocket.chat/core-typings';
import type { FindPaginated, ILivechatUnitModel } from '@rocket.chat/model-typings';
import { LivechatUnitMonitors, LivechatDepartment, LivechatRooms } from '@rocket.chat/models';
import type { FindOptions, Filter, FindCursor, Db, FilterOperators, UpdateResult, DeleteResult, Document, UpdateFilter } from 'mongodb';

import { BaseRaw } from '../../../../server/models/raw/BaseRaw';
import { getUnitsFromUser } from '../../../app/livechat-enterprise/server/lib/units';

const addQueryRestrictions = async (originalQuery: Filter<IOmnichannelBusinessUnit> = {}) => {
    const query: FilterOperators<IOmnichannelBusinessUnit> = { ...originalQuery, type: 'u' };

    const units = await getUnitsFromUser();
    if (Array.isArray(units)) {
        query.ancestors = { $in: units };
        const expressions = query.$and || [];
        const condition = { $or: [{ ancestors: { $in: units } }, { _id: { $in: units } }] };
        query.$and = [condition, ...expressions];
    }

    return query;
};

// We don't actually need Units to extends from Departments
export class LivechatUnitRaw extends BaseRaw<IOmnichannelBusinessUnit> implements ILivechatUnitModel {
    constructor(db: Db) {
        super(db, 'livechat_department');
    }

    findPaginatedUnits(
        query: Filter<IOmnichannelBusinessUnit>,
        options?: FindOptions<IOmnichannelBusinessUnit>,
    ): FindPaginated<FindCursor<IOmnichannelBusinessUnit>> {
        return super.findPaginated({ ...query, type: 'u' }, options);
    }

    // @ts-expect-error - Overriding base types :)
    async find(
        originalQuery: Filter<IOmnichannelBusinessUnit>,
        options: FindOptions<IOmnichannelBusinessUnit>,
    ): Promise<FindCursor<IOmnichannelBusinessUnit>> {
        const query = await addQueryRestrictions(originalQuery);
        return this.col.find(query, options) as FindCursor<IOmnichannelBusinessUnit>;
    }

    // @ts-expect-error - Overriding base types :)
    async findOne(
        originalQuery: Filter<IOmnichannelBusinessUnit>,
        options: FindOptions<IOmnichannelBusinessUnit>,
    ): Promise<IOmnichannelBusinessUnit | null> {
        const query = await addQueryRestrictions(originalQuery);
        return this.col.findOne(query, options);
    }

    remove(query: Filter<IOmnichannelBusinessUnit>): Promise<DeleteResult> {
        return this.deleteMany(query);
    }

    async createOrUpdateUnit(
        _id: string | null,
        { name, visibility }: { name: string; visibility: IOmnichannelBusinessUnit['visibility'] },
        ancestors: string[],
        monitors: { monitorId: string; username: string }[],
        departments: { departmentId: string }[],
    ): Promise<Omit<IOmnichannelBusinessUnit, '_updatedAt'>> {
        monitors = ([] as { monitorId: string; username: string }[]).concat(monitors || []);
        ancestors = ([] as string[]).concat(ancestors || []);

        const record = {
            name,
            visibility,
            type: 'u',
            numMonitors: monitors.length,
            numDepartments: departments.length,
        };

        if (_id) {
            await this.updateOne({ _id }, { $set: record });
        } else {
            _id = (await this.insertOne(record)).insertedId;
        }

        if (!_id) {
            throw new Error('Error creating/updating unit');
        }

        ancestors.splice(0, 0, _id);

        const savedMonitors = (await LivechatUnitMonitors.findByUnitId(_id).toArray()).map(({ monitorId }) => monitorId);
        const monitorsToSave = monitors.map(({ monitorId }) => monitorId);

        // remove other monitors
        for await (const monitorId of savedMonitors) {
            if (!monitorsToSave.includes(monitorId)) {
                await LivechatUnitMonitors.removeByUnitIdAndMonitorId(_id, monitorId);
            }
        }

        for await (const monitor of monitors) {
            await LivechatUnitMonitors.saveMonitor({
                monitorId: monitor.monitorId,
                unitId: _id,
                username: monitor.username,
            });
        }

        const savedDepartments = (await LivechatDepartment.findByParentId(_id, { projection: { _id: 1 } }).toArray()).map(({ _id }) => _id);
        const departmentsToSave = departments.map(({ departmentId }) => departmentId);

        // remove other departments
        for await (const departmentId of savedDepartments) {
            if (!departmentsToSave.includes(departmentId)) {
                await LivechatDepartment.updateOne(
                    { _id: departmentId },
                    {
                        $set: {
                            parentId: null,
                            ancestors: null,
                        },
                    },
                );
            }
        }

        for await (const departmentId of departmentsToSave) {
            await LivechatDepartment.updateOne(
                { _id: departmentId },
                {
                    $set: {
                        parentId: _id,
                        ancestors,
                    },
                },
            );
        }

        await LivechatRooms.associateRoomsWithDepartmentToUnit(departmentsToSave, _id);

        return {
            ...record,
            _id,
        };
    }

    removeParentAndAncestorById(parentId: string): Promise<UpdateResult | Document> {
        const query = {
            parentId,
        };

        const update: UpdateFilter<IOmnichannelBusinessUnit> = {
            $unset: { parentId: 1 },
            $pull: { ancestors: parentId },
        };

        return this.updateMany(query, update);
    }

    async removeById(_id: string): Promise<DeleteResult> {
        await LivechatUnitMonitors.removeByUnitId(_id);
        await this.removeParentAndAncestorById(_id);
        await LivechatRooms.removeUnitAssociationFromRooms(_id);

        const query = { _id };
        return this.deleteOne(query);
    }

    findOneByIdOrName(_idOrName: string, options: FindOptions<IOmnichannelBusinessUnit>): Promise<IOmnichannelBusinessUnit | null> {
        const query = {
            $or: [
                {
                    _id: _idOrName,
                },
                {
                    name: _idOrName,
                },
            ],
        };

        return this.findOne(query, options);
    }

    async findByMonitorId(monitorId: string): Promise<string[]> {
        const monitoredUnits = await LivechatUnitMonitors.findByMonitorId(monitorId).toArray();
        if (monitoredUnits.length === 0) {
            return [];
        }

        return monitoredUnits.map((u) => u.unitId);
    }

    async findMonitoredDepartmentsByMonitorId(monitorId: string, includeDisabled: boolean): Promise<ILivechatDepartment[]> {
        const monitoredUnits = await this.findByMonitorId(monitorId);

        if (includeDisabled) {
            return LivechatDepartment.findByUnitIds(monitoredUnits, {}).toArray();
        }
        return LivechatDepartment.findActiveByUnitIds(monitoredUnits, {}).toArray();
    }

    countUnits(): Promise<number> {
        return this.col.countDocuments({ type: 'u' });
    }
}