RocketChat/Rocket.Chat

View on GitHub
apps/meteor/server/models/raw/Sessions.ts

Summary

Maintainability
F
2 wks
Test Coverage
import type {
    ISession,
    UserSessionAggregation,
    DeviceSessionAggregation,
    OSSessionAggregation,
    UserSessionAggregationResult,
    DeviceSessionAggregationResult,
    DeviceManagementSession,
    DeviceManagementPopulatedSession,
    OSSessionAggregationResult,
    IUser,
    RocketChatRecordDeleted,
} from '@rocket.chat/core-typings';
import type { ISessionsModel } from '@rocket.chat/model-typings';
import { getCollectionName } from '@rocket.chat/models';
import type { PaginatedResult, WithItemCount } from '@rocket.chat/rest-typings';
import type {
    AggregationCursor,
    AnyBulkWriteOperation,
    BulkWriteResult,
    Collection,
    Document,
    FindCursor,
    Db,
    Filter,
    IndexDescription,
    UpdateResult,
    OptionalId,
} from 'mongodb';

import { readSecondaryPreferred } from '../../database/readSecondaryPreferred';
import { BaseRaw } from './BaseRaw';

type DestructuredDate = { year: number; month: number; day: number };
type DestructuredDateWithType = {
    year: number;
    month: number;
    day: number;
    type?: 'month' | 'week';
};
type DestructuredRange = { start: DestructuredDate; end: DestructuredDate };
type DateRange = { start: Date; end: Date };

type CustomSortOp = 'loginAt' | 'device.name' | 'device.os.name';
type CustomSortOpAdmin = CustomSortOp | '_user.username' | '_user.name';

const matchBasedOnDate = (start: DestructuredDate, end: DestructuredDate): Filter<ISession> => {
    if (start.year === end.year && start.month === end.month) {
        return {
            year: start.year,
            month: start.month,
            day: { $gte: start.day, $lte: end.day },
        };
    }

    if (start.year === end.year) {
        return {
            year: start.year,
            $and: [
                {
                    $or: [
                        {
                            month: { $gt: start.month },
                        },
                        {
                            month: start.month,
                            day: { $gte: start.day },
                        },
                    ],
                },
                {
                    $or: [
                        {
                            month: { $lt: end.month },
                        },
                        {
                            month: end.month,
                            day: { $lte: end.day },
                        },
                    ],
                },
            ],
        };
    }

    return {
        $and: [
            {
                $or: [
                    {
                        year: { $gt: start.year },
                    },
                    {
                        year: start.year,
                        month: { $gt: start.month },
                    },
                    {
                        year: start.year,
                        month: start.month,
                        day: { $gte: start.day },
                    },
                ],
            },
            {
                $or: [
                    {
                        year: { $lt: end.year },
                    },
                    {
                        year: end.year,
                        month: { $lt: end.month },
                    },
                    {
                        year: end.year,
                        month: end.month,
                        day: { $lte: end.day },
                    },
                ],
            },
        ],
    };
};

const getGroupSessionsByHour = (
    _id: { range: string; day: string; month: string; year: string } | string,
): { listGroup: object; countGroup: object } => {
    const isOpenSession = { $not: ['$session.closedAt'] };
    const isAfterLoginAt = { $gte: ['$range', { $hour: '$session.loginAt' }] };
    const isBeforeClosedAt = { $lte: ['$range', { $hour: '$session.closedAt' }] };

    const listGroup = {
        $group: {
            _id,
            usersList: {
                $addToSet: {
                    $cond: [
                        {
                            $or: [{ $and: [isOpenSession, isAfterLoginAt] }, { $and: [isAfterLoginAt, isBeforeClosedAt] }],
                        },
                        '$session.userId',
                        '$$REMOVE',
                    ],
                },
            },
        },
    };

    const countGroup = {
        $addFields: {
            users: { $size: '$usersList' },
        },
    };

    return { listGroup, countGroup };
};

const getSortByFullDate = (): { year: number; month: number; day: number } => ({
    year: -1,
    month: -1,
    day: -1,
});

const getProjectionByFullDate = (): { day: string; month: string; year: string } => ({
    day: '$_id.day',
    month: '$_id.month',
    year: '$_id.year',
});

const getProjectionFullDate = (): { day: string; month: string; year: string } => ({
    day: '$day',
    month: '$month',
    year: '$year',
});

export const aggregates = {
    dailySessions(
        collection: Collection<ISession>,
        { start, end }: DestructuredRange,
    ): AggregationCursor<
        Pick<ISession, 'mostImportantRole' | 'userId' | 'day' | 'year' | 'month' | 'type'> & {
            time: number;
            sessions: number;
            devices: ISession['device'][];
            _computedAt: string;
        }
    > {
        const pipeline = [
            {
                $match: {
                    userId: { $exists: true },
                    lastActivityAt: { $exists: true },
                    device: { $exists: true },
                    type: 'session',
                    ...matchBasedOnDate(start, end),
                },
            },
            {
                $project: {
                    userId: 1,
                    device: 1,
                    day: 1,
                    month: 1,
                    year: 1,
                    mostImportantRole: 1,
                    time: { $trunc: { $divide: [{ $subtract: ['$lastActivityAt', '$loginAt'] }, 1000] } },
                },
            },
            {
                $match: {
                    time: { $gt: 0 },
                },
            },
            {
                $group: {
                    _id: {
                        userId: '$userId',
                        device: '$device',
                        ...getProjectionFullDate(),
                    },
                    mostImportantRole: { $first: '$mostImportantRole' },
                    time: { $sum: '$time' },
                    sessions: { $sum: 1 },
                },
            },
            {
                $sort: {
                    time: -1,
                },
            },
            {
                $group: {
                    _id: {
                        userId: '$_id.userId',
                        ...getProjectionByFullDate(),
                    },
                    mostImportantRole: { $first: '$mostImportantRole' },
                    time: { $sum: '$time' },
                    sessions: { $sum: '$sessions' },
                    devices: {
                        $push: {
                            sessions: '$sessions',
                            time: '$time',
                            device: '$_id.device',
                        },
                    },
                },
            },
            {
                $sort: {
                    _id: 1,
                },
            },
            {
                $project: {
                    _id: 0,
                    type: { $literal: 'user_daily' },
                    _computedAt: { $literal: new Date() },
                    ...getProjectionByFullDate(),
                    userId: '$_id.userId',
                    mostImportantRole: 1,
                    time: 1,
                    sessions: 1,
                    devices: 1,
                },
            },
        ];

        return collection.aggregate<
            Pick<ISession, 'mostImportantRole' | 'userId' | 'day' | 'year' | 'month' | 'type'> & {
                time: number;
                sessions: number;
                devices: ISession['device'][];
                _computedAt: string;
            }
        >(pipeline, { allowDiskUse: true });
    },

    async getUniqueUsersOfYesterday(
        collection: Collection<ISession>,
        { year, month, day }: DestructuredDate,
    ): Promise<UserSessionAggregation[]> {
        return collection
            .aggregate<UserSessionAggregation>([
                {
                    $match: {
                        year,
                        month,
                        day,
                        type: 'user_daily',
                    },
                },
                {
                    $group: {
                        _id: {
                            ...getProjectionFullDate(),
                            mostImportantRole: '$mostImportantRole',
                        },
                        count: {
                            $sum: 1,
                        },
                        sessions: {
                            $sum: '$sessions',
                        },
                        time: {
                            $sum: '$time',
                        },
                    },
                },
                {
                    $group: {
                        _id: {
                            ...getProjectionFullDate(),
                        },
                        roles: {
                            $push: {
                                role: '$_id.mostImportantRole',
                                count: '$count',
                                sessions: '$sessions',
                                time: '$time',
                            },
                        },
                        count: {
                            $sum: '$count',
                        },
                        sessions: {
                            $sum: '$sessions',
                        },
                        time: {
                            $sum: '$time',
                        },
                    },
                },
                {
                    $project: {
                        _id: 0,
                        count: 1,
                        sessions: 1,
                        time: 1,
                        roles: 1,
                    },
                },
            ])
            .toArray();
    },

    async getUniqueUsersOfLastMonthOrWeek(
        collection: Collection<ISession>,
        { year, month, day, type = 'month' }: DestructuredDateWithType,
    ): Promise<UserSessionAggregation[]> {
        return collection
            .aggregate<UserSessionAggregation>(
                [
                    {
                        $match: {
                            type: 'user_daily',
                            ...aggregates.getMatchOfLastMonthOrWeek({ year, month, day, type }),
                        },
                    },
                    {
                        $group: {
                            _id: {
                                userId: '$userId',
                            },
                            mostImportantRole: { $first: '$mostImportantRole' },
                            sessions: {
                                $sum: '$sessions',
                            },
                            time: {
                                $sum: '$time',
                            },
                        },
                    },
                    {
                        $group: {
                            _id: {
                                mostImportantRole: '$mostImportantRole',
                            },
                            count: {
                                $sum: 1,
                            },
                            sessions: {
                                $sum: '$sessions',
                            },
                            time: {
                                $sum: '$time',
                            },
                        },
                    },
                    {
                        $sort: {
                            time: -1,
                        },
                    },
                    {
                        $group: {
                            _id: 1,
                            roles: {
                                $push: {
                                    role: '$_id.mostImportantRole',
                                    count: '$count',
                                    sessions: '$sessions',
                                    time: '$time',
                                },
                            },
                            count: {
                                $sum: '$count',
                            },
                            sessions: {
                                $sum: '$sessions',
                            },
                            time: {
                                $sum: '$time',
                            },
                        },
                    },
                    {
                        $project: {
                            _id: 0,
                            count: 1,
                            roles: 1,
                            sessions: 1,
                            time: 1,
                        },
                    },
                ],
                { allowDiskUse: true },
            )
            .toArray();
    },

    getMatchOfLastMonthOrWeek({ year, month, day, type = 'month' }: DestructuredDateWithType): Filter<ISession> {
        let startOfPeriod;

        if (type === 'month') {
            const pastMonthLastDay = new Date(year, month - 1, 0).getDate();
            const currMonthLastDay = new Date(year, month, 0).getDate();

            startOfPeriod = new Date(year, month - 1, day);
            startOfPeriod.setMonth(
                startOfPeriod.getMonth() - 1,
                (currMonthLastDay === day ? pastMonthLastDay : Math.min(pastMonthLastDay, day)) + 1,
            );
        } else {
            startOfPeriod = new Date(year, month - 1, day - 6);
        }

        const startOfPeriodObject = {
            year: startOfPeriod.getFullYear(),
            month: startOfPeriod.getMonth() + 1,
            day: startOfPeriod.getDate(),
        };

        if (year === startOfPeriodObject.year && month === startOfPeriodObject.month) {
            return {
                year,
                month,
                day: { $gte: startOfPeriodObject.day, $lte: day },
            };
        }

        if (year === startOfPeriodObject.year) {
            return {
                year,
                $and: [
                    {
                        $or: [
                            {
                                month: { $gt: startOfPeriodObject.month },
                            },
                            {
                                month: startOfPeriodObject.month,
                                day: { $gte: startOfPeriodObject.day },
                            },
                        ],
                    },
                    {
                        $or: [
                            {
                                month: { $lt: month },
                            },
                            {
                                month,
                                day: { $lte: day },
                            },
                        ],
                    },
                ],
            };
        }

        return {
            $and: [
                {
                    $or: [
                        {
                            year: { $gt: startOfPeriodObject.year },
                        },
                        {
                            year: startOfPeriodObject.year,
                            month: { $gt: startOfPeriodObject.month },
                        },
                        {
                            year: startOfPeriodObject.year,
                            month: startOfPeriodObject.month,
                            day: { $gte: startOfPeriodObject.day },
                        },
                    ],
                },
                {
                    $or: [
                        {
                            year: { $lt: year },
                        },
                        {
                            year,
                            month: { $lt: month },
                        },
                        {
                            year,
                            month,
                            day: { $lte: day },
                        },
                    ],
                },
            ],
        };
    },

    async getUniqueDevicesOfLastMonthOrWeek(
        collection: Collection<ISession>,
        { year, month, day, type = 'month' }: DestructuredDateWithType,
    ): Promise<DeviceSessionAggregation[]> {
        return collection
            .aggregate<DeviceSessionAggregation>(
                [
                    {
                        $match: {
                            type: 'user_daily',
                            ...aggregates.getMatchOfLastMonthOrWeek({ year, month, day, type }),
                        },
                    },
                    {
                        $unwind: '$devices',
                    },
                    {
                        $group: {
                            _id: {
                                type: '$devices.device.type',
                                name: '$devices.device.name',
                                version: '$devices.device.version',
                            },
                            count: {
                                $sum: '$devices.sessions',
                            },
                            time: {
                                $sum: '$devices.time',
                            },
                        },
                    },
                    {
                        $sort: {
                            time: -1,
                        },
                    },
                    {
                        $project: {
                            _id: 0,
                            type: '$_id.type',
                            name: '$_id.name',
                            version: '$_id.version',
                            count: 1,
                            time: 1,
                        },
                    },
                ],
                { allowDiskUse: true },
            )
            .toArray();
    },

    getUniqueDevicesOfYesterday(
        collection: Collection<ISession>,
        { year, month, day }: DestructuredDate,
    ): Promise<DeviceSessionAggregation[]> {
        return collection
            .aggregate<DeviceSessionAggregation>([
                {
                    $match: {
                        year,
                        month,
                        day,
                        type: 'user_daily',
                    },
                },
                {
                    $unwind: '$devices',
                },
                {
                    $group: {
                        _id: {
                            type: '$devices.device.type',
                            name: '$devices.device.name',
                            version: '$devices.device.version',
                        },
                        count: {
                            $sum: '$devices.sessions',
                        },
                        time: {
                            $sum: '$devices.time',
                        },
                    },
                },
                {
                    $sort: {
                        time: -1,
                    },
                },
                {
                    $project: {
                        _id: 0,
                        type: '$_id.type',
                        name: '$_id.name',
                        version: '$_id.version',
                        count: 1,
                        time: 1,
                    },
                },
            ])
            .toArray();
    },

    getUniqueOSOfLastMonthOrWeek(
        collection: Collection<ISession>,
        { year, month, day, type = 'month' }: DestructuredDateWithType,
    ): Promise<OSSessionAggregation[]> {
        return collection
            .aggregate<OSSessionAggregation>(
                [
                    {
                        $match: {
                            'type': 'user_daily',
                            'devices.device.os.name': {
                                $exists: true,
                            },
                            ...aggregates.getMatchOfLastMonthOrWeek({ year, month, day, type }),
                        },
                    },
                    {
                        $unwind: '$devices',
                    },
                    {
                        $group: {
                            _id: {
                                name: '$devices.device.os.name',
                                version: '$devices.device.os.version',
                            },
                            count: {
                                $sum: '$devices.sessions',
                            },
                            time: {
                                $sum: '$devices.time',
                            },
                        },
                    },
                    {
                        $sort: {
                            time: -1,
                        },
                    },
                    {
                        $project: {
                            _id: 0,
                            name: '$_id.name',
                            version: '$_id.version',
                            count: 1,
                            time: 1,
                        },
                    },
                ],
                { allowDiskUse: true },
            )
            .toArray();
    },

    getUniqueOSOfYesterday(collection: Collection<ISession>, { year, month, day }: DestructuredDate): Promise<OSSessionAggregation[]> {
        return collection
            .aggregate<OSSessionAggregation>([
                {
                    $match: {
                        year,
                        month,
                        day,
                        'type': 'user_daily',
                        'devices.device.os.name': {
                            $exists: true,
                        },
                    },
                },
                {
                    $unwind: '$devices',
                },
                {
                    $group: {
                        _id: {
                            name: '$devices.device.os.name',
                            version: '$devices.device.os.version',
                        },
                        count: {
                            $sum: '$devices.sessions',
                        },
                        time: {
                            $sum: '$devices.time',
                        },
                    },
                },
                {
                    $sort: {
                        time: -1,
                    },
                },
                {
                    $project: {
                        _id: 0,
                        name: '$_id.name',
                        version: '$_id.version',
                        count: 1,
                        time: 1,
                    },
                },
            ])
            .toArray();
    },
};

export class SessionsRaw extends BaseRaw<ISession> implements ISessionsModel {
    private secondaryCollection: Collection<ISession>;

    constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ISession>>) {
        super(db, 'sessions', trash);

        this.secondaryCollection = db.collection(getCollectionName('sessions'), { readPreference: readSecondaryPreferred(db) });
    }

    async aggregateSessionsByUserId({
        uid,
        sort,
        search,
        offset = 0,
        count = 10,
    }: {
        uid: string;
        sort?: Record<CustomSortOp, 1 | -1>;
        search?: string | null;
        offset?: number;
        count?: number;
    }): Promise<PaginatedResult<{ sessions: DeviceManagementSession[] }>> {
        const searchQuery = search ? [{ searchTerm: { $regex: search, $options: 'i' } }] : [];

        const matchOperator = {
            $match: {
                $and: [
                    ...searchQuery,
                    {
                        userId: {
                            $eq: uid,
                        },
                    },
                    {
                        loginToken: {
                            $exists: true,
                            $ne: '',
                        },
                    },
                    {
                        logoutAt: {
                            $exists: false,
                        },
                    },
                ],
            },
        };

        const sortOperator = {
            $sort: {
                loginAt: -1,
            },
        };
        const customSortOp = !sort ? [] : [{ $sort: sort }];

        const groupOperator = {
            $group: {
                _id: '$loginToken',
                sessionId: {
                    $first: '$sessionId',
                },
                userId: {
                    $first: '$userId',
                },
                device: {
                    $first: '$device',
                },
                host: {
                    $first: '$host',
                },
                ip: {
                    $first: '$ip',
                },
                loginAt: {
                    $first: '$loginAt',
                },
            },
        };

        const skipOperator = offset >= 1 ? [{ $skip: offset }] : [];
        const limitOperator = { $limit: count };

        const projectOperator = {
            $project: {
                _id: '$sessionId',
                sessionId: 1,
                userId: 1,
                device: 1,
                host: 1,
                ip: 1,
                loginAt: 1,
            },
        };

        const facetOperator = {
            $facet: {
                docs: [sortOperator, ...skipOperator, limitOperator, ...customSortOp],
                count: [
                    {
                        $count: 'total',
                    },
                ],
            },
        };

        const queryArray = [matchOperator, sortOperator, groupOperator, projectOperator, facetOperator];

        const [
            {
                docs: sessions,
                count: [{ total } = { total: 0 }],
            },
        ] = await this.col.aggregate<WithItemCount<{ docs: DeviceManagementSession[] }>>(queryArray).toArray();

        return { sessions, total, count, offset };
    }

    async aggregateSessionsAndPopulate({
        sort,
        search,
        offset = 0,
        count = 10,
    }: {
        sort?: Record<CustomSortOpAdmin, 1 | -1>;
        search?: string | null;
        offset?: number;
        count?: number;
    }): Promise<PaginatedResult<{ sessions: DeviceManagementPopulatedSession[] }>> {
        const searchQuery = search ? [{ searchTerm: { $regex: search, $options: 'i' } }] : [];

        const matchOperator = {
            $match: {
                $and: [
                    ...searchQuery,
                    {
                        loginToken: {
                            $exists: true,
                            $ne: '',
                        },
                        sessionId: {
                            $exists: true,
                            $ne: '',
                        },
                    },
                    {
                        logoutAt: {
                            $exists: false,
                        },
                    },
                ],
            },
        };

        const sortOperator = {
            $sort: {
                loginAt: -1,
            },
        };
        const customSortOp = !sort ? [] : [{ $sort: sort }];

        const groupOperator = {
            $group: {
                _id: '$loginToken',
                sessionId: {
                    $first: '$sessionId',
                },
                userId: {
                    $first: '$userId',
                },
                device: {
                    $first: '$device',
                },
                host: {
                    $first: '$host',
                },
                ip: {
                    $first: '$ip',
                },
                loginAt: {
                    $first: '$loginAt',
                },
            },
        };
        const limitOperator = { $limit: count };

        const skipOperator = offset >= 1 ? [{ $skip: offset }] : [];

        const lookupOperator = {
            $lookup: {
                from: 'users',
                localField: 'userId',
                foreignField: '_id',
                as: '_user',
            },
        };
        const unwindOperator = {
            $unwind: {
                path: '$_user',
                preserveNullAndEmptyArrays: true,
            },
        };

        const projectOperator = {
            $project: {
                _id: '$sessionId',
                sessionId: 1,
                device: 1,
                host: 1,
                ip: 1,
                loginAt: 1,
                userId: 1,
                _user: {
                    name: 1,
                    username: 1,
                    avatarETag: 1,
                    avatarOrigin: 1,
                },
            },
        };

        const facetOperator = {
            $facet: {
                docs: [sortOperator, ...skipOperator, limitOperator, lookupOperator, unwindOperator, projectOperator, ...customSortOp],
                count: [
                    {
                        $count: 'total',
                    },
                ],
            },
        };

        const queryArray = [matchOperator, sortOperator, groupOperator, facetOperator];

        const [
            {
                docs: sessions,
                count: [{ total } = { total: 0 }],
            },
        ] = await this.col.aggregate<WithItemCount<{ docs: DeviceManagementPopulatedSession[] }>>(queryArray).toArray();

        return { sessions, total, count, offset };
    }

    protected modelIndexes(): IndexDescription[] {
        return [
            { key: { createdAt: -1 } },
            { key: { loginAt: -1 } },
            { key: { searchTerm: 1 }, partialFilterExpression: { searchTerm: { $exists: true } }, background: true },
            { key: { ip: 1, loginAt: -1 } },
            { key: { userId: 1, sessionId: 1 } },
            { key: { type: 1, year: 1, month: 1, day: 1 } },
            { key: { sessionId: 1, instanceId: 1, year: 1, month: 1, day: 1 } },
            { key: { _computedAt: 1 }, expireAfterSeconds: 60 * 60 * 24 * 45 },
            {
                key: { 'loginToken': 1, 'logoutAt': 1, 'userId': 1, 'device.name': 1, 'device.os.name': 1, 'logintAt': -1 },
                partialFilterExpression: { loginToken: { $exists: true } },
                background: true,
            },
        ];
    }

    async getActiveUsersBetweenDates({ start, end }: DestructuredRange): Promise<ISession[]> {
        return this.col
            .aggregate<ISession>([
                {
                    $match: {
                        ...matchBasedOnDate(start, end),
                        type: 'user_daily',
                    },
                },
                {
                    $group: {
                        _id: '$userId',
                    },
                },
            ])
            .toArray();
    }

    async findLastLoginByIp(ip: string): Promise<ISession | null> {
        return this.findOne(
            {
                ip,
            },
            {
                sort: { loginAt: -1 },
                limit: 1,
            },
        );
    }

    findOneBySessionId(sessionId: string): Promise<ISession | null> {
        return this.findOne({ sessionId });
    }

    findOneBySessionIdAndUserId(sessionId: string, userId: string): Promise<ISession | null> {
        return this.findOne({ sessionId, userId, loginToken: { $exists: true, $ne: '' } });
    }

    findSessionsNotClosedByDateWithoutLastActivity({ year, month, day }: DestructuredDate): FindCursor<ISession> {
        return this.find({
            year,
            month,
            day,
            type: 'session',
            closedAt: { $exists: false },
            lastActivityAt: { $exists: false },
        });
    }

    async getActiveUsersOfPeriodByDayBetweenDates({ start, end }: DestructuredRange): Promise<
        {
            day: number;
            month: number;
            year: number;
            usersList: IUser['_id'][];
            users: number;
        }[]
    > {
        return this.col
            .aggregate<{
                day: number;
                month: number;
                year: number;
                usersList: IUser['_id'][];
                users: number;
            }>([
                {
                    $match: {
                        ...matchBasedOnDate(start, end),
                        type: 'user_daily',
                        mostImportantRole: { $ne: 'anonymous' },
                    },
                },
                {
                    $group: {
                        _id: {
                            ...getProjectionFullDate(),
                            userId: '$userId',
                        },
                    },
                },
                {
                    $group: {
                        _id: {
                            ...getProjectionByFullDate(),
                        },
                        usersList: {
                            $addToSet: '$_id.userId',
                        },
                        users: { $sum: 1 },
                    },
                },
                {
                    $project: {
                        _id: 0,
                        ...getProjectionByFullDate(),
                        usersList: 1,
                        users: 1,
                    },
                },
                {
                    $sort: {
                        ...getSortByFullDate(),
                    },
                },
            ])
            .toArray();
    }

    async getBusiestTimeWithinHoursPeriod({ start, end, groupSize }: DateRange & { groupSize: number }): Promise<
        {
            hour: number;
            users: number;
        }[]
    > {
        const match = {
            $match: {
                type: 'computed-session',
                loginAt: { $gte: start, $lte: end },
            },
        };
        const rangeProject = {
            $project: {
                range: {
                    $range: [0, 24, groupSize],
                },
                session: '$$ROOT',
            },
        };
        const unwind = {
            $unwind: '$range',
        };
        const groups = getGroupSessionsByHour('$range');
        const presentationProject = {
            $project: {
                _id: 0,
                hour: '$_id',
                users: 1,
            },
        };
        const sort = {
            $sort: {
                hour: -1,
            },
        };
        return this.col
            .aggregate<{
                hour: number;
                users: number;
            }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort])
            .toArray();
    }

    async getTotalOfSessionsByDayBetweenDates({ start, end }: DestructuredRange): Promise<
        {
            day: number;
            month: number;
            year: number;
            users: number;
        }[]
    > {
        return this.col
            .aggregate<{
                day: number;
                month: number;
                year: number;
                users: number;
            }>([
                {
                    $match: {
                        ...matchBasedOnDate(start, end),
                        type: 'user_daily',
                        mostImportantRole: { $ne: 'anonymous' },
                    },
                },
                {
                    $group: {
                        _id: { ...getProjectionFullDate() },
                        users: { $sum: 1 },
                    },
                },
                {
                    $project: {
                        _id: 0,
                        ...getProjectionByFullDate(),
                        users: 1,
                    },
                },
                {
                    $sort: {
                        ...getSortByFullDate(),
                    },
                },
            ])
            .toArray();
    }

    async getTotalOfSessionByHourAndDayBetweenDates({ start, end }: DateRange): Promise<
        {
            hour: number;
            day: number;
            month: number;
            year: number;
            users: number;
        }[]
    > {
        const match = {
            $match: {
                type: 'computed-session',
                loginAt: { $gte: start, $lte: end },
            },
        };
        const rangeProject = {
            $project: {
                range: {
                    $range: [{ $hour: '$loginAt' }, { $sum: [{ $ifNull: [{ $hour: '$closedAt' }, 23] }, 1] }],
                },
                session: '$$ROOT',
            },
        };
        const unwind = {
            $unwind: '$range',
        };
        const groups = getGroupSessionsByHour({
            range: '$range',
            day: '$session.day',
            month: '$session.month',
            year: '$session.year',
        });
        const presentationProject = {
            $project: {
                _id: 0,
                hour: '$_id.range',
                ...getProjectionByFullDate(),
                users: 1,
            },
        };
        const sort = {
            $sort: {
                ...getSortByFullDate(),
                hour: -1,
            },
        };

        return this.col
            .aggregate<{
                hour: number;
                day: number;
                month: number;
                year: number;
                users: number;
            }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort])
            .toArray();
    }

    async getUniqueUsersOfYesterday(): Promise<UserSessionAggregationResult> {
        const date = new Date();
        date.setDate(date.getDate() - 1);

        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        const day = date.getDate();

        return {
            year,
            month,
            day,
            data: await aggregates.getUniqueUsersOfYesterday(this.secondaryCollection, {
                year,
                month,
                day,
            }),
        };
    }

    async getUniqueUsersOfLastMonth(): Promise<UserSessionAggregationResult> {
        const date = new Date();
        date.setDate(date.getDate() - 1);

        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        const day = date.getDate();

        return {
            year,
            month,
            day,
            data: await aggregates.getUniqueUsersOfLastMonthOrWeek(this.secondaryCollection, {
                year,
                month,
                day,
            }),
        };
    }

    async getUniqueUsersOfLastWeek(): Promise<UserSessionAggregationResult> {
        const date = new Date();
        date.setDate(date.getDate() - 1);

        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        const day = date.getDate();

        return {
            year,
            month,
            day,
            data: await aggregates.getUniqueUsersOfLastMonthOrWeek(this.secondaryCollection, {
                year,
                month,
                day,
                type: 'week',
            }),
        };
    }

    async getUniqueDevicesOfYesterday(): Promise<DeviceSessionAggregationResult> {
        const date = new Date();
        date.setDate(date.getDate() - 1);

        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        const day = date.getDate();

        return {
            year,
            month,
            day,
            data: await aggregates.getUniqueDevicesOfYesterday(this.secondaryCollection, {
                year,
                month,
                day,
            }),
        };
    }

    async getUniqueDevicesOfLastMonth(): Promise<DeviceSessionAggregationResult> {
        const date = new Date();
        date.setDate(date.getDate() - 1);

        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        const day = date.getDate();

        return {
            year,
            month,
            day,
            data: await aggregates.getUniqueDevicesOfLastMonthOrWeek(this.secondaryCollection, {
                year,
                month,
                day,
            }),
        };
    }

    async getUniqueDevicesOfLastWeek(): Promise<DeviceSessionAggregationResult> {
        const date = new Date();
        date.setDate(date.getDate() - 1);

        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        const day = date.getDate();

        return {
            year,
            month,
            day,
            data: await aggregates.getUniqueDevicesOfLastMonthOrWeek(this.secondaryCollection, {
                year,
                month,
                day,
                type: 'week',
            }),
        };
    }

    async getUniqueOSOfYesterday(): Promise<OSSessionAggregationResult> {
        const date = new Date();
        date.setDate(date.getDate() - 1);

        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        const day = date.getDate();

        return {
            year,
            month,
            day,
            data: await aggregates.getUniqueOSOfYesterday(this.secondaryCollection, { year, month, day }),
        };
    }

    async getUniqueOSOfLastMonth(): Promise<OSSessionAggregationResult> {
        const date = new Date();
        date.setDate(date.getDate() - 1);

        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        const day = date.getDate();

        return {
            year,
            month,
            day,
            data: await aggregates.getUniqueOSOfLastMonthOrWeek(this.secondaryCollection, {
                year,
                month,
                day,
            }),
        };
    }

    async getUniqueOSOfLastWeek(): Promise<OSSessionAggregationResult> {
        const date = new Date();
        date.setDate(date.getDate() - 1);

        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        const day = date.getDate();

        return {
            year,
            month,
            day,
            data: await aggregates.getUniqueOSOfLastMonthOrWeek(this.secondaryCollection, {
                year,
                month,
                day,
                type: 'week',
            }),
        };
    }

    private isValidData(data: Omit<ISession, '_id' | 'createdAt' | '_updatedAt'>): boolean {
        return Boolean(data.year && data.month && data.day && data.sessionId && data.instanceId);
    }

    async createOrUpdate(data: Omit<ISession, '_id' | 'createdAt' | '_updatedAt'>): Promise<UpdateResult | undefined> {
        // TODO: check if we should create a session when there is no loginToken or not
        const { year, month, day, sessionId, instanceId } = data;

        if (!this.isValidData(data)) {
            return;
        }

        const now = new Date();

        return this.updateOne(
            { instanceId, sessionId, year, month, day },
            {
                $set: data,
                $setOnInsert: {
                    createdAt: now,
                },
            },
            { upsert: true },
        );
    }

    async closeByInstanceIdAndSessionId(instanceId: string, sessionId: string): Promise<UpdateResult> {
        const query = {
            instanceId,
            sessionId,
            closedAt: { $exists: false },
        };

        const closeTime = new Date();
        const update = {
            $set: {
                closedAt: closeTime,
                lastActivityAt: closeTime,
            },
        };

        return this.updateOne(query, update);
    }

    async updateActiveSessionsByDateAndInstanceIdAndIds(
        { year, month, day }: Partial<DestructuredDate> = {},
        instanceId: string,
        sessions: string[],
        data: Record<string, any> = {},
    ): Promise<UpdateResult | Document> {
        const query = {
            instanceId,
            year,
            month,
            day,
            sessionId: { $in: sessions },
            closedAt: { $exists: false },
        };

        const update = {
            $set: data,
        };

        return this.updateMany(query, update);
    }

    async updateActiveSessionsByDate(
        { year, month, day }: DestructuredDate,
        data: Record<string, any> = {},
    ): Promise<UpdateResult | Document> {
        const update = {
            $set: data,
        };

        return this.updateMany(
            {
                year,
                month,
                day,
                type: 'session',
                closedAt: { $exists: false },
                lastActivityAt: { $exists: false },
            },
            update,
        );
    }

    async logoutByInstanceIdAndSessionIdAndUserId(instanceId: string, sessionId: string, userId: string): Promise<UpdateResult> {
        const query = {
            instanceId,
            sessionId,
            userId,
            logoutAt: { $exists: false },
        };

        const logoutAt = new Date();
        const update = {
            $set: {
                logoutAt,
                lastActivityAt: logoutAt,
            },
        };

        return this.updateOne(query, update);
    }

    async logoutBySessionIdAndUserId({
        sessionId,
        userId,
    }: {
        sessionId: ISession['sessionId'];
        userId: IUser['_id'];
    }): Promise<UpdateResult | Document> {
        const query = {
            sessionId,
            userId,
            logoutAt: { $exists: false },
        };
        const session = await this.findOne<Pick<ISession, 'loginToken'>>(query, { projection: { loginToken: 1 } });

        const logoutAt = new Date();
        const updateObj = {
            $set: {
                logoutAt,
                lastActivityAt: logoutAt,
                logoutBy: userId,
            },
        };

        return this.updateMany({ userId, loginToken: session?.loginToken }, updateObj);
    }

    async logoutByloginTokenAndUserId({
        loginToken,
        userId,
        logoutBy,
    }: {
        loginToken: ISession['loginToken'];
        userId: IUser['_id'];
        logoutBy?: IUser['_id'];
    }): Promise<UpdateResult | Document> {
        const logoutAt = new Date();
        const updateObj = {
            $set: {
                logoutAt,
                logoutBy: logoutBy || userId,
            },
        };

        return this.updateMany({ userId, loginToken }, updateObj);
    }

    async createBatch(sessions: OptionalId<ISession>[]): Promise<BulkWriteResult | undefined> {
        if (!sessions || sessions.length === 0) {
            return;
        }

        const ops: AnyBulkWriteOperation<ISession>[] = [];
        sessions.forEach((doc) => {
            const { year, month, day, sessionId, instanceId } = doc;
            delete doc._id;
            if (this.isValidData(doc)) {
                ops.push({
                    updateOne: {
                        filter: { year, month, day, sessionId, instanceId },
                        update: {
                            $set: doc,
                        },
                        upsert: true,
                    },
                });
            }
        });

        return this.col.bulkWrite(ops, { ordered: false });
    }

    async updateDailySessionById(_id: ISession['_id'], record: Partial<ISession>): Promise<UpdateResult> {
        return this.updateOne({ _id }, { $set: record }, { upsert: true });
    }

    async updateAllSessionsByDateToComputed({ start, end }: DestructuredRange): Promise<UpdateResult | Document> {
        return this.updateMany(
            {
                type: 'session',
                ...matchBasedOnDate(start, end),
            },
            {
                $set: {
                    type: 'computed-session',
                    _computedAt: new Date(),
                },
            },
        );
    }
}