snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
This commit is contained in:
3217
packages/models/CHANGELOG.md
Normal file
3217
packages/models/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
6
packages/models/jest.config.ts
Normal file
6
packages/models/jest.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import server from '@rocket.chat/jest-presets/server';
|
||||
import type { Config } from 'jest';
|
||||
|
||||
export default {
|
||||
preset: server.preset,
|
||||
} satisfies Config;
|
||||
43
packages/models/package.json
Normal file
43
packages/models/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@rocket.chat/models",
|
||||
"version": "2.5.1",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"/dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsc",
|
||||
"dev": "tsc --watch --preserveWatchOutput",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"test": "jest",
|
||||
"testunit": "jest",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rocket.chat/core-typings": "workspace:^",
|
||||
"@rocket.chat/model-typings": "workspace:~",
|
||||
"@rocket.chat/random": "workspace:^",
|
||||
"@rocket.chat/rest-typings": "workspace:^",
|
||||
"@rocket.chat/sha256": "workspace:^",
|
||||
"@rocket.chat/tools": "workspace:~",
|
||||
"@rocket.chat/tracing": "workspace:^",
|
||||
"date-fns": "~4.1.0",
|
||||
"node-rsa": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rocket.chat/jest-presets": "workspace:~",
|
||||
"@rocket.chat/tsconfig": "workspace:*",
|
||||
"@types/jest": "~30.0.0",
|
||||
"@types/node-rsa": "^1.1.4",
|
||||
"eslint": "~9.39.5",
|
||||
"jest": "~30.2.0",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
223
packages/models/src/dummy/BaseDummy.ts
Normal file
223
packages/models/src/dummy/BaseDummy.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import type { RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type {
|
||||
DefaultFields,
|
||||
DocumentWithProjection,
|
||||
FindOptionsWithProjection,
|
||||
FindPaginated,
|
||||
IBaseModel,
|
||||
InsertionModel,
|
||||
DocumentWithDriverProjection,
|
||||
FindOneAndUpdateOptionsWithProjection,
|
||||
} from '@rocket.chat/model-typings';
|
||||
import type {
|
||||
BulkWriteOptions,
|
||||
ChangeStream,
|
||||
Collection,
|
||||
DeleteOptions,
|
||||
DeleteResult,
|
||||
Document,
|
||||
Filter,
|
||||
FindCursor,
|
||||
FindOptions,
|
||||
InsertManyResult,
|
||||
InsertOneOptions,
|
||||
InsertOneResult,
|
||||
UpdateFilter,
|
||||
UpdateOptions,
|
||||
UpdateResult,
|
||||
WithId,
|
||||
} from 'mongodb';
|
||||
|
||||
import { getCollectionName, UpdaterImpl } from '../index';
|
||||
import type { Updater } from '../updater';
|
||||
|
||||
export class BaseDummy<
|
||||
T extends { _id: string },
|
||||
C extends DefaultFields<T> = undefined,
|
||||
TDeleted extends RocketChatRecordDeleted<T> = RocketChatRecordDeleted<T>,
|
||||
> implements IBaseModel<T, C, TDeleted> {
|
||||
public readonly col: Collection<T>;
|
||||
|
||||
private collectionName: string;
|
||||
|
||||
constructor(protected name: string) {
|
||||
this.collectionName = getCollectionName(name);
|
||||
this.col = undefined as any;
|
||||
}
|
||||
|
||||
public async createIndexes(): Promise<string[] | void> {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
public getUpdater(): Updater<T> {
|
||||
return new UpdaterImpl<T>();
|
||||
}
|
||||
|
||||
public updateFromUpdater(query: Filter<T>, updater: Updater<T>): Promise<UpdateResult> {
|
||||
return this.updateOne(query, updater);
|
||||
}
|
||||
|
||||
getCollectionName(): string {
|
||||
return this.collectionName;
|
||||
}
|
||||
|
||||
async findOneAndDelete(): Promise<WithId<T> | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async findOneAndDeleteById(_id: T['_id']): Promise<WithId<T> | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async findOneAndUpdate<
|
||||
P extends Document = T,
|
||||
O extends FindOneAndUpdateOptionsWithProjection = FindOneAndUpdateOptionsWithProjection,
|
||||
>(): Promise<DocumentWithDriverProjection<P, O> | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async findOneById<P extends Document = T, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
_id: T['_id'],
|
||||
_options?: O,
|
||||
): Promise<DocumentWithProjection<P, O> | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async findOne<P extends Document = T, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
_query?: Filter<T> | T['_id'],
|
||||
_options?: O,
|
||||
): Promise<DocumentWithProjection<P, O> | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
find<P extends Document = T, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
_query?: Filter<T>,
|
||||
_options?: O,
|
||||
): FindCursor<DocumentWithProjection<P, O>> {
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
findPaginated<P extends Document = T, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
_query?: Filter<T>,
|
||||
_options?: O,
|
||||
): FindPaginated<FindCursor<DocumentWithProjection<P, O>>> {
|
||||
return {
|
||||
cursor: undefined as any,
|
||||
totalCount: Promise.resolve(0),
|
||||
};
|
||||
}
|
||||
|
||||
async update(
|
||||
filter: Filter<T>,
|
||||
update: UpdateFilter<T> | Partial<T>,
|
||||
options?: UpdateOptions & { multi?: true },
|
||||
): Promise<UpdateResult | Document> {
|
||||
return this.updateOne(filter, update, options);
|
||||
}
|
||||
|
||||
async updateOne(_filter: Filter<T>, _update: UpdateFilter<T> | Partial<T>, _options?: UpdateOptions): Promise<UpdateResult> {
|
||||
return {
|
||||
acknowledged: true,
|
||||
matchedCount: 0,
|
||||
modifiedCount: 0,
|
||||
upsertedCount: 0,
|
||||
upsertedId: '' as any,
|
||||
};
|
||||
}
|
||||
|
||||
async updateMany(filter: Filter<T>, update: UpdateFilter<T> | Partial<T>, options?: UpdateOptions): Promise<Document | UpdateResult> {
|
||||
return this.updateOne(filter, update, options);
|
||||
}
|
||||
|
||||
async insertMany(_docs: InsertionModel<T>[], _options?: BulkWriteOptions): Promise<InsertManyResult<T>> {
|
||||
return {
|
||||
acknowledged: true,
|
||||
insertedCount: 0,
|
||||
insertedIds: {},
|
||||
};
|
||||
}
|
||||
|
||||
async insertOne(_doc: InsertionModel<T>, _options?: InsertOneOptions): Promise<InsertOneResult<T>> {
|
||||
return {
|
||||
acknowledged: true,
|
||||
insertedId: '' as any,
|
||||
};
|
||||
}
|
||||
|
||||
async removeById(_id: T['_id']): Promise<DeleteResult> {
|
||||
return {
|
||||
acknowledged: true,
|
||||
deletedCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async removeByIds(_ids: T['_id'][]): Promise<DeleteResult> {
|
||||
return {
|
||||
acknowledged: true,
|
||||
deletedCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteOne(filter: Filter<T>, options?: DeleteOptions & { bypassDocumentValidation?: boolean }): Promise<DeleteResult> {
|
||||
return this.deleteMany(filter, options);
|
||||
}
|
||||
|
||||
async deleteMany(_filter: Filter<T>, _options?: DeleteOptions): Promise<DeleteResult> {
|
||||
return {
|
||||
acknowledged: true,
|
||||
deletedCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Trash
|
||||
trashFind<P extends TDeleted>(
|
||||
_query: Filter<TDeleted>,
|
||||
_options?: FindOptions<P extends TDeleted ? TDeleted : P>,
|
||||
): FindCursor<WithId<TDeleted>> | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
trashFindOneById(_id: TDeleted['_id']): Promise<TDeleted | null>;
|
||||
|
||||
trashFindOneById<P extends Document>(_id: TDeleted['_id'], options: FindOptions<P extends TDeleted ? TDeleted : P>): Promise<P | null>;
|
||||
|
||||
async trashFindOneById<P extends TDeleted>(
|
||||
_id: TDeleted['_id'],
|
||||
_options?: FindOptions<P extends TDeleted ? TDeleted : P>,
|
||||
): Promise<WithId<RocketChatRecordDeleted<P> | TDeleted> | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
trashFindDeletedAfter(deletedAt: Date): FindCursor<WithId<TDeleted>>;
|
||||
|
||||
trashFindDeletedAfter<P extends Document = TDeleted>(
|
||||
_deletedAt: Date,
|
||||
_query?: Filter<TDeleted>,
|
||||
_options?: FindOptions<P extends TDeleted ? TDeleted : P>,
|
||||
): FindCursor<WithId<TDeleted>> {
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
trashFindPaginatedDeletedAfter<P extends Document = TDeleted>(
|
||||
_deletedAt: Date,
|
||||
_query?: Filter<TDeleted>,
|
||||
_options?: FindOptions<P extends TDeleted ? TDeleted : P>,
|
||||
): FindPaginated<FindCursor<WithId<TDeleted>>> {
|
||||
return {
|
||||
cursor: undefined as any,
|
||||
totalCount: Promise.resolve(0),
|
||||
};
|
||||
}
|
||||
|
||||
watch(_pipeline?: object[]): ChangeStream<T> {
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
async countDocuments(): Promise<number> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
async estimatedDocumentCount(): Promise<number> {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
39
packages/models/src/dummy/ReadReceipts.ts
Normal file
39
packages/models/src/dummy/ReadReceipts.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { IReadReceipt } from '@rocket.chat/core-typings';
|
||||
import type { IReadReceiptsModel } from '@rocket.chat/model-typings';
|
||||
import type { FindCursor, DeleteResult } from 'mongodb';
|
||||
|
||||
import { BaseDummy } from './BaseDummy';
|
||||
|
||||
export class ReadReceiptsDummy extends BaseDummy<IReadReceipt> implements IReadReceiptsModel {
|
||||
constructor() {
|
||||
super('read_receipts');
|
||||
}
|
||||
|
||||
findByMessageId(_messageId: string): FindCursor<IReadReceipt> {
|
||||
return this.find({});
|
||||
}
|
||||
|
||||
removeByUserId(_userId: string): Promise<DeleteResult> {
|
||||
return this.deleteMany({});
|
||||
}
|
||||
|
||||
removeByRoomId(_roomId: string): Promise<DeleteResult> {
|
||||
return this.deleteMany({});
|
||||
}
|
||||
|
||||
removeByRoomIds(_roomIds: string[]): Promise<DeleteResult> {
|
||||
return this.deleteMany({});
|
||||
}
|
||||
|
||||
removeByMessageId(_messageId: string): Promise<DeleteResult> {
|
||||
return this.deleteMany({});
|
||||
}
|
||||
|
||||
removeByMessageIds(_messageIds: string[]): Promise<DeleteResult> {
|
||||
return this.deleteMany({});
|
||||
}
|
||||
|
||||
findOlderThan(_date: Date): FindCursor<IReadReceipt> {
|
||||
return this.find({});
|
||||
}
|
||||
}
|
||||
1
packages/models/src/helpers/index.ts
Normal file
1
packages/models/src/helpers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './omnichannel';
|
||||
51
packages/models/src/helpers/omnichannel/agentStatus.ts
Normal file
51
packages/models/src/helpers/omnichannel/agentStatus.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { UserStatus } from '@rocket.chat/core-typings';
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import type { Filter } from 'mongodb';
|
||||
|
||||
/**
|
||||
* Builds the query to find online and available agents for livechat auto-assignment.
|
||||
* * According to product rules, the primary conditions for auto-assignment are:
|
||||
* - User must have the 'livechat-agent' role.
|
||||
* - Livechat service status (`statusLivechat`) must be 'available'.
|
||||
* - User's global status must NOT be 'offline' Exceptions: Bots are exempt from this rule, and this check is skipped entirely if the `acceptChatsWithNoAgents` setting is enabled (allowing offline human agents to be assigned).
|
||||
* - If the "Accept new omnichannel requests when the agent is idle" (aka `Livechat_enabled_when_agent_idle`) setting is OFF,
|
||||
* then the statusConnection must NOT be 'away'.
|
||||
*/
|
||||
export const queryStatusAgentOnline = (
|
||||
extraFilters = {},
|
||||
isLivechatEnabledWhenAgentIdle?: boolean,
|
||||
acceptChatsWithNoAgents?: boolean,
|
||||
): Filter<IUser> => ({
|
||||
statusLivechat: 'available',
|
||||
roles: 'livechat-agent',
|
||||
// ignore deactivated users
|
||||
active: true,
|
||||
...(!acceptChatsWithNoAgents && {
|
||||
$or: [
|
||||
{ roles: 'bot' },
|
||||
{
|
||||
status: {
|
||||
$exists: true,
|
||||
$ne: UserStatus.OFFLINE,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
...extraFilters,
|
||||
...(isLivechatEnabledWhenAgentIdle === false && {
|
||||
statusConnection: { $ne: 'away' },
|
||||
}),
|
||||
});
|
||||
|
||||
export const queryAvailableAgentsForSelection = (
|
||||
extraFilters = {},
|
||||
isLivechatEnabledWhenAgentIdle?: boolean,
|
||||
acceptChatsWithNoAgents?: boolean,
|
||||
): Filter<IUser> => ({
|
||||
...queryStatusAgentOnline(extraFilters, isLivechatEnabledWhenAgentIdle, acceptChatsWithNoAgents),
|
||||
$and: [
|
||||
{
|
||||
$or: [{ agentLocked: { $exists: false } }, { agentLockedAt: { $lt: new Date(Date.now() - 5000) } }],
|
||||
},
|
||||
],
|
||||
});
|
||||
1
packages/models/src/helpers/omnichannel/index.ts
Normal file
1
packages/models/src/helpers/omnichannel/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './agentStatus';
|
||||
250
packages/models/src/index.ts
Normal file
250
packages/models/src/index.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import type { ILivechatDepartmentAgents, ILivechatInquiryRecord, ISubscription, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type {
|
||||
IAnalyticsModel,
|
||||
IAvatarsModel,
|
||||
IBannersDismissModel,
|
||||
IBannersModel,
|
||||
ICannedResponseModel,
|
||||
ICredentialTokensModel,
|
||||
ICustomSoundsModel,
|
||||
ICustomUserStatusModel,
|
||||
IEmailInboxModel,
|
||||
IEmailMessageHistoryModel,
|
||||
IEmojiCustomModel,
|
||||
IExportOperationsModel,
|
||||
IFederationKeysModel,
|
||||
IInstanceStatusModel,
|
||||
IIntegrationHistoryModel,
|
||||
IIntegrationsModel,
|
||||
IInvitesModel,
|
||||
IImportDataModel,
|
||||
ILivechatAgentActivityModel,
|
||||
ILivechatBusinessHoursModel,
|
||||
ILivechatContactsModel,
|
||||
ILivechatCustomFieldModel,
|
||||
ILivechatDepartmentAgentsModel,
|
||||
ILivechatDepartmentModel,
|
||||
ILivechatInquiryModel,
|
||||
ILivechatPriorityModel,
|
||||
ILivechatRoomsModel,
|
||||
ILivechatTagModel,
|
||||
ILivechatTriggerModel,
|
||||
ILivechatUnitModel,
|
||||
ILivechatUnitMonitorsModel,
|
||||
ILivechatVisitorsModel,
|
||||
ILoginServiceConfigurationModel,
|
||||
IMessagesModel,
|
||||
INotificationQueueModel,
|
||||
INpsModel,
|
||||
INpsVoteModel,
|
||||
IOAuthAppsModel,
|
||||
IOAuthAuthCodesModel,
|
||||
IOAuthAccessTokensModel,
|
||||
ILoginCodesModel,
|
||||
IOAuthRefreshTokensModel,
|
||||
IOEmbedCacheModel,
|
||||
IPushTokenModel,
|
||||
IPermissionsModel,
|
||||
IReadReceiptsModel,
|
||||
IMessageReadsModel,
|
||||
IReportsModel,
|
||||
IRolesModel,
|
||||
IRoomsModel,
|
||||
IServerEventsModel,
|
||||
ISessionsModel,
|
||||
ISettingsModel,
|
||||
ISmarshHistoryModel,
|
||||
IStatisticsModel,
|
||||
ISubscriptionsModel,
|
||||
ITeamMemberModel,
|
||||
ITeamModel,
|
||||
IUploadsModel,
|
||||
IUserDataFilesModel,
|
||||
IUsersSessionsModel,
|
||||
IUsersModel,
|
||||
IVideoConferenceModel,
|
||||
IWebdavAccountsModel,
|
||||
ICalendarEventModel,
|
||||
IOmnichannelServiceLevelAgreementsModel,
|
||||
IAppsModel,
|
||||
IAppsPersistenceModel,
|
||||
IAppLogsModel,
|
||||
IImportsModel,
|
||||
IAuditLogModel,
|
||||
ICronHistoryModel,
|
||||
IMigrationsModel,
|
||||
IModerationReportsModel,
|
||||
IWorkspaceCredentialsModel,
|
||||
IMediaCallsModel,
|
||||
IMediaCallNegotiationsModel,
|
||||
ICallHistoryModel,
|
||||
IAbacAttributesModel,
|
||||
ITwoFactorChallengesModel,
|
||||
ISamlUsedAssertionsModel,
|
||||
} from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db } from 'mongodb';
|
||||
|
||||
import {
|
||||
TeamMemberRaw,
|
||||
MessagesRaw,
|
||||
LivechatInquiryRaw,
|
||||
LivechatDepartmentAgentsRaw,
|
||||
PermissionsRaw,
|
||||
LoginServiceConfigurationRaw,
|
||||
InstanceStatusRaw,
|
||||
IntegrationHistoryRaw,
|
||||
IntegrationsRaw,
|
||||
EmailInboxRaw,
|
||||
LivechatRoomsRaw,
|
||||
LivechatPriorityRaw,
|
||||
UploadsRaw,
|
||||
LivechatVisitorsRaw,
|
||||
RolesRaw,
|
||||
RoomsRaw,
|
||||
SubscriptionsRaw,
|
||||
TeamRaw,
|
||||
UsersRaw,
|
||||
UsersSessionsRaw,
|
||||
AbacAttributesRaw,
|
||||
ServerEventsRaw,
|
||||
SamlUsedAssertionsRaw,
|
||||
CronHistoryRaw,
|
||||
} from './modelClasses';
|
||||
import { proxify, registerModel } from './proxify';
|
||||
|
||||
const prefix = 'rocketchat_';
|
||||
export function getCollectionName(name: string): string {
|
||||
return `${prefix}${name}`;
|
||||
}
|
||||
|
||||
export * from './modelClasses';
|
||||
|
||||
export * from './dummy/ReadReceipts';
|
||||
|
||||
export * from './helpers';
|
||||
|
||||
export { registerModel } from './proxify';
|
||||
export { type Updater, UpdaterImpl } from './updater';
|
||||
export { readSecondaryPreferred } from './readSecondaryPreferred';
|
||||
|
||||
export const Apps = proxify<IAppsModel>('IAppsModel');
|
||||
export const AppsPersistence = proxify<IAppsPersistenceModel>('IAppsPersistenceModel');
|
||||
export const AppLogs = proxify<IAppLogsModel>('IAppLogsModel');
|
||||
export const Analytics = proxify<IAnalyticsModel>('IAnalyticsModel');
|
||||
export const Avatars = proxify<IAvatarsModel>('IAvatarsModel');
|
||||
export const BannersDismiss = proxify<IBannersDismissModel>('IBannersDismissModel');
|
||||
export const Banners = proxify<IBannersModel>('IBannersModel');
|
||||
export const CallHistory = proxify<ICallHistoryModel>('ICallHistoryModel');
|
||||
export const CannedResponse = proxify<ICannedResponseModel>('ICannedResponseModel');
|
||||
export const CredentialTokens = proxify<ICredentialTokensModel>('ICredentialTokensModel');
|
||||
export const CustomSounds = proxify<ICustomSoundsModel>('ICustomSoundsModel');
|
||||
export const CustomUserStatus = proxify<ICustomUserStatusModel>('ICustomUserStatusModel');
|
||||
export const EmailInbox = proxify<IEmailInboxModel>('IEmailInboxModel');
|
||||
export const EmailMessageHistory = proxify<IEmailMessageHistoryModel>('IEmailMessageHistoryModel');
|
||||
export const EmojiCustom = proxify<IEmojiCustomModel>('IEmojiCustomModel');
|
||||
export const ExportOperations = proxify<IExportOperationsModel>('IExportOperationsModel');
|
||||
export const FederationKeys = proxify<IFederationKeysModel>('IFederationKeysModel');
|
||||
export const ImportData = proxify<IImportDataModel>('IImportDataModel');
|
||||
export const Imports = proxify<IImportsModel>('IImportsModel');
|
||||
export const InstanceStatus = proxify<IInstanceStatusModel>('IInstanceStatusModel');
|
||||
export const IntegrationHistory = proxify<IIntegrationHistoryModel>('IIntegrationHistoryModel');
|
||||
export const Integrations = proxify<IIntegrationsModel>('IIntegrationsModel');
|
||||
export const Invites = proxify<IInvitesModel>('IInvitesModel');
|
||||
export const LivechatAgentActivity = proxify<ILivechatAgentActivityModel>('ILivechatAgentActivityModel');
|
||||
export const LivechatBusinessHours = proxify<ILivechatBusinessHoursModel>('ILivechatBusinessHoursModel');
|
||||
export const LivechatContacts = proxify<ILivechatContactsModel>('ILivechatContactsModel');
|
||||
export const LivechatCustomField = proxify<ILivechatCustomFieldModel>('ILivechatCustomFieldModel');
|
||||
export const LivechatDepartmentAgents = proxify<ILivechatDepartmentAgentsModel>('ILivechatDepartmentAgentsModel');
|
||||
export const LivechatDepartment = proxify<ILivechatDepartmentModel>('ILivechatDepartmentModel');
|
||||
export const LivechatInquiry = proxify<ILivechatInquiryModel>('ILivechatInquiryModel');
|
||||
export const LivechatPriority = proxify<ILivechatPriorityModel>('ILivechatPriorityModel');
|
||||
export const LivechatRooms = proxify<ILivechatRoomsModel>('ILivechatRoomsModel');
|
||||
export const LivechatTag = proxify<ILivechatTagModel>('ILivechatTagModel');
|
||||
export const LivechatTrigger = proxify<ILivechatTriggerModel>('ILivechatTriggerModel');
|
||||
export const LivechatVisitors = proxify<ILivechatVisitorsModel>('ILivechatVisitorsModel');
|
||||
export const LivechatUnit = proxify<ILivechatUnitModel>('ILivechatUnitModel');
|
||||
export const LivechatUnitMonitors = proxify<ILivechatUnitMonitorsModel>('ILivechatUnitMonitorsModel');
|
||||
export const LoginServiceConfiguration = proxify<ILoginServiceConfigurationModel>('ILoginServiceConfigurationModel');
|
||||
export const Messages = proxify<IMessagesModel>('IMessagesModel');
|
||||
export const MediaCalls = proxify<IMediaCallsModel>('IMediaCallsModel');
|
||||
export const MediaCallNegotiations = proxify<IMediaCallNegotiationsModel>('IMediaCallNegotiationsModel');
|
||||
export const NotificationQueue = proxify<INotificationQueueModel>('INotificationQueueModel');
|
||||
export const Nps = proxify<INpsModel>('INpsModel');
|
||||
export const NpsVote = proxify<INpsVoteModel>('INpsVoteModel');
|
||||
export const OAuthApps = proxify<IOAuthAppsModel>('IOAuthAppsModel');
|
||||
export const OAuthAuthCodes = proxify<IOAuthAuthCodesModel>('IOAuthAuthCodesModel');
|
||||
export const OAuthAccessTokens = proxify<IOAuthAccessTokensModel>('IOAuthAccessTokensModel');
|
||||
export const LoginCodes = proxify<ILoginCodesModel>('ILoginCodesModel');
|
||||
export const OAuthRefreshTokens = proxify<IOAuthRefreshTokensModel>('IOAuthRefreshTokensModel');
|
||||
export const OEmbedCache = proxify<IOEmbedCacheModel>('IOEmbedCacheModel');
|
||||
export const PushToken = proxify<IPushTokenModel>('IPushTokenModel');
|
||||
export const Permissions = proxify<IPermissionsModel>('IPermissionsModel');
|
||||
export const ReadReceipts = proxify<IReadReceiptsModel>('IReadReceiptsModel');
|
||||
export const ReadReceiptsArchive = proxify<IReadReceiptsModel>('IReadReceiptsArchiveModel');
|
||||
export const MessageReads = proxify<IMessageReadsModel>('IMessageReadsModel');
|
||||
export const Reports = proxify<IReportsModel>('IReportsModel');
|
||||
export const Roles = proxify<IRolesModel>('IRolesModel');
|
||||
export const Rooms = proxify<IRoomsModel>('IRoomsModel');
|
||||
export const ServerEvents = proxify<IServerEventsModel>('IServerEventsModel');
|
||||
export const Sessions = proxify<ISessionsModel>('ISessionsModel');
|
||||
export const Settings = proxify<ISettingsModel>('ISettingsModel');
|
||||
export const SmarshHistory = proxify<ISmarshHistoryModel>('ISmarshHistoryModel');
|
||||
export const Statistics = proxify<IStatisticsModel>('IStatisticsModel');
|
||||
export const Subscriptions = proxify<ISubscriptionsModel>('ISubscriptionsModel');
|
||||
export const TeamMember = proxify<ITeamMemberModel>('ITeamMemberModel');
|
||||
export const Team = proxify<ITeamModel>('ITeamModel');
|
||||
export const Users = proxify<IUsersModel>('IUsersModel');
|
||||
export const Uploads = proxify<IUploadsModel>('IUploadsModel');
|
||||
export const UserDataFiles = proxify<IUserDataFilesModel>('IUserDataFilesModel');
|
||||
export const UsersSessions = proxify<IUsersSessionsModel>('IUsersSessionsModel');
|
||||
export const VideoConference = proxify<IVideoConferenceModel>('IVideoConferenceModel');
|
||||
export const WebdavAccounts = proxify<IWebdavAccountsModel>('IWebdavAccountsModel');
|
||||
export const CalendarEvent = proxify<ICalendarEventModel>('ICalendarEventModel');
|
||||
export const OmnichannelServiceLevelAgreements = proxify<IOmnichannelServiceLevelAgreementsModel>(
|
||||
'IOmnichannelServiceLevelAgreementsModel',
|
||||
);
|
||||
export const AuditLog = proxify<IAuditLogModel>('IAuditLogModel');
|
||||
export const CronHistory = proxify<ICronHistoryModel>('ICronHistoryModel');
|
||||
export const Migrations = proxify<IMigrationsModel>('IMigrationsModel');
|
||||
export const ModerationReports = proxify<IModerationReportsModel>('IModerationReportsModel');
|
||||
export const WorkspaceCredentials = proxify<IWorkspaceCredentialsModel>('IWorkspaceCredentialsModel');
|
||||
export const AbacAttributes = proxify<IAbacAttributesModel>('IAbacAttributesModel');
|
||||
export const TwoFactorChallenges = proxify<ITwoFactorChallengesModel>('ITwoFactorChallengesModel');
|
||||
export const SamlUsedAssertions = proxify<ISamlUsedAssertionsModel>('ISamlUsedAssertionsModel');
|
||||
|
||||
export function registerServiceModels(db: Db, trash?: Collection<RocketChatRecordDeleted<any>>): void {
|
||||
registerModel('IUsersSessionsModel', () => new UsersSessionsRaw(db));
|
||||
registerModel('IUsersModel', () => new UsersRaw(db));
|
||||
|
||||
registerModel('IRolesModel', () => new RolesRaw(db));
|
||||
registerModel('IRoomsModel', () => new RoomsRaw(db));
|
||||
registerModel('ISubscriptionsModel', () => new SubscriptionsRaw(db, trash as Collection<RocketChatRecordDeleted<ISubscription>>));
|
||||
registerModel('ITeamModel', () => new TeamRaw(db));
|
||||
registerModel('ITeamMemberModel', () => new TeamMemberRaw(db));
|
||||
|
||||
registerModel('IMessagesModel', () => new MessagesRaw(db));
|
||||
|
||||
registerModel(
|
||||
'ILivechatInquiryModel',
|
||||
() => new LivechatInquiryRaw(db, trash as Collection<RocketChatRecordDeleted<ILivechatInquiryRecord>>),
|
||||
);
|
||||
registerModel(
|
||||
'ILivechatDepartmentAgentsModel',
|
||||
() => new LivechatDepartmentAgentsRaw(db, trash as Collection<RocketChatRecordDeleted<ILivechatDepartmentAgents>>),
|
||||
);
|
||||
|
||||
registerModel('IPermissionsModel', () => new PermissionsRaw(db));
|
||||
registerModel('ILoginServiceConfigurationModel', () => new LoginServiceConfigurationRaw(db));
|
||||
registerModel('IInstanceStatusModel', () => new InstanceStatusRaw(db));
|
||||
registerModel('IIntegrationHistoryModel', () => new IntegrationHistoryRaw(db));
|
||||
registerModel('IIntegrationsModel', () => new IntegrationsRaw(db));
|
||||
registerModel('IEmailInboxModel', () => new EmailInboxRaw(db));
|
||||
registerModel('ILivechatPriorityModel', new LivechatPriorityRaw(db));
|
||||
registerModel('ILivechatRoomsModel', () => new LivechatRoomsRaw(db));
|
||||
registerModel('IUploadsModel', () => new UploadsRaw(db));
|
||||
registerModel('ILivechatVisitorsModel', () => new LivechatVisitorsRaw(db));
|
||||
registerModel('IAbacAttributesModel', () => new AbacAttributesRaw(db));
|
||||
registerModel('IServerEventsModel', () => new ServerEventsRaw(db));
|
||||
registerModel('ISamlUsedAssertionsModel', () => new SamlUsedAssertionsRaw(db));
|
||||
registerModel('ICronHistoryModel', () => new CronHistoryRaw(db));
|
||||
}
|
||||
76
packages/models/src/modelClasses.ts
Normal file
76
packages/models/src/modelClasses.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export * from './models/BaseRaw';
|
||||
export * from './models/AbacAttributes';
|
||||
export * from './models/Analytics';
|
||||
export * from './models/Apps';
|
||||
export * from './models/AppsPersistence';
|
||||
export * from './models/AppLogsModel';
|
||||
export * from './models/Avatars';
|
||||
export * from './models/Banners';
|
||||
export * from './models/BannersDismiss';
|
||||
export * from './models/CalendarEvent';
|
||||
export * from './models/CustomSounds';
|
||||
export * from './models/CustomUserStatus';
|
||||
export * from './models/EmailInbox';
|
||||
export * from './models/EmailMessageHistory';
|
||||
export * from './models/EmojiCustom';
|
||||
export * from './models/ExportOperations';
|
||||
export * from './models/FederationKeys';
|
||||
export * from './models/ImportData';
|
||||
export * from './models/Imports';
|
||||
export * from './models/InstanceStatus';
|
||||
export * from './models/IntegrationHistory';
|
||||
export * from './models/Integrations';
|
||||
export * from './models/Invites';
|
||||
export * from './models/LivechatAgentActivity';
|
||||
export * from './models/LivechatBusinessHours';
|
||||
export * from './models/LivechatContacts';
|
||||
export * from './models/LivechatCustomField';
|
||||
export * from './models/LivechatDepartment';
|
||||
export * from './models/LivechatDepartmentAgents';
|
||||
export * from './models/LivechatInquiry';
|
||||
export * from './models/LivechatPriority';
|
||||
export * from './models/LivechatRooms';
|
||||
export * from './models/LivechatTrigger';
|
||||
export * from './models/LivechatVisitors';
|
||||
export * from './models/LoginServiceConfiguration';
|
||||
export * from './models/Messages';
|
||||
export * from './models/NotificationQueue';
|
||||
export * from './models/Nps';
|
||||
export * from './models/NpsVote';
|
||||
export * from './models/OAuthAccessTokens';
|
||||
export * from './models/OAuthApps';
|
||||
export * from './models/OAuthAuthCodes';
|
||||
export * from './models/LoginCodes';
|
||||
export * from './models/OAuthRefreshTokens';
|
||||
export * from './models/OEmbedCache';
|
||||
export * from './models/Permissions';
|
||||
export * from './models/PushToken';
|
||||
export * from './models/Reports';
|
||||
export * from './models/Roles';
|
||||
export * from './models/Rooms';
|
||||
export * from './models/SamlUsedAssertions';
|
||||
export * from './models/ServerEvents';
|
||||
export * from './models/Sessions';
|
||||
export * from './models/Settings';
|
||||
export * from './models/SmarshHistory';
|
||||
export * from './models/Statistics';
|
||||
export * from './models/Subscriptions';
|
||||
export * from './models/Team';
|
||||
export * from './models/TeamMember';
|
||||
export * from './models/Uploads';
|
||||
export * from './models/UserDataFiles';
|
||||
export * from './models/Users';
|
||||
export * from './models/UsersSessions';
|
||||
export * from './models/VideoConference';
|
||||
export * from './models/WebdavAccounts';
|
||||
export * from './models/CredentialTokens';
|
||||
export * from './models/MessageReads';
|
||||
export * from './models/CronHistoryModel';
|
||||
export * from './models/Migrations';
|
||||
export * from './models/ModerationReports';
|
||||
export * from './models/MediaCalls';
|
||||
export * from './models/MediaCallNegotiations';
|
||||
export * from './models/WorkspaceCredentials';
|
||||
export * from './models/Trash';
|
||||
export * from './models/CallHistory';
|
||||
export * from './models/TwoFactorChallenges';
|
||||
39
packages/models/src/models/AbacAttributes.ts
Normal file
39
packages/models/src/models/AbacAttributes.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { IAbacAttribute } from '@rocket.chat/core-typings';
|
||||
import type { Db, FindOptions, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class AbacAttributesRaw extends BaseRaw<IAbacAttribute> {
|
||||
constructor(db: Db) {
|
||||
super(db, 'abac_attributes');
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { key: 1 }, unique: true }, { key: { values: 1 } }];
|
||||
}
|
||||
|
||||
findOneByKey(key: string, options: FindOptions<IAbacAttribute> = {}): Promise<IAbacAttribute | null> {
|
||||
return this.findOne({ key }, options);
|
||||
}
|
||||
|
||||
async countTotalValues(): Promise<number> {
|
||||
const [result] = await this.col
|
||||
.aggregate<{ totalValues: number }>([
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalValues: {
|
||||
$sum: {
|
||||
$size: {
|
||||
$ifNull: ['$values', []],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
.toArray();
|
||||
|
||||
return result?.totalValues ?? 0;
|
||||
}
|
||||
}
|
||||
331
packages/models/src/models/Analytics.ts
Normal file
331
packages/models/src/models/Analytics.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import type { IAnalytics, IRoom } from '@rocket.chat/core-typings';
|
||||
import type { IAnalyticsModel, IChannelsWithNumberOfMessagesBetweenDate } from '@rocket.chat/model-typings';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import type { AggregationCursor, Db, IndexDescription, FindOptions, UpdateResult, Document, Collection } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
import { readSecondaryPreferred } from '../readSecondaryPreferred';
|
||||
|
||||
export class AnalyticsRaw extends BaseRaw<IAnalytics> implements IAnalyticsModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'analytics', undefined, {
|
||||
collection: { readPreference: readSecondaryPreferred(db) },
|
||||
});
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{ key: { date: 1 } },
|
||||
{ key: { 'room._id': 1, 'date': 1 }, unique: true, partialFilterExpression: { type: 'rooms' } },
|
||||
{ key: { 'room.t': 1, 'date': 1 }, partialFilterExpression: { type: 'messages' } },
|
||||
];
|
||||
}
|
||||
|
||||
saveMessageSent({ room, date }: { room: IRoom; date: IAnalytics['date'] }): Promise<Document | UpdateResult> {
|
||||
return this.updateMany(
|
||||
{ date, 'room._id': room._id, 'type': 'messages' },
|
||||
{
|
||||
$set: {
|
||||
room: {
|
||||
_id: room._id,
|
||||
name: room.fname || room.name,
|
||||
t: room.t,
|
||||
usernames: room.usernames || [],
|
||||
},
|
||||
},
|
||||
$setOnInsert: {
|
||||
_id: Random.id(),
|
||||
date,
|
||||
type: 'messages' as const,
|
||||
},
|
||||
$inc: { messages: 1 },
|
||||
},
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
saveUserData({ date }: { date: IAnalytics['date'] }): Promise<Document | UpdateResult> {
|
||||
return this.updateMany(
|
||||
{ date, type: 'users' },
|
||||
{
|
||||
$setOnInsert: {
|
||||
_id: Random.id(),
|
||||
date,
|
||||
type: 'users' as const,
|
||||
},
|
||||
$inc: { users: 1 },
|
||||
},
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
saveMessageDeleted({ room, date }: { room: { _id: string }; date: IAnalytics['date'] }): Promise<Document | UpdateResult> {
|
||||
return this.updateMany(
|
||||
{ date, 'room._id': room._id },
|
||||
{
|
||||
$inc: { messages: -1 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
getMessagesSentTotalByDate({
|
||||
start,
|
||||
end,
|
||||
options = {},
|
||||
}: {
|
||||
start: IAnalytics['date'];
|
||||
end: IAnalytics['date'];
|
||||
options?: { sort?: FindOptions<IAnalytics>['sort']; count?: number };
|
||||
}): AggregationCursor<{
|
||||
_id: IAnalytics['date'];
|
||||
messages: number;
|
||||
}> {
|
||||
return this.col.aggregate<{
|
||||
_id: IAnalytics['date'];
|
||||
messages: number;
|
||||
}>(
|
||||
[
|
||||
{
|
||||
$match: {
|
||||
type: 'messages',
|
||||
date: { $gte: start, $lte: end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$date',
|
||||
messages: { $sum: '$messages' },
|
||||
},
|
||||
},
|
||||
...(options.sort ? [{ $sort: options.sort }] : []),
|
||||
...(options.count ? [{ $limit: options.count }] : []),
|
||||
],
|
||||
{ readPreference: readSecondaryPreferred() },
|
||||
);
|
||||
}
|
||||
|
||||
getMessagesOrigin({ start, end }: { start: IAnalytics['date']; end: IAnalytics['date'] }): AggregationCursor<{
|
||||
t: IRoom['t'];
|
||||
messages: number;
|
||||
}> {
|
||||
const params = [
|
||||
{
|
||||
$match: {
|
||||
type: 'messages',
|
||||
date: { $gte: start, $lte: end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: { t: '$room.t' },
|
||||
messages: { $sum: '$messages' },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
t: '$_id.t',
|
||||
messages: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
return this.col.aggregate(params, { readPreference: readSecondaryPreferred() });
|
||||
}
|
||||
|
||||
getMostPopularChannelsByMessagesSentQuantity({
|
||||
start,
|
||||
end,
|
||||
options = {},
|
||||
}: {
|
||||
start: IAnalytics['date'];
|
||||
end: IAnalytics['date'];
|
||||
options?: { sort?: FindOptions<IAnalytics>['sort']; count?: number };
|
||||
}): AggregationCursor<{
|
||||
t: IRoom['t'];
|
||||
name: string;
|
||||
messages: number;
|
||||
usernames: string[];
|
||||
}> {
|
||||
return this.col.aggregate(
|
||||
[
|
||||
{
|
||||
$match: {
|
||||
type: 'messages',
|
||||
date: { $gte: start, $lte: end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: { t: '$room.t', name: '$room.name', usernames: '$room.usernames' },
|
||||
messages: { $sum: '$messages' },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
t: '$_id.t',
|
||||
name: '$_id.name',
|
||||
usernames: '$_id.usernames',
|
||||
messages: 1,
|
||||
},
|
||||
},
|
||||
...(options.sort ? [{ $sort: options.sort }] : []),
|
||||
...(options.count ? [{ $limit: options.count }] : []),
|
||||
],
|
||||
{ readPreference: readSecondaryPreferred() },
|
||||
);
|
||||
}
|
||||
|
||||
getTotalOfRegisteredUsersByDate({
|
||||
start,
|
||||
end,
|
||||
options = {},
|
||||
}: {
|
||||
start: IAnalytics['date'];
|
||||
end: IAnalytics['date'];
|
||||
options?: { sort?: FindOptions<IAnalytics>['sort']; count?: number };
|
||||
}): AggregationCursor<{
|
||||
_id: IAnalytics['date'];
|
||||
users: number;
|
||||
}> {
|
||||
return this.col.aggregate<{
|
||||
_id: IAnalytics['date'];
|
||||
users: number;
|
||||
}>(
|
||||
[
|
||||
{
|
||||
$match: {
|
||||
type: 'users',
|
||||
date: { $gte: start, $lte: end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$date',
|
||||
users: { $sum: '$users' },
|
||||
},
|
||||
},
|
||||
...(options.sort ? [{ $sort: options.sort }] : []),
|
||||
...(options.count ? [{ $limit: options.count }] : []),
|
||||
],
|
||||
{ readPreference: readSecondaryPreferred() },
|
||||
);
|
||||
}
|
||||
|
||||
findOneByTypeBeforeDate({ type, date }: { type: IAnalytics['type']; date: IAnalytics['date'] }): Promise<IAnalytics | null> {
|
||||
return this.findOne({ type, date: { $lte: date } });
|
||||
}
|
||||
|
||||
getRoomsWithNumberOfMessagesBetweenDateQuery({
|
||||
types,
|
||||
start,
|
||||
end,
|
||||
startOfLastWeek,
|
||||
endOfLastWeek,
|
||||
options,
|
||||
}: {
|
||||
types: Array<IRoom['t']>;
|
||||
start: number;
|
||||
end: number;
|
||||
startOfLastWeek: number;
|
||||
endOfLastWeek: number;
|
||||
options?: any;
|
||||
}) {
|
||||
const typeAndDateMatch = {
|
||||
$match: {
|
||||
'type': 'messages',
|
||||
'room.t': { $in: types },
|
||||
'date': { $gte: startOfLastWeek, $lte: end },
|
||||
},
|
||||
};
|
||||
const roomsGroup = {
|
||||
$group: {
|
||||
_id: '$room._id',
|
||||
room: { $first: '$room' },
|
||||
messages: { $sum: { $cond: [{ $gte: ['$date', start] }, '$messages', 0] } },
|
||||
lastWeekMessages: { $sum: { $cond: [{ $lte: ['$date', endOfLastWeek] }, '$messages', 0] } },
|
||||
},
|
||||
};
|
||||
const lookup = {
|
||||
$lookup: {
|
||||
from: 'rocketchat_room',
|
||||
localField: '_id',
|
||||
foreignField: '_id',
|
||||
as: 'room',
|
||||
},
|
||||
};
|
||||
const roomsUnwind = {
|
||||
$unwind: {
|
||||
path: '$room',
|
||||
preserveNullAndEmptyArrays: false,
|
||||
},
|
||||
};
|
||||
const project = {
|
||||
$project: {
|
||||
_id: 0,
|
||||
room: {
|
||||
_id: '$room._id',
|
||||
name: { $ifNull: ['$room.name', '$room.fname'] },
|
||||
ts: '$room.ts',
|
||||
t: '$room.t',
|
||||
_updatedAt: '$room._updatedAt',
|
||||
usernames: '$room.usernames',
|
||||
},
|
||||
messages: '$messages',
|
||||
lastWeekMessages: '$lastWeekMessages',
|
||||
diffFromLastWeek: { $subtract: ['$messages', '$lastWeekMessages'] },
|
||||
},
|
||||
};
|
||||
|
||||
const sort = { $sort: options?.sort || { messages: -1 } };
|
||||
const sortAndPaginationParams: Exclude<Parameters<Collection<IRoom>['aggregate']>[0], undefined> = [sort];
|
||||
if (options?.offset) {
|
||||
sortAndPaginationParams.push({ $skip: options.offset });
|
||||
}
|
||||
|
||||
if (options?.count) {
|
||||
sortAndPaginationParams.push({ $limit: options.count });
|
||||
}
|
||||
const facet = {
|
||||
$facet: {
|
||||
channels: [...sortAndPaginationParams],
|
||||
total: [{ $count: 'total' }],
|
||||
},
|
||||
};
|
||||
const totalUnwind = { $unwind: '$total' };
|
||||
const totalProject = {
|
||||
$project: {
|
||||
channels: '$channels',
|
||||
total: '$total.total',
|
||||
},
|
||||
};
|
||||
|
||||
const params: Exclude<Parameters<Collection<IRoom>['aggregate']>[0], undefined> = [
|
||||
typeAndDateMatch,
|
||||
roomsGroup,
|
||||
lookup,
|
||||
roomsUnwind,
|
||||
project,
|
||||
facet,
|
||||
totalUnwind,
|
||||
totalProject,
|
||||
];
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
findRoomsByTypesWithNumberOfMessagesBetweenDate(params: {
|
||||
types: Array<IRoom['t']>;
|
||||
start: number;
|
||||
end: number;
|
||||
startOfLastWeek: number;
|
||||
endOfLastWeek: number;
|
||||
options?: any;
|
||||
}): AggregationCursor<{ channels: IChannelsWithNumberOfMessagesBetweenDate[]; total: number }> {
|
||||
const aggregationParams = this.getRoomsWithNumberOfMessagesBetweenDateQuery(params);
|
||||
return this.col.aggregate<{ channels: IChannelsWithNumberOfMessagesBetweenDate[]; total: number }>(aggregationParams, {
|
||||
allowDiskUse: true,
|
||||
readPreference: readSecondaryPreferred(),
|
||||
});
|
||||
}
|
||||
}
|
||||
58
packages/models/src/models/AppLogsModel.ts
Normal file
58
packages/models/src/models/AppLogsModel.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { IAppLogsModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class AppsLogsModel extends BaseRaw<any> implements IAppLogsModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'apps_logs', undefined);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
// This index is used to expire logs after 30 days
|
||||
{
|
||||
key: {
|
||||
_updatedAt: 1,
|
||||
},
|
||||
expireAfterSeconds: 60 * 60 * 24 * 30,
|
||||
},
|
||||
// Index for specific queries from the logs screen (most common)
|
||||
{
|
||||
key: {
|
||||
'appId': 1,
|
||||
'_updatedAt': -1,
|
||||
'entries.severity': 1,
|
||||
'instanceId': 1,
|
||||
'method': 1,
|
||||
},
|
||||
name: 'appId_indexed_query_v2',
|
||||
},
|
||||
// Index for queries on general logs endpoint
|
||||
{
|
||||
key: {
|
||||
'_updatedAt': -1,
|
||||
'entries.severity': 1,
|
||||
'instanceId': 1,
|
||||
'method': 1,
|
||||
},
|
||||
name: 'general_logs_index_v2',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async getDistinctFieldsForFilters(appId: string): Promise<{ instanceIds: string[]; methods: string[] }> {
|
||||
/**
|
||||
* The following aggregation uses the 'appId_indexed_query_v2' index to retrieve the distinct values
|
||||
* The execution is fully covered by the index, meaning the database should not need to scan any documents.
|
||||
*/
|
||||
const [{ instanceIds, methods }] = await this.col
|
||||
.aggregate([
|
||||
{ $match: { appId } },
|
||||
{ $group: { _id: null, instanceIds: { $addToSet: '$instanceId' }, methods: { $addToSet: '$method' } } },
|
||||
])
|
||||
.toArray();
|
||||
|
||||
return { instanceIds, methods };
|
||||
}
|
||||
}
|
||||
10
packages/models/src/models/Apps.ts
Normal file
10
packages/models/src/models/Apps.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { IAppsModel } from '@rocket.chat/model-typings';
|
||||
import type { Db } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class AppsModel extends BaseRaw<any> implements IAppsModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'apps');
|
||||
}
|
||||
}
|
||||
26
packages/models/src/models/AppsPersistence.ts
Normal file
26
packages/models/src/models/AppsPersistence.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { IAppsPersistenceModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, DeleteResult, Filter, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class AppsPersistenceModel extends BaseRaw<any> implements IAppsPersistenceModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'apps_persistence');
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{
|
||||
key: {
|
||||
appId: 1,
|
||||
associations: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Bypass trash collection
|
||||
remove(query: Filter<any>): Promise<DeleteResult> {
|
||||
return this.col.deleteMany(query);
|
||||
}
|
||||
}
|
||||
27
packages/models/src/models/Avatars.ts
Normal file
27
packages/models/src/models/Avatars.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { IAvatar, RocketChatRecordDeleted, IUser } from '@rocket.chat/core-typings';
|
||||
import type { IAvatarsModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, IndexDescription, FindOptions } from 'mongodb';
|
||||
|
||||
import { BaseUploadModelRaw } from './BaseUploadModel';
|
||||
|
||||
export class AvatarsRaw extends BaseUploadModelRaw implements IAvatarsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IAvatar>>) {
|
||||
super(db, 'avatars', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
...super.modelIndexes(),
|
||||
{ key: { userId: 1 }, sparse: true },
|
||||
{ key: { etag: 1 }, sparse: true }, // avatars are queried by etag (specially for federation)
|
||||
];
|
||||
}
|
||||
|
||||
findOneByUserId(userId: IUser['_id'], options?: FindOptions<IAvatar>) {
|
||||
return this.findOne({ userId }, options);
|
||||
}
|
||||
|
||||
findOneByETag(etag: string, options?: FindOptions<IAvatar>): Promise<IAvatar | null> {
|
||||
return this.findOne({ etag }, options);
|
||||
}
|
||||
}
|
||||
83
packages/models/src/models/Banners.ts
Normal file
83
packages/models/src/models/Banners.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { IBanner, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import { BannerPlatform } from '@rocket.chat/core-typings';
|
||||
import type { IBannersModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, FindCursor, Db, IndexDescription, InsertOneResult, UpdateResult, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class BannersRaw extends BaseRaw<IBanner> implements IBannersModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IBanner>>) {
|
||||
super(db, 'banner', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { platform: 1, startAt: 1, expireAt: 1 } }, { key: { platform: 1, startAt: 1, expireAt: 1, active: 1 } }];
|
||||
}
|
||||
|
||||
create(doc: IBanner): Promise<InsertOneResult<IBanner>> {
|
||||
const invalidPlatform = doc.platform?.some((platform) => !Object.values(BannerPlatform).includes(platform));
|
||||
if (invalidPlatform) {
|
||||
throw new Error('Invalid platform');
|
||||
}
|
||||
|
||||
if (doc.startAt > doc.expireAt) {
|
||||
throw new Error('Start date cannot be later than expire date');
|
||||
}
|
||||
|
||||
if (doc.expireAt < new Date()) {
|
||||
throw new Error('Cannot create banner already expired');
|
||||
}
|
||||
|
||||
return this.insertOne({
|
||||
active: true,
|
||||
...doc,
|
||||
});
|
||||
}
|
||||
|
||||
findActiveByRoleOrId<T extends Document = IBanner, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
roles: string[],
|
||||
platform: BannerPlatform,
|
||||
bannerId?: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const today = new Date();
|
||||
|
||||
const query = {
|
||||
...(bannerId && { _id: bannerId }),
|
||||
platform,
|
||||
startAt: { $lte: today },
|
||||
expireAt: { $gte: today },
|
||||
active: { $ne: false },
|
||||
$or: [{ roles: { $in: roles } }, { roles: { $exists: false } }],
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
disable(bannerId: string): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id: bannerId, active: { $ne: false } }, { $set: { active: false, inactivedAt: new Date() } });
|
||||
}
|
||||
|
||||
createOrUpdate(banner: IBanner): Promise<UpdateResult> {
|
||||
const invalidPlatform = banner.platform?.some((platform) => !Object.values(BannerPlatform).includes(platform));
|
||||
if (invalidPlatform) {
|
||||
throw new Error('Invalid platform');
|
||||
}
|
||||
|
||||
if (banner.startAt > banner.expireAt) {
|
||||
throw new Error('Start date cannot be later than expire date');
|
||||
}
|
||||
|
||||
if (banner.expireAt < new Date()) {
|
||||
throw new Error('Cannot create banner already expired');
|
||||
}
|
||||
|
||||
const { _id: bannerId, ...doc } = banner;
|
||||
|
||||
return this.updateOne({ _id: bannerId }, { $set: { active: true, ...doc } }, { upsert: true });
|
||||
}
|
||||
|
||||
findByIds(bannerIds: string[]): FindCursor<IBanner> {
|
||||
return this.find({ _id: { $in: bannerIds } });
|
||||
}
|
||||
}
|
||||
38
packages/models/src/models/BannersDismiss.ts
Normal file
38
packages/models/src/models/BannersDismiss.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { IBannerDismiss, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IBannersDismissModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, FindCursor, Db, IndexDescription, Document, FindOptions } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class BannersDismissRaw extends BaseRaw<IBannerDismiss> implements IBannersDismissModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IBannerDismiss>>) {
|
||||
super(db, 'banner_dismiss', trash);
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { userId: 1, bannerId: 1 } }];
|
||||
}
|
||||
|
||||
findByUserIdAndBannerId(userId: string, bannerIds: string[]): FindCursor<IBannerDismiss>;
|
||||
|
||||
findByUserIdAndBannerId(userId: string, bannerIds: string[], options: FindOptions<IBannerDismiss>): FindCursor<IBannerDismiss>;
|
||||
|
||||
findByUserIdAndBannerId<P extends Document>(
|
||||
userId: string,
|
||||
bannerIds: string[],
|
||||
options: FindOptions<P extends IBannerDismiss ? IBannerDismiss : P>,
|
||||
): FindCursor<P>;
|
||||
|
||||
findByUserIdAndBannerId<P extends Document>(
|
||||
userId: string,
|
||||
bannerIds: string[],
|
||||
options?: FindOptions<IBannerDismiss> | FindOptions<P extends IBannerDismiss ? IBannerDismiss : P>,
|
||||
): FindCursor<P> | FindCursor<IBannerDismiss> {
|
||||
const query = {
|
||||
userId,
|
||||
bannerId: { $in: bannerIds },
|
||||
};
|
||||
|
||||
return options ? this.col.find(query, options) : this.col.find(query);
|
||||
}
|
||||
}
|
||||
52
packages/models/src/models/BaseRaw.spec.ts
Normal file
52
packages/models/src/models/BaseRaw.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Collection, Db, FindOptions } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
// `BaseRaw` imports `..`, whose barrel pulls in every model and cycles back here.
|
||||
jest.mock('..', () => ({
|
||||
getCollectionName: (name: string) => name,
|
||||
UpdaterImpl: class {},
|
||||
}));
|
||||
|
||||
const find = jest.fn();
|
||||
|
||||
class TestModel extends BaseRaw<{ _id: string; name: string; password: string }> {
|
||||
constructor() {
|
||||
super({ collection: () => ({ find }) } as unknown as Db, 'test');
|
||||
}
|
||||
}
|
||||
|
||||
const projectionSentToDriver = (projection: Record<string, unknown>): unknown => {
|
||||
new TestModel().find({}, { projection } as FindOptions<{ _id: string; name: string; password: string }>);
|
||||
return find.mock.calls.at(-1)?.[1]?.projection;
|
||||
};
|
||||
|
||||
describe('doNotMixInclusionAndExclusionFields', () => {
|
||||
beforeEach(() => find.mockReset().mockReturnValue({} as unknown as Collection<any>));
|
||||
|
||||
it('should keep an inclusion-only projection untouched', () => {
|
||||
expect(projectionSentToDriver({ name: 1 })).toEqual({ name: 1 });
|
||||
expect(projectionSentToDriver({ name: true })).toEqual({ name: true });
|
||||
});
|
||||
|
||||
it('should keep an exclusion-only projection untouched, whichever notation is used', () => {
|
||||
expect(projectionSentToDriver({ password: 0 })).toEqual({ password: 0 });
|
||||
expect(projectionSentToDriver({ password: false })).toEqual({ password: false });
|
||||
expect(projectionSentToDriver({ password: 0, name: false })).toEqual({ password: 0, name: false });
|
||||
});
|
||||
|
||||
it('should drop the exclusions from a mixed projection, whichever notation is used', () => {
|
||||
expect(projectionSentToDriver({ name: 1, password: 0 })).toEqual({ name: 1 });
|
||||
expect(projectionSentToDriver({ name: 1, password: false })).toEqual({ name: 1 });
|
||||
expect(projectionSentToDriver({ name: true, password: false })).toEqual({ name: true });
|
||||
expect(projectionSentToDriver({ name: 1, _id: false })).toEqual({ name: 1 });
|
||||
});
|
||||
|
||||
it('should not mutate the projection owned by the caller', () => {
|
||||
const options = { projection: { name: 1, password: false } } as unknown as FindOptions<{ _id: string; name: string; password: string }>;
|
||||
|
||||
new TestModel().find({}, options);
|
||||
|
||||
expect(options.projection).toEqual({ name: 1, password: false });
|
||||
});
|
||||
});
|
||||
561
packages/models/src/models/BaseRaw.ts
Normal file
561
packages/models/src/models/BaseRaw.ts
Normal file
@@ -0,0 +1,561 @@
|
||||
import type { RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type {
|
||||
IBaseModel,
|
||||
DefaultFields,
|
||||
ResultFields,
|
||||
FindPaginated,
|
||||
InsertionModel,
|
||||
DocumentWithProjection,
|
||||
FindOptionsWithProjection,
|
||||
DocumentWithDriverProjection,
|
||||
FindOneAndUpdateOptionsWithProjection,
|
||||
} from '@rocket.chat/model-typings';
|
||||
import { traceInstanceMethods } from '@rocket.chat/tracing';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import type {
|
||||
BulkWriteOptions,
|
||||
ChangeStream,
|
||||
Collection,
|
||||
CollectionOptions,
|
||||
Db,
|
||||
Filter,
|
||||
FindOneAndUpdateOptions,
|
||||
IndexDescription,
|
||||
InsertOneOptions,
|
||||
OptionalUnlessRequiredId,
|
||||
UpdateFilter,
|
||||
WithId,
|
||||
UpdateOptions,
|
||||
Document,
|
||||
FindOptions,
|
||||
FindCursor,
|
||||
UpdateResult,
|
||||
InsertManyResult,
|
||||
InsertOneResult,
|
||||
DeleteResult,
|
||||
DeleteOptions,
|
||||
FindOneAndDeleteOptions,
|
||||
CountDocumentsOptions,
|
||||
ClientSession,
|
||||
} from 'mongodb';
|
||||
|
||||
import { getCollectionName, UpdaterImpl } from '..';
|
||||
import type { Updater } from '../updater';
|
||||
import { setUpdatedAt } from './setUpdatedAt';
|
||||
|
||||
const warnFields =
|
||||
process.env.NODE_ENV !== 'production' || process.env.SHOW_WARNINGS === 'true'
|
||||
? (...rest: any): void => {
|
||||
console.warn(...rest, new Error().stack);
|
||||
}
|
||||
: new Function();
|
||||
|
||||
type ModelOptions = {
|
||||
preventSetUpdatedAt?: boolean;
|
||||
collectionNameResolver?: (name: string) => string;
|
||||
collection?: CollectionOptions;
|
||||
};
|
||||
|
||||
export abstract class BaseRaw<
|
||||
T extends { _id: string },
|
||||
C extends DefaultFields<T> = undefined,
|
||||
TDeleted extends RocketChatRecordDeleted<T> = RocketChatRecordDeleted<T>,
|
||||
> implements IBaseModel<T, C, TDeleted> {
|
||||
protected defaultFields: C | undefined;
|
||||
|
||||
public readonly col: Collection<T>;
|
||||
|
||||
private preventSetUpdatedAt: boolean;
|
||||
|
||||
/**
|
||||
* Collection name to store data.
|
||||
*/
|
||||
private collectionName: string;
|
||||
|
||||
/**
|
||||
* @param db MongoDB instance
|
||||
* @param name Name of the model without any prefix. Used by trash records to set the `__collection__` field.
|
||||
* @param trash Trash collection instance
|
||||
* @param options Model options
|
||||
*/
|
||||
constructor(
|
||||
private db: Db,
|
||||
protected name: string,
|
||||
protected trash?: Collection<TDeleted>,
|
||||
options?: ModelOptions,
|
||||
) {
|
||||
this.collectionName = options?.collectionNameResolver ? options.collectionNameResolver(name) : getCollectionName(name);
|
||||
|
||||
this.col = this.db.collection(this.collectionName, options?.collection || {});
|
||||
|
||||
void this.createIndexes();
|
||||
|
||||
this.preventSetUpdatedAt = options?.preventSetUpdatedAt ?? false;
|
||||
|
||||
return traceInstanceMethods(this);
|
||||
}
|
||||
|
||||
private pendingIndexes: Promise<void> | undefined;
|
||||
|
||||
public async createIndexes() {
|
||||
const indexes = this.modelIndexes();
|
||||
|
||||
if (indexes?.length) {
|
||||
if (this.pendingIndexes) {
|
||||
await this.pendingIndexes;
|
||||
}
|
||||
|
||||
this.pendingIndexes = this.col.createIndexes(indexes).catch((e) => {
|
||||
console.warn(`Some indexes for collection '${this.collectionName}' could not be created:\n\t${e.message}`);
|
||||
}) as unknown as Promise<void>;
|
||||
|
||||
void this.pendingIndexes.finally(() => {
|
||||
this.pendingIndexes = undefined;
|
||||
});
|
||||
|
||||
return this.pendingIndexes;
|
||||
}
|
||||
}
|
||||
|
||||
protected modelIndexes(): IndexDescription[] | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getCollectionName(): string {
|
||||
return this.collectionName;
|
||||
}
|
||||
|
||||
public getUpdater(): Updater<T> {
|
||||
return new UpdaterImpl<T>();
|
||||
}
|
||||
|
||||
public updateFromUpdater(query: Filter<T>, updater: Updater<T>, options: UpdateOptions = {}): Promise<UpdateResult> {
|
||||
const updateFilter = updater.getUpdateFilter();
|
||||
return this.updateOne(query, updateFilter, options).catch((e) => {
|
||||
console.warn(e, updateFilter);
|
||||
return Promise.reject(e);
|
||||
});
|
||||
}
|
||||
|
||||
private doNotMixInclusionAndExclusionFields(options: FindOptions<T> = {}): FindOptions<T> {
|
||||
const optionsDef = this.ensureDefaultFields(options);
|
||||
if (optionsDef?.projection === undefined) {
|
||||
return optionsDef;
|
||||
}
|
||||
|
||||
const projection: Record<string, any> = optionsDef?.projection;
|
||||
const entries = Object.entries(projection);
|
||||
const inclusionEntries = entries.filter(([, value]) => value !== 0 && value !== false);
|
||||
const isMixed = inclusionEntries.length > 0 && inclusionEntries.length < entries.length;
|
||||
|
||||
return {
|
||||
...optionsDef,
|
||||
// a mixed projection gets its exclusions dropped into a new object; `projection` may be owned by the caller
|
||||
projection: isMixed ? Object.fromEntries(inclusionEntries) : projection,
|
||||
};
|
||||
}
|
||||
|
||||
private ensureDefaultFields<P extends Document>(options: FindOptions<P>): FindOptions<P>;
|
||||
|
||||
private ensureDefaultFields<P extends Document>(
|
||||
options?: FindOptions<P> & { fields?: FindOptions<P>['projection'] },
|
||||
): FindOptions<P> | FindOptions<T> | undefined {
|
||||
if (options?.fields) {
|
||||
warnFields("Using 'fields' in models is deprecated.", options);
|
||||
}
|
||||
|
||||
if (this.defaultFields === undefined) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const { fields: deprecatedFields, projection, ...rest } = options || {};
|
||||
|
||||
const fields = { ...deprecatedFields, ...projection };
|
||||
|
||||
return {
|
||||
projection: this.defaultFields,
|
||||
...(fields && Object.values(fields).length && { projection: fields }),
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Every finder below asserts its result. `this.col` is a `Collection<T>` and yields `WithId<T>`,
|
||||
* while the signatures return `DocumentWithProjection<P, O>` — a conditional type TS cannot reduce
|
||||
* while `P` and `O` are still type parameters, so it cannot verify the assignment either way.
|
||||
* Assert to the declared type rather than `any`, so a Promise/FindCursor mix-up still fails to compile.
|
||||
* The cursor cases need the `unknown` hop because `FindCursor` is invariant in its element type.
|
||||
*/
|
||||
|
||||
public findOneAndUpdate<P extends Document = T, O extends FindOneAndUpdateOptionsWithProjection = FindOneAndUpdateOptionsWithProjection>(
|
||||
query: Filter<T>,
|
||||
update: UpdateFilter<T> | T,
|
||||
options?: O,
|
||||
): Promise<DocumentWithDriverProjection<P, O> | null> {
|
||||
this.setUpdatedAt(update);
|
||||
|
||||
if (options?.upsert && !('_id' in update || (update.$set && '_id' in update.$set)) && !('_id' in query)) {
|
||||
update.$setOnInsert = {
|
||||
...(update.$setOnInsert || {}),
|
||||
_id: new ObjectId().toHexString(),
|
||||
} as Partial<T> & { _id: string };
|
||||
}
|
||||
|
||||
const result = this.col.findOneAndUpdate(query, update, (options || {}) as FindOneAndUpdateOptions);
|
||||
|
||||
return result as Promise<DocumentWithDriverProjection<P, O> | null>;
|
||||
}
|
||||
|
||||
async findOneById<P extends Document = T, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
_id: T['_id'],
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<P, O> | null> {
|
||||
return this.findOne<P, O>({ _id } as Filter<T>, options);
|
||||
}
|
||||
|
||||
async findOne<P extends Document = T, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
query: Filter<T> | T['_id'] = {},
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<P, O> | null> {
|
||||
const q: Filter<T> = typeof query === 'string' ? ({ _id: query } as Filter<T>) : query;
|
||||
const optionsDef = this.doNotMixInclusionAndExclusionFields(options);
|
||||
if (optionsDef) {
|
||||
return this.col.findOne(q, optionsDef) as Promise<DocumentWithProjection<P, O> | null>;
|
||||
}
|
||||
return this.col.findOne(q) as Promise<DocumentWithProjection<P, O> | null>;
|
||||
}
|
||||
|
||||
find<P extends Document = T, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
query: Filter<T> = {},
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<P, O>> {
|
||||
const optionsDef = this.doNotMixInclusionAndExclusionFields(options);
|
||||
return this.col.find(query, optionsDef) as unknown as FindCursor<DocumentWithProjection<P, O>>;
|
||||
}
|
||||
|
||||
findPaginated<P extends Document = T, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
query: Filter<T> = {},
|
||||
options?: O,
|
||||
): FindPaginated<FindCursor<DocumentWithProjection<P, O>>> {
|
||||
const optionsDef = this.doNotMixInclusionAndExclusionFields(options);
|
||||
|
||||
const cursor = optionsDef ? this.col.find(query, optionsDef) : this.col.find(query);
|
||||
const totalCount = this.col.countDocuments(query);
|
||||
|
||||
return {
|
||||
cursor: cursor as unknown as FindCursor<DocumentWithProjection<P, O>>,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link updateOne} or {@link updateAny} instead
|
||||
*/
|
||||
update(
|
||||
filter: Filter<T>,
|
||||
update: UpdateFilter<T> | Partial<T>,
|
||||
options?: UpdateOptions & { multi?: true },
|
||||
): Promise<UpdateResult | Document> {
|
||||
const operation = options?.multi ? 'updateMany' : 'updateOne';
|
||||
|
||||
return this[operation](filter, update, options);
|
||||
}
|
||||
|
||||
updateOne(filter: Filter<T>, update: UpdateFilter<T>, options?: UpdateOptions): Promise<UpdateResult> {
|
||||
this.setUpdatedAt(update);
|
||||
if (options) {
|
||||
if (options.upsert && !('_id' in update || (update.$set && '_id' in update.$set)) && !('_id' in filter)) {
|
||||
update.$setOnInsert = {
|
||||
...(update.$setOnInsert || {}),
|
||||
_id: new ObjectId().toHexString(),
|
||||
} as Partial<T> & { _id: string };
|
||||
}
|
||||
return this.col.updateOne(filter, update, options);
|
||||
}
|
||||
return this.col.updateOne(filter, update);
|
||||
}
|
||||
|
||||
updateMany(filter: Filter<T>, update: UpdateFilter<T> | Partial<T>, options?: UpdateOptions): Promise<Document | UpdateResult> {
|
||||
this.setUpdatedAt(update);
|
||||
if (options) {
|
||||
return this.col.updateMany(filter, update, options);
|
||||
}
|
||||
return this.col.updateMany(filter, update);
|
||||
}
|
||||
|
||||
insertMany(docs: InsertionModel<T>[], options?: BulkWriteOptions): Promise<InsertManyResult<T>> {
|
||||
docs = docs.map((doc) => {
|
||||
if (!doc._id || typeof doc._id !== 'string') {
|
||||
const oid = new ObjectId();
|
||||
doc = { ...doc, _id: oid.toHexString() } as InsertionModel<T>;
|
||||
}
|
||||
this.setUpdatedAt(doc);
|
||||
return doc;
|
||||
});
|
||||
|
||||
// TODO reavaluate following type casting
|
||||
return this.col.insertMany(docs as unknown as OptionalUnlessRequiredId<T>[], options || {});
|
||||
}
|
||||
|
||||
insertOne(doc: InsertionModel<T>, options?: InsertOneOptions): Promise<InsertOneResult<T>> {
|
||||
if (!doc._id || typeof doc._id !== 'string') {
|
||||
const oid = new ObjectId();
|
||||
doc = { _id: oid.toHexString(), ...doc };
|
||||
}
|
||||
|
||||
this.setUpdatedAt(doc);
|
||||
|
||||
// TODO reavaluate following type casting
|
||||
return this.col.insertOne(doc as unknown as OptionalUnlessRequiredId<T>, options || {});
|
||||
}
|
||||
|
||||
removeById(_id: T['_id'], options?: { session?: ClientSession }): Promise<DeleteResult> {
|
||||
return this.deleteOne({ _id } as Filter<T>, { session: options?.session });
|
||||
}
|
||||
|
||||
removeByIds(ids: T['_id'][]): Promise<DeleteResult> {
|
||||
return this.deleteMany({ _id: { $in: ids } } as unknown as Filter<T>);
|
||||
}
|
||||
|
||||
async deleteOne(filter: Filter<T>, options?: DeleteOptions & { bypassDocumentValidation?: boolean }): Promise<DeleteResult> {
|
||||
if (!this.trash) {
|
||||
if (options) {
|
||||
return this.col.deleteOne(filter, options);
|
||||
}
|
||||
return this.col.deleteOne(filter);
|
||||
}
|
||||
|
||||
const doc = await this.findOne(filter);
|
||||
|
||||
if (doc) {
|
||||
const { _id, ...record } = doc;
|
||||
|
||||
const trash: TDeleted = {
|
||||
...record,
|
||||
_deletedAt: new Date(),
|
||||
__collection__: this.name,
|
||||
} as unknown as TDeleted;
|
||||
|
||||
// since the operation is not atomic, we need to make sure that the record is not already deleted/inserted
|
||||
await this.trash?.updateOne(
|
||||
{ _id } as Filter<TDeleted>,
|
||||
{ $set: trash },
|
||||
{
|
||||
upsert: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (options) {
|
||||
return this.col.deleteOne(filter, options);
|
||||
}
|
||||
return this.col.deleteOne(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* No projection narrowing: whether the model archives to a trash collection is a runtime detail
|
||||
* (a constructor argument), and the trash path has to read the whole document to archive it, so
|
||||
* it returns every field regardless of the projection. Narrowing here would claim a filtering
|
||||
* that only happens for models without a trash collection.
|
||||
*/
|
||||
async findOneAndDelete(filter: Filter<T>, options?: FindOneAndDeleteOptions): Promise<WithId<T> | null> {
|
||||
if (!this.trash) {
|
||||
return this.col.findOneAndDelete(filter, options || {});
|
||||
}
|
||||
|
||||
const doc = await this.col.findOne(filter);
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { _id, ...record } = doc;
|
||||
const trash: TDeleted = {
|
||||
...record,
|
||||
_deletedAt: new Date(),
|
||||
__collection__: this.name,
|
||||
} as unknown as TDeleted;
|
||||
|
||||
await this.trash?.updateOne(
|
||||
{ _id } as Filter<TDeleted>,
|
||||
{ $set: trash },
|
||||
{
|
||||
upsert: true,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await this.col.deleteOne({ _id } as Filter<T>);
|
||||
} catch (e) {
|
||||
await this.trash?.deleteOne({ _id } as Filter<TDeleted>);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
findOneAndDeleteById(_id: T['_id'], options?: FindOneAndDeleteOptions): Promise<WithId<T> | null> {
|
||||
return this.findOneAndDelete({ _id } as Filter<T>, options);
|
||||
}
|
||||
|
||||
async deleteMany(filter: Filter<T>, options?: DeleteOptions & { onTrash?: (record: ResultFields<T, C>) => void }): Promise<DeleteResult> {
|
||||
if (!this.trash) {
|
||||
if (options) {
|
||||
return this.col.deleteMany(filter, options);
|
||||
}
|
||||
return this.col.deleteMany(filter);
|
||||
}
|
||||
|
||||
const cursor = this.find<ResultFields<T, C>>(filter, { session: options?.session });
|
||||
|
||||
const ids: T['_id'][] = [];
|
||||
for await (const doc of cursor) {
|
||||
const { _id, ...record } = doc as T;
|
||||
|
||||
const trash: TDeleted = {
|
||||
...record,
|
||||
_deletedAt: new Date(),
|
||||
__collection__: this.name,
|
||||
} as unknown as TDeleted;
|
||||
|
||||
ids.push(_id);
|
||||
|
||||
// since the operation is not atomic, we need to make sure that the record is not already deleted/inserted
|
||||
await this.trash?.updateOne(
|
||||
{ _id } as Filter<TDeleted>,
|
||||
{ $set: trash },
|
||||
{
|
||||
upsert: true,
|
||||
session: options?.session,
|
||||
},
|
||||
);
|
||||
|
||||
void options?.onTrash?.(doc);
|
||||
}
|
||||
|
||||
if (options) {
|
||||
return this.col.deleteMany({ _id: { $in: ids } } as unknown as Filter<T>, options);
|
||||
}
|
||||
return this.col.deleteMany({ _id: { $in: ids } } as unknown as Filter<T>);
|
||||
}
|
||||
|
||||
// Trash
|
||||
trashFind<P extends TDeleted>(
|
||||
query: Filter<TDeleted>,
|
||||
options?: FindOptions<P extends TDeleted ? TDeleted : P>,
|
||||
): FindCursor<WithId<TDeleted>> | undefined {
|
||||
if (!this.trash) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
return this.trash.find(
|
||||
{
|
||||
__collection__: this.name,
|
||||
...query,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
return this.trash.find({
|
||||
__collection__: this.name,
|
||||
...query,
|
||||
});
|
||||
}
|
||||
|
||||
trashFindOneById(_id: TDeleted['_id']): Promise<TDeleted | null>;
|
||||
|
||||
trashFindOneById<P extends Document>(_id: TDeleted['_id'], options: FindOptions<P extends TDeleted ? TDeleted : P>): Promise<P | null>;
|
||||
|
||||
async trashFindOneById<P extends TDeleted>(
|
||||
_id: TDeleted['_id'],
|
||||
options?: FindOptions<P extends TDeleted ? TDeleted : P>,
|
||||
): Promise<WithId<P | TDeleted> | null> {
|
||||
const query = {
|
||||
_id,
|
||||
__collection__: this.name,
|
||||
} as Filter<P | TDeleted>;
|
||||
|
||||
if (!this.trash) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
return (this.trash as Collection<P | TDeleted>).findOne(query, options);
|
||||
}
|
||||
return (this.trash as Collection<P | TDeleted>).findOne(query);
|
||||
}
|
||||
|
||||
private setUpdatedAt(record: UpdateFilter<T> | InsertionModel<T>): void {
|
||||
if (this.preventSetUpdatedAt) {
|
||||
return;
|
||||
}
|
||||
setUpdatedAt(record);
|
||||
}
|
||||
|
||||
trashFindDeletedAfter(deletedAt: Date): FindCursor<WithId<TDeleted>>;
|
||||
|
||||
trashFindDeletedAfter<P extends Document = TDeleted>(
|
||||
deletedAt: Date,
|
||||
query?: Filter<TDeleted>,
|
||||
options?: FindOptions<P extends TDeleted ? TDeleted : P>,
|
||||
): FindCursor<WithId<TDeleted>> {
|
||||
const q = {
|
||||
__collection__: this.name,
|
||||
_deletedAt: {
|
||||
$gt: deletedAt,
|
||||
},
|
||||
...query,
|
||||
} as Filter<TDeleted>;
|
||||
|
||||
if (!this.trash) {
|
||||
throw new Error('Trash is not enabled for this collection');
|
||||
}
|
||||
|
||||
if (options) {
|
||||
return this.trash.find(q, options);
|
||||
}
|
||||
return this.trash.find(q);
|
||||
}
|
||||
|
||||
trashFindPaginatedDeletedAfter<P extends Document = TDeleted>(
|
||||
deletedAt: Date,
|
||||
query?: Filter<TDeleted>,
|
||||
options?: FindOptions<P extends TDeleted ? TDeleted : P>,
|
||||
): FindPaginated<FindCursor<WithId<TDeleted>>> {
|
||||
const q: Filter<TDeleted> = {
|
||||
__collection__: this.name,
|
||||
_deletedAt: {
|
||||
$gt: deletedAt,
|
||||
},
|
||||
...query,
|
||||
} as Filter<TDeleted>;
|
||||
|
||||
if (!this.trash) {
|
||||
throw new Error('Trash is not enabled for this collection');
|
||||
}
|
||||
|
||||
const cursor = options ? this.trash.find(q, options) : this.trash.find(q);
|
||||
const totalCount = this.trash.countDocuments(q);
|
||||
|
||||
return {
|
||||
cursor,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
|
||||
watch(pipeline?: object[]): ChangeStream<T> {
|
||||
return this.col.watch(pipeline);
|
||||
}
|
||||
|
||||
countDocuments(query: Filter<T>, options?: CountDocumentsOptions): Promise<number> {
|
||||
if (options) {
|
||||
return this.col.countDocuments(query, options);
|
||||
}
|
||||
return this.col.countDocuments(query);
|
||||
}
|
||||
|
||||
estimatedDocumentCount(): Promise<number> {
|
||||
return this.col.estimatedDocumentCount();
|
||||
}
|
||||
}
|
||||
158
packages/models/src/models/BaseUploadModel.ts
Normal file
158
packages/models/src/models/BaseUploadModel.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import type { EncryptedContent, IUpload } from '@rocket.chat/core-typings';
|
||||
import type { IBaseUploadsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type {
|
||||
DeleteResult,
|
||||
IndexDescription,
|
||||
UpdateResult,
|
||||
Document,
|
||||
InsertOneResult,
|
||||
WithId,
|
||||
Filter,
|
||||
FindCursor,
|
||||
ClientSession,
|
||||
} from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
type T = IUpload;
|
||||
|
||||
export abstract class BaseUploadModelRaw extends BaseRaw<T> implements IBaseUploadsModel<T> {
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{ key: { name: 1 }, sparse: true },
|
||||
{ key: { rid: 1 }, sparse: true },
|
||||
{ key: { expiresAt: 1 }, sparse: true },
|
||||
];
|
||||
}
|
||||
|
||||
async insertFileInit(userId: string, store: string, file: { name: string }, extra: object = {}): Promise<InsertOneResult<WithId<T>>> {
|
||||
const fileData: Partial<T> = {
|
||||
userId,
|
||||
store,
|
||||
complete: false,
|
||||
uploading: true,
|
||||
progress: 0,
|
||||
extension: file.name.split('.').pop(),
|
||||
uploadedAt: new Date(),
|
||||
...file,
|
||||
...extra,
|
||||
};
|
||||
|
||||
if (fileData.type) {
|
||||
fileData.typeGroup = fileData.type.split('/').shift();
|
||||
}
|
||||
|
||||
return this.insertOne(fileData);
|
||||
}
|
||||
|
||||
updateFileComplete(fileId: string, userId: string, file: object): Promise<Document | UpdateResult> | undefined {
|
||||
if (!fileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = {
|
||||
_id: fileId,
|
||||
userId,
|
||||
};
|
||||
|
||||
const update: Filter<T> = {
|
||||
$set: {
|
||||
complete: true,
|
||||
uploading: false,
|
||||
progress: 1,
|
||||
},
|
||||
};
|
||||
|
||||
update.$set = Object.assign(file, update.$set);
|
||||
|
||||
if (update.$set.type) {
|
||||
update.$set.typeGroup = update.$set.type.split('/').shift();
|
||||
}
|
||||
|
||||
return this.updateOne(filter, update);
|
||||
}
|
||||
|
||||
confirmTemporaryFile(fileId: string, userId: string): Promise<Document | UpdateResult> | undefined {
|
||||
if (!fileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = {
|
||||
_id: fileId,
|
||||
userId,
|
||||
};
|
||||
|
||||
const update: Filter<T> = {
|
||||
$unset: {
|
||||
expiresAt: 1,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(filter, update);
|
||||
}
|
||||
|
||||
findByIds<T_ extends Document = T, O extends FindOptionsWithProjection<T_> = FindOptionsWithProjection<T_>>(
|
||||
_ids: string[],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T_, O>> {
|
||||
const query = {
|
||||
_id: {
|
||||
$in: _ids,
|
||||
},
|
||||
};
|
||||
|
||||
return this.find<T_, O>(query, options);
|
||||
}
|
||||
|
||||
async findOneByName(name: string, options?: { session?: ClientSession }): Promise<T | null> {
|
||||
return this.findOne<T>({ name }, { session: options?.session });
|
||||
}
|
||||
|
||||
async findOneByRoomId(rid: string): Promise<T | null> {
|
||||
return this.findOne({ rid });
|
||||
}
|
||||
|
||||
findExpiredTemporaryFiles<T_ extends Document = T, O extends FindOptionsWithProjection<T_> = FindOptionsWithProjection<T_>>(
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T_, O>> {
|
||||
return this.find<T_, O>(
|
||||
{
|
||||
expiresAt: {
|
||||
$lte: new Date(),
|
||||
},
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
async updateFileNameById(fileId: string, name: string): Promise<Document | UpdateResult> {
|
||||
const filter = { _id: fileId };
|
||||
const update = {
|
||||
$set: {
|
||||
name,
|
||||
},
|
||||
};
|
||||
return this.updateOne(filter, update);
|
||||
}
|
||||
|
||||
async deleteFile(fileId: string, options?: { session?: ClientSession }): Promise<DeleteResult> {
|
||||
return this.deleteOne({ _id: fileId }, { session: options?.session });
|
||||
}
|
||||
|
||||
async findOneByIdAndUserIdAndRoomId<T_ extends Document = T, O extends FindOptionsWithProjection<T_> = FindOptionsWithProjection<T_>>(
|
||||
fileId: string,
|
||||
userId: string,
|
||||
rid: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T_, O> | null> {
|
||||
return this.findOne<T_, O>({ _id: fileId, userId, rid }, options);
|
||||
}
|
||||
|
||||
async updateFileMetadata(
|
||||
fileId: string,
|
||||
userId: string,
|
||||
metadata: { name?: string; description?: string; typeGroup?: string; content?: EncryptedContent },
|
||||
): Promise<UpdateResult | null> {
|
||||
return this.updateOne({ _id: fileId, userId }, { $set: metadata });
|
||||
}
|
||||
}
|
||||
189
packages/models/src/models/CalendarEvent.ts
Normal file
189
packages/models/src/models/CalendarEvent.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import type { ICalendarEvent, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ICalendarEventModel } from '@rocket.chat/model-typings';
|
||||
import type { FindCursor, IndexDescription, Collection, Db, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class CalendarEventRaw extends BaseRaw<ICalendarEvent> implements ICalendarEventModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ICalendarEvent>>) {
|
||||
super(db, 'calendar_event', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{
|
||||
key: { startTime: -1, uid: 1, externalId: 1 },
|
||||
},
|
||||
{
|
||||
key: { reminderTime: -1, notificationSent: 1 },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public async findOneByExternalIdAndUserId(
|
||||
externalId: Required<ICalendarEvent>['externalId'],
|
||||
uid: ICalendarEvent['uid'],
|
||||
): Promise<ICalendarEvent | null> {
|
||||
return this.findOne({
|
||||
externalId,
|
||||
uid,
|
||||
});
|
||||
}
|
||||
|
||||
public findByUserIdAndDate(uid: IUser['_id'], date: Date): FindCursor<ICalendarEvent> {
|
||||
const startTime = new Date(date.toISOString());
|
||||
startTime.setHours(0, 0, 0, 0);
|
||||
|
||||
const finalTime = new Date(date.valueOf());
|
||||
finalTime.setDate(finalTime.getDate() + 1);
|
||||
|
||||
return this.find(
|
||||
{
|
||||
uid,
|
||||
startTime: { $gte: startTime, $lt: finalTime },
|
||||
},
|
||||
{
|
||||
sort: { startTime: 1 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async updateEvent(
|
||||
eventId: ICalendarEvent['_id'],
|
||||
{ subject, description, startTime, endTime, meetingUrl, reminderMinutesBeforeStart, reminderTime, busy }: Partial<ICalendarEvent>,
|
||||
): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{ _id: eventId },
|
||||
{
|
||||
$set: {
|
||||
...(subject !== undefined ? { subject } : {}),
|
||||
...(description !== undefined ? { description } : {}),
|
||||
...(startTime ? { startTime } : {}),
|
||||
...(endTime && { endTime }),
|
||||
...(meetingUrl !== undefined ? { meetingUrl } : {}),
|
||||
...(reminderMinutesBeforeStart ? { reminderMinutesBeforeStart } : {}),
|
||||
...(reminderTime ? { reminderTime } : {}),
|
||||
...(typeof busy === 'boolean' && { busy }),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async findNextNotificationDate(): Promise<Date | null> {
|
||||
const nextEvent = await this.findOne<Pick<ICalendarEvent, 'reminderTime'>>(
|
||||
{
|
||||
reminderTime: {
|
||||
$gt: new Date(),
|
||||
},
|
||||
notificationSent: false,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
reminderTime: 1,
|
||||
},
|
||||
projection: {
|
||||
reminderTime: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return nextEvent?.reminderTime || null;
|
||||
}
|
||||
|
||||
public findEventsToNotify(notificationTime: Date, minutes: number): FindCursor<ICalendarEvent> {
|
||||
// Find all the events between notificationTime and +minutes that have not been notified yet
|
||||
const maxDate = new Date(notificationTime.toISOString());
|
||||
maxDate.setMinutes(maxDate.getMinutes() + minutes);
|
||||
|
||||
return this.find(
|
||||
{
|
||||
reminderTime: {
|
||||
$gte: notificationTime,
|
||||
$lt: maxDate,
|
||||
},
|
||||
notificationSent: false,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
reminderTime: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async flagNotificationSent(eventId: ICalendarEvent['_id']): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: eventId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
notificationSent: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public findOverlappingEvents(
|
||||
eventId: ICalendarEvent['_id'],
|
||||
uid: IUser['_id'],
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
): FindCursor<ICalendarEvent> {
|
||||
return this.find({
|
||||
_id: { $ne: eventId }, // Exclude current event
|
||||
uid,
|
||||
busy: { $ne: false },
|
||||
$or: [
|
||||
// Event starts during our event
|
||||
{ startTime: { $gte: startTime, $lt: endTime } },
|
||||
// Event ends during our event
|
||||
{ endTime: { $gt: startTime, $lte: endTime } },
|
||||
// Event completely contains our event
|
||||
{ startTime: { $lte: startTime }, endTime: { $gte: endTime } },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
public async findNextFutureEvent(startTime: Date): Promise<Pick<ICalendarEvent, '_id' | 'startTime'> | null> {
|
||||
return this.findOne(
|
||||
{
|
||||
startTime: { $gte: startTime },
|
||||
busy: { $ne: false },
|
||||
endTime: { $exists: true },
|
||||
},
|
||||
{
|
||||
sort: { startTime: 1 },
|
||||
projection: {
|
||||
startTime: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public findEventsStartingNow({
|
||||
now,
|
||||
offset = 1000,
|
||||
}: {
|
||||
now: Date;
|
||||
offset?: number;
|
||||
}): FindCursor<Pick<ICalendarEvent, '_id' | 'uid' | 'startTime' | 'endTime'>> {
|
||||
return this.find(
|
||||
{
|
||||
startTime: {
|
||||
$gte: new Date(now.getTime() - offset),
|
||||
$lt: new Date(now.getTime() + offset),
|
||||
},
|
||||
busy: { $ne: false },
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
_id: 1,
|
||||
uid: 1,
|
||||
startTime: 1,
|
||||
endTime: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
92
packages/models/src/models/CallHistory.ts
Normal file
92
packages/models/src/models/CallHistory.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { CallHistoryItem, IRegisterUser, IUser } from '@rocket.chat/core-typings';
|
||||
import type { FindPaginated, ICallHistoryModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
import type { Db, Filter, FindCursor, IndexDescription, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class CallHistoryRaw extends BaseRaw<CallHistoryItem> implements ICallHistoryModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'call_history');
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { uid: 1, callId: 1 }, unique: true }, { key: { uid: 1, ts: -1 } }];
|
||||
}
|
||||
|
||||
async findOneByIdAndUid<T extends Document = CallHistoryItem, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
_id: CallHistoryItem['_id'],
|
||||
uid: CallHistoryItem['uid'],
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>({ _id, uid }, options);
|
||||
}
|
||||
|
||||
async findOneByCallIdAndUid<T extends Document = CallHistoryItem, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
callId: CallHistoryItem['callId'],
|
||||
uid: CallHistoryItem['uid'],
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>({ callId, uid }, options);
|
||||
}
|
||||
|
||||
public async updateUserReferences(
|
||||
userId: IRegisterUser['_id'],
|
||||
username: IRegisterUser['username'],
|
||||
name?: IRegisterUser['name'],
|
||||
): Promise<void> {
|
||||
await this.updateMany(
|
||||
{
|
||||
contactId: userId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
contactUsername: username,
|
||||
...(name && { contactName: name }),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public findAllByUserIdAndSearchFilters<
|
||||
T extends Document = CallHistoryItem,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(
|
||||
uid: IUser['_id'],
|
||||
filters: {
|
||||
type?: CallHistoryItem['type'];
|
||||
searchTerm?: string;
|
||||
direction?: CallHistoryItem['direction'];
|
||||
inStates?: CallHistoryItem['state'][];
|
||||
},
|
||||
options?: O,
|
||||
): FindPaginated<FindCursor<DocumentWithProjection<T, O>>> {
|
||||
const { type, direction, inStates, searchTerm } = filters;
|
||||
|
||||
const textSearch = searchTerm ? { $regex: escapeRegExp(searchTerm), $options: 'i' } : null;
|
||||
|
||||
const query: Filter<CallHistoryItem> = {
|
||||
uid,
|
||||
...(type && { type }),
|
||||
...(direction && { direction }),
|
||||
...(inStates?.length && { state: { $in: inStates } }),
|
||||
...(textSearch && {
|
||||
$or: [
|
||||
{
|
||||
contactName: textSearch,
|
||||
},
|
||||
{
|
||||
external: false,
|
||||
contactUsername: textSearch,
|
||||
},
|
||||
{
|
||||
external: true,
|
||||
contactExtension: textSearch,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
return this.findPaginated<T, O>(query, options);
|
||||
}
|
||||
}
|
||||
44
packages/models/src/models/CredentialTokens.ts
Normal file
44
packages/models/src/models/CredentialTokens.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { ICredentialToken, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ICredentialTokensModel, InsertionModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class CredentialTokensRaw extends BaseRaw<ICredentialToken> implements ICredentialTokensModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ICredentialToken>>) {
|
||||
super(db, 'credential_tokens', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { expireAt: 1 }, sparse: true, expireAfterSeconds: 0 }];
|
||||
}
|
||||
|
||||
async create(_id: string, userInfo: ICredentialToken['userInfo']): Promise<void> {
|
||||
const validForMilliseconds = 60000; // Valid for 60 seconds
|
||||
const token: InsertionModel<ICredentialToken> = {
|
||||
_id,
|
||||
userInfo,
|
||||
expireAt: new Date(Date.now() + validForMilliseconds),
|
||||
};
|
||||
|
||||
await this.insertOne(token);
|
||||
}
|
||||
|
||||
findOneNotExpiredById(_id: string): Promise<ICredentialToken | null> {
|
||||
const query = {
|
||||
_id,
|
||||
expireAt: { $gt: new Date() },
|
||||
};
|
||||
|
||||
return this.findOne(query);
|
||||
}
|
||||
|
||||
removeNotExpiredById(_id: string): Promise<ICredentialToken | null> {
|
||||
const query = {
|
||||
_id,
|
||||
expireAt: { $gt: new Date() },
|
||||
};
|
||||
|
||||
return this.findOneAndDelete(query);
|
||||
}
|
||||
}
|
||||
15
packages/models/src/models/CronHistoryModel.ts
Normal file
15
packages/models/src/models/CronHistoryModel.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ICronHistoryItem } from '@rocket.chat/core-typings';
|
||||
import type { ICronHistoryModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class CronHistoryRaw extends BaseRaw<ICronHistoryItem> implements ICronHistoryModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'cron_history');
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { intendedAt: 1, name: 1 }, unique: true }];
|
||||
}
|
||||
}
|
||||
51
packages/models/src/models/CustomSounds.ts
Normal file
51
packages/models/src/models/CustomSounds.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { ICustomSound, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ICustomSoundsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, FindCursor, Db, IndexDescription, InsertOneResult, UpdateResult, WithId, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class CustomSoundsRaw extends BaseRaw<ICustomSound> implements ICustomSoundsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ICustomSound>>) {
|
||||
super(db, 'custom_sounds', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { name: 1 } }];
|
||||
}
|
||||
|
||||
// find
|
||||
findByName<T extends Document = ICustomSound, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
name: string,
|
||||
exceptId?: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
name,
|
||||
...(exceptId && { _id: { $nin: [exceptId] } }),
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
findOneByName<T extends Document = ICustomSound, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
name: string,
|
||||
exceptId?: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
const query = {
|
||||
name,
|
||||
...(exceptId && { _id: { $nin: [exceptId] } }),
|
||||
};
|
||||
|
||||
return this.findOne<T, O>(query, options);
|
||||
}
|
||||
|
||||
// INSERT
|
||||
create(data: Omit<ICustomSound, '_id' | '_updatedAt'>): Promise<InsertOneResult<WithId<ICustomSound>>> {
|
||||
return this.insertOne(data);
|
||||
}
|
||||
|
||||
updateById(_id: string, data: Partial<Omit<ICustomSound, '_id'>>): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id }, { $set: data });
|
||||
}
|
||||
}
|
||||
86
packages/models/src/models/CustomUserStatus.ts
Normal file
86
packages/models/src/models/CustomUserStatus.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { ICustomUserStatus, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ICustomUserStatusModel, InsertionModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, FindCursor, Db, IndexDescription, InsertOneResult, UpdateResult, WithId, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class CustomUserStatusRaw extends BaseRaw<ICustomUserStatus> implements ICustomUserStatusModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ICustomUserStatus>>) {
|
||||
super(db, 'custom_user_status', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { name: 1 } }];
|
||||
}
|
||||
|
||||
async findOneByName<T extends Document = ICustomUserStatus, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
name: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return options ? this.findOne<T, O>({ name }, options) : this.findOne<T, O>({ name });
|
||||
}
|
||||
|
||||
findOneByNameExceptId<T extends Document = ICustomUserStatus, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
name: string,
|
||||
except: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
const query = {
|
||||
_id: { $nin: [except] },
|
||||
name,
|
||||
};
|
||||
|
||||
return this.findOne<T, O>(query, options);
|
||||
}
|
||||
|
||||
// find
|
||||
findByName<T extends Document = ICustomUserStatus, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
name: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
name,
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
findByNameExceptId<T extends Document = ICustomUserStatus, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
name: string,
|
||||
except: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
_id: { $nin: [except] },
|
||||
name,
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
// update
|
||||
setName(_id: string, name: string): Promise<UpdateResult> {
|
||||
const update = {
|
||||
$set: {
|
||||
name,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne({ _id }, update);
|
||||
}
|
||||
|
||||
setStatusType(_id: string, statusType: string): Promise<UpdateResult> {
|
||||
const update = {
|
||||
$set: {
|
||||
statusType,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne({ _id }, update);
|
||||
}
|
||||
|
||||
// INSERT
|
||||
create(data: InsertionModel<ICustomUserStatus>): Promise<InsertOneResult<WithId<ICustomUserStatus>>> {
|
||||
return this.insertOne(data);
|
||||
}
|
||||
}
|
||||
39
packages/models/src/models/EmailInbox.ts
Normal file
39
packages/models/src/models/EmailInbox.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { IEmailInbox, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IEmailInboxModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, FindCursor, IndexDescription, InsertOneResult, UpdateFilter, WithId } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class EmailInboxRaw extends BaseRaw<IEmailInbox> implements IEmailInboxModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IEmailInbox>>) {
|
||||
super(db, 'email_inbox', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { email: 1 }, unique: true }];
|
||||
}
|
||||
|
||||
async setDisabledById(id: IEmailInbox['_id']): Promise<null | WithId<IEmailInbox>> {
|
||||
return this.findOneAndUpdate({ _id: id, active: true }, { $set: { active: false } }, { returnDocument: 'after' });
|
||||
}
|
||||
|
||||
async create(emailInbox: IEmailInbox): Promise<InsertOneResult<IEmailInbox>> {
|
||||
return this.insertOne(emailInbox);
|
||||
}
|
||||
|
||||
async updateById(id: IEmailInbox['_id'], data: UpdateFilter<IEmailInbox>): Promise<null | WithId<Pick<IEmailInbox, '_id'>>> {
|
||||
// findOneAndUpdate doesn't accept generics, so we had to type cast
|
||||
return this.findOneAndUpdate({ _id: id }, data, {
|
||||
returnDocument: 'after',
|
||||
projection: { _id: 1 },
|
||||
});
|
||||
}
|
||||
|
||||
findActive(): FindCursor<IEmailInbox> {
|
||||
return this.find({ active: true });
|
||||
}
|
||||
|
||||
async findByEmail(email: IEmailInbox['email']): Promise<IEmailInbox | null> {
|
||||
return this.findOne({ email });
|
||||
}
|
||||
}
|
||||
23
packages/models/src/models/EmailMessageHistory.ts
Normal file
23
packages/models/src/models/EmailMessageHistory.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { IEmailMessageHistory, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IEmailMessageHistoryModel, InsertionModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, InsertOneResult, WithId, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class EmailMessageHistoryRaw extends BaseRaw<IEmailMessageHistory> implements IEmailMessageHistoryModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IEmailMessageHistory>>) {
|
||||
super(db, 'email_message_history', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { createdAt: 1 }, expireAfterSeconds: 60 * 60 * 24 }];
|
||||
}
|
||||
|
||||
async create({ _id, email }: InsertionModel<IEmailMessageHistory>): Promise<InsertOneResult<WithId<IEmailMessageHistory>>> {
|
||||
return this.insertOne({
|
||||
_id,
|
||||
email,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
121
packages/models/src/models/EmojiCustom.ts
Normal file
121
packages/models/src/models/EmojiCustom.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { IEmojiCustom, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IEmojiCustomModel, InsertionModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Filter, FindCursor, Db, IndexDescription, InsertOneResult, UpdateResult, WithId, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class EmojiCustomRaw extends BaseRaw<IEmojiCustom> implements IEmojiCustomModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IEmojiCustom>>) {
|
||||
super(db, 'custom_emoji', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { name: 1 } }, { key: { aliases: 1 } }, { key: { extension: 1 } }];
|
||||
}
|
||||
|
||||
// find
|
||||
findByNameOrAlias<T extends Document = IEmojiCustom, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
emojiName: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
let name = emojiName;
|
||||
|
||||
if (typeof emojiName === 'string') {
|
||||
name = emojiName.replaceAll(':', '');
|
||||
}
|
||||
|
||||
const query = {
|
||||
$or: [{ name }, { aliases: name }],
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
findOneByNamesOrAliases<T extends Document = IEmojiCustom, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
names: string[],
|
||||
exceptId?: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
const query: Filter<IEmojiCustom> = {
|
||||
...(exceptId && { _id: { $nin: [exceptId] } }),
|
||||
$or: [{ name: { $in: names } }, { aliases: { $in: names } }],
|
||||
};
|
||||
|
||||
return this.findOne<T, O>(query, options);
|
||||
}
|
||||
|
||||
findByNameOrAliasExceptID<T extends Document = IEmojiCustom, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
name: string,
|
||||
except: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
_id: { $nin: [except] },
|
||||
$or: [{ name }, { aliases: name }],
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
// update
|
||||
setName(_id: string, name: string): Promise<UpdateResult> {
|
||||
const update = {
|
||||
$set: {
|
||||
name,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne({ _id }, update);
|
||||
}
|
||||
|
||||
setAliases(_id: string, aliases: string[]): Promise<UpdateResult> {
|
||||
const update = {
|
||||
$set: {
|
||||
aliases,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne({ _id }, update);
|
||||
}
|
||||
|
||||
setExtension(_id: string, extension: string): Promise<UpdateResult> {
|
||||
const update = {
|
||||
$set: {
|
||||
extension,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne({ _id }, update);
|
||||
}
|
||||
|
||||
setETagByName(name: string, etag: string): Promise<UpdateResult> {
|
||||
const update = {
|
||||
$set: {
|
||||
etag,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne({ name }, update);
|
||||
}
|
||||
|
||||
// INSERT
|
||||
create(data: InsertionModel<IEmojiCustom>): Promise<InsertOneResult<WithId<IEmojiCustom>>> {
|
||||
return this.insertOne(data);
|
||||
}
|
||||
|
||||
countByNameOrAlias(name: string): Promise<number> {
|
||||
const query = {
|
||||
$or: [{ name }, { aliases: name }],
|
||||
};
|
||||
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
|
||||
// TODO: convert name: string to branded type using to enforce validation also replace this type cross the models/apis
|
||||
findOneByName<T extends Document = IEmojiCustom, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
name: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>({ name }, options);
|
||||
}
|
||||
}
|
||||
70
packages/models/src/models/ExportOperations.ts
Normal file
70
packages/models/src/models/ExportOperations.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { IExportOperation, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IExportOperationsModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, IndexDescription, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class ExportOperationsRaw extends BaseRaw<IExportOperation> implements IExportOperationsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IExportOperation>>) {
|
||||
super(db, 'export_operations', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { userId: 1 } }, { key: { status: 1 } }];
|
||||
}
|
||||
|
||||
findOnePending(): Promise<IExportOperation | null> {
|
||||
const query = {
|
||||
status: { $nin: ['completed', 'skipped'] },
|
||||
};
|
||||
|
||||
return this.findOne(query);
|
||||
}
|
||||
|
||||
async create(data: IExportOperation): Promise<string> {
|
||||
const result = await this.insertOne({
|
||||
...data,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
return result.insertedId;
|
||||
}
|
||||
|
||||
findLastOperationByUser(userId: string, fullExport = false): Promise<IExportOperation | null> {
|
||||
const query = {
|
||||
userId,
|
||||
fullExport,
|
||||
};
|
||||
|
||||
return this.findOne(query, { sort: { createdAt: -1 } });
|
||||
}
|
||||
|
||||
countAllPendingBeforeMyRequest(requestDay: Date): Promise<number> {
|
||||
const query = {
|
||||
status: { $nin: ['completed', 'skipped'] },
|
||||
createdAt: { $lt: requestDay },
|
||||
};
|
||||
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
|
||||
updateOperation(data: IExportOperation): Promise<UpdateResult> {
|
||||
const update = {
|
||||
$set: {
|
||||
roomList: data.roomList,
|
||||
status: data.status,
|
||||
fileList: data.fileList,
|
||||
generatedFile: data.generatedFile,
|
||||
fileId: data.fileId,
|
||||
userNameTable: data.userNameTable,
|
||||
userData: data.userData,
|
||||
generatedUserFile: data.generatedUserFile,
|
||||
generatedAvatar: data.generatedAvatar,
|
||||
exportPath: data.exportPath,
|
||||
assetsPath: data.assetsPath,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne({ _id: data._id }, update);
|
||||
}
|
||||
}
|
||||
68
packages/models/src/models/FederationKeys.ts
Normal file
68
packages/models/src/models/FederationKeys.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { FederationKey, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IFederationKeysModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, Collection } from 'mongodb';
|
||||
import NodeRSA from 'node-rsa';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class FederationKeysRaw extends BaseRaw<FederationKey> implements IFederationKeysModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<FederationKey>>) {
|
||||
super(db, 'federation_keys', trash);
|
||||
}
|
||||
|
||||
async getKey(type: FederationKey['type']): Promise<string | null> {
|
||||
const keyResource = await this.findOne({ type });
|
||||
|
||||
if (!keyResource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return keyResource.key;
|
||||
}
|
||||
|
||||
loadKey(keyData: NodeRSA.Key, type: FederationKey['type']): NodeRSA {
|
||||
return new NodeRSA(keyData, `pkcs8-${type}-pem`);
|
||||
}
|
||||
|
||||
async generateKeys(): Promise<{
|
||||
privateKey: '' | NodeRSA | null;
|
||||
publicKey: '' | NodeRSA | null;
|
||||
}> {
|
||||
const key = new NodeRSA({ b: 2048 });
|
||||
|
||||
key.generateKeyPair();
|
||||
|
||||
await this.deleteMany({});
|
||||
|
||||
await this.insertOne({
|
||||
type: 'private',
|
||||
key: key.exportKey('pkcs8-private-pem').replace(/\n|\r/g, ''),
|
||||
});
|
||||
|
||||
await this.insertOne({
|
||||
type: 'public',
|
||||
key: key.exportKey('pkcs8-public-pem').replace(/\n|\r/g, ''),
|
||||
});
|
||||
|
||||
return {
|
||||
privateKey: await this.getPrivateKey(),
|
||||
publicKey: await this.getPublicKey(),
|
||||
};
|
||||
}
|
||||
|
||||
async getPrivateKey(): Promise<'' | NodeRSA | null> {
|
||||
const keyData = await this.getKey('private');
|
||||
|
||||
return keyData && this.loadKey(keyData, 'private');
|
||||
}
|
||||
|
||||
async getPublicKey(): Promise<'' | NodeRSA | null> {
|
||||
const keyData = await this.getKey('public');
|
||||
|
||||
return keyData && this.loadKey(keyData, 'public');
|
||||
}
|
||||
|
||||
getPublicKeyString(): Promise<string | null> {
|
||||
return this.getKey('public');
|
||||
}
|
||||
}
|
||||
109
packages/models/src/models/ImportData.ts
Normal file
109
packages/models/src/models/ImportData.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type {
|
||||
IImportChannelRecord,
|
||||
IImportRecord,
|
||||
IImportUserRecord,
|
||||
IImportContactRecord,
|
||||
RocketChatRecordDeleted,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import type { IImportDataModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class ImportDataRaw extends BaseRaw<IImportRecord> implements IImportDataModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IImportRecord>>) {
|
||||
super(db, 'import_data', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { dataType: 1 } }];
|
||||
}
|
||||
|
||||
getAllUsersForSelection(): Promise<Array<IImportUserRecord>> {
|
||||
return this.find<IImportUserRecord>(
|
||||
{
|
||||
dataType: 'user',
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
'data.importIds': 1,
|
||||
'data.username': 1,
|
||||
'data.emails': 1,
|
||||
'data.deleted': 1,
|
||||
'data.type': 1,
|
||||
},
|
||||
},
|
||||
).toArray();
|
||||
}
|
||||
|
||||
getAllChannelsForSelection(): Promise<Array<IImportChannelRecord>> {
|
||||
return this.find<IImportChannelRecord>(
|
||||
{
|
||||
'dataType': 'channel',
|
||||
'data.t': {
|
||||
$ne: 'd',
|
||||
},
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
'data.importIds': 1,
|
||||
'data.name': 1,
|
||||
'data.archived': 1,
|
||||
'data.t': 1,
|
||||
},
|
||||
},
|
||||
).toArray();
|
||||
}
|
||||
|
||||
getAllContactsForSelection(): Promise<IImportContactRecord[]> {
|
||||
return this.find<IImportContactRecord>(
|
||||
{
|
||||
dataType: 'contact',
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
'data.importIds': 1,
|
||||
'data.name': 1,
|
||||
'data.phones': 1,
|
||||
'data.emails': 1,
|
||||
},
|
||||
},
|
||||
).toArray();
|
||||
}
|
||||
|
||||
async checkIfDirectMessagesExists(): Promise<boolean> {
|
||||
return (
|
||||
(await this.countDocuments({
|
||||
'dataType': 'channel',
|
||||
'data.t': 'd',
|
||||
})) > 0
|
||||
);
|
||||
}
|
||||
|
||||
async countMessages(): Promise<number> {
|
||||
return this.countDocuments({ dataType: 'message' });
|
||||
}
|
||||
|
||||
async findChannelImportIdByNameOrImportId(channelIdentifier: string): Promise<string | undefined> {
|
||||
const channel = await this.findOne<IImportChannelRecord>(
|
||||
{
|
||||
dataType: 'channel',
|
||||
$or: [
|
||||
{
|
||||
'data.name': channelIdentifier,
|
||||
},
|
||||
{
|
||||
'data.importIds': channelIdentifier,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
'data.importIds': 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return channel?.data?.importIds?.shift();
|
||||
}
|
||||
}
|
||||
52
packages/models/src/models/Imports.ts
Normal file
52
packages/models/src/models/Imports.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { IImport } from '@rocket.chat/core-typings';
|
||||
import type { IImportsModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, Document, UpdateResult, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class ImportsModel extends BaseRaw<IImport> implements IImportsModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'import');
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { ts: -1 } }, { key: { valid: 1 } }];
|
||||
}
|
||||
|
||||
async findLastImport(): Promise<IImport | undefined> {
|
||||
const imports = await this.find({}, { sort: { ts: -1 }, limit: 1 }).toArray();
|
||||
|
||||
return imports.shift();
|
||||
}
|
||||
|
||||
invalidateAllOperations(): Promise<UpdateResult | Document> {
|
||||
return this.updateMany({ valid: { $ne: false } }, { $set: { valid: false } });
|
||||
}
|
||||
|
||||
invalidateOperationsExceptId(id: string): Promise<UpdateResult | Document> {
|
||||
return this.updateMany({ valid: { $ne: false }, _id: { $ne: id } }, { $set: { valid: false } });
|
||||
}
|
||||
|
||||
async increaseTotalCount(id: string, recordType: 'users' | 'channels' | 'messages', increaseBy = 1): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{ _id: id },
|
||||
{
|
||||
$inc: {
|
||||
'count.total': increaseBy,
|
||||
[`count.${recordType}`]: increaseBy,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async setOperationStatus(id: string, status: IImport['status']): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{ _id: id },
|
||||
{
|
||||
$set: {
|
||||
status,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
60
packages/models/src/models/InstanceStatus.ts
Normal file
60
packages/models/src/models/InstanceStatus.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { IInstanceStatus } from '@rocket.chat/core-typings';
|
||||
import type { IInstanceStatusModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, UpdateResult, DeleteResult, ChangeStream } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class InstanceStatusRaw extends BaseRaw<IInstanceStatus> implements IInstanceStatusModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'instances', undefined, {
|
||||
preventSetUpdatedAt: true,
|
||||
collectionNameResolver(name) {
|
||||
return name;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
watchActiveInstances(): ChangeStream<IInstanceStatus> {
|
||||
return this.col.watch();
|
||||
}
|
||||
|
||||
async removeInstanceById(_id: IInstanceStatus['_id']): Promise<DeleteResult> {
|
||||
return this.deleteOne({ _id });
|
||||
}
|
||||
|
||||
async setDocumentHeartbeat(documentId: string): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id: documentId }, { $currentDate: { _updatedAt: true } });
|
||||
}
|
||||
|
||||
async upsertInstance(instance: Partial<IInstanceStatus>): Promise<IInstanceStatus | null> {
|
||||
return this.findOneAndUpdate(
|
||||
{
|
||||
_id: instance._id,
|
||||
},
|
||||
{
|
||||
$set: instance,
|
||||
$currentDate: {
|
||||
_createdAt: true,
|
||||
_updatedAt: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
returnDocument: 'after',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async updateConnections(_id: IInstanceStatus['_id'], conns: number) {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'extraInformation.conns': conns,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
38
packages/models/src/models/IntegrationHistory.ts
Normal file
38
packages/models/src/models/IntegrationHistory.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { IIntegrationHistory } from '@rocket.chat/core-typings';
|
||||
import type { IIntegrationHistoryModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, IndexDescription, InsertOneResult, FindOneAndUpdateOptions } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class IntegrationHistoryRaw extends BaseRaw<IIntegrationHistory> implements IIntegrationHistoryModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'integration_history');
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{ key: { 'integration._id': 1, 'integration._createdBy._id': 1 } },
|
||||
{ key: { _updatedAt: 1 }, expireAfterSeconds: 30 * 24 * 60 * 60 },
|
||||
];
|
||||
}
|
||||
|
||||
removeByIntegrationId(integrationId: string): ReturnType<BaseRaw<IIntegrationHistory>['deleteMany']> {
|
||||
return this.deleteMany({ 'integration._id': integrationId });
|
||||
}
|
||||
|
||||
findOneByIntegrationIdAndHistoryId(integrationId: string, historyId: string): Promise<IIntegrationHistory | null> {
|
||||
return this.findOne({ 'integration._id': integrationId, '_id': historyId });
|
||||
}
|
||||
|
||||
async create(integrationHistory: IIntegrationHistory): Promise<InsertOneResult<IIntegrationHistory>> {
|
||||
return this.insertOne({ ...integrationHistory, _createdAt: new Date() });
|
||||
}
|
||||
|
||||
async updateById(
|
||||
_id: IIntegrationHistory['_id'],
|
||||
data: Partial<IIntegrationHistory>,
|
||||
options?: FindOneAndUpdateOptions,
|
||||
): Promise<IIntegrationHistory | null> {
|
||||
return this.findOneAndUpdate({ _id }, { $set: data }, { returnDocument: 'after', ...options });
|
||||
}
|
||||
}
|
||||
123
packages/models/src/models/Integrations.ts
Normal file
123
packages/models/src/models/Integrations.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { IIntegration, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type {
|
||||
IBaseModel,
|
||||
IIntegrationsModel,
|
||||
IntegrationsStatistics,
|
||||
DocumentWithProjection,
|
||||
FindOptionsWithProjection,
|
||||
} from '@rocket.chat/model-typings';
|
||||
import type { AggregateOptions, Collection, Db, FindCursor, IndexDescription, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class IntegrationsRaw extends BaseRaw<IIntegration> implements IIntegrationsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IIntegration>>) {
|
||||
super(db, 'integrations', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { type: 1 } }];
|
||||
}
|
||||
|
||||
findOneByUrl(url: string): Promise<IIntegration | null> {
|
||||
return this.findOne({ url });
|
||||
}
|
||||
|
||||
updateRoomName(oldRoomName: string, newRoomName: string): ReturnType<IBaseModel<IIntegration>['updateMany']> {
|
||||
const hashedOldRoomName = `#${oldRoomName}`;
|
||||
const hashedNewRoomName = `#${newRoomName}`;
|
||||
|
||||
return this.updateMany(
|
||||
{
|
||||
channel: hashedOldRoomName,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'channel.$': hashedNewRoomName,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
findOneByIdAndCreatedByIfExists({
|
||||
_id,
|
||||
createdBy,
|
||||
}: {
|
||||
_id: IIntegration['_id'];
|
||||
createdBy?: IUser['_id'];
|
||||
}): Promise<IIntegration | null> {
|
||||
return this.findOne({
|
||||
_id,
|
||||
...(createdBy && { '_createdBy._id': createdBy }),
|
||||
});
|
||||
}
|
||||
|
||||
removeByIdAndCreatedByIfExists({ _id, createdBy }: { _id: IIntegration['_id']; createdBy?: IUser['_id'] }): Promise<IIntegration | null> {
|
||||
return this.findOneAndDelete({
|
||||
_id,
|
||||
...(createdBy && { '_createdBy._id': createdBy }),
|
||||
});
|
||||
}
|
||||
|
||||
disableByUserId(userId: IIntegration['userId']): ReturnType<IBaseModel<IIntegration>['updateMany']> {
|
||||
return this.updateMany({ userId }, { $set: { enabled: false } });
|
||||
}
|
||||
|
||||
findByUserId(userId: IIntegration['userId']): FindCursor<Pick<IIntegration, '_id'>> {
|
||||
return this.find({ userId }, { projection: { _id: 1 } });
|
||||
}
|
||||
|
||||
findByChannels(channels: IIntegration['channel']): FindCursor<IIntegration> {
|
||||
return this.find({ channel: { $in: channels } });
|
||||
}
|
||||
|
||||
findOneByIdAndToken<P extends Document = IIntegration, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
id: IIntegration['_id'],
|
||||
token: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<P, O> | null> {
|
||||
return this.findOne<P, O>({ _id: id, token }, options);
|
||||
}
|
||||
|
||||
async getStatistics(options?: AggregateOptions): Promise<IntegrationsStatistics> {
|
||||
const results = await this.col
|
||||
.aggregate<{ _id: IIntegration['type']; total: number; active: number; scriptEnabled: number }>(
|
||||
[
|
||||
{
|
||||
$group: {
|
||||
_id: '$type',
|
||||
total: { $sum: 1 },
|
||||
active: { $sum: { $cond: ['$enabled', 1, 0] } },
|
||||
scriptEnabled: { $sum: { $cond: ['$scriptEnabled', 1, 0] } },
|
||||
},
|
||||
},
|
||||
],
|
||||
options,
|
||||
)
|
||||
.toArray();
|
||||
|
||||
const statistics: IntegrationsStatistics = {
|
||||
totalIntegrations: 0,
|
||||
totalIncoming: 0,
|
||||
totalIncomingActive: 0,
|
||||
totalOutgoing: 0,
|
||||
totalOutgoingActive: 0,
|
||||
totalWithScriptEnabled: 0,
|
||||
};
|
||||
|
||||
for (const { _id, total, active, scriptEnabled } of results) {
|
||||
statistics.totalIntegrations += total;
|
||||
statistics.totalWithScriptEnabled += scriptEnabled;
|
||||
|
||||
if (_id === 'webhook-incoming') {
|
||||
statistics.totalIncoming += total;
|
||||
statistics.totalIncomingActive += active;
|
||||
} else if (_id === 'webhook-outgoing') {
|
||||
statistics.totalOutgoing += total;
|
||||
statistics.totalOutgoingActive += active;
|
||||
}
|
||||
}
|
||||
|
||||
return statistics;
|
||||
}
|
||||
}
|
||||
39
packages/models/src/models/Invites.ts
Normal file
39
packages/models/src/models/Invites.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { IInvite, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IInvitesModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class InvitesRaw extends BaseRaw<IInvite> implements IInvitesModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IInvite>>) {
|
||||
super(db, 'invites', trash);
|
||||
}
|
||||
|
||||
findOneByUserRoomMaxUsesAndExpiration(userId: string, rid: string, maxUses: number, daysToExpire: number): Promise<IInvite | null> {
|
||||
return this.findOne({
|
||||
rid,
|
||||
userId,
|
||||
days: daysToExpire,
|
||||
maxUses,
|
||||
...(daysToExpire > 0 ? { expires: { $gt: new Date() } } : {}),
|
||||
...(maxUses > 0 ? { uses: { $lt: maxUses } } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
increaseUsageById(_id: string, uses = 1): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{ _id },
|
||||
{
|
||||
$inc: {
|
||||
uses,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async countUses(): Promise<number> {
|
||||
const [result] = await this.col.aggregate<{ totalUses: number }>([{ $group: { _id: null, totalUses: { $sum: '$uses' } } }]).toArray();
|
||||
|
||||
return result?.totalUses || 0;
|
||||
}
|
||||
}
|
||||
238
packages/models/src/models/LivechatAgentActivity.ts
Normal file
238
packages/models/src/models/LivechatAgentActivity.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import type { ILivechatAgentActivity, IServiceHistory, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ILivechatAgentActivityModel } from '@rocket.chat/model-typings';
|
||||
import { parseISO, format } from 'date-fns';
|
||||
import type { AggregationCursor, Collection, Document, FindCursor, Db, WithId, IndexDescription, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
import { readSecondaryPreferred } from '../readSecondaryPreferred';
|
||||
|
||||
export class LivechatAgentActivityRaw extends BaseRaw<ILivechatAgentActivity> implements ILivechatAgentActivityModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatAgentActivity>>) {
|
||||
super(db, 'livechat_agent_activity', trash);
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { date: 1 } }, { key: { agentId: 1, date: 1 }, unique: true }];
|
||||
}
|
||||
|
||||
findOneByAgendIdAndDate(agentId: string, date: ILivechatAgentActivity['date']): Promise<ILivechatAgentActivity | null> {
|
||||
return this.findOne({ agentId, date });
|
||||
}
|
||||
|
||||
async createOrUpdate(
|
||||
data: Partial<Pick<ILivechatAgentActivity, 'date' | 'agentId' | 'lastStartedAt'>> = {},
|
||||
): Promise<null | WithId<ILivechatAgentActivity>> {
|
||||
const { date, agentId, lastStartedAt } = data;
|
||||
|
||||
if (!date || !agentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.findOneAndUpdate(
|
||||
{ agentId, date },
|
||||
{
|
||||
$unset: {
|
||||
lastStoppedAt: 1,
|
||||
},
|
||||
$set: {
|
||||
lastStartedAt: lastStartedAt || new Date(),
|
||||
},
|
||||
$setOnInsert: {
|
||||
date,
|
||||
agentId,
|
||||
},
|
||||
},
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
updateLastStoppedAt({
|
||||
agentId,
|
||||
date,
|
||||
lastStoppedAt,
|
||||
availableTime,
|
||||
}: Pick<ILivechatAgentActivity, 'date' | 'agentId' | 'lastStoppedAt' | 'availableTime'>): Promise<UpdateResult | Document> {
|
||||
const query = {
|
||||
agentId,
|
||||
date,
|
||||
};
|
||||
const update = {
|
||||
$inc: { availableTime },
|
||||
$set: {
|
||||
lastStoppedAt,
|
||||
},
|
||||
};
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
updateServiceHistory({
|
||||
agentId,
|
||||
date,
|
||||
serviceHistory,
|
||||
}: Pick<ILivechatAgentActivity, 'date' | 'agentId'> & { serviceHistory: IServiceHistory }): Promise<UpdateResult | Document> {
|
||||
const query = {
|
||||
agentId,
|
||||
date,
|
||||
};
|
||||
const update = {
|
||||
$addToSet: {
|
||||
serviceHistory,
|
||||
},
|
||||
};
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
findOpenSessions(): FindCursor<ILivechatAgentActivity> {
|
||||
const query = {
|
||||
lastStoppedAt: { $exists: false },
|
||||
};
|
||||
|
||||
return this.find(query);
|
||||
}
|
||||
|
||||
findAllAverageAvailableServiceTime({ date, departmentId }: { date: Date; departmentId?: string }): Promise<
|
||||
{
|
||||
averageAvailableServiceTimeInSeconds: number;
|
||||
}[]
|
||||
> {
|
||||
const match = { $match: { date } };
|
||||
const lookup = {
|
||||
$lookup: {
|
||||
from: 'rocketchat_livechat_department_agents',
|
||||
localField: 'agentId',
|
||||
foreignField: 'agentId',
|
||||
as: 'departments',
|
||||
},
|
||||
};
|
||||
const unwind = {
|
||||
$unwind: {
|
||||
path: '$departments',
|
||||
preserveNullAndEmptyArrays: true,
|
||||
},
|
||||
};
|
||||
const departmentsMatch = {
|
||||
$match: {
|
||||
'departments.departmentId': departmentId,
|
||||
},
|
||||
};
|
||||
const sumAvailableTimeWithCurrentTime = {
|
||||
$sum: [{ $divide: [{ $subtract: [new Date(), '$lastStartedAt'] }, 1000] }, '$availableTime'],
|
||||
};
|
||||
const group = {
|
||||
$group: {
|
||||
_id: null,
|
||||
allAvailableTimeInSeconds: {
|
||||
$sum: {
|
||||
$cond: [{ $ifNull: ['$lastStoppedAt', false] }, '$availableTime', sumAvailableTimeWithCurrentTime],
|
||||
},
|
||||
},
|
||||
rooms: { $sum: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const project = {
|
||||
$project: {
|
||||
averageAvailableServiceTimeInSeconds: {
|
||||
$trunc: {
|
||||
$cond: [{ $eq: ['$rooms', 0] }, 0, { $divide: ['$allAvailableTimeInSeconds', '$rooms'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const params = [match] as object[];
|
||||
if (departmentId && (departmentId !== 'undefined' || departmentId !== undefined)) {
|
||||
params.push(lookup);
|
||||
params.push(unwind);
|
||||
params.push(departmentsMatch);
|
||||
}
|
||||
params.push(group);
|
||||
params.push(project);
|
||||
return this.col
|
||||
.aggregate<{
|
||||
averageAvailableServiceTimeInSeconds: number;
|
||||
}>(params, { readPreference: readSecondaryPreferred() })
|
||||
.toArray();
|
||||
}
|
||||
|
||||
findAvailableServiceTimeHistory(p: {
|
||||
start: string;
|
||||
end: string;
|
||||
fullReport: boolean;
|
||||
onlyCount: true;
|
||||
options?: { sort?: Record<string, number>; offset?: number; count?: number };
|
||||
}): AggregationCursor<{ total: number }>;
|
||||
|
||||
findAvailableServiceTimeHistory(p: {
|
||||
start: string;
|
||||
end: string;
|
||||
fullReport: boolean;
|
||||
onlyCount?: false;
|
||||
options?: { sort?: Record<string, number>; offset?: number; count?: number };
|
||||
}): AggregationCursor<ILivechatAgentActivity>;
|
||||
|
||||
findAvailableServiceTimeHistory({
|
||||
start,
|
||||
end,
|
||||
fullReport,
|
||||
onlyCount = false,
|
||||
options = {},
|
||||
}: {
|
||||
start: string;
|
||||
end: string;
|
||||
fullReport: boolean;
|
||||
onlyCount?: boolean;
|
||||
options?: { sort?: Record<string, number>; offset?: number; count?: number };
|
||||
}): AggregationCursor<ILivechatAgentActivity> | AggregationCursor<{ total: number }> {
|
||||
const match = {
|
||||
$match: {
|
||||
date: {
|
||||
$gte: parseInt(format(parseISO(start), 'yyyyMMdd')),
|
||||
$lte: parseInt(format(parseISO(end), 'yyyyMMdd')),
|
||||
},
|
||||
},
|
||||
};
|
||||
const lookup = {
|
||||
$lookup: {
|
||||
from: 'users',
|
||||
localField: 'agentId',
|
||||
foreignField: '_id',
|
||||
as: 'user',
|
||||
},
|
||||
};
|
||||
const unwind = {
|
||||
$unwind: {
|
||||
path: '$user',
|
||||
},
|
||||
};
|
||||
const group = {
|
||||
$group: {
|
||||
_id: { _id: '$user._id', username: '$user.username' },
|
||||
serviceHistory: { $first: '$serviceHistory' },
|
||||
availableTimeInSeconds: { $sum: '$availableTime' },
|
||||
},
|
||||
};
|
||||
const project = {
|
||||
$project: {
|
||||
_id: 0,
|
||||
username: '$_id.username',
|
||||
availableTimeInSeconds: 1,
|
||||
...(fullReport && { serviceHistory: 1 }),
|
||||
},
|
||||
};
|
||||
|
||||
const sort = { $sort: options.sort || { username: 1 } };
|
||||
const params = [match, lookup, unwind, group, project, sort] as object[];
|
||||
if (onlyCount) {
|
||||
params.push({ $count: 'total' });
|
||||
return this.col.aggregate<{ total: number }>(params);
|
||||
}
|
||||
if (options.offset) {
|
||||
params.push({ $skip: options.offset });
|
||||
}
|
||||
if (options.count) {
|
||||
params.push({ $limit: options.count });
|
||||
}
|
||||
return this.col.aggregate<ILivechatAgentActivity>(params, { allowDiskUse: true, readPreference: readSecondaryPreferred() });
|
||||
}
|
||||
}
|
||||
179
packages/models/src/models/LivechatBusinessHours.ts
Normal file
179
packages/models/src/models/LivechatBusinessHours.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import type { ILivechatBusinessHour, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
|
||||
import type { ILivechatBusinessHoursModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
interface IWorkHoursCronJobsItem {
|
||||
day: string;
|
||||
times: string[];
|
||||
}
|
||||
|
||||
export interface IWorkHoursCronJobsWrapper {
|
||||
start: IWorkHoursCronJobsItem[];
|
||||
finish: IWorkHoursCronJobsItem[];
|
||||
}
|
||||
|
||||
export class LivechatBusinessHoursRaw extends BaseRaw<ILivechatBusinessHour> implements ILivechatBusinessHoursModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatBusinessHour>>) {
|
||||
super(db, 'livechat_business_hours', trash);
|
||||
}
|
||||
|
||||
findOneDefaultBusinessHour<
|
||||
P extends Document = ILivechatBusinessHour,
|
||||
O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>,
|
||||
>(options?: O): Promise<DocumentWithProjection<P, O> | null> {
|
||||
return this.findOne<P, O>({ type: LivechatBusinessHourTypes.DEFAULT }, options);
|
||||
}
|
||||
|
||||
findActiveAndOpenBusinessHoursByDay<
|
||||
T extends Document = ILivechatBusinessHour,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(day: string, options?: O): Promise<DocumentWithProjection<T, O>[]> {
|
||||
return this.find<T, O>(
|
||||
{
|
||||
active: true,
|
||||
workHours: {
|
||||
$elemMatch: {
|
||||
$or: [{ 'start.cron.dayOfWeek': day }, { 'finish.cron.dayOfWeek': day }],
|
||||
open: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
options,
|
||||
).toArray();
|
||||
}
|
||||
|
||||
findActiveBusinessHours<
|
||||
T extends Document = ILivechatBusinessHour,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(options?: O): Promise<DocumentWithProjection<T, O>[]> {
|
||||
return this.find<T, O>(
|
||||
{
|
||||
active: true,
|
||||
},
|
||||
options,
|
||||
).toArray();
|
||||
}
|
||||
|
||||
findDefaultActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise<ILivechatBusinessHour[]> {
|
||||
return this.find(
|
||||
{
|
||||
type: LivechatBusinessHourTypes.DEFAULT,
|
||||
active: true,
|
||||
workHours: {
|
||||
$elemMatch: {
|
||||
$or: [{ 'start.cron.dayOfWeek': day }, { 'finish.cron.dayOfWeek': day }],
|
||||
open: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
options,
|
||||
).toArray();
|
||||
}
|
||||
|
||||
override async insertOne(data: Omit<ILivechatBusinessHour, '_id' | '_updatedAt'>): Promise<any> {
|
||||
return super.insertOne({
|
||||
...data,
|
||||
ts: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
findHoursToScheduleJobs(): Promise<IWorkHoursCronJobsWrapper[]> {
|
||||
return this.col
|
||||
.aggregate([
|
||||
{
|
||||
$facet: {
|
||||
start: [
|
||||
{ $match: { active: true } },
|
||||
{ $project: { _id: 0, workHours: 1 } },
|
||||
{ $unwind: { path: '$workHours' } },
|
||||
{ $match: { 'workHours.open': true } },
|
||||
{
|
||||
$group: {
|
||||
_id: { day: '$workHours.start.cron.dayOfWeek' },
|
||||
times: { $addToSet: '$workHours.start.cron.time' },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
day: '$_id.day',
|
||||
times: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
finish: [
|
||||
{ $match: { active: true } },
|
||||
{ $project: { _id: 0, workHours: 1 } },
|
||||
{ $unwind: { path: '$workHours' } },
|
||||
{ $match: { 'workHours.open': true } },
|
||||
{
|
||||
$group: {
|
||||
_id: { day: '$workHours.finish.cron.dayOfWeek' },
|
||||
times: { $addToSet: '$workHours.finish.cron.time' },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
day: '$_id.day',
|
||||
times: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
.toArray() as any;
|
||||
}
|
||||
|
||||
async findActiveBusinessHoursToOpen(
|
||||
day: string,
|
||||
start: string,
|
||||
type?: LivechatBusinessHourTypes,
|
||||
options?: any,
|
||||
): Promise<ILivechatBusinessHour[]> {
|
||||
const query: Record<string, any> = {
|
||||
active: true,
|
||||
workHours: {
|
||||
$elemMatch: {
|
||||
'start.cron.dayOfWeek': day,
|
||||
'start.cron.time': start,
|
||||
'open': true,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (type) {
|
||||
query.type = type;
|
||||
}
|
||||
return this.col.find(query, options).toArray();
|
||||
}
|
||||
|
||||
async findActiveBusinessHoursToClose(
|
||||
day: string,
|
||||
finish: string,
|
||||
type?: LivechatBusinessHourTypes,
|
||||
options?: any,
|
||||
): Promise<ILivechatBusinessHour[]> {
|
||||
const query: Record<string, any> = {
|
||||
active: true,
|
||||
workHours: {
|
||||
$elemMatch: {
|
||||
'finish.cron.dayOfWeek': day,
|
||||
'finish.cron.time': finish,
|
||||
'open': true,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (type) {
|
||||
query.type = type;
|
||||
}
|
||||
return this.col.find(query, options).toArray();
|
||||
}
|
||||
|
||||
disableBusinessHour(businessHourId: string): Promise<any> {
|
||||
return this.updateOne({ _id: businessHourId }, { $set: { active: false } });
|
||||
}
|
||||
}
|
||||
444
packages/models/src/models/LivechatContacts.ts
Normal file
444
packages/models/src/models/LivechatContacts.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
import type {
|
||||
AtLeast,
|
||||
ILivechatContact,
|
||||
ILivechatContactChannel,
|
||||
ILivechatContactVisitorAssociation,
|
||||
ILivechatVisitor,
|
||||
RocketChatRecordDeleted,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import type {
|
||||
FindPaginated,
|
||||
ILivechatContactsModel,
|
||||
InsertionModel,
|
||||
Updater,
|
||||
DocumentWithProjection,
|
||||
FindOptionsWithProjection,
|
||||
} from '@rocket.chat/model-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
import type {
|
||||
Document,
|
||||
Collection,
|
||||
Db,
|
||||
RootFilterOperators,
|
||||
Filter,
|
||||
FindCursor,
|
||||
IndexDescription,
|
||||
UpdateResult,
|
||||
UpdateFilter,
|
||||
UpdateOptions,
|
||||
FindOneAndUpdateOptions,
|
||||
AggregationCursor,
|
||||
} from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
import { readSecondaryPreferred } from '../readSecondaryPreferred';
|
||||
|
||||
export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements ILivechatContactsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatContact>>) {
|
||||
super(db, 'livechat_contact', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{
|
||||
key: { name: 1 },
|
||||
unique: false,
|
||||
name: 'name_insensitive',
|
||||
collation: { locale: 'en', strength: 2, caseLevel: false },
|
||||
},
|
||||
{
|
||||
key: { 'emails.address': 1 },
|
||||
unique: false,
|
||||
name: 'emails_insensitive',
|
||||
partialFilterExpression: { emails: { $exists: true } },
|
||||
collation: { locale: 'en', strength: 2, caseLevel: false },
|
||||
},
|
||||
{
|
||||
key: { 'phones.phoneNumber': 1 },
|
||||
partialFilterExpression: { phones: { $exists: true } },
|
||||
unique: false,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
'channels.visitor.visitorId': 1,
|
||||
'channels.visitor.source.type': 1,
|
||||
'channels.visitor.source.id': 1,
|
||||
},
|
||||
name: 'visitorAssociation',
|
||||
unique: false,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
'channels.field': 1,
|
||||
'channels.value': 1,
|
||||
'channels.verified': 1,
|
||||
},
|
||||
partialFilterExpression: { 'channels.verified': true },
|
||||
name: 'verificationKey',
|
||||
unique: false,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
preRegistration: 1,
|
||||
},
|
||||
sparse: true,
|
||||
unique: false,
|
||||
},
|
||||
{
|
||||
key: { activity: 1 },
|
||||
sparse: true,
|
||||
},
|
||||
{
|
||||
key: { channels: 1 },
|
||||
unique: false,
|
||||
},
|
||||
{
|
||||
key: { 'channels.blocked': 1 },
|
||||
sparse: true,
|
||||
},
|
||||
{
|
||||
key: { 'channels.verified': 1 },
|
||||
sparse: true,
|
||||
},
|
||||
{
|
||||
key: { unknown: 1 },
|
||||
unique: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async insertContact(
|
||||
data: InsertionModel<Omit<ILivechatContact, 'createdAt'>> & { createdAt?: ILivechatContact['createdAt'] },
|
||||
): Promise<ILivechatContact['_id']> {
|
||||
const result = await this.insertOne({
|
||||
createdAt: new Date(),
|
||||
...data,
|
||||
preRegistration: !data.channels.length,
|
||||
});
|
||||
|
||||
return result.insertedId;
|
||||
}
|
||||
|
||||
async patchContact(
|
||||
contactId: string,
|
||||
changes: {
|
||||
set?: Partial<ILivechatContact>;
|
||||
unset?: Partial<Record<keyof ILivechatContact, '' | 1>>;
|
||||
},
|
||||
options?: FindOneAndUpdateOptions,
|
||||
) {
|
||||
const { set = {}, unset = {} } = changes;
|
||||
|
||||
const $set = {
|
||||
...set,
|
||||
unknown: false,
|
||||
...(set.channels && { preRegistration: !set.channels.length }),
|
||||
};
|
||||
|
||||
return this.findOneAndUpdate(
|
||||
{ _id: contactId, enabled: { $ne: false } },
|
||||
{
|
||||
$set,
|
||||
...(Object.keys(unset).length > 0 && { $unset: unset }),
|
||||
},
|
||||
{ returnDocument: 'after', ...options },
|
||||
);
|
||||
}
|
||||
|
||||
updateById(contactId: string, update: UpdateFilter<ILivechatContact>, options?: UpdateOptions): Promise<Document | UpdateResult> {
|
||||
return this.updateOne({ _id: contactId, enabled: { $ne: false } }, update, options);
|
||||
}
|
||||
|
||||
findPaginatedContacts<T extends Document = ILivechatContact, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
search: { searchText?: string; unknown?: boolean },
|
||||
options?: O,
|
||||
): FindPaginated<FindCursor<DocumentWithProjection<T, O>>> {
|
||||
const { searchText, unknown = false } = search;
|
||||
const searchRegex = escapeRegExp(searchText || '');
|
||||
const match: Filter<ILivechatContact & RootFilterOperators<ILivechatContact>> = {
|
||||
$or: [
|
||||
{ name: { $regex: searchRegex, $options: 'i' } },
|
||||
{ 'emails.address': { $regex: searchRegex, $options: 'i' } },
|
||||
{ 'phones.phoneNumber': { $regex: searchRegex, $options: 'i' } },
|
||||
],
|
||||
unknown,
|
||||
enabled: { $ne: false },
|
||||
};
|
||||
|
||||
// safe to merge into `O`: only `O['projection']` feeds the return type, and it survives the spread
|
||||
return this.findPaginated<T, O>({ ...match }, {
|
||||
allowDiskUse: true,
|
||||
...options,
|
||||
} as unknown as O);
|
||||
}
|
||||
|
||||
async findContactMatchingVisitor(visitor: AtLeast<ILivechatVisitor, 'visitorEmails' | 'phone'>): Promise<ILivechatContact | null> {
|
||||
const emails = visitor.visitorEmails?.map(({ address }) => address).filter((email) => Boolean(email)) || [];
|
||||
const phoneNumbers = visitor.phone?.map(({ phoneNumber }) => phoneNumber).filter((phone) => Boolean(phone)) || [];
|
||||
|
||||
if (!emails?.length && !phoneNumbers?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = {
|
||||
$and: [
|
||||
{
|
||||
$or: [
|
||||
...emails?.map((email) => ({ 'emails.address': email })),
|
||||
...phoneNumbers?.map((phone) => ({ 'phones.phoneNumber': phone })),
|
||||
],
|
||||
},
|
||||
{
|
||||
preRegistration: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return this.findOne(query);
|
||||
}
|
||||
|
||||
async findContactByEmailAndContactManager(email: string): Promise<Pick<ILivechatContact, 'contactManager'> | null> {
|
||||
return this.findOne(
|
||||
{ emails: { $elemMatch: { address: email } }, contactManager: { $exists: true } },
|
||||
{ projection: { contactManager: 1 } },
|
||||
);
|
||||
}
|
||||
|
||||
private makeQueryForVisitor(
|
||||
visitor: ILivechatContactVisitorAssociation,
|
||||
extraFilters?: Filter<Required<ILivechatContact>['channels'][number]>,
|
||||
): Filter<ILivechatContact> {
|
||||
return {
|
||||
channels: {
|
||||
$elemMatch: {
|
||||
'visitor.visitorId': visitor.visitorId,
|
||||
'visitor.source.type': visitor.source.type,
|
||||
...(visitor.source.id ? { 'visitor.source.id': visitor.source.id } : {}),
|
||||
...extraFilters,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async findOneByVisitor<T extends Document = ILivechatContact, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
visitor: ILivechatContactVisitorAssociation,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>(this.makeQueryForVisitor(visitor), options);
|
||||
}
|
||||
|
||||
async addChannel(contactId: string, channel: ILivechatContactChannel): Promise<void> {
|
||||
await this.updateOne({ _id: contactId, enabled: { $ne: false } }, { $push: { channels: channel }, $set: { preRegistration: false } });
|
||||
}
|
||||
|
||||
async updateLastChatById(
|
||||
contactId: string,
|
||||
visitor: ILivechatContactVisitorAssociation,
|
||||
lastChat: ILivechatContact['lastChat'],
|
||||
): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
...this.makeQueryForVisitor(visitor),
|
||||
_id: contactId,
|
||||
},
|
||||
{ $set: { lastChat, 'channels.$.lastChat': lastChat } },
|
||||
);
|
||||
}
|
||||
|
||||
async isChannelBlocked(visitor: ILivechatContactVisitorAssociation): Promise<boolean> {
|
||||
return Boolean(await this.findOne(this.makeQueryForVisitor(visitor, { blocked: true }), { projection: { _id: 1 } }));
|
||||
}
|
||||
|
||||
setChannelBlockStatus(visitor: ILivechatContactVisitorAssociation, blocked: boolean): Promise<UpdateResult> {
|
||||
return this.updateOne(this.makeQueryForVisitor(visitor), { $set: { 'channels.$.blocked': blocked } });
|
||||
}
|
||||
|
||||
setChannelVerifiedStatus(visitor: ILivechatContactVisitorAssociation, verified: boolean): Promise<UpdateResult> {
|
||||
return this.updateOne(this.makeQueryForVisitor(visitor), {
|
||||
$set: {
|
||||
'channels.$.verified': verified,
|
||||
...(verified && { 'channels.$.verifiedAt': new Date() }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setVerifiedUpdateQuery(verified: boolean, contactUpdater: Updater<ILivechatContact>): Updater<ILivechatContact> {
|
||||
if (verified) {
|
||||
contactUpdater.set('channels.$.verifiedAt', new Date());
|
||||
}
|
||||
return contactUpdater.set('channels.$.verified', verified);
|
||||
}
|
||||
|
||||
setFieldAndValueUpdateQuery(field: string, value: string, contactUpdater: Updater<ILivechatContact>): Updater<ILivechatContact> {
|
||||
contactUpdater.set('channels.$.field', field);
|
||||
return contactUpdater.set('channels.$.value', value);
|
||||
}
|
||||
|
||||
updateFromUpdaterByAssociation(
|
||||
visitor: ILivechatContactVisitorAssociation,
|
||||
contactUpdater: Updater<ILivechatContact>,
|
||||
options: UpdateOptions = {},
|
||||
): Promise<UpdateResult> {
|
||||
return this.updateFromUpdater(this.makeQueryForVisitor(visitor), contactUpdater, options);
|
||||
}
|
||||
|
||||
async findSimilarVerifiedContacts<
|
||||
T extends Document = ILivechatContact,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(
|
||||
{ field, value }: Pick<ILivechatContactChannel, 'field' | 'value'>,
|
||||
originalContactId: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O>[]> {
|
||||
return this.find<T, O>(
|
||||
{
|
||||
channels: {
|
||||
$elemMatch: {
|
||||
field,
|
||||
value,
|
||||
verified: true,
|
||||
},
|
||||
},
|
||||
_id: { $ne: originalContactId },
|
||||
},
|
||||
options,
|
||||
).toArray();
|
||||
}
|
||||
|
||||
findAllByVisitorId(visitorId: string): FindCursor<ILivechatContact> {
|
||||
return this.find({
|
||||
'channels.visitor.visitorId': visitorId,
|
||||
});
|
||||
}
|
||||
|
||||
async findOneEnabledById<P extends Document = ILivechatContact, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
_id: ILivechatContact['_id'],
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<P, O> | null> {
|
||||
return this.findOne<P, O>({ _id, enabled: { $ne: false } }, options);
|
||||
}
|
||||
|
||||
disableByVisitorId(visitorId: string): Promise<UpdateResult | Document> {
|
||||
return this.updateOne(
|
||||
{ 'channels.visitor.visitorId': visitorId },
|
||||
{
|
||||
$set: { enabled: false },
|
||||
$unset: {
|
||||
emails: 1,
|
||||
customFields: 1,
|
||||
lastChat: 1,
|
||||
channels: 1,
|
||||
name: 1,
|
||||
phones: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
disableByContactId(contactId: string): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{ _id: contactId },
|
||||
{
|
||||
$set: { enabled: false },
|
||||
$unset: {
|
||||
emails: 1,
|
||||
customFields: 1,
|
||||
lastChat: 1,
|
||||
channels: 1,
|
||||
name: 1,
|
||||
phones: 1,
|
||||
conflictingFields: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async addEmail(contactId: string, email: string): Promise<ILivechatContact | null> {
|
||||
const updatedContact = await this.findOneAndUpdate({ _id: contactId }, { $addToSet: { emails: { address: email } } });
|
||||
|
||||
return updatedContact;
|
||||
}
|
||||
|
||||
isContactActiveOnPeriod(visitor: ILivechatContactVisitorAssociation, period: string): Promise<number> {
|
||||
const query = {
|
||||
...this.makeQueryForVisitor(visitor),
|
||||
activity: period,
|
||||
};
|
||||
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
|
||||
markContactActiveForPeriod(visitor: ILivechatContactVisitorAssociation, period: string): Promise<UpdateResult> {
|
||||
const update = {
|
||||
$push: {
|
||||
activity: {
|
||||
$each: [period],
|
||||
$slice: -12,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(this.makeQueryForVisitor(visitor), update);
|
||||
}
|
||||
|
||||
countContactsOnPeriod(period: string): Promise<number> {
|
||||
return this.countDocuments({
|
||||
activity: period,
|
||||
});
|
||||
}
|
||||
|
||||
countByContactInfo({ contactId, email, phone }: { contactId?: string; email?: string; phone?: string }): Promise<number> {
|
||||
const filter = {
|
||||
...(email && { 'emails.address': email }),
|
||||
...(phone && { 'phones.phoneNumber': phone }),
|
||||
...(contactId && { _id: contactId }),
|
||||
};
|
||||
|
||||
return this.countDocuments(filter);
|
||||
}
|
||||
|
||||
countUnknown(): Promise<number> {
|
||||
return this.countDocuments({ unknown: true }, { readPreference: readSecondaryPreferred() });
|
||||
}
|
||||
|
||||
countBlocked(): Promise<number> {
|
||||
return this.countDocuments({ 'channels.blocked': true }, { readPreference: readSecondaryPreferred() });
|
||||
}
|
||||
|
||||
countFullyBlocked(): Promise<number> {
|
||||
return this.countDocuments(
|
||||
{
|
||||
'channels.blocked': true,
|
||||
'channels': { $not: { $elemMatch: { $or: [{ blocked: false }, { blocked: { $exists: false } }] } } },
|
||||
},
|
||||
{ readPreference: readSecondaryPreferred() },
|
||||
);
|
||||
}
|
||||
|
||||
countVerified(): Promise<number> {
|
||||
return this.countDocuments({ 'channels.verified': true }, { readPreference: readSecondaryPreferred() });
|
||||
}
|
||||
|
||||
countContactsWithoutChannels(): Promise<number> {
|
||||
return this.countDocuments({ channels: { $size: 0 } }, { readPreference: readSecondaryPreferred() });
|
||||
}
|
||||
|
||||
getStatistics(): AggregationCursor<{ totalConflicts: number; avgChannelsPerContact: number }> {
|
||||
return this.col.aggregate<{ totalConflicts: number; avgChannelsPerContact: number }>(
|
||||
[
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalConflicts: {
|
||||
$sum: { $size: { $cond: [{ $isArray: '$conflictingFields' }, '$conflictingFields', []] } },
|
||||
},
|
||||
avgChannelsPerContact: {
|
||||
$avg: { $size: { $cond: [{ $isArray: '$channels' }, '$channels', []] } },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{ allowDiskUse: true, readPreference: readSecondaryPreferred() },
|
||||
);
|
||||
}
|
||||
}
|
||||
78
packages/models/src/models/LivechatCustomField.ts
Normal file
78
packages/models/src/models/LivechatCustomField.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { ILivechatCustomField, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ILivechatCustomFieldModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Db, Collection, IndexDescription, FindCursor, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class LivechatCustomFieldRaw extends BaseRaw<ILivechatCustomField> implements ILivechatCustomFieldModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatCustomField>>) {
|
||||
super(db, 'livechat_custom_field', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { scope: 1 } }];
|
||||
}
|
||||
|
||||
findByScope<T extends Document = ILivechatCustomField, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
scope: ILivechatCustomField['scope'],
|
||||
options?: O,
|
||||
includeHidden = true,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>({ scope, ...(includeHidden === true ? {} : { visibility: { $ne: 'hidden' } }) }, options);
|
||||
}
|
||||
|
||||
findMatchingCustomFields<
|
||||
T extends Document = ILivechatCustomField,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(scope: ILivechatCustomField['scope'], searchable = true, options?: O): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
scope,
|
||||
searchable,
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
findMatchingCustomFieldsByIds<
|
||||
T extends Document = ILivechatCustomField,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(
|
||||
ids: ILivechatCustomField['_id'][],
|
||||
scope: ILivechatCustomField['scope'],
|
||||
searchable = true,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
_id: { $in: ids },
|
||||
scope,
|
||||
searchable,
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
async createOrUpdateCustomField(
|
||||
_id: string | null,
|
||||
field: string,
|
||||
label: ILivechatCustomField['label'],
|
||||
scope: ILivechatCustomField['scope'],
|
||||
visibility: ILivechatCustomField['visibility'],
|
||||
extraData: any,
|
||||
) {
|
||||
const record = {
|
||||
label,
|
||||
scope,
|
||||
visibility,
|
||||
...extraData,
|
||||
};
|
||||
|
||||
if (_id) {
|
||||
await this.updateOne({ _id }, { $set: record });
|
||||
} else {
|
||||
record._id = field;
|
||||
await this.insertOne(record);
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
}
|
||||
440
packages/models/src/models/LivechatDepartment.ts
Normal file
440
packages/models/src/models/LivechatDepartment.ts
Normal file
@@ -0,0 +1,440 @@
|
||||
import type { ILivechatDepartment, LivechatDepartmentDTO, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ILivechatDepartmentModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
import type { Collection, FindCursor, Db, Filter, FindOptions, UpdateResult, Document, IndexDescription, AggregationCursor } from 'mongodb';
|
||||
|
||||
import { LivechatDepartmentAgents, LivechatUnitMonitors } from '../index';
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class LivechatDepartmentRaw extends BaseRaw<ILivechatDepartment> implements ILivechatDepartmentModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatDepartment>>) {
|
||||
super(db, 'livechat_department', trash);
|
||||
}
|
||||
|
||||
removeParentAndAncestorById(_id: string): Promise<Document | UpdateResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected override modelIndexes(): Array<IndexDescription> {
|
||||
return [
|
||||
{
|
||||
key: {
|
||||
name: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
businessHourId: 1,
|
||||
},
|
||||
sparse: true,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
type: 1,
|
||||
},
|
||||
sparse: true,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
numAgents: 1,
|
||||
enabled: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
parentId: 1,
|
||||
},
|
||||
sparse: true,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
ancestors: 1,
|
||||
},
|
||||
sparse: true,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
archived: 1,
|
||||
},
|
||||
sparse: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
countTotal(): Promise<number> {
|
||||
return this.estimatedDocumentCount();
|
||||
}
|
||||
|
||||
findInIds<T extends Document = ILivechatDepartment, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
departmentsIds: string[],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = { _id: { $in: departmentsIds } };
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
findByNameRegexWithExceptionsAndConditions<
|
||||
T extends Document = ILivechatDepartment,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(
|
||||
searchTerm: string,
|
||||
exceptions: string[] = [],
|
||||
conditions: Filter<ILivechatDepartment> = {},
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
if (!Array.isArray(exceptions)) {
|
||||
exceptions = [exceptions];
|
||||
}
|
||||
|
||||
const nameRegex = new RegExp(`^${escapeRegExp(searchTerm).trim()}`, 'i');
|
||||
|
||||
const query = {
|
||||
name: nameRegex,
|
||||
_id: {
|
||||
$nin: exceptions,
|
||||
},
|
||||
...conditions,
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
findByBusinessHourId<T extends Document = ILivechatDepartment, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
businessHourId: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = { businessHourId };
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
countByBusinessHourIdExcludingDepartmentId(businessHourId: string, departmentId: string): Promise<number> {
|
||||
const query = { businessHourId, _id: { $ne: departmentId } };
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
|
||||
findEnabledByBusinessHourId<
|
||||
T extends Document = ILivechatDepartment,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(businessHourId: string, options?: O): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = { businessHourId, enabled: true };
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
findActiveDepartmentsWithoutBusinessHour<
|
||||
T extends Document = ILivechatDepartment,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(options?: O): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
enabled: true,
|
||||
businessHourId: { $exists: false },
|
||||
};
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
findEnabledInIds<T extends Document = ILivechatDepartment, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
departmentsIds: string[],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = { _id: { $in: departmentsIds }, enabled: true };
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
addBusinessHourToDepartmentsByIds(ids: string[] = [], businessHourId: string): Promise<Document | UpdateResult> {
|
||||
const query = {
|
||||
_id: { $in: ids },
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
businessHourId,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
removeBusinessHourFromDepartmentsByIdsAndBusinessHourId(ids: string[] = [], businessHourId: string): Promise<Document | UpdateResult> {
|
||||
const query = {
|
||||
_id: { $in: ids },
|
||||
businessHourId,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$unset: {
|
||||
businessHourId: 1,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId: string): Promise<Document | UpdateResult> {
|
||||
const query = {
|
||||
businessHourId,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$unset: {
|
||||
businessHourId: 1,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
unarchiveDepartment(_id: string): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id }, { $set: { archived: false } });
|
||||
}
|
||||
|
||||
archiveDepartment(_id: string): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id }, { $set: { archived: true, enabled: false } });
|
||||
}
|
||||
|
||||
addDepartmentToUnit(_id: string, unitId: string, ancestors: string[]): Promise<Document | UpdateResult> {
|
||||
return this.updateOne({ _id }, { $set: { parentId: unitId, ancestors } });
|
||||
}
|
||||
|
||||
removeDepartmentFromUnit(_id: string): Promise<Document | UpdateResult> {
|
||||
return this.updateOne({ _id }, { $unset: { parentId: 1, ancestors: 1 } });
|
||||
}
|
||||
|
||||
async createOrUpdateDepartment(_id: string | null, data: LivechatDepartmentDTO & { type?: string }): Promise<ILivechatDepartment> {
|
||||
const current = _id ? await this.findOneById(_id) : null;
|
||||
|
||||
const record = {
|
||||
...data,
|
||||
} as ILivechatDepartment;
|
||||
|
||||
if (_id) {
|
||||
await this.updateOne({ _id }, { $set: record });
|
||||
} else {
|
||||
_id = (await this.insertOne(record)).insertedId;
|
||||
}
|
||||
|
||||
if (current?.enabled !== data.enabled) {
|
||||
await LivechatDepartmentAgents.setDepartmentEnabledByDepartmentId(_id, data.enabled);
|
||||
}
|
||||
|
||||
const latestDept = await this.findOneById(_id);
|
||||
if (!latestDept) {
|
||||
throw new Error(`Department ${_id} not found`);
|
||||
}
|
||||
|
||||
return latestDept;
|
||||
}
|
||||
|
||||
unsetFallbackDepartmentByDepartmentId(departmentId: string): Promise<Document | UpdateResult> {
|
||||
return this.updateMany({ fallbackDepartment: departmentId }, { $unset: { fallbackDepartment: 1 } });
|
||||
}
|
||||
|
||||
removeDepartmentFromForwardListById(_departmentId: string): Promise<void> {
|
||||
throw new Error('Method not implemented in Community Edition.');
|
||||
}
|
||||
|
||||
updateById(_id: string, update: Partial<ILivechatDepartment>): Promise<Document | UpdateResult> {
|
||||
return this.updateOne({ _id }, update);
|
||||
}
|
||||
|
||||
updateNumAgentsById(_id: string, numAgents: number): Promise<Document | UpdateResult> {
|
||||
return this.updateOne({ _id }, { $set: { numAgents } });
|
||||
}
|
||||
|
||||
decreaseNumberOfAgentsByIds(_ids: string[]): Promise<Document | UpdateResult> {
|
||||
return this.updateMany({ _id: { $in: _ids } }, { $inc: { numAgents: -1 } });
|
||||
}
|
||||
|
||||
findEnabledWithAgents<T extends Document = ILivechatDepartment>(projection: FindOptions<T>['projection'] = {}): FindCursor<T> {
|
||||
const query = {
|
||||
numAgents: { $gt: 0 },
|
||||
enabled: true,
|
||||
};
|
||||
return this.find<T>(query, projection && { projection });
|
||||
}
|
||||
|
||||
findEnabledWithAgentsAndRegistration<T extends Document = ILivechatDepartment>(
|
||||
projection: FindOptions<T>['projection'] = {},
|
||||
): FindCursor<T> {
|
||||
const query = {
|
||||
numAgents: { $gt: 0 },
|
||||
enabled: true,
|
||||
showOnRegistration: true,
|
||||
};
|
||||
return this.find<T>(query, projection && { projection });
|
||||
}
|
||||
|
||||
findOneEnabledWithAgentsAndRegistration<T extends Document = ILivechatDepartment>(
|
||||
projection: FindOptions<T>['projection'] = {},
|
||||
): Promise<T | null> {
|
||||
const query = {
|
||||
numAgents: { $gt: 0 },
|
||||
enabled: true,
|
||||
showOnRegistration: true,
|
||||
};
|
||||
return this.findOne<T>(query, projection && { projection });
|
||||
}
|
||||
|
||||
findEnabledWithAgentsAndBusinessUnit<T extends Document = ILivechatDepartment>(
|
||||
_: any,
|
||||
projection?: FindOptions<T>['projection'],
|
||||
): FindCursor<T> {
|
||||
return this.findEnabledWithAgents(projection);
|
||||
}
|
||||
|
||||
findOneByIdOrName<T extends Document = ILivechatDepartment, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
_idOrName: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
const query = {
|
||||
$or: [
|
||||
{
|
||||
_id: _idOrName,
|
||||
},
|
||||
{
|
||||
name: _idOrName,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return this.findOne<T, O>(query, options);
|
||||
}
|
||||
|
||||
findByUnitIds<T extends Document = ILivechatDepartment, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
unitIds: string[],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
parentId: {
|
||||
$exists: true,
|
||||
$in: unitIds,
|
||||
},
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
countDepartmentsInUnit(unitId: string): Promise<number> {
|
||||
return this.countDocuments({ parentId: unitId });
|
||||
}
|
||||
|
||||
findActiveByUnitIds<T extends Document = ILivechatDepartment, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
_unitIds: string[],
|
||||
_options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
throw new Error('not-implemented');
|
||||
}
|
||||
|
||||
findNotArchived<T extends Document = ILivechatDepartment, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = { archived: { $ne: false } };
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
getBusinessHoursWithDepartmentStatuses(): Promise<
|
||||
{
|
||||
_id: string;
|
||||
validDepartments: string[];
|
||||
invalidDepartments: string[];
|
||||
}[]
|
||||
> {
|
||||
return this.col
|
||||
.aggregate<{ _id: string; validDepartments: string[]; invalidDepartments: string[] }>([
|
||||
{
|
||||
$match: {
|
||||
businessHourId: {
|
||||
$exists: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$businessHourId',
|
||||
validDepartments: {
|
||||
$push: {
|
||||
$cond: {
|
||||
if: {
|
||||
$or: [
|
||||
{
|
||||
$eq: ['$enabled', true],
|
||||
},
|
||||
{
|
||||
$ne: ['$archived', true],
|
||||
},
|
||||
],
|
||||
},
|
||||
then: '$_id',
|
||||
else: '$$REMOVE',
|
||||
},
|
||||
},
|
||||
},
|
||||
invalidDepartments: {
|
||||
$push: {
|
||||
$cond: {
|
||||
if: {
|
||||
$or: [{ $eq: ['$enabled', false] }, { $eq: ['$archived', true] }],
|
||||
},
|
||||
then: '$_id',
|
||||
else: '$$REMOVE',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
.toArray();
|
||||
}
|
||||
|
||||
checkIfMonitorIsMonitoringDepartmentById(monitorId: string, departmentId: string): Promise<boolean> {
|
||||
const aggregation = [
|
||||
{
|
||||
$match: {
|
||||
enabled: true,
|
||||
_id: departmentId,
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: LivechatUnitMonitors.getCollectionName(),
|
||||
localField: 'parentId',
|
||||
foreignField: 'unitId',
|
||||
as: 'monitors',
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
monitorId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
monitors: {
|
||||
$exists: true,
|
||||
$ne: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return this.col.aggregate(aggregation).hasNext();
|
||||
}
|
||||
|
||||
countArchived(): Promise<number> {
|
||||
return this.countDocuments({ archived: true });
|
||||
}
|
||||
|
||||
findByParentId(_parentId: string, _options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment> {
|
||||
throw new Error('Method not implemented in CE');
|
||||
}
|
||||
|
||||
findAgentsByBusinessHourId(_businessHourId: string): AggregationCursor<{ agentIds: string[] }> {
|
||||
throw new Error('Method not implemented in CE');
|
||||
}
|
||||
}
|
||||
320
packages/models/src/models/LivechatDepartmentAgents.ts
Normal file
320
packages/models/src/models/LivechatDepartmentAgents.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import type { AvailableAgentsAggregation, ILivechatDepartmentAgents, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type {
|
||||
FindPaginated,
|
||||
ILivechatDepartmentAgentsModel,
|
||||
DocumentWithProjection,
|
||||
FindOptionsWithProjection,
|
||||
} from '@rocket.chat/model-typings';
|
||||
import type {
|
||||
Collection,
|
||||
FindCursor,
|
||||
Db,
|
||||
Filter,
|
||||
Document,
|
||||
UpdateResult,
|
||||
DeleteResult,
|
||||
IndexDescription,
|
||||
SortDirection,
|
||||
AggregationCursor,
|
||||
} from 'mongodb';
|
||||
|
||||
import { Users } from '../index';
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class LivechatDepartmentAgentsRaw extends BaseRaw<ILivechatDepartmentAgents> implements ILivechatDepartmentAgentsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatDepartmentAgents>>) {
|
||||
super(db, 'livechat_department_agents', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): Array<IndexDescription> {
|
||||
return [
|
||||
{
|
||||
key: {
|
||||
departmentId: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
departmentEnabled: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
agentId: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
username: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
findByAgentIds<T extends Document = ILivechatDepartmentAgents, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
agentIds: string[],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>({ agentId: { $in: agentIds } }, options);
|
||||
}
|
||||
|
||||
findByAgentId<T extends Document = ILivechatDepartmentAgents, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
agentId: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>({ agentId }, options);
|
||||
}
|
||||
|
||||
findAgentsByDepartmentId<
|
||||
P extends Document = ILivechatDepartmentAgents,
|
||||
O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>,
|
||||
>(departmentId: string, options?: O): FindPaginated<FindCursor<DocumentWithProjection<P, O>>> {
|
||||
const query = { departmentId };
|
||||
|
||||
if (options === undefined) {
|
||||
return this.findPaginated<P, O>(query);
|
||||
}
|
||||
|
||||
return this.findPaginated<P, O>(query, options);
|
||||
}
|
||||
|
||||
findByDepartmentIds(departmentIds: string[], options = {}): FindCursor<ILivechatDepartmentAgents> {
|
||||
return this.find({ departmentId: { $in: departmentIds } }, options);
|
||||
}
|
||||
|
||||
setDepartmentEnabledByDepartmentId(departmentId: string, departmentEnabled: boolean): Promise<Document | UpdateResult> {
|
||||
return this.updateMany({ departmentId }, { $set: { departmentEnabled } });
|
||||
}
|
||||
|
||||
removeByDepartmentId(departmentId: string): Promise<DeleteResult> {
|
||||
return this.deleteMany({ departmentId });
|
||||
}
|
||||
|
||||
findByDepartmentId<T extends Document = ILivechatDepartmentAgents, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
departmentId: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>({ departmentId }, options);
|
||||
}
|
||||
|
||||
findOneByAgentIdAndDepartmentId<
|
||||
T extends Document = ILivechatDepartmentAgents,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(agentId: string, departmentId: string, options?: O): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>({ agentId, departmentId }, options);
|
||||
}
|
||||
|
||||
saveAgent(agent: {
|
||||
agentId: string;
|
||||
departmentId: string;
|
||||
username: string;
|
||||
departmentEnabled: boolean;
|
||||
count: number;
|
||||
order: number;
|
||||
}): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
agentId: agent.agentId,
|
||||
departmentId: agent.departmentId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
username: agent.username,
|
||||
departmentEnabled: agent.departmentEnabled,
|
||||
count: parseInt(`${agent.count}`),
|
||||
order: parseInt(`${agent.order}`),
|
||||
},
|
||||
},
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
async removeByAgentId(agentId: string): Promise<DeleteResult> {
|
||||
return this.deleteMany({ agentId });
|
||||
}
|
||||
|
||||
async getNextAgentForDepartment(
|
||||
departmentId: ILivechatDepartmentAgents['departmentId'],
|
||||
isLivechatEnabledWhenAgentIdle?: boolean,
|
||||
ignoreAgentId?: ILivechatDepartmentAgents['agentId'],
|
||||
extraQuery?: Filter<AvailableAgentsAggregation>,
|
||||
acceptChatsWithNoAgents?: boolean,
|
||||
): Promise<Pick<ILivechatDepartmentAgents, '_id' | 'agentId' | 'departmentId' | 'username'> | null | undefined> {
|
||||
const agents = await this.findByDepartmentId(departmentId).toArray();
|
||||
|
||||
if (agents.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onlineUsers = await Users.findOnlineUserFromList(
|
||||
agents.map((agent) => agent.username),
|
||||
isLivechatEnabledWhenAgentIdle,
|
||||
acceptChatsWithNoAgents,
|
||||
).toArray();
|
||||
|
||||
const onlineUsernames = onlineUsers.map((user) => user.username).filter(isStringValue);
|
||||
|
||||
// get fully booked agents, to ignore them from the query
|
||||
const currentUnavailableAgents = (
|
||||
await Users.getUnavailableAgents(departmentId, extraQuery, isLivechatEnabledWhenAgentIdle, acceptChatsWithNoAgents)
|
||||
).map((u) => u.username);
|
||||
|
||||
const query: Filter<ILivechatDepartmentAgents> = {
|
||||
departmentId,
|
||||
username: {
|
||||
$in: onlineUsernames,
|
||||
$nin: currentUnavailableAgents,
|
||||
},
|
||||
...(ignoreAgentId && { agentId: { $ne: ignoreAgentId } }),
|
||||
};
|
||||
|
||||
const update = {
|
||||
$inc: {
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const sort: { [k: string]: SortDirection } = {
|
||||
count: 1,
|
||||
order: 1,
|
||||
username: 1,
|
||||
};
|
||||
|
||||
const projection = {
|
||||
_id: 1,
|
||||
agentId: 1,
|
||||
departmentId: 1,
|
||||
username: 1,
|
||||
} as const;
|
||||
|
||||
return this.findOneAndUpdate(query, update, { sort, projection, returnDocument: 'after' });
|
||||
}
|
||||
|
||||
async countBotsForDepartment(departmentId: string): Promise<number> {
|
||||
const agents = await this.findByDepartmentId(departmentId, { projection: { username: 1 } }).toArray();
|
||||
|
||||
if (agents.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Users.countBotAgents(agents.map((a) => a.username));
|
||||
}
|
||||
|
||||
async getNextBotForDepartment(
|
||||
departmentId: ILivechatDepartmentAgents['departmentId'],
|
||||
ignoreAgentId?: ILivechatDepartmentAgents['agentId'],
|
||||
): Promise<Pick<ILivechatDepartmentAgents, '_id' | 'agentId' | 'departmentId' | 'username'> | null | undefined> {
|
||||
const agents = await this.findByDepartmentId(departmentId).toArray();
|
||||
|
||||
if (!agents.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const botUsernames = (await Users.findBotAgents(agents.map((a) => a.username)).toArray())
|
||||
.map((user) => user.username)
|
||||
.filter(isStringValue);
|
||||
|
||||
const query = {
|
||||
departmentId,
|
||||
username: {
|
||||
$in: botUsernames,
|
||||
},
|
||||
...(ignoreAgentId && { agentId: { $ne: ignoreAgentId } }),
|
||||
};
|
||||
|
||||
const update = {
|
||||
$inc: {
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const sort: { [k: string]: SortDirection } = {
|
||||
count: 1,
|
||||
order: 1,
|
||||
username: 1,
|
||||
};
|
||||
|
||||
const projection = {
|
||||
_id: 1,
|
||||
agentId: 1,
|
||||
departmentId: 1,
|
||||
username: 1,
|
||||
} as const;
|
||||
|
||||
return this.findOneAndUpdate(query, update, { sort, projection, returnDocument: 'after' });
|
||||
}
|
||||
|
||||
replaceUsernameOfAgentByUserId(userId: string, username: string): Promise<UpdateResult | Document> {
|
||||
const query = { agentId: userId };
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
username,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
countByDepartmentId(departmentId: string): Promise<number> {
|
||||
return this.countDocuments({ departmentId });
|
||||
}
|
||||
|
||||
disableAgentsByDepartmentId(departmentId: string): Promise<UpdateResult | Document> {
|
||||
return this.updateMany({ departmentId }, { $set: { departmentEnabled: false } });
|
||||
}
|
||||
|
||||
enableAgentsByDepartmentId(departmentId: string): Promise<UpdateResult | Document> {
|
||||
return this.updateMany({ departmentId }, { $set: { departmentEnabled: true } });
|
||||
}
|
||||
|
||||
findAllAgentsConnectedToListOfDepartments(departmentIds: string[]): Promise<string[]> {
|
||||
return this.col.distinct('agentId', { departmentId: { $in: departmentIds }, departmentEnabled: true });
|
||||
}
|
||||
|
||||
findByAgentsAndDepartmentId<
|
||||
T extends Document = ILivechatDepartmentAgents,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(
|
||||
agentsIds: ILivechatDepartmentAgents['agentId'][],
|
||||
departmentId: ILivechatDepartmentAgents['departmentId'],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>({ agentId: { $in: agentsIds }, departmentId }, options);
|
||||
}
|
||||
|
||||
findDepartmentsOfAgent(agentId: string, enabled = false): AggregationCursor<ILivechatDepartmentAgents & { departmentName: string }> {
|
||||
return this.col.aggregate<ILivechatDepartmentAgents & { departmentName: string }>([
|
||||
{
|
||||
$match: {
|
||||
agentId,
|
||||
...(enabled && { departmentEnabled: true }),
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'rocketchat_livechat_department',
|
||||
localField: 'departmentId',
|
||||
foreignField: '_id',
|
||||
as: 'department',
|
||||
},
|
||||
},
|
||||
{ $unwind: '$department' },
|
||||
{
|
||||
$project: {
|
||||
_id: '$_id',
|
||||
agentId: '$agentId',
|
||||
departmentId: '$departmentId',
|
||||
departmentName: '$department.name',
|
||||
username: '$username',
|
||||
count: '$count',
|
||||
order: '$order',
|
||||
departmentEnabled: '$departmentEnabled',
|
||||
_updatedAt: '$_updatedAt',
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const isStringValue = (value: any): value is string => typeof value === 'string';
|
||||
471
packages/models/src/models/LivechatInquiry.ts
Normal file
471
packages/models/src/models/LivechatInquiry.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
import type {
|
||||
ILivechatInquiryRecord,
|
||||
IMessage,
|
||||
RocketChatRecordDeleted,
|
||||
ILivechatPriority,
|
||||
SelectedAgent,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import { LivechatInquiryStatus } from '@rocket.chat/core-typings';
|
||||
import type { ILivechatInquiryModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type {
|
||||
Collection,
|
||||
Db,
|
||||
Document,
|
||||
FindOptions,
|
||||
UpdateResult,
|
||||
Filter,
|
||||
DeleteResult,
|
||||
IndexDescription,
|
||||
FindCursor,
|
||||
UpdateFilter,
|
||||
DeleteOptions,
|
||||
AggregateOptions,
|
||||
WithId,
|
||||
} from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
import { readSecondaryPreferred } from '../readSecondaryPreferred';
|
||||
|
||||
const { INQUIRY_LOCK_TIMEOUT = '10000' } = process.env;
|
||||
|
||||
export class LivechatInquiryRaw extends BaseRaw<ILivechatInquiryRecord> implements ILivechatInquiryModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatInquiryRecord>>) {
|
||||
super(db, 'livechat_inquiry', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): Array<IndexDescription> {
|
||||
return [
|
||||
{
|
||||
key: {
|
||||
rid: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
name: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
message: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
ts: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
department: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
status: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
priorityId: 1,
|
||||
priorityWeight: 1,
|
||||
},
|
||||
sparse: true,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
priorityWeight: 1,
|
||||
ts: 1,
|
||||
},
|
||||
partialFilterExpression: {
|
||||
status: { $eq: LivechatInquiryStatus.QUEUED },
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
estimatedWaitingTimeQueue: 1,
|
||||
ts: 1,
|
||||
},
|
||||
partialFilterExpression: {
|
||||
status: { $eq: LivechatInquiryStatus.QUEUED },
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
'v.token': 1,
|
||||
'status': 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: {
|
||||
locked: 1,
|
||||
lockedAt: 1,
|
||||
},
|
||||
sparse: true,
|
||||
},
|
||||
{ key: { 'v._id': 1 } },
|
||||
];
|
||||
}
|
||||
|
||||
findOneByRoomId<T extends Document = ILivechatInquiryRecord, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
rid: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
const query = {
|
||||
rid,
|
||||
};
|
||||
return this.findOne<T, O>(query, options);
|
||||
}
|
||||
|
||||
getDistinctQueuedDepartments(options: AggregateOptions): Promise<{ _id: string | null }[]> {
|
||||
return this.col
|
||||
.aggregate<{ _id: string | null }>(
|
||||
[
|
||||
{ $match: { status: LivechatInquiryStatus.QUEUED } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$department',
|
||||
},
|
||||
},
|
||||
],
|
||||
options,
|
||||
)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
async setDepartmentByInquiryId(inquiryId: string, department: string): Promise<ILivechatInquiryRecord | null> {
|
||||
return this.findOneAndUpdate({ _id: inquiryId }, { $set: { department } }, { returnDocument: 'after' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the `lastMessage` of inquiries that are not taken yet, after they're taken we only need to update room's `lastMessage`
|
||||
*/
|
||||
async setLastMessageByRoomId(rid: ILivechatInquiryRecord['rid'], message: IMessage): Promise<ILivechatInquiryRecord | null> {
|
||||
return this.findOneAndUpdate(
|
||||
{ rid, status: { $ne: LivechatInquiryStatus.TAKEN } },
|
||||
{ $set: { lastMessage: message } },
|
||||
{ returnDocument: 'after' },
|
||||
);
|
||||
}
|
||||
|
||||
async setLastMessageById(inquiryId: string, lastMessage: IMessage): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id: inquiryId }, { $set: { lastMessage } });
|
||||
}
|
||||
|
||||
async findNextAndLock(
|
||||
queueSortBy: FindOptions<ILivechatInquiryRecord>['sort'],
|
||||
department: string | null,
|
||||
): Promise<ILivechatInquiryRecord | null> {
|
||||
const date = new Date();
|
||||
return this.findOneAndUpdate(
|
||||
{
|
||||
status: LivechatInquiryStatus.QUEUED,
|
||||
...(department ? { department } : { department: { $exists: false } }),
|
||||
$or: [
|
||||
{
|
||||
locked: true,
|
||||
lockedAt: {
|
||||
$lte: new Date(date.getTime() - parseInt(INQUIRY_LOCK_TIMEOUT)),
|
||||
},
|
||||
},
|
||||
{
|
||||
locked: { $ne: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
locked: true,
|
||||
// apply INQUIRY_LOCK_TIMEOUT secs lock lifetime
|
||||
lockedAt: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
sort: queueSortBy,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async unlock(inquiryId: string): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id: inquiryId }, { $unset: { locked: 1, lockedAt: 1 } });
|
||||
}
|
||||
|
||||
async unlockAll(): Promise<UpdateResult | Document> {
|
||||
return this.updateMany(
|
||||
{ locked: { $exists: true } },
|
||||
{ $unset: { locked: 1, lockedAt: 1 }, $set: { status: LivechatInquiryStatus.QUEUED, queuedAt: new Date() } },
|
||||
);
|
||||
}
|
||||
|
||||
async getCurrentSortedQueueAsync({
|
||||
inquiryId,
|
||||
department,
|
||||
queueSortBy,
|
||||
}: {
|
||||
inquiryId?: string;
|
||||
department?: string;
|
||||
queueSortBy: FindOptions<ILivechatInquiryRecord>['sort'];
|
||||
}): Promise<(Pick<ILivechatInquiryRecord, '_id' | 'rid' | 'name' | 'ts' | 'status' | 'department'> & { position: number })[]> {
|
||||
const filter: Filter<ILivechatInquiryRecord>[] = [
|
||||
{
|
||||
$match: {
|
||||
status: 'queued',
|
||||
...(department && { department }),
|
||||
},
|
||||
},
|
||||
{ $sort: queueSortBy },
|
||||
{
|
||||
$group: {
|
||||
_id: 1,
|
||||
inquiry: {
|
||||
$push: {
|
||||
_id: '$_id',
|
||||
rid: '$rid',
|
||||
name: '$name',
|
||||
ts: '$ts',
|
||||
status: '$status',
|
||||
department: '$department',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$unwind: {
|
||||
path: '$inquiry',
|
||||
includeArrayIndex: 'position',
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: '$inquiry._id',
|
||||
rid: '$inquiry.rid',
|
||||
name: '$inquiry.name',
|
||||
ts: '$inquiry.ts',
|
||||
status: '$inquiry.status',
|
||||
department: '$inquiry.department',
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// To get the current room position in the queue, we need to apply the next $match after the $project
|
||||
if (inquiryId) {
|
||||
filter.push({ $match: { _id: inquiryId } });
|
||||
}
|
||||
|
||||
return this.col
|
||||
.aggregate<Pick<ILivechatInquiryRecord, '_id' | 'rid' | 'name' | 'ts' | 'status' | 'department'> & { position: number }>(filter, {
|
||||
readPreference: readSecondaryPreferred(),
|
||||
})
|
||||
.toArray();
|
||||
}
|
||||
|
||||
setSlaForRoom(_rid: string, _data: { estimatedWaitingTimeQueue: number; slaId: string }): Promise<null | WithId<ILivechatInquiryRecord>> {
|
||||
throw new Error('Method not implemented on the community edition.');
|
||||
}
|
||||
|
||||
unsetSlaForRoom(_roomId: string): Promise<null | WithId<ILivechatInquiryRecord>> {
|
||||
throw new Error('Method not implemented on the community edition.');
|
||||
}
|
||||
|
||||
bulkUnsetSla(_roomIds: string[]): Promise<Document | UpdateResult> {
|
||||
throw new Error('Method not implemented on the community edition.');
|
||||
}
|
||||
|
||||
setPriorityForRoom(_rid: string, _priority: Pick<ILivechatPriority, '_id' | 'sortItem'>): Promise<null | WithId<ILivechatInquiryRecord>> {
|
||||
throw new Error('Method not implemented on the community edition.');
|
||||
}
|
||||
|
||||
unsetPriorityForRoom(_rid: string): Promise<null | WithId<ILivechatInquiryRecord>> {
|
||||
throw new Error('Method not implemented on the community edition.');
|
||||
}
|
||||
|
||||
async removeByRoomId(rid: string, options?: DeleteOptions): Promise<DeleteResult> {
|
||||
return this.deleteOne({ rid }, options);
|
||||
}
|
||||
|
||||
getQueuedInquiries<T extends Document = ILivechatInquiryRecord, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>({ status: LivechatInquiryStatus.QUEUED }, options);
|
||||
}
|
||||
|
||||
takeInquiry(inquiryId: string, lockedAt?: Date): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: inquiryId,
|
||||
...(lockedAt && { lockedAt }),
|
||||
},
|
||||
{
|
||||
$set: { status: LivechatInquiryStatus.TAKEN, takenAt: new Date() },
|
||||
$unset: { defaultAgent: 1, estimatedInactivityCloseTimeAt: 1, queuedAt: 1 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async queueInquiry(
|
||||
inquiryId: string,
|
||||
lastMessage?: IMessage,
|
||||
defaultAgent?: SelectedAgent | null,
|
||||
): Promise<ILivechatInquiryRecord | null> {
|
||||
return this.findOneAndUpdate(
|
||||
{
|
||||
_id: inquiryId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
status: LivechatInquiryStatus.QUEUED,
|
||||
queuedAt: new Date(),
|
||||
...(lastMessage && { lastMessage }),
|
||||
...(defaultAgent && { defaultAgent }),
|
||||
},
|
||||
$unset: { takenAt: 1 },
|
||||
},
|
||||
{ returnDocument: 'after' },
|
||||
);
|
||||
}
|
||||
|
||||
queueInquiryAndRemoveDefaultAgent(inquiryId: string): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: inquiryId,
|
||||
},
|
||||
{
|
||||
$set: { status: LivechatInquiryStatus.QUEUED, queuedAt: new Date() },
|
||||
$unset: { takenAt: 1, defaultAgent: 1 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
readyInquiry(inquiryId: string): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: inquiryId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
status: LivechatInquiryStatus.READY,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async changeDepartmentIdByRoomId(rid: string, department: string): Promise<UpdateResult> {
|
||||
const query = {
|
||||
rid,
|
||||
};
|
||||
const updateObj = {
|
||||
$set: {
|
||||
department,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, updateObj);
|
||||
}
|
||||
|
||||
async getStatus(inquiryId: string): Promise<ILivechatInquiryRecord['status'] | undefined> {
|
||||
return (await this.findOne({ _id: inquiryId }))?.status;
|
||||
}
|
||||
|
||||
updateVisitorStatus(token: string, status: ILivechatInquiryRecord['v']['status']): Promise<UpdateResult> {
|
||||
const query: Filter<ILivechatInquiryRecord> = {
|
||||
'v.token': token,
|
||||
'status': LivechatInquiryStatus.QUEUED,
|
||||
};
|
||||
|
||||
const update: UpdateFilter<ILivechatInquiryRecord> = {
|
||||
$set: {
|
||||
'v.status': status,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
|
||||
setDefaultAgentById(inquiryId: string, defaultAgent: ILivechatInquiryRecord['defaultAgent']): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: inquiryId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
defaultAgent,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async setStatusById(inquiryId: string, status: LivechatInquiryStatus): Promise<ILivechatInquiryRecord> {
|
||||
const result = await this.findOneAndUpdate(
|
||||
{ _id: inquiryId },
|
||||
{ $set: { status } },
|
||||
{
|
||||
upsert: true,
|
||||
returnDocument: 'after',
|
||||
},
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('error-failed-to-set-inquiry-status');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
setNameByRoomId(rid: string, name: string): Promise<UpdateResult> {
|
||||
const query = { rid };
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
name,
|
||||
},
|
||||
};
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
|
||||
findOneByToken(token: string): Promise<ILivechatInquiryRecord | null> {
|
||||
const query: Filter<ILivechatInquiryRecord> = {
|
||||
'v.token': token,
|
||||
'status': LivechatInquiryStatus.QUEUED,
|
||||
};
|
||||
|
||||
return this.findOne(query);
|
||||
}
|
||||
|
||||
removeDefaultAgentById(inquiryId: string): Promise<UpdateResult | Document> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: inquiryId,
|
||||
},
|
||||
{
|
||||
$unset: { defaultAgent: 1 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async markInquiryActiveForPeriod(rid: ILivechatInquiryRecord['rid'], period: string): Promise<ILivechatInquiryRecord | null> {
|
||||
// goes through this.col since BaseRaw's setUpdatedAt mishandles pipeline updates
|
||||
return this.col.findOneAndUpdate(
|
||||
{ rid },
|
||||
[{ $set: { 'v.activity': { $setUnion: [{ $ifNull: ['$v.activity', []] }, [period]] }, '_updatedAt': '$$NOW' } }],
|
||||
{ returnDocument: 'after' },
|
||||
);
|
||||
}
|
||||
|
||||
updateNameByVisitorIds(visitorIds: string[], name: string): Promise<UpdateResult | Document> {
|
||||
const query = { 'v._id': { $in: visitorIds } };
|
||||
|
||||
const update = {
|
||||
$set: { name },
|
||||
};
|
||||
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
findByVisitorIds<T extends Document = ILivechatInquiryRecord, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
visitorIds: string[],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>({ 'v._id': { $in: visitorIds } }, options);
|
||||
}
|
||||
}
|
||||
82
packages/models/src/models/LivechatPriority.ts
Normal file
82
packages/models/src/models/LivechatPriority.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { ILivechatPriority } from '@rocket.chat/core-typings';
|
||||
import type { ILivechatPriorityModel } from '@rocket.chat/model-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
import type { Db, UpdateFilter, WithId, IndexDescription, FindCursor } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
// TODO need to define type for LivechatPriority object
|
||||
export class LivechatPriorityRaw extends BaseRaw<ILivechatPriority> implements ILivechatPriorityModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'livechat_priority');
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{
|
||||
key: {
|
||||
name: 1,
|
||||
},
|
||||
unique: true,
|
||||
partialFilterExpression: {
|
||||
$and: [{ name: { $exists: true } }, { name: { $gt: '' } }],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
findOneByIdOrName(_idOrName: string, options = {}): Promise<ILivechatPriority | null> {
|
||||
const query = {
|
||||
$or: [
|
||||
{
|
||||
_id: _idOrName,
|
||||
},
|
||||
{
|
||||
name: _idOrName,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return this.findOne(query, options);
|
||||
}
|
||||
|
||||
findOneNameUsingRegex(_idOrName: string, options = {}): Promise<ILivechatPriority | null> {
|
||||
const query = {
|
||||
name: new RegExp(`^${escapeRegExp(_idOrName.trim())}$`, 'i'),
|
||||
};
|
||||
|
||||
return this.findOne(query, options);
|
||||
}
|
||||
|
||||
findByDirty(): FindCursor<Pick<ILivechatPriority, '_id'>> {
|
||||
return this.find({ dirty: true }, { projection: { _id: 1 } });
|
||||
}
|
||||
|
||||
async canResetPriorities(): Promise<boolean> {
|
||||
return Boolean(await this.findOne({ dirty: true }, { projection: { _id: 1 } }));
|
||||
}
|
||||
|
||||
async resetPriorities(ids: ILivechatPriority['_id'][]): Promise<void> {
|
||||
await this.updateMany({ _id: { $in: ids } }, [{ $set: { dirty: false } }, { $unset: 'name' }]);
|
||||
}
|
||||
|
||||
async updatePriority(_id: string, reset: boolean, name?: string): Promise<null | WithId<ILivechatPriority>> {
|
||||
const query = {
|
||||
_id,
|
||||
};
|
||||
|
||||
const update: Pick<UpdateFilter<ILivechatPriority>, '$set' | '$unset'> = {
|
||||
...((reset && {
|
||||
$set: { dirty: false },
|
||||
$unset: { name: 1 },
|
||||
}) || {
|
||||
// Trim value before inserting
|
||||
$set: { name: name?.trim(), dirty: true },
|
||||
}),
|
||||
};
|
||||
|
||||
return this.findOneAndUpdate(query, update, {
|
||||
returnDocument: 'after',
|
||||
});
|
||||
}
|
||||
}
|
||||
34
packages/models/src/models/LivechatRooms.spec.ts
Normal file
34
packages/models/src/models/LivechatRooms.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Db } from 'mongodb';
|
||||
|
||||
import { LivechatRoomsRaw } from './LivechatRooms';
|
||||
import { UpdaterImpl } from '../updater';
|
||||
|
||||
// `LivechatRooms` imports `..`, whose barrel pulls in every model and cycles back here.
|
||||
jest.mock('..', () => ({
|
||||
getCollectionName: (name: string) => name,
|
||||
UpdaterImpl: jest.requireActual('../updater').UpdaterImpl,
|
||||
}));
|
||||
|
||||
const db = { collection: () => ({ createIndexes: jest.fn().mockResolvedValue(undefined) }) } as unknown as Db;
|
||||
|
||||
describe('getVisitorActiveForPeriodUpdateQuery', () => {
|
||||
const model = new LivechatRoomsRaw(db);
|
||||
|
||||
it('should $addToSet the period when the current activity is an array', () => {
|
||||
const updater = model.getVisitorActiveForPeriodUpdateQuery('2026-09', new UpdaterImpl(), ['2026-08']);
|
||||
|
||||
expect(updater.getRawUpdateFilter()).toEqual({ $addToSet: { 'v.activity': { $each: ['2026-09'] } } });
|
||||
});
|
||||
|
||||
it('should $addToSet the period when the current activity is absent', () => {
|
||||
const updater = model.getVisitorActiveForPeriodUpdateQuery('2026-09', new UpdaterImpl(), undefined);
|
||||
|
||||
expect(updater.getRawUpdateFilter()).toEqual({ $addToSet: { 'v.activity': { $each: ['2026-09'] } } });
|
||||
});
|
||||
|
||||
it('should $set the period when the current activity is null', () => {
|
||||
const updater = model.getVisitorActiveForPeriodUpdateQuery('2026-09', new UpdaterImpl(), null);
|
||||
|
||||
expect(updater.getRawUpdateFilter()).toEqual({ $set: { 'v.activity': ['2026-09'] } });
|
||||
});
|
||||
});
|
||||
2779
packages/models/src/models/LivechatRooms.ts
Normal file
2779
packages/models/src/models/LivechatRooms.ts
Normal file
File diff suppressed because it is too large
Load Diff
23
packages/models/src/models/LivechatTrigger.ts
Normal file
23
packages/models/src/models/LivechatTrigger.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { ILivechatTrigger, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ILivechatTriggerModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, FindCursor, Db, IndexDescription, UpdateFilter, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class LivechatTriggerRaw extends BaseRaw<ILivechatTrigger> implements ILivechatTriggerModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatTrigger>>) {
|
||||
super(db, 'livechat_trigger', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { enabled: 1 } }];
|
||||
}
|
||||
|
||||
findEnabled(): FindCursor<ILivechatTrigger> {
|
||||
return this.find({ enabled: true });
|
||||
}
|
||||
|
||||
updateById(_id: string, data: Omit<ILivechatTrigger, '_id' | '_updatedAt'>): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id }, { $set: data } as UpdateFilter<ILivechatTrigger>); // TODO: remove this cast when TypeScript is updated
|
||||
}
|
||||
}
|
||||
527
packages/models/src/models/LivechatVisitors.ts
Normal file
527
packages/models/src/models/LivechatVisitors.ts
Normal file
@@ -0,0 +1,527 @@
|
||||
import type { IVisitorExternalIdentifier, ILivechatVisitor, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { FindPaginated, ILivechatVisitorsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
import type {
|
||||
AggregationCursor,
|
||||
Collection,
|
||||
FindCursor,
|
||||
Db,
|
||||
Document,
|
||||
Filter,
|
||||
FindOptions,
|
||||
UpdateResult,
|
||||
IndexDescription,
|
||||
DeleteResult,
|
||||
UpdateFilter,
|
||||
WithId,
|
||||
FindOneAndUpdateOptions,
|
||||
} from 'mongodb';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
import { Settings } from '../index';
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class LivechatVisitorsRaw extends BaseRaw<ILivechatVisitor> implements ILivechatVisitorsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatVisitor>>) {
|
||||
super(db, 'livechat_visitor', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{ key: { token: 1 } },
|
||||
{ key: { 'phone.phoneNumber': 1 }, sparse: true },
|
||||
{ key: { 'visitorEmails.address': 1 }, sparse: true },
|
||||
{ key: { 'externalIds.entityId': 1 }, sparse: true },
|
||||
{ key: { name: 1 }, sparse: true },
|
||||
{ key: { username: 1 } },
|
||||
{ key: { 'contactMananger.username': 1 }, sparse: true },
|
||||
{ key: { 'livechatData.$**': 1 } },
|
||||
// TODO: remove this index in the next major release (8.0.0)
|
||||
// { key: { activity: 1 }, partialFilterExpression: { activity: { $exists: true } } },
|
||||
{ key: { disabled: 1 }, partialFilterExpression: { disabled: { $exists: true } } },
|
||||
];
|
||||
}
|
||||
|
||||
findOneVisitorByPhone(phone: string): Promise<ILivechatVisitor | null> {
|
||||
const query = {
|
||||
'phone.phoneNumber': phone,
|
||||
};
|
||||
|
||||
return this.findOne(query);
|
||||
}
|
||||
|
||||
async findOneVisitorByPhoneOrEmailAndAddExternalId(
|
||||
contactData: { phone: string } | { email: string },
|
||||
appId: string,
|
||||
externalId: Omit<IVisitorExternalIdentifier, 'appId'>,
|
||||
): Promise<ILivechatVisitor | null> {
|
||||
const query =
|
||||
'phone' in contactData ? { 'phone.phoneNumber': contactData.phone } : { 'visitorEmails.address': contactData.email.toLowerCase() };
|
||||
|
||||
const visitor = await this.findOne(query);
|
||||
if (!visitor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filteredIds = visitor.externalIds?.filter((e) => e.appId !== appId) ?? [];
|
||||
const newExternalIds = [...filteredIds, { appId, ...externalId }];
|
||||
|
||||
await this.updateOne({ _id: visitor._id }, { $set: { externalIds: newExternalIds } });
|
||||
|
||||
return { ...visitor, externalIds: newExternalIds };
|
||||
}
|
||||
|
||||
findOneByExternalId(entityId: string): Promise<ILivechatVisitor | null> {
|
||||
return this.findOne({
|
||||
'externalIds.entityId': entityId,
|
||||
});
|
||||
}
|
||||
|
||||
async updateExternalIdById(
|
||||
_id: string,
|
||||
appId: string,
|
||||
externalId: Omit<IVisitorExternalIdentifier, 'appId'>,
|
||||
): Promise<ILivechatVisitor | null> {
|
||||
const visitor = await this.findOne({ _id });
|
||||
if (!visitor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filteredIds = visitor.externalIds?.filter((e) => e.appId !== appId) ?? [];
|
||||
const newExternalIds = [...filteredIds, { appId, ...externalId }];
|
||||
|
||||
await this.updateOne({ _id }, { $set: { externalIds: newExternalIds } });
|
||||
|
||||
return { ...visitor, externalIds: newExternalIds };
|
||||
}
|
||||
|
||||
async findOneGuestByEmailAddress(emailAddress: string): Promise<ILivechatVisitor | null> {
|
||||
if (!emailAddress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = {
|
||||
'visitorEmails.address': String(emailAddress).toLowerCase(),
|
||||
};
|
||||
|
||||
return this.findOne(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find visitors by _id
|
||||
* @param {string} token - Visitor token
|
||||
*/
|
||||
findById<T extends Document = ILivechatVisitor, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
_id: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
_id,
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
findEnabled<T extends Document = ILivechatVisitor, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
query: Filter<ILivechatVisitor>,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>(
|
||||
{
|
||||
...query,
|
||||
disabled: { $ne: true },
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
findOneEnabledById<T extends Document = ILivechatVisitor, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
_id: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
const query = {
|
||||
_id,
|
||||
disabled: { $ne: true },
|
||||
};
|
||||
|
||||
return this.findOne<T, O>(query, options);
|
||||
}
|
||||
|
||||
getVisitorByToken<T extends Document = ILivechatVisitor, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
token: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
const query = {
|
||||
token,
|
||||
};
|
||||
|
||||
return this.findOne<T, O>(query, options);
|
||||
}
|
||||
|
||||
countVisitorsBetweenDate({ start, end, department }: { start: Date; end: Date; department?: string }): Promise<number> {
|
||||
const query = {
|
||||
disabled: { $ne: true },
|
||||
_updatedAt: {
|
||||
$gte: new Date(start),
|
||||
$lt: new Date(end),
|
||||
},
|
||||
...(department && department !== 'undefined' && { department }),
|
||||
};
|
||||
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
|
||||
async getNextVisitorUsername(): Promise<string> {
|
||||
// TODO remove dependency from another model - this logic should be inside a service/function
|
||||
const livechatCount = await Settings.incrementValueById('Livechat_guest_count', 1, { returnDocument: 'after' });
|
||||
|
||||
if (!livechatCount) {
|
||||
throw new Error("Can't find Livechat_guest_count setting");
|
||||
}
|
||||
|
||||
return `guest-${livechatCount.value}`;
|
||||
}
|
||||
|
||||
findByNameRegexWithExceptionsAndConditions<P extends Document = ILivechatVisitor>(
|
||||
searchTerm: string,
|
||||
exceptions: string[] = [],
|
||||
conditions: Filter<ILivechatVisitor> = {},
|
||||
options: FindOptions<P extends ILivechatVisitor ? ILivechatVisitor : P> = {},
|
||||
): AggregationCursor<
|
||||
P & {
|
||||
custom_name: string;
|
||||
}
|
||||
> {
|
||||
if (!Array.isArray(exceptions)) {
|
||||
exceptions = [exceptions];
|
||||
}
|
||||
|
||||
const nameRegex = new RegExp(`^${escapeRegExp(searchTerm).trim()}`, 'i');
|
||||
|
||||
const match = {
|
||||
$match: {
|
||||
name: nameRegex,
|
||||
_id: {
|
||||
$nin: exceptions,
|
||||
},
|
||||
...conditions,
|
||||
},
|
||||
};
|
||||
|
||||
const { projection, sort, skip, limit } = options;
|
||||
const project = {
|
||||
$project: {
|
||||
// TODO: move this logic to client
|
||||
custom_name: { $concat: ['$username', ' - ', '$name'] },
|
||||
...projection,
|
||||
},
|
||||
};
|
||||
|
||||
const order = { $sort: sort || { name: 1 } };
|
||||
const params: Record<string, unknown>[] = [match, order, skip && { $skip: skip }, limit && { $limit: limit }, project].filter(
|
||||
Boolean,
|
||||
) as Record<string, unknown>[];
|
||||
|
||||
return this.col.aggregate(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find visitors by their email or phone or username or name
|
||||
*/
|
||||
async findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField<
|
||||
T extends Document = ILivechatVisitor,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(
|
||||
emailOrPhone?: string,
|
||||
nameOrUsername?: RegExp,
|
||||
allowedCustomFields: string[] = [],
|
||||
options?: O,
|
||||
): Promise<FindPaginated<FindCursor<DocumentWithProjection<T, O>>>> {
|
||||
if (!emailOrPhone && !nameOrUsername && allowedCustomFields.length === 0) {
|
||||
return this.findPaginated<T, O>({ disabled: { $ne: true } }, options);
|
||||
}
|
||||
|
||||
const query: Filter<ILivechatVisitor> = {
|
||||
$or: [
|
||||
...(emailOrPhone
|
||||
? [
|
||||
{
|
||||
'visitorEmails.address': emailOrPhone,
|
||||
},
|
||||
{
|
||||
'phone.phoneNumber': emailOrPhone,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(nameOrUsername
|
||||
? [
|
||||
{
|
||||
name: nameOrUsername,
|
||||
},
|
||||
{
|
||||
username: nameOrUsername,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...allowedCustomFields.map((c: string) => ({ [`livechatData.${c}`]: nameOrUsername })),
|
||||
],
|
||||
disabled: { $ne: true },
|
||||
};
|
||||
|
||||
return this.findPaginated<T, O>(query, options);
|
||||
}
|
||||
|
||||
async findOneByEmailAndPhoneAndCustomField(
|
||||
email: string | null | undefined,
|
||||
phone: string | null | undefined,
|
||||
customFields?: { [key: string]: RegExp },
|
||||
): Promise<ILivechatVisitor | null> {
|
||||
const query = Object.assign(
|
||||
{
|
||||
disabled: { $ne: true },
|
||||
},
|
||||
{
|
||||
...(email && { visitorEmails: { address: email } }),
|
||||
...(phone && { phone: { phoneNumber: phone } }),
|
||||
...customFields,
|
||||
},
|
||||
);
|
||||
|
||||
if (Object.keys(query).length === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.findOne(query);
|
||||
}
|
||||
|
||||
updateAllLivechatDataByToken(token: string, livechatDataToUpdate: Record<string, string>): Promise<UpdateResult> {
|
||||
return this.updateOne({ token }, { $set: livechatDataToUpdate });
|
||||
}
|
||||
|
||||
async updateLivechatDataByToken(
|
||||
token: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
overwrite = true,
|
||||
): Promise<UpdateResult | Document | boolean> {
|
||||
const query = {
|
||||
token,
|
||||
};
|
||||
|
||||
if (!overwrite) {
|
||||
const user = await this.getVisitorByToken(token, { projection: { livechatData: 1 } });
|
||||
if (user?.livechatData && typeof user.livechatData[key] !== 'undefined') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const update: UpdateFilter<ILivechatVisitor> = {
|
||||
$set: {
|
||||
[`livechatData.${key}`]: value,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
|
||||
updateLastAgentByToken(token: string, lastAgent: ILivechatVisitor['lastAgent']): Promise<Document | UpdateResult> {
|
||||
const query = {
|
||||
token,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
lastAgent,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
|
||||
updateById(_id: string, update: UpdateFilter<ILivechatVisitor>): Promise<Document | UpdateResult> {
|
||||
return this.updateOne({ _id }, update);
|
||||
}
|
||||
|
||||
async updateOneByIdOrToken(
|
||||
update: Partial<ILivechatVisitor>,
|
||||
options?: FindOneAndUpdateOptions,
|
||||
): Promise<null | WithId<ILivechatVisitor>> {
|
||||
let query: Filter<ILivechatVisitor> = {};
|
||||
|
||||
if (update._id) {
|
||||
query = { _id: update._id };
|
||||
} else if (update.token) {
|
||||
query = { token: update.token };
|
||||
update._id = new ObjectId().toHexString();
|
||||
}
|
||||
|
||||
return this.findOneAndUpdate(query, { $set: update }, options);
|
||||
}
|
||||
|
||||
saveGuestById(
|
||||
_id: string,
|
||||
data: { name?: string; username?: string; email?: string; phone?: string; livechatData: { [k: string]: any } },
|
||||
): Promise<UpdateResult | Document | boolean> {
|
||||
const setData: DeepWriteable<UpdateFilter<ILivechatVisitor>['$set']> = {};
|
||||
const unsetData: DeepWriteable<UpdateFilter<ILivechatVisitor>['$unset']> = {};
|
||||
|
||||
if (data.name) {
|
||||
if (data.name?.trim()) {
|
||||
setData.name = data.name.trim();
|
||||
} else {
|
||||
unsetData.name = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.email) {
|
||||
if (data.email?.trim()) {
|
||||
setData.visitorEmails = [{ address: data.email.trim() }];
|
||||
} else {
|
||||
unsetData.visitorEmails = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.phone) {
|
||||
if (data.phone?.trim()) {
|
||||
setData.phone = [{ phoneNumber: data.phone.trim() }];
|
||||
} else {
|
||||
unsetData.phone = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.livechatData) {
|
||||
Object.keys(data.livechatData).forEach((key) => {
|
||||
const value = data.livechatData[key]?.trim();
|
||||
if (value) {
|
||||
setData[`livechatData.${key}`] = value;
|
||||
} else {
|
||||
unsetData[`livechatData.${key}`] = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const update: UpdateFilter<ILivechatVisitor> = {
|
||||
...(Object.keys(setData).length && { $set: setData as UpdateFilter<ILivechatVisitor>['$set'] }),
|
||||
...(Object.keys(unsetData).length && { $unset: unsetData as UpdateFilter<ILivechatVisitor>['$unset'] }),
|
||||
};
|
||||
|
||||
if (!Object.keys(update).length) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return this.updateOne({ _id }, update);
|
||||
}
|
||||
|
||||
removeDepartmentById(_id: string): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id }, { $unset: { department: 1 } });
|
||||
}
|
||||
|
||||
override removeById(_id: string): Promise<DeleteResult> {
|
||||
return this.deleteOne({ _id });
|
||||
}
|
||||
|
||||
async saveGuestEmailPhoneById(_id: string, emails: string[], phones: string[]): Promise<UpdateResult | Document | void> {
|
||||
const saveEmail = ([] as string[])
|
||||
.concat(emails)
|
||||
.filter((email) => email?.trim())
|
||||
.map((email) => ({ address: email }));
|
||||
|
||||
const savePhone = ([] as string[])
|
||||
.concat(phones)
|
||||
.filter((phone) => phone?.trim().replace(/[^\d]/g, ''))
|
||||
.map((phone) => ({ phoneNumber: phone }));
|
||||
|
||||
if (!saveEmail.length && !savePhone.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// TODO: Lead Capture features: Either create a migration script fixing records or deprecate it
|
||||
// Temporary workaround for legacy data.
|
||||
// Some old records have 'visitorEmails' or 'phone' set to 'null', which breaks the $addToSet below.
|
||||
// Pending Product decision: either run a proper DB migration to fix all nulls,
|
||||
// or deprecate this feature entirely. Remove this once a decision is made.
|
||||
if (saveEmail.length) {
|
||||
await this.updateOne({ _id, visitorEmails: null as any }, { $set: { visitorEmails: [] } });
|
||||
}
|
||||
if (savePhone.length) {
|
||||
await this.updateOne({ _id, phone: null }, { $set: { phone: [] } });
|
||||
}
|
||||
|
||||
return this.updateOne(
|
||||
{ _id },
|
||||
{
|
||||
$addToSet: {
|
||||
...(saveEmail.length && { visitorEmails: { $each: saveEmail } }),
|
||||
...(savePhone.length && { phone: { $each: savePhone } }),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
removeContactManagerByUsername(manager: string): Promise<Document | UpdateResult> {
|
||||
return this.updateMany(
|
||||
{
|
||||
contactManager: {
|
||||
username: manager,
|
||||
},
|
||||
},
|
||||
{
|
||||
$unset: {
|
||||
contactManager: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
disableById(_id: string): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{ _id },
|
||||
{
|
||||
$set: { disabled: true },
|
||||
$unset: {
|
||||
department: 1,
|
||||
contactManager: 1,
|
||||
token: 1,
|
||||
visitorEmails: 1,
|
||||
phone: 1,
|
||||
name: 1,
|
||||
livechatData: 1,
|
||||
lastChat: 1,
|
||||
ip: 1,
|
||||
host: 1,
|
||||
userAgent: 1,
|
||||
username: 1,
|
||||
ts: 1,
|
||||
status: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setLastChatById(_id: string, lastChat: Required<ILivechatVisitor['lastChat']>): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{ _id },
|
||||
{
|
||||
$set: {
|
||||
lastChat,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
updateDepartmentById(_id: string, department: string) {
|
||||
return this.findOneAndUpdate({ _id }, { $set: { department } }, { returnDocument: 'after' });
|
||||
}
|
||||
|
||||
findByIds<T extends Document = ILivechatVisitor, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
ids: string[],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
_id: { $in: ids },
|
||||
};
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
}
|
||||
|
||||
type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
|
||||
38
packages/models/src/models/LoginCodes.ts
Normal file
38
packages/models/src/models/LoginCodes.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
import type { ILoginCode } from '@rocket.chat/core-typings';
|
||||
import type { ILoginCodesModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
const CODE_TTL_MS = 60 * 1000;
|
||||
|
||||
export class LoginCodesRaw extends BaseRaw<ILoginCode> implements ILoginCodesModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'login_codes');
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { expireAt: 1 }, expireAfterSeconds: 0 }];
|
||||
}
|
||||
|
||||
async createCode(userId: string): Promise<string> {
|
||||
const now = new Date();
|
||||
const code = randomBytes(32).toString('hex');
|
||||
await this.insertOne({
|
||||
_id: code,
|
||||
userId,
|
||||
createdAt: now,
|
||||
expireAt: new Date(now.getTime() + CODE_TTL_MS),
|
||||
});
|
||||
return code;
|
||||
}
|
||||
|
||||
findOneNotExpiredByCodeAndDelete(code: string): Promise<ILoginCode | null> {
|
||||
return this.findOneAndDelete({
|
||||
_id: code,
|
||||
expireAt: { $gt: new Date() },
|
||||
});
|
||||
}
|
||||
}
|
||||
57
packages/models/src/models/LoginServiceConfiguration.ts
Normal file
57
packages/models/src/models/LoginServiceConfiguration.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { LoginServiceConfiguration, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ILoginServiceConfigurationModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class LoginServiceConfigurationRaw extends BaseRaw<LoginServiceConfiguration> implements ILoginServiceConfigurationModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<LoginServiceConfiguration>>) {
|
||||
super(db, 'meteor_accounts_loginServiceConfiguration', trash, {
|
||||
preventSetUpdatedAt: true,
|
||||
collectionNameResolver(name) {
|
||||
return name;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createOrUpdateService(
|
||||
serviceName: LoginServiceConfiguration['service'],
|
||||
serviceData: Partial<LoginServiceConfiguration>,
|
||||
): Promise<LoginServiceConfiguration['_id']> {
|
||||
const service = serviceName.toLowerCase();
|
||||
|
||||
const existing = await this.findOne({ service });
|
||||
if (!existing) {
|
||||
const insertResult = await this.insertOne({
|
||||
service,
|
||||
...serviceData,
|
||||
});
|
||||
|
||||
return insertResult.insertedId;
|
||||
}
|
||||
|
||||
if (Object.keys(serviceData).length > 0) {
|
||||
await this.updateOne(
|
||||
{
|
||||
_id: existing._id,
|
||||
},
|
||||
{
|
||||
$set: serviceData,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return existing._id;
|
||||
}
|
||||
|
||||
removeByService(serviceName: LoginServiceConfiguration['service']): Promise<Pick<LoginServiceConfiguration, '_id'> | null> {
|
||||
return this.findOneAndDelete({ service: serviceName.toLowerCase() }, { projection: { _id: 1 } });
|
||||
}
|
||||
|
||||
async findOneByService<
|
||||
P extends Document = LoginServiceConfiguration,
|
||||
O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>,
|
||||
>(serviceName: LoginServiceConfiguration['service'], options?: O): Promise<DocumentWithProjection<P, O> | null> {
|
||||
return this.findOne<P, O>({ service: serviceName.toLowerCase() }, options);
|
||||
}
|
||||
}
|
||||
94
packages/models/src/models/MediaCallNegotiations.ts
Normal file
94
packages/models/src/models/MediaCallNegotiations.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type {
|
||||
RocketChatRecordDeleted,
|
||||
IMediaCallNegotiation,
|
||||
MediaCallNegotiationStream,
|
||||
RTCSessionDescriptionInit,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import type { IMediaCallNegotiationsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { IndexDescription, Collection, Db, Document, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class MediaCallNegotiationsRaw extends BaseRaw<IMediaCallNegotiation> implements IMediaCallNegotiationsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IMediaCallNegotiation>>) {
|
||||
super(db, 'media_call_negotiations', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { callId: 1, requestTimestamp: -1 }, unique: false }];
|
||||
}
|
||||
|
||||
public async findLatestByCallId<
|
||||
T extends Document = IMediaCallNegotiation,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(callId: IMediaCallNegotiation['callId'], options?: O): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>(
|
||||
{
|
||||
callId,
|
||||
},
|
||||
// safe to merge into `O`: only `O['projection']` feeds the return type, and it survives the spread.
|
||||
// note the model's `sort`/`limit` win over caller-supplied ones.
|
||||
{
|
||||
...options,
|
||||
sort: {
|
||||
requestTimestamp: -1,
|
||||
},
|
||||
limit: 1,
|
||||
} as unknown as O,
|
||||
);
|
||||
}
|
||||
|
||||
public async setOfferById(
|
||||
id: string,
|
||||
offer: RTCSessionDescriptionInit,
|
||||
offerStreams?: MediaCallNegotiationStream[],
|
||||
): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: id,
|
||||
offer: { $exists: false },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
offer,
|
||||
offerTimestamp: new Date(),
|
||||
...(offerStreams && { offerStreams }),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async setAnswerById(
|
||||
id: string,
|
||||
answer: RTCSessionDescriptionInit,
|
||||
answerStreams?: MediaCallNegotiationStream[],
|
||||
): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: id,
|
||||
answer: { $exists: false },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
answer,
|
||||
answerTimestamp: new Date(),
|
||||
...(answerStreams && { answerStreams }),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async setStableById(id: string): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: id,
|
||||
stableTimestamp: { $exists: false },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
stableTimestamp: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
227
packages/models/src/models/MediaCalls.ts
Normal file
227
packages/models/src/models/MediaCalls.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import type {
|
||||
IMediaCall,
|
||||
RocketChatRecordDeleted,
|
||||
MediaCallActorType,
|
||||
MediaCallSignedContact,
|
||||
MediaCallContact,
|
||||
IUser,
|
||||
MediaCallActor,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import type { IMediaCallsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { IndexDescription, Collection, Db, UpdateFilter, UpdateOptions, UpdateResult, Document, FindCursor } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class MediaCallsRaw extends BaseRaw<IMediaCall> implements IMediaCallsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IMediaCall>>) {
|
||||
super(db, 'media_calls', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{ key: { createdAt: 1 }, unique: false },
|
||||
{ key: { ended: 1, uids: 1, expiresAt: 1 }, unique: false },
|
||||
{
|
||||
key: { 'caller.type': 1, 'caller.id': 1, 'callerRequestedId': 1 },
|
||||
sparse: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public async findOneByIdAndCallee<T extends Document = IMediaCall, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
id: IMediaCall['_id'],
|
||||
callee: MediaCallActor,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>(
|
||||
{
|
||||
'_id': id,
|
||||
'callee.type': callee.type,
|
||||
'callee.id': callee.id,
|
||||
...(callee.contractId && { 'callee.contractId': callee.contractId }),
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
public async findOneByCallerRequestedId<
|
||||
T extends Document = IMediaCall,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(
|
||||
id: Required<IMediaCall>['callerRequestedId'],
|
||||
caller: { type: MediaCallActorType; id: string },
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>(
|
||||
{
|
||||
'caller.type': caller.type,
|
||||
'caller.id': caller.id,
|
||||
'callerRequestedId': id,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
public updateOneById(
|
||||
_id: string,
|
||||
update: UpdateFilter<IMediaCall> | Partial<IMediaCall>,
|
||||
options?: UpdateOptions,
|
||||
): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id }, update, options);
|
||||
}
|
||||
|
||||
public async startRingingById(callId: string, expiresAt: Date): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: callId,
|
||||
state: 'none',
|
||||
},
|
||||
{ $set: { state: 'ringing', expiresAt } },
|
||||
);
|
||||
}
|
||||
|
||||
public async acceptCallById(
|
||||
callId: string,
|
||||
data: { calleeContractId: string; supportedFeatures: string[]; sipCallId?: string },
|
||||
expiresAt: Date,
|
||||
): Promise<UpdateResult> {
|
||||
const { calleeContractId, sipCallId } = data;
|
||||
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: callId,
|
||||
state: { $in: ['none', 'ringing'] },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'state': 'accepted',
|
||||
'callee.contractId': calleeContractId,
|
||||
'acceptedAt': new Date(),
|
||||
expiresAt,
|
||||
...(sipCallId && { sipCallId }),
|
||||
},
|
||||
$pull: {
|
||||
features: {
|
||||
$nin: data.supportedFeatures,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async activateCallById(callId: string, expiresAt: Date): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: callId,
|
||||
state: 'accepted',
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
state: 'active',
|
||||
activatedAt: new Date(),
|
||||
expiresAt,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async hangupCallById(callId: string, params?: { endedBy?: IMediaCall['endedBy']; reason?: string }): Promise<UpdateResult> {
|
||||
const { endedBy, reason } = params || {};
|
||||
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: callId,
|
||||
ended: false,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
state: 'hangup',
|
||||
ended: true,
|
||||
endedAt: new Date(),
|
||||
...(endedBy && { endedBy }),
|
||||
...(reason && { hangupReason: reason }),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async setExpiresAtById(callId: string, expiresAt: Date): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: callId,
|
||||
ended: false,
|
||||
},
|
||||
{
|
||||
$set: { expiresAt },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async transferCallById(callId: string, params: { by: MediaCallSignedContact; to: MediaCallContact }): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: callId,
|
||||
state: {
|
||||
$in: ['accepted', 'active'],
|
||||
},
|
||||
transferredAt: {
|
||||
$exists: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
transferredAt: new Date(),
|
||||
transferredBy: params.by,
|
||||
transferredTo: params.to,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public findAllExpiredCalls<T extends Document = IMediaCall, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>(
|
||||
{
|
||||
ended: false,
|
||||
expiresAt: {
|
||||
$lte: new Date(),
|
||||
},
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
public findAllNotOverByUid<T extends Document = IMediaCall, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
uid: IUser['_id'],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>(
|
||||
{
|
||||
ended: false,
|
||||
expiresAt: {
|
||||
$gt: new Date(),
|
||||
},
|
||||
uids: uid,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
public async hasUnfinishedCalls(): Promise<boolean> {
|
||||
const count = await this.countDocuments({ ended: false }, { limit: 1 });
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public async hasUnfinishedCallsByUid(uid: IUser['_id'], exceptCallId?: string): Promise<boolean> {
|
||||
const count = await this.countDocuments(
|
||||
{
|
||||
ended: false,
|
||||
uids: uid,
|
||||
...(exceptCallId && { _id: { $ne: exceptCallId } }),
|
||||
},
|
||||
{ limit: 1 },
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
62
packages/models/src/models/MessageReads.ts
Normal file
62
packages/models/src/models/MessageReads.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { MessageReads, IUser, IMessage, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IMessageReadsModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, IndexDescription, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class MessageReadsRaw extends BaseRaw<MessageReads> implements IMessageReadsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<MessageReads>>) {
|
||||
super(db, 'message_reads', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { tmid: 1, userId: 1 }, unique: true }, { key: { ls: 1 } }];
|
||||
}
|
||||
|
||||
async findOneByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise<MessageReads | null> {
|
||||
return this.findOne({ userId, tmid });
|
||||
}
|
||||
|
||||
getMinimumLastSeenByThreadId(tmid: IMessage['_id']): Promise<MessageReads | null> {
|
||||
return this.findOne(
|
||||
{
|
||||
tmid,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
ls: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
updateReadTimestampByUserIdAndThreadId(userId: IUser['_id'], tmid: IMessage['_id']): Promise<UpdateResult> {
|
||||
const query = {
|
||||
userId,
|
||||
tmid,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
ls: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update, { upsert: true });
|
||||
}
|
||||
|
||||
async countByThreadAndUserIds(tmid: IMessage['_id'], userIds: IUser['_id'][]): Promise<number> {
|
||||
const query = {
|
||||
tmid,
|
||||
userId: { $in: userIds },
|
||||
};
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
|
||||
async countByThreadId(tmid: IMessage['_id']): Promise<number> {
|
||||
const query = {
|
||||
tmid,
|
||||
};
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
}
|
||||
1839
packages/models/src/models/Messages.ts
Normal file
1839
packages/models/src/models/Messages.ts
Normal file
File diff suppressed because it is too large
Load Diff
15
packages/models/src/models/Migrations.ts
Normal file
15
packages/models/src/models/Migrations.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { IControl } from '@rocket.chat/core-typings';
|
||||
import type { IMigrationsModel } from '@rocket.chat/model-typings';
|
||||
import type { Db } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class MigrationsRaw extends BaseRaw<IControl> implements IMigrationsModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'migrations', undefined, {
|
||||
collectionNameResolver(name) {
|
||||
return name;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
441
packages/models/src/models/ModerationReports.ts
Normal file
441
packages/models/src/models/ModerationReports.ts
Normal file
@@ -0,0 +1,441 @@
|
||||
import type {
|
||||
IMessage,
|
||||
IModerationAudit,
|
||||
IModerationReport,
|
||||
RocketChatRecordDeleted,
|
||||
MessageReport,
|
||||
UserReport,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import type { FindPaginated, IModerationReportsModel, PaginationParams } from '@rocket.chat/model-typings';
|
||||
import type { AggregationCursor, Collection, Db, Document, FindCursor, FindOptions, IndexDescription, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
import { readSecondaryPreferred } from '../readSecondaryPreferred';
|
||||
|
||||
export class ModerationReportsRaw extends BaseRaw<IModerationReport> implements IModerationReportsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IModerationReport>>) {
|
||||
super(db, 'moderation_reports', trash);
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] | undefined {
|
||||
return [
|
||||
// TODO deprecated. remove within a migration in v7.0
|
||||
// { key: { 'ts': 1, 'reports.ts': 1 } },
|
||||
// { key: { 'message.u._id': 1, 'ts': 1 } },
|
||||
// { key: { 'reportedUser._id': 1, 'ts': 1 } },
|
||||
// { key: { 'message.rid': 1, 'ts': 1 } },
|
||||
// { key: { 'message._id': 1, 'ts': 1 } },
|
||||
// { key: { userId: 1, ts: 1 } },
|
||||
{ key: { _hidden: 1, ts: 1 } },
|
||||
{ key: { 'message._id': 1, '_hidden': 1, 'ts': 1 } },
|
||||
{ key: { 'message.u._id': 1, '_hidden': 1, 'ts': 1 } },
|
||||
{ key: { 'reportedUser._id': 1, '_hidden': 1, 'ts': 1 } },
|
||||
];
|
||||
}
|
||||
|
||||
createWithMessageDescriptionAndUserId(
|
||||
message: IMessage,
|
||||
description: IModerationReport['description'],
|
||||
room: IModerationReport['room'],
|
||||
reportedBy: IModerationReport['reportedBy'],
|
||||
): ReturnType<BaseRaw<IModerationReport>['insertOne']> {
|
||||
const record: Pick<IModerationReport, 'message' | 'description' | 'ts' | 'reportedBy' | 'room'> = {
|
||||
message,
|
||||
description,
|
||||
reportedBy,
|
||||
room,
|
||||
ts: new Date(),
|
||||
};
|
||||
return this.insertOne(record);
|
||||
}
|
||||
|
||||
createWithDescriptionAndUser(
|
||||
reportedUser: UserReport['reportedUser'],
|
||||
description: UserReport['description'],
|
||||
reportedBy: UserReport['reportedBy'],
|
||||
): ReturnType<BaseRaw<IModerationReport>['insertOne']> {
|
||||
const record = {
|
||||
description,
|
||||
reportedBy,
|
||||
reportedUser,
|
||||
ts: new Date(),
|
||||
};
|
||||
|
||||
return this.insertOne(record);
|
||||
}
|
||||
|
||||
findMessageReportsGroupedByUser(
|
||||
latest: Date,
|
||||
oldest: Date,
|
||||
selector: string,
|
||||
pagination: PaginationParams<IModerationReport>,
|
||||
): AggregationCursor<IModerationAudit> {
|
||||
const query = {
|
||||
_hidden: {
|
||||
$ne: true,
|
||||
},
|
||||
ts: {
|
||||
$lt: latest,
|
||||
$gt: oldest,
|
||||
},
|
||||
...this.getSearchQueryForSelector(selector),
|
||||
};
|
||||
|
||||
const { sort, offset, count } = pagination;
|
||||
|
||||
const params = [
|
||||
{ $match: query },
|
||||
{
|
||||
$group: {
|
||||
_id: { user: '$message.u._id' },
|
||||
reports: { $first: '$$ROOT' },
|
||||
rooms: { $addToSet: '$room' }, // to be replaced with room
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: sort || {
|
||||
'reports.ts': -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$skip: offset,
|
||||
},
|
||||
{
|
||||
$limit: count,
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'users',
|
||||
localField: '_id.user',
|
||||
foreignField: '_id',
|
||||
as: 'user',
|
||||
},
|
||||
},
|
||||
{
|
||||
$unwind: {
|
||||
path: '$user',
|
||||
preserveNullAndEmptyArrays: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
// TODO: maybe clean up the projection, i.e. exclude things we don't need
|
||||
$project: {
|
||||
_id: 0,
|
||||
message: '$reports.message.msg',
|
||||
msgId: '$reports.message._id',
|
||||
ts: '$reports.ts',
|
||||
username: '$reports.message.u.username',
|
||||
name: '$reports.message.u.name',
|
||||
userId: '$reports.message.u._id',
|
||||
isUserDeleted: { $cond: ['$user', false, true] },
|
||||
count: 1,
|
||||
rooms: 1,
|
||||
roomIds: { $map: { input: '$rooms', as: 'r', in: '$$r._id' } },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return this.col.aggregate(params, { allowDiskUse: true });
|
||||
}
|
||||
|
||||
findUserReports(
|
||||
latest: Date,
|
||||
oldest: Date,
|
||||
selector: string,
|
||||
pagination: PaginationParams<IModerationReport>,
|
||||
): AggregationCursor<Pick<UserReport, '_id' | 'reportedUser' | 'ts'> & { count: number }> {
|
||||
const query = {
|
||||
_hidden: {
|
||||
$ne: true,
|
||||
},
|
||||
ts: {
|
||||
$lt: latest,
|
||||
$gt: oldest,
|
||||
},
|
||||
...this.getSearchQueryForSelectorUsers(selector),
|
||||
};
|
||||
|
||||
const { sort, offset, count } = pagination;
|
||||
|
||||
const pipeline = [
|
||||
{ $match: query },
|
||||
{
|
||||
$sort: {
|
||||
ts: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$reportedUser._id',
|
||||
count: { $sum: 1 },
|
||||
reports: { $first: '$$ROOT' },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: sort || {
|
||||
'reports.ts': -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$skip: offset,
|
||||
},
|
||||
{
|
||||
$limit: count,
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
reportedUser: '$reports.reportedUser',
|
||||
ts: '$reports.ts',
|
||||
count: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return this.col.aggregate(pipeline, { allowDiskUse: true, readPreference: readSecondaryPreferred() });
|
||||
}
|
||||
|
||||
async getTotalUniqueReportedUsers(latest: Date, oldest: Date, selector: string, isMessageReports?: boolean): Promise<number> {
|
||||
const query = {
|
||||
_hidden: {
|
||||
$ne: true,
|
||||
},
|
||||
ts: {
|
||||
$lt: latest,
|
||||
$gt: oldest,
|
||||
},
|
||||
...(isMessageReports ? this.getSearchQueryForSelector(selector) : this.getSearchQueryForSelectorUsers(selector)),
|
||||
};
|
||||
|
||||
const field = isMessageReports ? 'message.u._id' : 'reportedUser._id';
|
||||
const pipeline = [{ $match: query }, { $group: { _id: `$${field}` } }, { $group: { _id: null, count: { $sum: 1 } } }];
|
||||
|
||||
const result = await this.col.aggregate(pipeline).toArray();
|
||||
return result[0]?.count || 0;
|
||||
}
|
||||
|
||||
countMessageReportsInRange(latest: Date, oldest: Date, selector: string): Promise<number> {
|
||||
return this.countDocuments({
|
||||
_hidden: { $ne: true },
|
||||
ts: { $lt: latest, $gt: oldest },
|
||||
...this.getSearchQueryForSelector(selector),
|
||||
});
|
||||
}
|
||||
|
||||
findReportedMessagesByReportedUserId(
|
||||
userId: string,
|
||||
selector: string,
|
||||
pagination: PaginationParams<IModerationReport>,
|
||||
options: FindOptions<IModerationReport> = {},
|
||||
): FindPaginated<FindCursor<Pick<MessageReport, '_id' | 'message' | 'ts' | 'room'>>> {
|
||||
const query = {
|
||||
'_hidden': {
|
||||
$ne: true,
|
||||
},
|
||||
'message.u._id': userId,
|
||||
};
|
||||
|
||||
const { sort, offset, count } = pagination;
|
||||
|
||||
const fuzzyQuery = selector
|
||||
? {
|
||||
'message.msg': {
|
||||
$regex: selector,
|
||||
$options: 'i',
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
const params = {
|
||||
sort: sort || {
|
||||
ts: -1,
|
||||
},
|
||||
skip: offset,
|
||||
limit: count,
|
||||
projection: {
|
||||
_id: 1,
|
||||
message: 1,
|
||||
ts: 1,
|
||||
room: 1,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
return this.findPaginated({ ...query, ...fuzzyQuery }, params);
|
||||
}
|
||||
|
||||
findUserReportsByReportedUserId(
|
||||
userId: string,
|
||||
selector: string,
|
||||
pagination: PaginationParams<IModerationReport>,
|
||||
options: FindOptions<IModerationReport> = {},
|
||||
): FindPaginated<FindCursor<Omit<UserReport, 'moderationInfo'>>> {
|
||||
const query = {
|
||||
'_hidden': {
|
||||
$ne: true,
|
||||
},
|
||||
'reportedUser._id': userId,
|
||||
...this.getSearchQueryForSelectorUsers(selector),
|
||||
};
|
||||
|
||||
const { count, offset, sort } = pagination;
|
||||
|
||||
const opts = {
|
||||
sort: sort || {
|
||||
ts: -1,
|
||||
},
|
||||
skip: offset,
|
||||
limit: count,
|
||||
projection: {
|
||||
_id: 1,
|
||||
description: 1,
|
||||
ts: 1,
|
||||
reportedBy: 1,
|
||||
reportedUser: 1,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
return this.findPaginated(query, opts);
|
||||
}
|
||||
|
||||
findReportsByMessageId(
|
||||
messageId: string,
|
||||
selector: string,
|
||||
pagination: PaginationParams<IModerationReport>,
|
||||
options: FindOptions<IModerationReport> = {},
|
||||
): FindPaginated<FindCursor<Pick<IModerationReport, '_id' | 'description' | 'reportedBy' | 'ts' | 'room'>>> {
|
||||
const query = {
|
||||
'_hidden': {
|
||||
$ne: true,
|
||||
},
|
||||
'message._id': messageId,
|
||||
...this.getSearchQueryForSelector(selector),
|
||||
};
|
||||
|
||||
const { count, offset, sort } = pagination;
|
||||
|
||||
const opts = {
|
||||
sort: sort || {
|
||||
ts: -1,
|
||||
},
|
||||
skip: offset,
|
||||
limit: count,
|
||||
projection: {
|
||||
_id: 1,
|
||||
description: 1,
|
||||
ts: 1,
|
||||
reportedBy: 1,
|
||||
room: 1,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
return this.findPaginated(query, opts);
|
||||
}
|
||||
|
||||
async hideMessageReportsByMessageId(messageId: string, userId: string, reason: string, action: string): Promise<UpdateResult | Document> {
|
||||
const query = {
|
||||
'message._id': messageId,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
_hidden: true,
|
||||
moderationInfo: { hiddenAt: new Date(), moderatedBy: userId, reason, action },
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
async hideMessageReportsByUserId(userId: string, moderatorId: string, reason: string, action: string): Promise<UpdateResult | Document> {
|
||||
const query = {
|
||||
'message.u._id': userId,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
_hidden: true,
|
||||
moderationInfo: { hiddenAt: new Date(), moderatedBy: moderatorId, reason, action },
|
||||
},
|
||||
};
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
async hideUserReportsByUserId(userId: string, moderatorId: string, reason: string, action: string): Promise<UpdateResult | Document> {
|
||||
const query = {
|
||||
'reportedUser._id': userId,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
_hidden: true,
|
||||
moderationInfo: { hiddenAt: new Date(), moderatedBy: moderatorId, reason, action },
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
private getSearchQueryForSelector(selector?: string): any {
|
||||
const messageExistsQuery = { message: { $exists: true } };
|
||||
if (!selector) {
|
||||
return messageExistsQuery;
|
||||
}
|
||||
return {
|
||||
...messageExistsQuery,
|
||||
$or: [
|
||||
{
|
||||
'message.msg': {
|
||||
$regex: selector,
|
||||
$options: 'i',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: {
|
||||
$regex: selector,
|
||||
$options: 'i',
|
||||
},
|
||||
},
|
||||
{
|
||||
'message.u.username': {
|
||||
$regex: selector,
|
||||
$options: 'i',
|
||||
},
|
||||
},
|
||||
{
|
||||
'message.u.name': {
|
||||
$regex: selector,
|
||||
$options: 'i',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private getSearchQueryForSelectorUsers(selector?: string): any {
|
||||
const messageAbsentQuery = { message: { $exists: false } };
|
||||
if (!selector) {
|
||||
return messageAbsentQuery;
|
||||
}
|
||||
return {
|
||||
...messageAbsentQuery,
|
||||
$or: [
|
||||
{
|
||||
'reportedUser.username': {
|
||||
$regex: selector,
|
||||
$options: 'i',
|
||||
},
|
||||
},
|
||||
{
|
||||
'reportedUser.name': {
|
||||
$regex: selector,
|
||||
$options: 'i',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
102
packages/models/src/models/NotificationQueue.ts
Normal file
102
packages/models/src/models/NotificationQueue.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { INotification, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { INotificationQueueModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, Document, IndexDescription, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class NotificationQueueRaw extends BaseRaw<INotification> implements INotificationQueueModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<INotification>>) {
|
||||
super(db, 'notification_queue', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{ key: { uid: 1 } },
|
||||
{ key: { ts: 1 }, expireAfterSeconds: 2 * 60 * 60 },
|
||||
{ key: { schedule: 1 }, sparse: true },
|
||||
{ key: { sending: 1 }, sparse: true },
|
||||
{ key: { error: 1 }, sparse: true },
|
||||
];
|
||||
}
|
||||
|
||||
unsetSendingById(_id: string): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{ _id },
|
||||
{
|
||||
$unset: {
|
||||
sending: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setErrorById(_id: string, error: any): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
error,
|
||||
},
|
||||
$unset: {
|
||||
sending: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
clearScheduleByUserId(uid: string): Promise<UpdateResult | Document> {
|
||||
return this.updateMany(
|
||||
{
|
||||
uid,
|
||||
schedule: { $exists: true },
|
||||
},
|
||||
{
|
||||
$unset: {
|
||||
schedule: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async clearQueueByUserId(uid: string): Promise<number | undefined> {
|
||||
const op = await this.deleteMany({
|
||||
uid,
|
||||
});
|
||||
|
||||
return op.deletedCount;
|
||||
}
|
||||
|
||||
async findNextInQueueOrExpired(expired: Date): Promise<INotification | null> {
|
||||
const now = new Date();
|
||||
|
||||
const result = await this.findOneAndUpdate(
|
||||
{
|
||||
$and: [
|
||||
{
|
||||
$or: [{ sending: { $exists: false } }, { sending: { $lte: expired } }],
|
||||
},
|
||||
{
|
||||
$or: [{ schedule: { $exists: false } }, { schedule: { $lte: now } }],
|
||||
},
|
||||
{
|
||||
error: { $exists: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
sending: now,
|
||||
},
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
ts: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
98
packages/models/src/models/Nps.ts
Normal file
98
packages/models/src/models/Nps.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { INps, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import { NPSStatus } from '@rocket.chat/core-typings';
|
||||
import type { INpsModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, Document, IndexDescription, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class NpsRaw extends BaseRaw<INps> implements INpsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<INps>>) {
|
||||
super(db, 'nps', trash);
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { status: 1, expireAt: 1 } }];
|
||||
}
|
||||
|
||||
// get expired surveys still in progress
|
||||
async getOpenExpiredAndStartSending(): Promise<INps | null> {
|
||||
const today = new Date();
|
||||
|
||||
const query = {
|
||||
status: NPSStatus.OPEN,
|
||||
expireAt: { $lte: today },
|
||||
};
|
||||
const update = {
|
||||
$set: {
|
||||
status: NPSStatus.SENDING,
|
||||
},
|
||||
};
|
||||
const value = await this.findOneAndUpdate(query, update, { sort: { expireAt: 1 } });
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// get expired surveys already sending results
|
||||
async getOpenExpiredAlreadySending(): Promise<INps | null> {
|
||||
const today = new Date();
|
||||
|
||||
const query = {
|
||||
status: NPSStatus.SENDING,
|
||||
expireAt: { $lte: today },
|
||||
};
|
||||
|
||||
return this.col.findOne(query);
|
||||
}
|
||||
|
||||
updateStatusById(_id: INps['_id'], status: INps['status']): Promise<UpdateResult> {
|
||||
const update = {
|
||||
$set: {
|
||||
status,
|
||||
},
|
||||
};
|
||||
return this.col.updateOne({ _id }, update);
|
||||
}
|
||||
|
||||
save({
|
||||
_id,
|
||||
startAt,
|
||||
expireAt,
|
||||
createdBy,
|
||||
status,
|
||||
}: Pick<INps, '_id' | 'startAt' | 'expireAt' | 'createdBy' | 'status'>): Promise<UpdateResult> {
|
||||
return this.col.updateOne(
|
||||
{
|
||||
_id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
startAt,
|
||||
_updatedAt: new Date(),
|
||||
},
|
||||
$setOnInsert: {
|
||||
expireAt,
|
||||
createdBy,
|
||||
createdAt: new Date(),
|
||||
status,
|
||||
},
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
closeAllByStatus(status: NPSStatus): Promise<UpdateResult | Document> {
|
||||
const query = {
|
||||
status,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
status: NPSStatus.CLOSED,
|
||||
},
|
||||
};
|
||||
|
||||
return this.col.updateMany(query, update);
|
||||
}
|
||||
}
|
||||
125
packages/models/src/models/NpsVote.ts
Normal file
125
packages/models/src/models/NpsVote.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import type { INpsVote, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import { INpsVoteStatus } from '@rocket.chat/core-typings';
|
||||
import type { INpsVoteModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, FindCursor, Db, Document, IndexDescription, UpdateResult } from 'mongodb';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class NpsVoteRaw extends BaseRaw<INpsVote> implements INpsVoteModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<INpsVote>>) {
|
||||
super(db, 'nps_vote', trash);
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { npsId: 1, status: 1, sentAt: 1 } }, { key: { npsId: 1, identifier: 1 }, unique: true }];
|
||||
}
|
||||
|
||||
// `sort` and `limit` are branded away because the cursor below overwrites both; a caller passing
|
||||
// them would have them silently dropped
|
||||
findNotSentByNpsId<T extends Document = INpsVote, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
npsId: string,
|
||||
options?: O & { sort?: never; limit?: never },
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
npsId,
|
||||
status: INpsVoteStatus.NEW,
|
||||
};
|
||||
const cursor = options ? this.find<T, O>(query, options) : this.find<T, O>(query);
|
||||
|
||||
return cursor.sort({ ts: 1 }).limit(1000);
|
||||
}
|
||||
|
||||
findByNpsIdAndStatus<T extends Document = INpsVote, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
npsId: string,
|
||||
status: INpsVoteStatus,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
npsId,
|
||||
status,
|
||||
};
|
||||
if (options) {
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
return this.find<T, O>(query);
|
||||
}
|
||||
|
||||
findByNpsId<T extends Document = INpsVote, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
npsId: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
npsId,
|
||||
};
|
||||
if (options) {
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
return this.find<T, O>(query);
|
||||
}
|
||||
|
||||
save(vote: Omit<INpsVote, '_id' | '_updatedAt'>): Promise<UpdateResult> {
|
||||
const { npsId, identifier } = vote;
|
||||
|
||||
const query = {
|
||||
npsId,
|
||||
identifier,
|
||||
};
|
||||
const update = {
|
||||
$set: {
|
||||
...vote,
|
||||
_updatedAt: new Date(),
|
||||
},
|
||||
$setOnInsert: {
|
||||
_id: new ObjectId().toHexString(), // TODO this should be done by BaseRaw
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update, { upsert: true });
|
||||
}
|
||||
|
||||
updateVotesToSent(voteIds: string[]): Promise<UpdateResult | Document> {
|
||||
const query = {
|
||||
_id: { $in: voteIds },
|
||||
};
|
||||
const update = {
|
||||
$set: {
|
||||
status: INpsVoteStatus.SENT,
|
||||
},
|
||||
};
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
updateOldSendingToNewByNpsId(npsId: string): Promise<UpdateResult | Document> {
|
||||
const fiveMinutes = new Date();
|
||||
fiveMinutes.setMinutes(fiveMinutes.getMinutes() - 5);
|
||||
|
||||
const query = {
|
||||
npsId,
|
||||
status: INpsVoteStatus.SENDING,
|
||||
sentAt: { $lt: fiveMinutes },
|
||||
};
|
||||
const update = {
|
||||
$set: {
|
||||
status: INpsVoteStatus.NEW,
|
||||
},
|
||||
$unset: {
|
||||
sentAt: 1 as const, // why do you do this to me TypeScript?
|
||||
},
|
||||
};
|
||||
return this.updateMany(query, update);
|
||||
}
|
||||
|
||||
countByNpsId(npsId: string): Promise<number> {
|
||||
return this.countDocuments({ npsId });
|
||||
}
|
||||
|
||||
countByNpsIdAndStatus(npsId: string, status: INpsVoteStatus): Promise<number> {
|
||||
const query = {
|
||||
npsId,
|
||||
status,
|
||||
};
|
||||
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
}
|
||||
49
packages/models/src/models/OAuthAccessTokens.ts
Normal file
49
packages/models/src/models/OAuthAccessTokens.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { IOAuthAccessToken, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IOAuthAccessTokensModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Db, Collection, DeleteResult, IndexDescription, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class OAuthAccessTokensRaw extends BaseRaw<IOAuthAccessToken> implements IOAuthAccessTokensModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IOAuthAccessToken>>) {
|
||||
super(db, 'oauth_access_tokens', trash);
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{ key: { accessToken: 1 } },
|
||||
{ key: { refreshToken: 1 } },
|
||||
{ key: { userId: 1 } },
|
||||
{ key: { expires: 1 }, expireAfterSeconds: 60 * 60 * 24 * 30 },
|
||||
{ key: { refreshTokenExpiresAt: 1 }, expireAfterSeconds: 60 * 60 * 24 * 30 },
|
||||
];
|
||||
}
|
||||
|
||||
async findOneByAccessToken<T extends Document = IOAuthAccessToken, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
accessToken: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
if (typeof accessToken !== 'string' || !accessToken) {
|
||||
return null;
|
||||
}
|
||||
return this.findOne<T, O>({ accessToken }, options);
|
||||
}
|
||||
|
||||
async findOneByRefreshToken<
|
||||
T extends Document = IOAuthAccessToken,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(refreshToken: string, options?: O): Promise<DocumentWithProjection<T, O> | null> {
|
||||
if (typeof refreshToken !== 'string' || !refreshToken) {
|
||||
return null;
|
||||
}
|
||||
return this.findOne<T, O>({ refreshToken }, options);
|
||||
}
|
||||
|
||||
async deleteByUserId(userId: string): Promise<DeleteResult> {
|
||||
return this.deleteMany({ userId });
|
||||
}
|
||||
|
||||
async deleteByUserIds(userIds: string[]): Promise<DeleteResult> {
|
||||
return this.deleteMany({ userId: { $in: userIds } });
|
||||
}
|
||||
}
|
||||
69
packages/models/src/models/OAuthApps.ts
Normal file
69
packages/models/src/models/OAuthApps.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { IOAuthApps, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IOAuthAppsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Db, Collection, IndexDescription, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class OAuthAppsRaw extends BaseRaw<IOAuthApps> implements IOAuthAppsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IOAuthApps>>) {
|
||||
super(db, 'oauth_apps', trash);
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { clientId: 1, clientSecret: 1 } }, { key: { appId: 1 } }];
|
||||
}
|
||||
|
||||
findOneAuthAppByIdOrClientId<T extends Document = IOAuthApps, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
props: { clientId: string } | { appId: string } | { _id: string },
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>(
|
||||
{
|
||||
...('_id' in props && { _id: props._id }),
|
||||
...('appId' in props && { _id: props.appId }),
|
||||
...('clientId' in props && { clientId: props.clientId }),
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
findOneActiveByClientId<T extends Document = IOAuthApps, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
clientId: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
if (typeof clientId !== 'string' || !clientId) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.findOne<T, O>(
|
||||
{
|
||||
active: true,
|
||||
clientId,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
updateById(
|
||||
_id: IOAuthApps['_id'],
|
||||
data: Partial<Pick<IOAuthApps, 'name' | 'active' | 'redirectUri' | '_updatedBy'>>,
|
||||
): Promise<IOAuthApps | null> {
|
||||
return this.findOneAndUpdate({ _id }, { $set: data }, { returnDocument: 'after' });
|
||||
}
|
||||
|
||||
findOneActiveByClientIdAndClientSecret<
|
||||
T extends Document = IOAuthApps,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(clientId: string, clientSecret: string, options?: O): Promise<DocumentWithProjection<T, O> | null> {
|
||||
if (typeof clientId !== 'string' || !clientId || typeof clientSecret !== 'string' || !clientSecret) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.findOne<T, O>(
|
||||
{
|
||||
active: true,
|
||||
clientId,
|
||||
clientSecret,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
33
packages/models/src/models/OAuthAuthCodes.ts
Normal file
33
packages/models/src/models/OAuthAuthCodes.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { IOAuthAuthCode, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IOAuthAuthCodesModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Db, Collection, DeleteResult, IndexDescription, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class OAuthAuthCodesRaw extends BaseRaw<IOAuthAuthCode> implements IOAuthAuthCodesModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IOAuthAuthCode>>) {
|
||||
super(db, 'oauth_auth_codes', trash);
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { authCode: 1 } }, { key: { userId: 1 } }, { key: { expires: 1 }, expireAfterSeconds: 60 * 5 }];
|
||||
}
|
||||
|
||||
findOneByAuthCode<T extends Document = IOAuthAuthCode, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
authCode: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
if (typeof authCode !== 'string' || !authCode) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.findOne<T, O>({ authCode }, options);
|
||||
}
|
||||
|
||||
async deleteByUserId(userId: string): Promise<DeleteResult> {
|
||||
return this.deleteMany({ userId });
|
||||
}
|
||||
|
||||
async deleteByUserIds(userIds: string[]): Promise<DeleteResult> {
|
||||
return this.deleteMany({ userId: { $in: userIds } });
|
||||
}
|
||||
}
|
||||
33
packages/models/src/models/OAuthRefreshTokens.ts
Normal file
33
packages/models/src/models/OAuthRefreshTokens.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { IOAuthRefreshToken, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IOAuthRefreshTokensModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Db, Collection, DeleteResult, IndexDescription, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class OAuthRefreshTokensRaw extends BaseRaw<IOAuthRefreshToken> implements IOAuthRefreshTokensModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IOAuthRefreshToken>>) {
|
||||
super(db, 'oauth_refresh_tokens', trash);
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { refreshToken: 1 } }, { key: { userId: 1 } }, { key: { expires: 1 }, expireAfterSeconds: 60 * 60 * 24 * 30 }];
|
||||
}
|
||||
|
||||
findOneByRefreshToken<T extends Document = IOAuthRefreshToken, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
refreshToken: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
if (typeof refreshToken !== 'string' || !refreshToken) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.findOne<T, O>({ refreshToken }, options);
|
||||
}
|
||||
|
||||
async deleteByUserId(userId: string): Promise<DeleteResult> {
|
||||
return this.deleteMany({ userId });
|
||||
}
|
||||
|
||||
async deleteByUserIds(userIds: string[]): Promise<DeleteResult> {
|
||||
return this.deleteMany({ userId: { $in: userIds } });
|
||||
}
|
||||
}
|
||||
35
packages/models/src/models/OEmbedCache.ts
Normal file
35
packages/models/src/models/OEmbedCache.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { IOEmbedCache, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IOEmbedCacheModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, DeleteResult, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class OEmbedCacheRaw extends BaseRaw<IOEmbedCache> implements IOEmbedCacheModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IOEmbedCache>>) {
|
||||
super(db, 'oembed_cache', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { updatedAt: 1 } }];
|
||||
}
|
||||
|
||||
async createWithIdAndData(_id: string, data: any): Promise<IOEmbedCache> {
|
||||
const record = {
|
||||
_id,
|
||||
data,
|
||||
updatedAt: new Date(),
|
||||
_updatedAt: new Date(), // TODO: insertOne inserts the field `_updatedAt` synchronously but it's not guaranteed
|
||||
};
|
||||
record._id = (await this.insertOne(record)).insertedId;
|
||||
return record;
|
||||
}
|
||||
|
||||
removeBeforeDate(date: Date): Promise<DeleteResult> {
|
||||
const query = {
|
||||
updatedAt: {
|
||||
$lte: date,
|
||||
},
|
||||
};
|
||||
return this.deleteMany(query);
|
||||
}
|
||||
}
|
||||
67
packages/models/src/models/Permissions.ts
Normal file
67
packages/models/src/models/Permissions.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { IPermission, IRole, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IPermissionsModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, FindCursor, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class PermissionsRaw extends BaseRaw<IPermission> implements IPermissionsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IPermission>>) {
|
||||
super(db, 'permissions', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{
|
||||
key: {
|
||||
level: 1,
|
||||
settingId: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async createOrUpdate(name: string, roles: IRole['_id'][]): Promise<IPermission['_id']> {
|
||||
const exists = await this.findOne<Pick<IPermission, '_id'>>(
|
||||
{
|
||||
_id: name,
|
||||
roles,
|
||||
},
|
||||
{ projection: { _id: 1 } },
|
||||
);
|
||||
|
||||
if (exists) {
|
||||
return exists._id;
|
||||
}
|
||||
|
||||
await this.updateOne({ _id: name }, { $set: { roles } }, { upsert: true });
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
async create(id: string, roles: IRole['_id'][]): Promise<IPermission['_id']> {
|
||||
const exists = await this.findOneById<Pick<IPermission, '_id'>>(id, { projection: { _id: 1 } });
|
||||
if (exists) {
|
||||
return exists._id;
|
||||
}
|
||||
|
||||
await this.updateOne({ _id: id }, { $set: { roles } }, { upsert: true });
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async addRole(permission: string, role: IRole['_id']): Promise<void> {
|
||||
await this.updateOne({ _id: permission, roles: { $ne: role } }, { $addToSet: { roles: role } });
|
||||
}
|
||||
|
||||
async setRoles(permission: string, roles: IRole['_id'][]): Promise<void> {
|
||||
await this.updateOne({ _id: permission }, { $set: { roles } });
|
||||
}
|
||||
|
||||
async removeRole(permission: string, role: IRole['_id']): Promise<void> {
|
||||
await this.updateOne({ _id: permission, roles: role }, { $pull: { roles: role } });
|
||||
}
|
||||
|
||||
findByLevel(level: 'settings', settingId?: string): FindCursor<IPermission> {
|
||||
return this.find({ level, ...(settingId && { settingId }) });
|
||||
}
|
||||
}
|
||||
182
packages/models/src/models/PushToken.ts
Normal file
182
packages/models/src/models/PushToken.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import type { IPushToken, IUser, AtLeast } from '@rocket.chat/core-typings';
|
||||
import type { IPushTokenModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Db, DeleteResult, IndexDescription, InsertOneResult, UpdateResult, FindCursor, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class PushTokenRaw extends BaseRaw<IPushToken> implements IPushTokenModel {
|
||||
constructor(db: Db) {
|
||||
super(db, '_raix_push_app_tokens', undefined, {
|
||||
collectionNameResolver(name) {
|
||||
return name;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { userId: 1, authToken: 1 } }, { key: { appName: 1, token: 1 } }];
|
||||
}
|
||||
|
||||
countApnTokens() {
|
||||
const query = {
|
||||
'token.apn': { $exists: true },
|
||||
};
|
||||
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
|
||||
countGcmTokens() {
|
||||
const query = {
|
||||
'token.gcm': { $exists: true },
|
||||
};
|
||||
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
|
||||
countTokensByUserId(userId: IUser['_id']) {
|
||||
const query = {
|
||||
userId,
|
||||
$or: [{ 'token.apn': { $exists: true } }, { 'token.gcm': { $exists: true } }],
|
||||
};
|
||||
|
||||
return this.countDocuments(query);
|
||||
}
|
||||
|
||||
async findFirstByUserId<T extends Document = IPushToken, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
userId: IUser['_id'],
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>({ userId }, options);
|
||||
}
|
||||
|
||||
findAllTokensByUserId<T extends Document = IPushToken, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
userId: IUser['_id'],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>(
|
||||
{
|
||||
userId,
|
||||
$or: [{ 'token.apn': { $exists: true } }, { 'token.gcm': { $exists: true } }],
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
findTokensByUserIdExceptId<T extends Document = IPushToken, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
userId: IUser['_id'],
|
||||
idToIgnore: IPushToken['_id'],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>(
|
||||
{
|
||||
_id: { $ne: idToIgnore },
|
||||
userId,
|
||||
$or: [{ 'token.apn': { $exists: true } }, { 'token.gcm': { $exists: true } }],
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
async insertToken(data: AtLeast<IPushToken, 'token' | 'authToken' | 'appName' | 'userId'>): Promise<InsertOneResult<IPushToken>> {
|
||||
return this.insertOne({
|
||||
enabled: true,
|
||||
createdAt: new Date(),
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
async refreshTokenById(
|
||||
id: IPushToken['_id'],
|
||||
data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId' | 'voipToken'>,
|
||||
): Promise<UpdateResult<IPushToken>> {
|
||||
return this.updateOne(
|
||||
{ _id: id },
|
||||
{
|
||||
$set: {
|
||||
token: data.token,
|
||||
authToken: data.authToken,
|
||||
appName: data.appName,
|
||||
userId: data.userId,
|
||||
...(data.voipToken && { voipToken: data.voipToken }),
|
||||
},
|
||||
...(!data.voipToken && { $unset: { voipToken: 1 } }),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
findOneByTokenAndAppName(token: IPushToken['token'], appName: IPushToken['appName']): Promise<IPushToken | null> {
|
||||
return this.findOne({
|
||||
token,
|
||||
appName,
|
||||
});
|
||||
}
|
||||
|
||||
removeByUserIdExceptTokens(userId: string, tokens: IPushToken['authToken'][]): Promise<DeleteResult> {
|
||||
return this.deleteMany({
|
||||
userId,
|
||||
authToken: { $nin: tokens },
|
||||
});
|
||||
}
|
||||
|
||||
removeDuplicateTokens(tokenData: Pick<IPushToken, '_id' | 'token' | 'appName' | 'authToken'>): Promise<DeleteResult> {
|
||||
return this.deleteMany({
|
||||
_id: { $ne: tokenData._id },
|
||||
$or: [
|
||||
{
|
||||
token: tokenData.token,
|
||||
appName: tokenData.appName,
|
||||
},
|
||||
{
|
||||
authToken: tokenData.authToken,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
removeAllByUserId(userId: string): Promise<DeleteResult> {
|
||||
return this.deleteMany({
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
removeAllByTokenStringAndUserId(token: string, userId: string): Promise<DeleteResult> {
|
||||
return this.deleteMany({
|
||||
$or: [
|
||||
{
|
||||
'token.apn': token,
|
||||
},
|
||||
{
|
||||
'token.gcm': token,
|
||||
},
|
||||
{
|
||||
voipToken: token,
|
||||
},
|
||||
],
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
async removeOrUnsetByTokenString(token: string): Promise<void> {
|
||||
await this.deleteMany({
|
||||
$or: [
|
||||
{
|
||||
'token.apn': token,
|
||||
},
|
||||
{
|
||||
'token.gcm': token,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await this.updateMany(
|
||||
{
|
||||
voipToken: token,
|
||||
},
|
||||
{
|
||||
$unset: {
|
||||
voipToken: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
21
packages/models/src/models/Reports.ts
Normal file
21
packages/models/src/models/Reports.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { IMessage, IReport, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IReportsModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, Collection } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class ReportsRaw extends BaseRaw<IReport> implements IReportsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IReport>>) {
|
||||
super(db, 'reports', trash);
|
||||
}
|
||||
|
||||
createWithMessageDescriptionAndUserId(message: IMessage, description: string, userId: string): ReturnType<BaseRaw<IReport>['insertOne']> {
|
||||
const record: Pick<IReport, 'message' | 'description' | 'ts' | 'userId'> = {
|
||||
message,
|
||||
description,
|
||||
ts: new Date(),
|
||||
userId,
|
||||
};
|
||||
return this.insertOne(record);
|
||||
}
|
||||
}
|
||||
268
packages/models/src/models/Roles.ts
Normal file
268
packages/models/src/models/Roles.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import type { IRole, IRoom, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IRolesModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, FindCursor, Db, Filter, FindOptions, Document, CountDocumentsOptions } from 'mongodb';
|
||||
|
||||
import { Subscriptions, Users } from '../index';
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class RolesRaw extends BaseRaw<IRole> implements IRolesModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IRole>>) {
|
||||
super(db, 'roles', trash);
|
||||
}
|
||||
|
||||
findByUpdatedDate<T extends Document = IRole, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
updatedAfterDate: Date,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
_updatedAt: { $gte: new Date(updatedAfterDate) },
|
||||
};
|
||||
|
||||
return options ? this.find<T, O>(query, options) : this.find<T, O>(query);
|
||||
}
|
||||
|
||||
async isUserInRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: IRoom['_id']): Promise<boolean> {
|
||||
if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) {
|
||||
throw new Error('Roles.isUserInRoles method received a role scope instead of a scope value.');
|
||||
}
|
||||
|
||||
for (const roleId of roles) {
|
||||
const role = await this.findOneById<Pick<IRole, '_id' | 'scope'>>(roleId, { projection: { scope: 1 } });
|
||||
|
||||
if (!role) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (role.scope) {
|
||||
case 'Subscriptions':
|
||||
if (await Subscriptions.isUserInRole(userId, roleId, scope)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'Users':
|
||||
default:
|
||||
if (await Users.isUserInRole(userId, roleId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
findOneByIdOrName<P extends Document = IRole, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
_idOrName: IRole['_id'],
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<P, O> | null> {
|
||||
const query: Filter<IRole> = {
|
||||
$or: [
|
||||
{
|
||||
_id: _idOrName,
|
||||
},
|
||||
{
|
||||
name: _idOrName,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return this.findOne<P, O>(query, options);
|
||||
}
|
||||
|
||||
async findOneByName<P = IRole>(name: IRole['name'], options?: any): Promise<IRole | P | null> {
|
||||
const query: Filter<IRole> = {
|
||||
name,
|
||||
};
|
||||
|
||||
return this.findOne<IRole>(query, options);
|
||||
}
|
||||
|
||||
findInIds<P>(ids: IRole['_id'][], options?: FindOptions<IRole>): P extends Pick<IRole, '_id'> ? FindCursor<P> : FindCursor<IRole> {
|
||||
const query: Filter<IRole> = {
|
||||
_id: {
|
||||
$in: ids,
|
||||
},
|
||||
};
|
||||
|
||||
return this.find(query, options || {}) as P extends Pick<IRole, '_id'> ? FindCursor<P> : FindCursor<IRole>;
|
||||
}
|
||||
|
||||
findInIdsOrNames<P>(
|
||||
_idsOrNames: IRole['_id'][],
|
||||
options?: FindOptions<IRole>,
|
||||
): P extends Pick<IRole, '_id'> ? FindCursor<P> : FindCursor<IRole> {
|
||||
const query: Filter<IRole> = {
|
||||
$or: [
|
||||
{
|
||||
_id: {
|
||||
$in: _idsOrNames,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
$in: _idsOrNames,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return this.find(query, options || {}) as P extends Pick<IRole, '_id'> ? FindCursor<P> : FindCursor<IRole>;
|
||||
}
|
||||
|
||||
findByScope<T extends Document = IRole, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
scope: IRole['scope'],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = {
|
||||
scope,
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
countByScope(scope: IRole['scope'], options?: CountDocumentsOptions): Promise<number> {
|
||||
const query = {
|
||||
scope,
|
||||
};
|
||||
|
||||
return this.countDocuments(query, options);
|
||||
}
|
||||
|
||||
findCustomRoles<T extends Document = IRole, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query: Filter<IRole> = {
|
||||
protected: false,
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
countCustomRoles(options?: CountDocumentsOptions): Promise<number> {
|
||||
const query: Filter<IRole> = {
|
||||
protected: false,
|
||||
};
|
||||
|
||||
return this.countDocuments(query, options || {});
|
||||
}
|
||||
|
||||
async updateById(
|
||||
_id: IRole['_id'],
|
||||
name: IRole['name'],
|
||||
scope: IRole['scope'],
|
||||
description: IRole['description'] = '',
|
||||
mandatory2fa: IRole['mandatory2fa'] = false,
|
||||
): Promise<IRole> {
|
||||
const response = await this.findOneAndUpdate(
|
||||
{ _id },
|
||||
{
|
||||
$set: {
|
||||
name,
|
||||
scope,
|
||||
description,
|
||||
mandatory2fa,
|
||||
},
|
||||
},
|
||||
{ upsert: true, returnDocument: 'after' },
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
console.log('Role not found', _id, name);
|
||||
throw new Error('Role not found');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
findUsersInRole(roleId: IRole['_id'], scope?: IRoom['_id']): Promise<FindCursor<IUser>>;
|
||||
|
||||
findUsersInRole(roleId: IRole['_id'], scope: IRoom['_id'] | undefined, options: FindOptions<IUser>): Promise<FindCursor<IUser>>;
|
||||
|
||||
findUsersInRole<P extends Document>(
|
||||
roleId: IRole['_id'],
|
||||
scope: IRoom['_id'] | undefined,
|
||||
options: FindOptions<P extends IUser ? IUser : P>,
|
||||
): Promise<FindCursor<P extends IUser ? IUser : P>>;
|
||||
|
||||
/** @deprecated function getUsersInRole should be used instead */
|
||||
async findUsersInRole<P extends Document>(
|
||||
roleId: IRole['_id'],
|
||||
scope: IRoom['_id'] | undefined,
|
||||
options?: any,
|
||||
): Promise<FindCursor<IUser | P>> {
|
||||
if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) {
|
||||
throw new Error('Roles.findUsersInRole method received a role scope instead of a scope value.');
|
||||
}
|
||||
|
||||
const role = await this.findOneById<Pick<IRole, '_id' | 'scope'>>(roleId, { projection: { scope: 1 } });
|
||||
|
||||
if (!role) {
|
||||
throw new Error('RolesRaw.findUsersInRole: role not found');
|
||||
}
|
||||
|
||||
switch (role.scope) {
|
||||
case 'Subscriptions':
|
||||
return Subscriptions.findUsersInRoles([role._id], scope, options);
|
||||
case 'Users':
|
||||
default:
|
||||
return Users.findUsersInRoles<IUser | P>([role._id], null, options);
|
||||
}
|
||||
}
|
||||
|
||||
async countUsersInRole(roleId: IRole['_id'], scope?: IRoom['_id']): Promise<number> {
|
||||
const role = await this.findOneById<Pick<IRole, '_id' | 'scope'>>(roleId, { projection: { scope: 1 } });
|
||||
|
||||
if (!role) {
|
||||
throw new Error('RolesRaw.countUsersInRole: role not found');
|
||||
}
|
||||
|
||||
switch (role.scope) {
|
||||
case 'Subscriptions':
|
||||
return Subscriptions.countUsersInRoles([role._id], scope);
|
||||
case 'Users':
|
||||
default:
|
||||
return Users.countUsersInRoles([role._id]);
|
||||
}
|
||||
}
|
||||
|
||||
async createWithRandomId(
|
||||
name: IRole['name'],
|
||||
scope: IRole['scope'] = 'Users',
|
||||
description = '',
|
||||
protectedRole = true,
|
||||
mandatory2fa = false,
|
||||
): Promise<IRole> {
|
||||
const role = {
|
||||
name,
|
||||
scope,
|
||||
description,
|
||||
protected: protectedRole,
|
||||
mandatory2fa,
|
||||
};
|
||||
|
||||
const res = await this.insertOne(role);
|
||||
|
||||
return {
|
||||
_id: res.insertedId,
|
||||
_updatedAt: new Date(), // TODO: this should be set by the BaseRaw, but we need to set it here to return a complete IRole object
|
||||
...role,
|
||||
};
|
||||
}
|
||||
|
||||
async canAddUserToRole(uid: IUser['_id'], roleId: IRole['_id'], scope?: IRoom['_id']): Promise<boolean> {
|
||||
if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) {
|
||||
throw new Error('Roles.canAddUserToRole method received a role scope instead of a scope value.');
|
||||
}
|
||||
|
||||
const role = await this.findOne({ _id: roleId }, { projection: { scope: 1 } });
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (role.scope) {
|
||||
case 'Subscriptions':
|
||||
return Subscriptions.isUserInRoleScope(uid, scope);
|
||||
case 'Users':
|
||||
default:
|
||||
return Users.isUserInRoleScope(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
2272
packages/models/src/models/Rooms.ts
Normal file
2272
packages/models/src/models/Rooms.ts
Normal file
File diff suppressed because it is too large
Load Diff
37
packages/models/src/models/SamlUsedAssertions.ts
Normal file
37
packages/models/src/models/SamlUsedAssertions.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import type { ISamlUsedAssertions, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ISamlUsedAssertionsModel } from '@rocket.chat/model-typings';
|
||||
import type { MongoServerError, Collection, Db, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
const DUPLICATE_KEY_ERROR_CODE = 11000;
|
||||
|
||||
export class SamlUsedAssertionsRaw extends BaseRaw<ISamlUsedAssertions> implements ISamlUsedAssertionsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ISamlUsedAssertions>>) {
|
||||
super(db, 'saml_used_assertions', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { expireAt: 1 }, sparse: true, expireAfterSeconds: 0 }];
|
||||
}
|
||||
|
||||
async markUsed(assertionId: string, issuer: string, expireAt: Date): Promise<boolean> {
|
||||
try {
|
||||
await this.insertOne({
|
||||
_id: crypto.createHash('sha256').update(JSON.stringify({ issuer, assertionId })).digest('hex'),
|
||||
assertionId,
|
||||
issuer,
|
||||
expireAt,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
if (typeof error === 'object' && error !== null && (error as MongoServerError).code === DUPLICATE_KEY_ERROR_CODE) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
packages/models/src/models/ServerEvents.ts
Normal file
98
packages/models/src/models/ServerEvents.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type {
|
||||
ExtractDataToParams,
|
||||
IAuditServerActor,
|
||||
IServerEvent,
|
||||
IServerEvents,
|
||||
RocketChatRecordDeleted,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import { ServerEventType } from '@rocket.chat/core-typings';
|
||||
import type { IServerEventsModel } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class ServerEventsRaw extends BaseRaw<IServerEvent> implements IServerEventsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IServerEvent>>) {
|
||||
super(db, 'server_events', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { t: 1, ip: 1, ts: -1 } }, { key: { 't': 1, 'u.username': 1, 'ts': -1 } }];
|
||||
}
|
||||
|
||||
async findLastFailedAttemptByIp(ip: string): Promise<IServerEvent | null> {
|
||||
return this.findOne<IServerEvent>(
|
||||
{
|
||||
ip,
|
||||
t: ServerEventType.FAILED_LOGIN_ATTEMPT,
|
||||
},
|
||||
{ sort: { ts: -1 } },
|
||||
);
|
||||
}
|
||||
|
||||
async findLastFailedAttemptByUsername(username: string): Promise<IServerEvent | null> {
|
||||
return this.findOne<IServerEvent>(
|
||||
{
|
||||
'u.username': username,
|
||||
't': ServerEventType.FAILED_LOGIN_ATTEMPT,
|
||||
},
|
||||
{ sort: { ts: -1 } },
|
||||
);
|
||||
}
|
||||
|
||||
async findLastSuccessfulAttemptByIp(ip: string): Promise<IServerEvent | null> {
|
||||
return this.findOne<IServerEvent>(
|
||||
{
|
||||
ip,
|
||||
t: ServerEventType.LOGIN,
|
||||
},
|
||||
{ sort: { ts: -1 } },
|
||||
);
|
||||
}
|
||||
|
||||
async findLastSuccessfulAttemptByUsername(username: string): Promise<IServerEvent | null> {
|
||||
return this.findOne<IServerEvent>(
|
||||
{
|
||||
'u.username': username,
|
||||
't': ServerEventType.LOGIN,
|
||||
},
|
||||
{ sort: { ts: -1 } },
|
||||
);
|
||||
}
|
||||
|
||||
async countFailedAttemptsByUsernameSince(username: string, since: Date): Promise<number> {
|
||||
return this.countDocuments({
|
||||
'u.username': username,
|
||||
't': ServerEventType.FAILED_LOGIN_ATTEMPT,
|
||||
'ts': {
|
||||
$gte: since,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
countFailedAttemptsByIpSince(ip: string, since: Date): Promise<number> {
|
||||
return this.countDocuments({
|
||||
ip,
|
||||
t: ServerEventType.FAILED_LOGIN_ATTEMPT,
|
||||
ts: {
|
||||
$gte: since,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createAuditServerEvent<K extends keyof IServerEvents, E extends IServerEvents[K]>(
|
||||
key: K,
|
||||
data: ExtractDataToParams<E>,
|
||||
actor: IAuditServerActor,
|
||||
): Promise<void> {
|
||||
await this.insertOne({
|
||||
t: key,
|
||||
ts: new Date(),
|
||||
actor,
|
||||
data: Object.entries(data).map(([key, value]) => ({ key, value })),
|
||||
// deprecated just to keep backward compatibility
|
||||
ip: '0.0.0.0',
|
||||
...(actor.type === 'user' && { ip: actor?.ip || '0.0.0.0', u: { _id: actor._id, username: actor.username } }),
|
||||
});
|
||||
}
|
||||
}
|
||||
1580
packages/models/src/models/Sessions.ts
Normal file
1580
packages/models/src/models/Sessions.ts
Normal file
File diff suppressed because it is too large
Load Diff
256
packages/models/src/models/Settings.ts
Normal file
256
packages/models/src/models/Settings.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import type { ISetting, ISettingColor, ISettingSelectOption, RocketChatRecordDeleted, SettingValue } from '@rocket.chat/core-typings';
|
||||
import type { ISettingsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type {
|
||||
Collection,
|
||||
FindCursor,
|
||||
Db,
|
||||
Filter,
|
||||
UpdateFilter,
|
||||
UpdateResult,
|
||||
Document,
|
||||
FindOneAndUpdateOptions,
|
||||
WithId,
|
||||
UpdateOptions,
|
||||
} from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
type PublicSettingFields<T extends ISetting> = T extends ISettingColor
|
||||
? Pick<T, '_id' | 'value' | 'editor' | 'enterprise' | 'invalidValue' | 'modules' | 'requiredOnWizard'>
|
||||
: Pick<T, '_id' | 'value' | 'enterprise' | 'invalidValue' | 'modules' | 'requiredOnWizard'>;
|
||||
|
||||
export class SettingsRaw extends BaseRaw<ISetting> implements ISettingsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ISetting>>) {
|
||||
super(db, 'settings', trash);
|
||||
}
|
||||
|
||||
async getValueById<T extends SettingValue = SettingValue>(_id: string): Promise<T | undefined> {
|
||||
const setting = await this.findOne<Pick<ISetting, 'value'>>({ _id }, { projection: { value: 1 } });
|
||||
|
||||
return setting?.value as T;
|
||||
}
|
||||
|
||||
findNotHidden({ updatedAfter }: { updatedAfter?: Date } = {}): FindCursor<ISetting> {
|
||||
const query: Filter<ISetting> = {
|
||||
hidden: { $ne: true },
|
||||
};
|
||||
|
||||
if (updatedAfter) {
|
||||
query._updatedAt = { $gt: updatedAfter };
|
||||
}
|
||||
|
||||
return this.find(query);
|
||||
}
|
||||
|
||||
findOneNotHiddenById(_id: string): Promise<ISetting | null> {
|
||||
const query = {
|
||||
_id,
|
||||
hidden: { $ne: true },
|
||||
};
|
||||
|
||||
return this.findOne(query);
|
||||
}
|
||||
|
||||
findByIds<T extends Document = ISetting, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
_id: string[] | string = [],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
if (typeof _id === 'string') {
|
||||
_id = [_id];
|
||||
}
|
||||
|
||||
const query = {
|
||||
_id: {
|
||||
$in: _id,
|
||||
},
|
||||
};
|
||||
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
updateValueById(
|
||||
_id: string,
|
||||
value: (ISetting['value'] extends undefined ? never : ISetting['value']) | null,
|
||||
options?: UpdateOptions,
|
||||
): Promise<UpdateResult> {
|
||||
const query = {
|
||||
blocked: { $ne: true },
|
||||
value: { $ne: value },
|
||||
_id,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
value,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update, options);
|
||||
}
|
||||
|
||||
async resetValueById(
|
||||
_id: string,
|
||||
value?: (ISetting['value'] extends undefined ? never : ISetting['value']) | null,
|
||||
): Promise<Document | UpdateResult | undefined> {
|
||||
if (value == null) {
|
||||
const record = await this.findOneById(_id);
|
||||
if (record) {
|
||||
const prop = record.valueSource || 'packageValue';
|
||||
value = record[prop];
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.updateValueById(_id, value);
|
||||
}
|
||||
|
||||
async incrementValueById(
|
||||
_id: ISetting['_id'],
|
||||
value?: ISetting['value'],
|
||||
options?: FindOneAndUpdateOptions,
|
||||
): Promise<null | WithId<ISetting>> {
|
||||
return this.findOneAndUpdate(
|
||||
{
|
||||
blocked: { $ne: true },
|
||||
_id,
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
value: value || 1,
|
||||
},
|
||||
} as unknown as UpdateFilter<ISetting>,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
updateOptionsById<T extends ISetting = ISetting>(
|
||||
_id: ISetting['_id'],
|
||||
options: UpdateFilter<T>['$set'],
|
||||
): Promise<Document | UpdateResult> {
|
||||
const query = {
|
||||
blocked: { $ne: true },
|
||||
_id,
|
||||
};
|
||||
|
||||
const update = { $set: options };
|
||||
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
|
||||
updateValueNotHiddenById<T extends ISetting['value'] = ISetting['value']>(
|
||||
_id: ISetting['_id'],
|
||||
value: T,
|
||||
): Promise<Document | UpdateResult> {
|
||||
const query = {
|
||||
_id,
|
||||
hidden: { $ne: true },
|
||||
blocked: { $ne: true },
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
value,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
|
||||
updateValueAndEditorById<T extends ISetting['value'] = ISetting['value']>(
|
||||
_id: ISetting['_id'],
|
||||
value: T,
|
||||
editor: ISettingColor['editor'],
|
||||
): Promise<Document | UpdateResult> {
|
||||
const query = {
|
||||
blocked: { $ne: true },
|
||||
value: { $ne: value },
|
||||
_id,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
value,
|
||||
editor,
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
|
||||
findNotHiddenPublic<T extends ISetting = ISetting>(ids: ISetting['_id'][] = []): FindCursor<PublicSettingFields<T>> {
|
||||
const filter: Filter<ISetting> = {
|
||||
hidden: { $ne: true },
|
||||
public: true,
|
||||
};
|
||||
|
||||
if (ids.length > 0) {
|
||||
filter._id = { $in: ids };
|
||||
}
|
||||
|
||||
// the projection below matches PublicSettingFields, but TypeScript cannot check that against
|
||||
// a conditional type whose input is still a type parameter
|
||||
return this.find(filter, {
|
||||
projection: {
|
||||
_id: 1,
|
||||
value: 1,
|
||||
editor: 1,
|
||||
enterprise: 1,
|
||||
invalidValue: 1,
|
||||
modules: 1,
|
||||
requiredOnWizard: 1,
|
||||
},
|
||||
}) as unknown as FindCursor<PublicSettingFields<T>>;
|
||||
}
|
||||
|
||||
findSetupWizardSettings(): FindCursor<ISetting> {
|
||||
return this.find({ wizard: { $exists: true } });
|
||||
}
|
||||
|
||||
addOptionValueById(_id: ISetting['_id'], option: ISettingSelectOption): Promise<Document | UpdateResult> {
|
||||
const query = {
|
||||
blocked: { $ne: true },
|
||||
_id,
|
||||
};
|
||||
|
||||
const { key, i18nLabel } = option;
|
||||
const update = {
|
||||
$addToSet: {
|
||||
values: {
|
||||
key,
|
||||
i18nLabel,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
|
||||
findNotHiddenPublicUpdatedAfter(updatedAt: Date): FindCursor<ISetting> {
|
||||
const filter = {
|
||||
hidden: { $ne: true },
|
||||
public: true,
|
||||
_updatedAt: {
|
||||
$gt: updatedAt,
|
||||
},
|
||||
};
|
||||
|
||||
return this.find(filter, {
|
||||
projection: {
|
||||
_id: 1,
|
||||
value: 1,
|
||||
editor: 1,
|
||||
enterprise: 1,
|
||||
invalidValue: 1,
|
||||
modules: 1,
|
||||
requiredOnWizard: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findEnterpriseSettings(): FindCursor<ISetting> {
|
||||
return this.find({ enterprise: true });
|
||||
}
|
||||
}
|
||||
11
packages/models/src/models/SmarshHistory.ts
Normal file
11
packages/models/src/models/SmarshHistory.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ISmarshHistory, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ISmarshHistoryModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, Collection } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class SmarshHistoryRaw extends BaseRaw<ISmarshHistory> implements ISmarshHistoryModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ISmarshHistory>>) {
|
||||
super(db, 'smarsh_history', trash);
|
||||
}
|
||||
}
|
||||
93
packages/models/src/models/Statistics.ts
Normal file
93
packages/models/src/models/Statistics.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { IStats } from '@rocket.chat/core-typings';
|
||||
import type { IStatisticsModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class StatisticsRaw extends BaseRaw<IStats> implements IStatisticsModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'statistics');
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{ key: { createdAt: -1 } },
|
||||
{ key: { createdAt: 1 }, expireAfterSeconds: 365 * 24 * 60 * 60 }, // expire after 1 year
|
||||
];
|
||||
}
|
||||
|
||||
async findLast(): Promise<IStats> {
|
||||
const records = await this.find(
|
||||
{},
|
||||
{
|
||||
sort: {
|
||||
createdAt: -1,
|
||||
},
|
||||
limit: 1,
|
||||
},
|
||||
).toArray();
|
||||
return records?.[0];
|
||||
}
|
||||
|
||||
async findLastStatsToken(): Promise<IStats['statsToken']> {
|
||||
const records = await this.find(
|
||||
{
|
||||
statsToken: { $exists: true },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
createdAt: -1,
|
||||
},
|
||||
projection: {
|
||||
statsToken: 1,
|
||||
},
|
||||
limit: 1,
|
||||
},
|
||||
).toArray();
|
||||
return records?.[0]?.statsToken;
|
||||
}
|
||||
|
||||
async findMonthlyPeakConnections() {
|
||||
const oneMonthAgo = new Date();
|
||||
oneMonthAgo.setDate(oneMonthAgo.getDate() - 30);
|
||||
oneMonthAgo.setHours(0, 0, 0, 0);
|
||||
|
||||
return this.findOne<Pick<IStats, 'dailyPeakConnections' | 'createdAt'>>(
|
||||
{
|
||||
createdAt: { $gte: oneMonthAgo },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
dailyPeakConnections: -1,
|
||||
},
|
||||
projection: {
|
||||
dailyPeakConnections: 1,
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async findInstallationDates() {
|
||||
return this.col
|
||||
.aggregate<Pick<IStats, 'version' | 'installedAt'>>([
|
||||
{
|
||||
$group: {
|
||||
_id: '$version',
|
||||
installedAt: { $min: '$installedAt' },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
version: '$_id',
|
||||
installedAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { installedAt: 1 },
|
||||
},
|
||||
])
|
||||
.toArray();
|
||||
}
|
||||
}
|
||||
2060
packages/models/src/models/Subscriptions.ts
Normal file
2060
packages/models/src/models/Subscriptions.ts
Normal file
File diff suppressed because it is too large
Load Diff
212
packages/models/src/models/Team.ts
Normal file
212
packages/models/src/models/Team.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import type { ITeam, RocketChatRecordDeleted, TeamType } from '@rocket.chat/core-typings';
|
||||
import type { FindPaginated, ITeamModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, FindCursor, Db, DeleteResult, Document, Filter, IndexDescription, UpdateResult, FindOptions } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class TeamRaw extends BaseRaw<ITeam> implements ITeamModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ITeam>>) {
|
||||
super(db, 'team', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { name: 1 }, unique: true }];
|
||||
}
|
||||
|
||||
findByNames(names: Array<string>): FindCursor<ITeam>;
|
||||
|
||||
findByNames(names: Array<string>, options: FindOptions<ITeam>): FindCursor<ITeam>;
|
||||
|
||||
findByNames<P extends Document>(names: Array<string>, options: FindOptions<P extends ITeam ? ITeam : P>): FindCursor<P>;
|
||||
|
||||
findByNames<P extends Document>(
|
||||
names: Array<string>,
|
||||
options?: FindOptions<ITeam> | FindOptions<P extends ITeam ? ITeam : P>,
|
||||
): FindCursor<P> | FindCursor<ITeam> {
|
||||
if (options === undefined) {
|
||||
return this.col.find({ name: { $in: names } });
|
||||
}
|
||||
return this.col.find({ name: { $in: names } }, options);
|
||||
}
|
||||
|
||||
findByIds<P extends Document = ITeam, O extends FindOptionsWithProjection<P> = FindOptionsWithProjection<P>>(
|
||||
ids: Array<string>,
|
||||
options?: O,
|
||||
query?: Filter<ITeam>,
|
||||
): FindCursor<DocumentWithProjection<P, O>> {
|
||||
if (options === undefined) {
|
||||
return this.find<P, O>({ ...query, _id: { $in: ids } });
|
||||
}
|
||||
|
||||
return this.find<P, O>({ ...query, _id: { $in: ids } }, options);
|
||||
}
|
||||
|
||||
findByIdsPaginated<T extends Document = ITeam, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
ids: Array<string>,
|
||||
options?: O,
|
||||
query?: Filter<ITeam>,
|
||||
): FindPaginated<FindCursor<DocumentWithProjection<T, O>>> {
|
||||
if (options === undefined) {
|
||||
return this.findPaginated<T, O>({ ...query, _id: { $in: ids } });
|
||||
}
|
||||
|
||||
return this.findPaginated<T, O>({ ...query, _id: { $in: ids } }, options);
|
||||
}
|
||||
|
||||
findByIdsAndType(ids: Array<string>, type: TeamType): FindCursor<ITeam>;
|
||||
|
||||
findByIdsAndType(ids: Array<string>, type: TeamType, options: FindOptions<ITeam>): FindCursor<ITeam>;
|
||||
|
||||
findByIdsAndType<P extends Document>(
|
||||
ids: Array<string>,
|
||||
type: TeamType,
|
||||
options: FindOptions<P extends ITeam ? ITeam : P>,
|
||||
): FindCursor<P>;
|
||||
|
||||
findByIdsAndType<P extends Document>(
|
||||
ids: Array<string>,
|
||||
type: TeamType,
|
||||
options?: FindOptions<ITeam> | FindOptions<P extends ITeam ? ITeam : P>,
|
||||
): FindCursor<P> | FindCursor<ITeam> {
|
||||
if (options === undefined) {
|
||||
return this.col.find({ _id: { $in: ids }, type });
|
||||
}
|
||||
return this.col.find({ _id: { $in: ids }, type }, options);
|
||||
}
|
||||
|
||||
findByType(type: number): FindCursor<ITeam>;
|
||||
|
||||
findByType(type: number, options: FindOptions<ITeam>): FindCursor<ITeam>;
|
||||
|
||||
findByType<P extends Document>(type: number, options: FindOptions<P extends ITeam ? ITeam : P>): FindCursor<P>;
|
||||
|
||||
findByType<P extends Document>(
|
||||
type: number,
|
||||
options?: FindOptions<ITeam> | FindOptions<P extends ITeam ? ITeam : P>,
|
||||
): FindCursor<ITeam> | FindCursor<P> {
|
||||
if (options === undefined) {
|
||||
return this.col.find({ type }, options);
|
||||
}
|
||||
return this.col.find({ type }, options);
|
||||
}
|
||||
|
||||
findByNameAndTeamIds(name: string | RegExp, teamIds: Array<string>): FindCursor<ITeam>;
|
||||
|
||||
findByNameAndTeamIds(name: string | RegExp, teamIds: Array<string>, options: FindOptions<ITeam>): FindCursor<ITeam>;
|
||||
|
||||
findByNameAndTeamIds<P extends Document>(
|
||||
name: string | RegExp,
|
||||
teamIds: Array<string>,
|
||||
options: FindOptions<P extends ITeam ? ITeam : P>,
|
||||
): FindCursor<P>;
|
||||
|
||||
findByNameAndTeamIds<P extends Document>(
|
||||
name: string | RegExp,
|
||||
teamIds: Array<string>,
|
||||
options?: FindOptions<ITeam> | FindOptions<P extends ITeam ? ITeam : P>,
|
||||
): FindCursor<P> | FindCursor<ITeam> {
|
||||
if (options === undefined) {
|
||||
return this.col.find({
|
||||
name,
|
||||
$or: [
|
||||
{
|
||||
type: 0,
|
||||
},
|
||||
{
|
||||
_id: {
|
||||
$in: teamIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return this.col.find(
|
||||
{
|
||||
name,
|
||||
$or: [
|
||||
{
|
||||
type: 0,
|
||||
},
|
||||
{
|
||||
_id: {
|
||||
$in: teamIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
findOneByName(name: string | RegExp): Promise<ITeam | null>;
|
||||
|
||||
findOneByName(name: string | RegExp, options: FindOptions<ITeam>): Promise<ITeam | null>;
|
||||
|
||||
findOneByName<P extends Document>(name: string | RegExp, options: FindOptions<P>): Promise<P | null>;
|
||||
|
||||
findOneByName<P extends Document>(
|
||||
name: string | RegExp,
|
||||
options?: FindOptions<ITeam> | FindOptions<P extends ITeam ? ITeam : P>,
|
||||
): Promise<P | null> | Promise<ITeam | null> {
|
||||
if (options === undefined) {
|
||||
return this.col.findOne({ name });
|
||||
}
|
||||
return this.col.findOne({ name }, options);
|
||||
}
|
||||
|
||||
findOneByMainRoomId(roomId: string): Promise<ITeam | null>;
|
||||
|
||||
findOneByMainRoomId(roomId: string, options: FindOptions<ITeam>): Promise<ITeam | null>;
|
||||
|
||||
findOneByMainRoomId<P extends Document>(roomId: string, options: FindOptions<P>): Promise<P | null>;
|
||||
|
||||
findOneByMainRoomId<P extends Document>(
|
||||
roomId: string,
|
||||
options?: FindOptions<ITeam> | FindOptions<P extends ITeam ? ITeam : P>,
|
||||
): Promise<P | null> | Promise<ITeam | null> {
|
||||
return options ? this.col.findOne({ roomId }, options) : this.col.findOne({ roomId });
|
||||
}
|
||||
|
||||
updateMainRoomForTeam(id: string, roomId: string): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
_id: id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
roomId,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
deleteOneById(id: string): Promise<DeleteResult> {
|
||||
return this.col.deleteOne({
|
||||
_id: id,
|
||||
});
|
||||
}
|
||||
|
||||
deleteOneByName(name: string): Promise<DeleteResult> {
|
||||
return this.col.deleteOne({ name });
|
||||
}
|
||||
|
||||
updateNameAndType(teamId: string, nameAndType: { name?: string; type?: TeamType }): Promise<UpdateResult> {
|
||||
const query = {
|
||||
_id: teamId,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {},
|
||||
};
|
||||
|
||||
if (nameAndType.name) {
|
||||
Object.assign(update.$set, { name: nameAndType.name });
|
||||
}
|
||||
|
||||
if (typeof nameAndType.type !== 'undefined') {
|
||||
Object.assign(update.$set, { type: nameAndType.type });
|
||||
}
|
||||
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
}
|
||||
186
packages/models/src/models/TeamMember.ts
Normal file
186
packages/models/src/models/TeamMember.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import type { IRole, ITeamMember, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { FindPaginated, ITeamMemberModel } from '@rocket.chat/model-typings';
|
||||
import type {
|
||||
Collection,
|
||||
FindCursor,
|
||||
Db,
|
||||
DeleteResult,
|
||||
Document,
|
||||
Filter,
|
||||
FindOptions,
|
||||
IndexDescription,
|
||||
InsertOneResult,
|
||||
UpdateResult,
|
||||
} from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class TeamMemberRaw extends BaseRaw<ITeamMember> implements ITeamMemberModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ITeamMember>>) {
|
||||
super(db, 'team_member', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{
|
||||
key: { teamId: 1 },
|
||||
},
|
||||
{
|
||||
key: { teamId: 1, userId: 1 },
|
||||
unique: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
findByUserId(userId: string): FindCursor<ITeamMember>;
|
||||
|
||||
findByUserId(userId: string, options: FindOptions<ITeamMember>): FindCursor<ITeamMember>;
|
||||
|
||||
findByUserId<P extends Document>(userId: string, options: FindOptions<P>): FindCursor<P>;
|
||||
|
||||
findByUserId<P extends Document>(
|
||||
userId: string,
|
||||
options?: FindOptions<ITeamMember> | FindOptions<P extends ITeamMember ? ITeamMember : P>,
|
||||
): FindCursor<P> | FindCursor<ITeamMember> {
|
||||
return options ? this.col.find({ userId }, options) : this.col.find({ userId }, options);
|
||||
}
|
||||
|
||||
findOneByUserIdAndTeamId(userId: string, teamId: string): Promise<ITeamMember | null>;
|
||||
|
||||
findOneByUserIdAndTeamId(userId: string, teamId: string, options: FindOptions<ITeamMember>): Promise<ITeamMember | null>;
|
||||
|
||||
findOneByUserIdAndTeamId<P extends Document>(userId: string, teamId: string, options: FindOptions<P>): Promise<P | null>;
|
||||
|
||||
findOneByUserIdAndTeamId<P extends Document>(
|
||||
userId: string,
|
||||
teamId: string,
|
||||
options?: FindOptions<ITeamMember> | FindOptions<P extends ITeamMember ? ITeamMember : P>,
|
||||
): Promise<P | null | ITeamMember> {
|
||||
return options ? this.col.findOne({ userId, teamId }, options) : this.col.findOne({ userId, teamId }, options);
|
||||
}
|
||||
|
||||
findByTeamId(teamId: string): FindCursor<ITeamMember>;
|
||||
|
||||
findByTeamId(teamId: string, options: FindOptions<ITeamMember>): FindCursor<ITeamMember>;
|
||||
|
||||
findByTeamId<P extends Document>(teamId: string, options: FindOptions<P>): FindCursor<P>;
|
||||
|
||||
findByTeamId<P extends Document>(
|
||||
teamId: string,
|
||||
options?: FindOptions<ITeamMember> | FindOptions<P extends ITeamMember ? ITeamMember : P>,
|
||||
): FindCursor<P> | FindCursor<ITeamMember> {
|
||||
return options ? this.col.find({ teamId }, options) : this.col.find({ teamId }, options);
|
||||
}
|
||||
|
||||
countByTeamId(teamId: string): Promise<number> {
|
||||
return this.countDocuments({ teamId });
|
||||
}
|
||||
|
||||
findByTeamIds(teamIds: Array<string>): FindCursor<ITeamMember>;
|
||||
|
||||
findByTeamIds(teamIds: Array<string>, options: FindOptions<ITeamMember>): FindCursor<ITeamMember>;
|
||||
|
||||
findByTeamIds<P extends Document>(teamIds: Array<string>, options: FindOptions<P>): FindCursor<P>;
|
||||
|
||||
findByTeamIds<P extends Document>(
|
||||
teamIds: Array<string>,
|
||||
options?: FindOptions<ITeamMember> | FindOptions<P extends ITeamMember ? ITeamMember : P>,
|
||||
): FindCursor<P> | FindCursor<ITeamMember> {
|
||||
return options ? this.col.find({ teamId: { $in: teamIds } }, options) : this.col.find({ teamId: { $in: teamIds } }, options);
|
||||
}
|
||||
|
||||
countByTeamIdAndRole(teamId: string, role: IRole['_id']): Promise<number> {
|
||||
return this.countDocuments({ teamId, roles: role });
|
||||
}
|
||||
|
||||
findByUserIdAndTeamIds(userId: string, teamIds: Array<string>, options: FindOptions<ITeamMember> = {}): FindCursor<ITeamMember> {
|
||||
const query = {
|
||||
userId,
|
||||
teamId: {
|
||||
$in: teamIds,
|
||||
},
|
||||
};
|
||||
|
||||
return this.col.find(query, options);
|
||||
}
|
||||
|
||||
findPaginatedMembersInfoByTeamId(
|
||||
teamId: string,
|
||||
limit: number,
|
||||
skip: number,
|
||||
query?: Filter<ITeamMember>,
|
||||
): FindPaginated<FindCursor<Pick<ITeamMember, '_id' | 'userId' | 'roles' | 'createdBy' | 'createdAt'>>> {
|
||||
return this.findPaginated(
|
||||
{ ...query, teamId },
|
||||
{
|
||||
limit,
|
||||
skip,
|
||||
projection: {
|
||||
userId: 1,
|
||||
roles: 1,
|
||||
createdBy: 1,
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
updateOneByUserIdAndTeamId(userId: string, teamId: string, update: Partial<ITeamMember>): Promise<UpdateResult> {
|
||||
return this.updateOne({ userId, teamId }, { $set: update });
|
||||
}
|
||||
|
||||
createOneByTeamIdAndUserId(
|
||||
teamId: string,
|
||||
userId: string,
|
||||
createdBy: Pick<IUser, '_id' | 'username'>,
|
||||
): Promise<InsertOneResult<ITeamMember>> {
|
||||
return this.insertOne({
|
||||
teamId,
|
||||
userId,
|
||||
createdAt: new Date(),
|
||||
createdBy,
|
||||
_updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
updateRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array<IRole['_id']>): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
teamId,
|
||||
userId,
|
||||
},
|
||||
{
|
||||
$addToSet: {
|
||||
roles: { $each: roles },
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
removeRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array<IRole['_id']>): Promise<UpdateResult> {
|
||||
return this.updateOne(
|
||||
{
|
||||
teamId,
|
||||
userId,
|
||||
},
|
||||
{
|
||||
$pull: {
|
||||
roles: { $in: roles },
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
deleteByUserIdAndTeamId(userId: string, teamId: string): Promise<DeleteResult> {
|
||||
return this.col.deleteOne({
|
||||
teamId,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
deleteByTeamId(teamId: string): Promise<DeleteResult> {
|
||||
return this.col.deleteMany({
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
}
|
||||
22
packages/models/src/models/Trash.ts
Normal file
22
packages/models/src/models/Trash.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { Db, IndexDescription } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class TrashRaw extends BaseRaw<RocketChatRecordDeleted<any>> {
|
||||
constructor(db: Db) {
|
||||
super(db, 'rocketchat__trash', undefined, {
|
||||
collectionNameResolver(name) {
|
||||
return name;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] | undefined {
|
||||
return [
|
||||
{ key: { __collection__: 1 } },
|
||||
{ key: { _deletedAt: 1 }, expireAfterSeconds: 60 * 60 * 24 * 30 },
|
||||
{ key: { rid: 1, __collection__: 1, _deletedAt: 1 } },
|
||||
];
|
||||
}
|
||||
}
|
||||
41
packages/models/src/models/TwoFactorChallenges.ts
Normal file
41
packages/models/src/models/TwoFactorChallenges.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
import type { ITwoFactorChallenge } from '@rocket.chat/core-typings';
|
||||
import type { ITwoFactorChallengesModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Db, IndexDescription, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class TwoFactorChallengesRaw extends BaseRaw<ITwoFactorChallenge> implements ITwoFactorChallengesModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'two_factor_challenges');
|
||||
}
|
||||
|
||||
override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { expireAt: 1 }, expireAfterSeconds: 0 }];
|
||||
}
|
||||
|
||||
findOneByPendingChallengeId<
|
||||
T extends Document = ITwoFactorChallenge,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(pendingChallengeId: string, options?: O): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>({ _id: pendingChallengeId }, options);
|
||||
}
|
||||
|
||||
removeByPendingChallengeId(pendingChallengeId: string) {
|
||||
return this.deleteOne({ _id: pendingChallengeId });
|
||||
}
|
||||
|
||||
async createTwoFactorChallenge(userId: string, method: ITwoFactorChallenge['method']): Promise<string> {
|
||||
const now = new Date();
|
||||
const challengeId = randomBytes(32).toString('hex');
|
||||
await this.insertOne({
|
||||
_id: challengeId,
|
||||
userId,
|
||||
method,
|
||||
createdAt: now,
|
||||
expireAt: new Date(now.getTime() + 1000 * 60 * 5),
|
||||
});
|
||||
return challengeId;
|
||||
}
|
||||
}
|
||||
73
packages/models/src/models/Uploads.ts
Normal file
73
packages/models/src/models/Uploads.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { IUpload, RocketChatRecordDeleted, IRoom } from '@rocket.chat/core-typings';
|
||||
import type { FindPaginated, IUploadsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, FindCursor, Db, IndexDescription, WithId, Filter, FindOptions, UpdateResult, Document } from 'mongodb';
|
||||
|
||||
import { BaseUploadModelRaw } from './BaseUploadModel';
|
||||
|
||||
export class UploadsRaw extends BaseUploadModelRaw implements IUploadsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IUpload>>) {
|
||||
super(db, 'uploads', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
...super.modelIndexes(),
|
||||
{ key: { uploadedAt: -1 } },
|
||||
{ key: { rid: 1, _hidden: 1, typeGroup: 1 } },
|
||||
{ key: { 'federation.mediaId': 1, 'federation.serverName': 1 }, unique: true, sparse: true },
|
||||
];
|
||||
}
|
||||
|
||||
findByFederationMediaIdAndServerName(mediaId: string, serverName: string): Promise<IUpload | null> {
|
||||
return this.findOne({ 'federation.mediaId': mediaId, 'federation.serverName': serverName });
|
||||
}
|
||||
|
||||
setFederationInfo(fileId: IUpload['_id'], info: Required<IUpload>['federation']): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id: fileId }, { $set: { federation: info } });
|
||||
}
|
||||
|
||||
setFederationRoomInfo(fileId: IUpload['_id'], rid: IRoom['_id'], mrid: string): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id: fileId }, { $set: { rid, 'federation.mrid': mrid } });
|
||||
}
|
||||
|
||||
findPaginatedWithoutThumbs(query: Filter<IUpload> = {}, options?: FindOptions<IUpload>): FindPaginated<FindCursor<WithId<IUpload>>> {
|
||||
return this.findPaginated(
|
||||
{
|
||||
typeGroup: { $ne: 'thumb' },
|
||||
...query,
|
||||
_hidden: { $ne: true },
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
findImagesByRoomId(
|
||||
rid: IRoom['_id'],
|
||||
uploadedAt?: Date,
|
||||
options: Omit<FindOptions<IUpload>, 'sort'> = {},
|
||||
): FindPaginated<FindCursor<WithId<IUpload>>> {
|
||||
return this.findPaginated(
|
||||
{
|
||||
rid,
|
||||
_hidden: { $ne: true },
|
||||
typeGroup: 'image',
|
||||
...(Boolean(uploadedAt) && {
|
||||
uploadedAt: {
|
||||
$lte: uploadedAt,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
...options,
|
||||
sort: { uploadedAt: -1 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
findAllByOriginalFileId<T extends Document = IUpload, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
originalFileId: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>({ originalFileId }, options);
|
||||
}
|
||||
}
|
||||
28
packages/models/src/models/UserDataFiles.ts
Normal file
28
packages/models/src/models/UserDataFiles.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { IUserDataFile, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IUserDataFilesModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, Db, IndexDescription, Document } from 'mongodb';
|
||||
|
||||
import { BaseUploadModelRaw } from './BaseUploadModel';
|
||||
|
||||
export class UserDataFilesRaw extends BaseUploadModelRaw implements IUserDataFilesModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IUserDataFile>>) {
|
||||
super(db, 'user_data_files', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [...super.modelIndexes(), { key: { userId: 1 } }];
|
||||
}
|
||||
|
||||
findLastFileByUser<T extends Document = IUserDataFile, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
userId: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
const query = {
|
||||
userId,
|
||||
};
|
||||
|
||||
// merging into `O` is a lie only about `sort`; `DocumentWithProjection` reads `O['projection']`
|
||||
// alone, and that comes straight from `options`, so the declared return type stays accurate
|
||||
return this.findOne<T, O>(query, { ...options, sort: { _updatedAt: -1 } } as unknown as O);
|
||||
}
|
||||
}
|
||||
3321
packages/models/src/models/Users.ts
Normal file
3321
packages/models/src/models/Users.ts
Normal file
File diff suppressed because it is too large
Load Diff
119
packages/models/src/models/UsersSessions.ts
Normal file
119
packages/models/src/models/UsersSessions.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { IUserSession, IUserSessionConnection, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IUsersSessionsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { FindCursor, Collection, Db, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class UsersSessionsRaw extends BaseRaw<IUserSession> implements IUsersSessionsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IUserSession>>) {
|
||||
super(db, 'usersSessions', trash, {
|
||||
preventSetUpdatedAt: true,
|
||||
collectionNameResolver(name) {
|
||||
return name;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateConnectionStatusById(uid: string, connectionId: string, status: string): ReturnType<BaseRaw<IUserSession>['updateOne']> {
|
||||
const query = {
|
||||
'_id': uid,
|
||||
'connections.id': connectionId,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
'connections.$.status': status,
|
||||
'connections.$._updatedAt': new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne(query, update);
|
||||
}
|
||||
|
||||
async removeConnectionsFromInstanceId(instanceId: string): ReturnType<BaseRaw<IUserSession>['updateMany']> {
|
||||
return this.updateMany(
|
||||
{
|
||||
'connections.instanceId': instanceId,
|
||||
},
|
||||
{
|
||||
$pull: {
|
||||
connections: {
|
||||
instanceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
removeConnectionsFromOtherInstanceIds(instanceIds: string[]): ReturnType<BaseRaw<IUserSession>['updateMany']> {
|
||||
return this.updateMany(
|
||||
{},
|
||||
{
|
||||
$pull: {
|
||||
connections: {
|
||||
instanceId: {
|
||||
$nin: instanceIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async removeConnectionByConnectionId(connectionId: string): ReturnType<BaseRaw<IUserSession>['updateMany']> {
|
||||
return this.updateMany(
|
||||
{
|
||||
'connections.id': connectionId,
|
||||
},
|
||||
{
|
||||
$pull: {
|
||||
connections: {
|
||||
id: connectionId,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
findByInstanceId(instanceId: string): FindCursor<IUserSession> {
|
||||
return this.find({
|
||||
'connections.instanceId': instanceId,
|
||||
});
|
||||
}
|
||||
|
||||
addConnectionById(
|
||||
userId: string,
|
||||
{ id, instanceId, status }: Pick<IUserSessionConnection, 'id' | 'instanceId' | 'status'>,
|
||||
): ReturnType<BaseRaw<IUserSession>['updateOne']> {
|
||||
const now = new Date();
|
||||
|
||||
const update = {
|
||||
$push: {
|
||||
connections: {
|
||||
id,
|
||||
instanceId,
|
||||
status,
|
||||
_createdAt: now,
|
||||
_updatedAt: now,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return this.updateOne({ _id: userId }, update, { upsert: true });
|
||||
}
|
||||
|
||||
findByOtherInstanceIds<T extends Document = IUserSession, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
instanceIds: string[],
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
return this.find<T, O>(
|
||||
{
|
||||
'connections.instanceId': {
|
||||
$exists: true,
|
||||
$nin: instanceIds,
|
||||
},
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
307
packages/models/src/models/VideoConference.ts
Normal file
307
packages/models/src/models/VideoConference.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import type {
|
||||
VideoConference,
|
||||
IGroupVideoConference,
|
||||
ILivechatVideoConference,
|
||||
IUser,
|
||||
IRoom,
|
||||
RocketChatRecordDeleted,
|
||||
IVoIPVideoConference,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import { VideoConferenceStatus } from '@rocket.chat/core-typings';
|
||||
import type { FindPaginated, InsertionModel, IVideoConferenceModel } from '@rocket.chat/model-typings';
|
||||
import type {
|
||||
FindCursor,
|
||||
UpdateOptions,
|
||||
UpdateFilter,
|
||||
UpdateResult,
|
||||
IndexDescription,
|
||||
Collection,
|
||||
Db,
|
||||
CountDocumentsOptions,
|
||||
} from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class VideoConferenceRaw extends BaseRaw<VideoConference> implements IVideoConferenceModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<VideoConference>>) {
|
||||
super(db, 'video_conference', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [
|
||||
{ key: { rid: 1, createdAt: 1 }, unique: false },
|
||||
{ key: { type: 1, status: 1 }, unique: false },
|
||||
{ key: { discussionRid: 1 }, unique: false },
|
||||
];
|
||||
}
|
||||
|
||||
public findPaginatedByRoomId(
|
||||
rid: IRoom['_id'],
|
||||
{ offset, count }: { offset?: number; count?: number } = {},
|
||||
): FindPaginated<FindCursor<VideoConference>> {
|
||||
// No data is lost — `providerData` is optional — but `Omit` over the `VideoConference` union collapses it into a single
|
||||
// object type, so the explicit type argument opts out of projection inference to preserve the discriminated union.
|
||||
return this.findPaginated<VideoConference>(
|
||||
{ rid },
|
||||
{
|
||||
sort: { createdAt: -1 },
|
||||
skip: offset,
|
||||
limit: count,
|
||||
projection: {
|
||||
providerData: 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async findAllLongRunning(minDate: Date): Promise<FindCursor<Pick<VideoConference, '_id'>>> {
|
||||
return this.find(
|
||||
{
|
||||
createdAt: {
|
||||
$lte: minDate,
|
||||
},
|
||||
endedAt: {
|
||||
$exists: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
_id: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async countByTypeAndStatus(
|
||||
type: VideoConference['type'],
|
||||
status: VideoConferenceStatus,
|
||||
options: CountDocumentsOptions,
|
||||
): Promise<number> {
|
||||
return this.countDocuments(
|
||||
{
|
||||
type,
|
||||
status,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
public async createDirect({
|
||||
providerName,
|
||||
...callDetails
|
||||
}: Pick<VideoConference, 'rid' | 'createdBy' | 'providerName'>): Promise<string> {
|
||||
const call: InsertionModel<VideoConference> = {
|
||||
type: 'direct',
|
||||
users: [],
|
||||
messages: {},
|
||||
status: VideoConferenceStatus.CALLING,
|
||||
createdAt: new Date(),
|
||||
providerName: providerName.toLowerCase(),
|
||||
ringing: true,
|
||||
...callDetails,
|
||||
};
|
||||
|
||||
return (await this.insertOne(call)).insertedId;
|
||||
}
|
||||
|
||||
public async createGroup({
|
||||
providerName,
|
||||
...callDetails
|
||||
}: Required<Pick<IGroupVideoConference, 'rid' | 'title' | 'createdBy' | 'providerName' | 'ringing'>>): Promise<string> {
|
||||
const call: InsertionModel<IGroupVideoConference> = {
|
||||
type: 'videoconference',
|
||||
users: [],
|
||||
messages: {},
|
||||
status: VideoConferenceStatus.STARTED,
|
||||
anonymousUsers: 0,
|
||||
createdAt: new Date(),
|
||||
providerName: providerName.toLowerCase(),
|
||||
...callDetails,
|
||||
};
|
||||
|
||||
return (await this.insertOne(call)).insertedId;
|
||||
}
|
||||
|
||||
public async createLivechat({
|
||||
providerName,
|
||||
...callDetails
|
||||
}: Required<Pick<ILivechatVideoConference, 'rid' | 'createdBy' | 'providerName'>>): Promise<string> {
|
||||
const call: InsertionModel<ILivechatVideoConference> = {
|
||||
type: 'livechat',
|
||||
users: [],
|
||||
messages: {},
|
||||
status: VideoConferenceStatus.STARTED,
|
||||
createdAt: new Date(),
|
||||
providerName: providerName.toLowerCase(),
|
||||
...callDetails,
|
||||
};
|
||||
|
||||
return (await this.insertOne(call)).insertedId;
|
||||
}
|
||||
|
||||
public async createVoIP(call: InsertionModel<IVoIPVideoConference>): Promise<string | undefined> {
|
||||
const { externalId, ...data } = call;
|
||||
|
||||
const doc = await this.findOneAndUpdate({ externalId }, { $set: data }, { upsert: true, returnDocument: 'after' });
|
||||
return doc?._id;
|
||||
}
|
||||
|
||||
public updateOneById(
|
||||
_id: string,
|
||||
update: UpdateFilter<VideoConference> | Partial<VideoConference>,
|
||||
options?: UpdateOptions,
|
||||
): Promise<UpdateResult> {
|
||||
return this.updateOne({ _id }, update, options);
|
||||
}
|
||||
|
||||
public async setEndedById(callId: string, endedBy?: { _id: string; name: string; username: string }, endedAt?: Date): Promise<void> {
|
||||
await this.updateOneById(callId, {
|
||||
$set: {
|
||||
endedBy,
|
||||
endedAt: endedAt || new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async setDataById(callId: string, data: Partial<Omit<VideoConference, '_id'>>): Promise<void> {
|
||||
await this.updateOneById(callId, {
|
||||
$set: data,
|
||||
});
|
||||
}
|
||||
|
||||
public async setRingingById(callId: string, ringing: boolean): Promise<void> {
|
||||
await this.updateOneById(callId, {
|
||||
$set: {
|
||||
ringing,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async setStatusById(callId: string, status: VideoConference['status']): Promise<void> {
|
||||
await this.updateOneById(callId, {
|
||||
$set: {
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async setUrlById(callId: string, url: string): Promise<void> {
|
||||
await this.updateOneById(callId, {
|
||||
$set: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async setProviderDataById(callId: string, providerData: Record<string, any> | undefined): Promise<void> {
|
||||
await this.updateOneById(callId, {
|
||||
...(providerData
|
||||
? {
|
||||
$set: {
|
||||
providerData,
|
||||
},
|
||||
}
|
||||
: {
|
||||
$unset: {
|
||||
providerData: 1 as const,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public async addUserById(
|
||||
callId: string,
|
||||
user: Required<Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'>> & { ts?: Date },
|
||||
): Promise<void> {
|
||||
await this.updateOneById(callId, {
|
||||
$addToSet: {
|
||||
users: {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
avatarETag: user.avatarETag,
|
||||
ts: user.ts || new Date(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async setMessageById(callId: string, messageType: keyof VideoConference['messages'], messageId: string): Promise<void> {
|
||||
await this.updateOneById(callId, {
|
||||
$set: {
|
||||
[`messages.${messageType}`]: messageId,
|
||||
},
|
||||
}); // TODO: Remove this cast when TypeScript is updated
|
||||
// TypeScript is not smart enough to infer that `messages.${'start' | 'end'}` matches two keys of `VideoConference`
|
||||
}
|
||||
|
||||
public async updateUserReferences(userId: IUser['_id'], username: IUser['username'], name: IUser['name']): Promise<void> {
|
||||
await this.updateMany(
|
||||
{
|
||||
'users._id': userId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'users.$.name': name,
|
||||
'users.$.username': username,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await this.updateMany(
|
||||
{
|
||||
'createdBy._id': userId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'createdBy.name': name,
|
||||
'createdBy.username': username,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await this.updateMany(
|
||||
{
|
||||
'endedBy._id': userId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'endedBy.name': name,
|
||||
'endedBy.username': username,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async increaseAnonymousCount(callId: IGroupVideoConference['_id']): Promise<void> {
|
||||
await this.updateOne(
|
||||
{ _id: callId },
|
||||
{
|
||||
$inc: {
|
||||
anonymousUsers: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async setDiscussionRidById(callId: string, discussionRid: IRoom['_id']): Promise<void> {
|
||||
await this.updateOne({ _id: callId }, { $set: { discussionRid } });
|
||||
}
|
||||
|
||||
public async unsetDiscussionRidById(callId: string): Promise<void> {
|
||||
await this.updateOne({ _id: callId }, { $unset: { discussionRid: true } });
|
||||
}
|
||||
|
||||
public async unsetDiscussionRid(discussionRid: IRoom['_id']): Promise<void> {
|
||||
await this.updateMany(
|
||||
{
|
||||
discussionRid,
|
||||
},
|
||||
{
|
||||
$unset: {
|
||||
discussionRid: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
53
packages/models/src/models/WebdavAccounts.ts
Normal file
53
packages/models/src/models/WebdavAccounts.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { IWebdavAccount, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { IWebdavAccountsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings';
|
||||
import type { Collection, FindCursor, Db, DeleteResult, IndexDescription, Document } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class WebdavAccountsRaw extends BaseRaw<IWebdavAccount> implements IWebdavAccountsModel {
|
||||
constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IWebdavAccount>>) {
|
||||
super(db, 'webdav_accounts', trash);
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { userId: 1 } }];
|
||||
}
|
||||
|
||||
findOneByIdAndUserId<T extends Document = IWebdavAccount, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
_id: string,
|
||||
userId: string,
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>({ _id, userId }, options);
|
||||
}
|
||||
|
||||
findOneByUserIdServerUrlAndUsername<
|
||||
T extends Document = IWebdavAccount,
|
||||
O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>,
|
||||
>(
|
||||
{
|
||||
userId,
|
||||
serverURL,
|
||||
username,
|
||||
}: {
|
||||
userId: string;
|
||||
serverURL: string;
|
||||
username: string;
|
||||
},
|
||||
options?: O,
|
||||
): Promise<DocumentWithProjection<T, O> | null> {
|
||||
return this.findOne<T, O>({ userId, serverURL, username }, options);
|
||||
}
|
||||
|
||||
findWithUserId<T extends Document = IWebdavAccount, O extends FindOptionsWithProjection<T> = FindOptionsWithProjection<T>>(
|
||||
userId: string,
|
||||
options?: O,
|
||||
): FindCursor<DocumentWithProjection<T, O>> {
|
||||
const query = { userId };
|
||||
return this.find<T, O>(query, options);
|
||||
}
|
||||
|
||||
removeByUserAndId(_id: string, userId: string): Promise<DeleteResult> {
|
||||
return this.deleteOne({ _id, userId });
|
||||
}
|
||||
}
|
||||
47
packages/models/src/models/WorkspaceCredentials.ts
Normal file
47
packages/models/src/models/WorkspaceCredentials.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { IWorkspaceCredentials } from '@rocket.chat/core-typings';
|
||||
import type { IWorkspaceCredentialsModel } from '@rocket.chat/model-typings';
|
||||
import type { Db, DeleteResult, Filter, IndexDescription, UpdateResult } from 'mongodb';
|
||||
|
||||
import { BaseRaw } from './BaseRaw';
|
||||
|
||||
export class WorkspaceCredentialsRaw extends BaseRaw<IWorkspaceCredentials> implements IWorkspaceCredentialsModel {
|
||||
constructor(db: Db) {
|
||||
super(db, 'workspace_credentials');
|
||||
}
|
||||
|
||||
protected override modelIndexes(): IndexDescription[] {
|
||||
return [{ key: { scope: 1, expirationDate: 1, accessToken: 1 }, unique: true }];
|
||||
}
|
||||
|
||||
getCredentialByScope(scope = ''): Promise<IWorkspaceCredentials | null> {
|
||||
const query: Filter<IWorkspaceCredentials> = { scope };
|
||||
|
||||
return this.findOne(query);
|
||||
}
|
||||
|
||||
updateCredentialByScope({
|
||||
scope,
|
||||
accessToken,
|
||||
expirationDate,
|
||||
}: {
|
||||
scope: string;
|
||||
accessToken: string;
|
||||
expirationDate: Date;
|
||||
}): Promise<UpdateResult> {
|
||||
const record = {
|
||||
$set: {
|
||||
scope,
|
||||
accessToken,
|
||||
expirationDate,
|
||||
},
|
||||
};
|
||||
|
||||
const query: Filter<IWorkspaceCredentials> = { scope };
|
||||
|
||||
return this.updateOne(query, record, { upsert: true });
|
||||
}
|
||||
|
||||
removeAllCredentials(): Promise<DeleteResult> {
|
||||
return this.col.deleteMany({});
|
||||
}
|
||||
}
|
||||
8
packages/models/src/models/setUpdatedAt.ts
Normal file
8
packages/models/src/models/setUpdatedAt.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function setUpdatedAt(record: Record<string, any>): void {
|
||||
if (/(^|,)\$/.test(Object.keys(record).join(','))) {
|
||||
record.$set = record.$set || {};
|
||||
record.$set._updatedAt = new Date();
|
||||
} else {
|
||||
record._updatedAt = new Date();
|
||||
}
|
||||
}
|
||||
91
packages/models/src/proxify.spec.ts
Normal file
91
packages/models/src/proxify.spec.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { proxify, registerModel } from './proxify';
|
||||
|
||||
type MockedModel = {
|
||||
method: () => MockedModel;
|
||||
};
|
||||
|
||||
describe('non lazy proxify', () => {
|
||||
it('should keep this inside functions', () => {
|
||||
const collectionMocked = proxify('collection') as MockedModel;
|
||||
const collection = {
|
||||
method() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
registerModel<any>('collection', collection);
|
||||
|
||||
expect(collectionMocked.method()).toBe(collection);
|
||||
});
|
||||
it('should throw an error if the model is not found', () => {
|
||||
const collectionMocked = proxify('collection-not-found') as MockedModel;
|
||||
expect(() => collectionMocked.method()).toThrow('Model collection-not-found not found');
|
||||
});
|
||||
|
||||
it('should return a proxified property', () => {
|
||||
const collectionMocked = proxify('collection-prop') as {
|
||||
prop: string;
|
||||
};
|
||||
const collection = {
|
||||
prop: 'value',
|
||||
};
|
||||
registerModel<any>('collection-prop', collection);
|
||||
expect(collectionMocked.prop).toBe('value');
|
||||
});
|
||||
|
||||
it('should throw an error if trying to set a property from the proxified object', () => {
|
||||
const collectionMocked = proxify('collection-prop') as {
|
||||
prop: string;
|
||||
};
|
||||
const collection = {
|
||||
prop: 'value',
|
||||
};
|
||||
registerModel<any>('collection-prop', collection);
|
||||
expect(() => {
|
||||
collectionMocked.prop = 'new value';
|
||||
}).toThrow('Models accessed via proxify are read-only, use the model instance directly to modify it.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lazy proxify', () => {
|
||||
it('should keep this inside functions', () => {
|
||||
const collectionMocked = proxify('collection-lazy') as MockedModel;
|
||||
const collection = {
|
||||
method() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
|
||||
registerModel<any>('collection-lazy', () => collection);
|
||||
|
||||
expect(collectionMocked.method()).toBe(collection);
|
||||
});
|
||||
|
||||
it('should throw an error if the model is not found', () => {
|
||||
const collectionMocked = proxify('collection-not-found') as MockedModel;
|
||||
expect(() => collectionMocked.method()).toThrow('Model collection-not-found not found');
|
||||
});
|
||||
|
||||
it('should return a proxified property', () => {
|
||||
const collectionMocked = proxify('collection-prop') as {
|
||||
prop: string;
|
||||
};
|
||||
const collection = {
|
||||
prop: 'value',
|
||||
};
|
||||
registerModel<any>('collection-prop', () => collection);
|
||||
expect(collectionMocked.prop).toBe('value');
|
||||
});
|
||||
|
||||
it('should throw an error if trying to set a property from the proxified object', () => {
|
||||
const collectionMocked = proxify('collection-prop') as {
|
||||
prop: string;
|
||||
};
|
||||
const collection = {
|
||||
prop: 'value',
|
||||
};
|
||||
registerModel<any>('collection-prop', () => collection);
|
||||
expect(() => {
|
||||
collectionMocked.prop = 'new value';
|
||||
}).toThrow('Models accessed via proxify are read-only, use the model instance directly to modify it.');
|
||||
});
|
||||
});
|
||||
59
packages/models/src/proxify.ts
Normal file
59
packages/models/src/proxify.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { IBaseModel } from '@rocket.chat/model-typings';
|
||||
|
||||
const lazyModels = new Map<string, () => IBaseModel<any>>();
|
||||
const models = new Map<string, IBaseModel<any>>();
|
||||
|
||||
function handler<T extends object>(namespace: string): ProxyHandler<T> {
|
||||
return {
|
||||
get: (_target: T, nameProp: keyof IBaseModel<any>): any => {
|
||||
if (!models.has(namespace) && lazyModels.has(namespace)) {
|
||||
const getModel = lazyModels.get(namespace);
|
||||
if (getModel) {
|
||||
models.set(namespace, getModel());
|
||||
}
|
||||
}
|
||||
|
||||
const model = models.get(namespace);
|
||||
|
||||
if (!model) {
|
||||
throw new Error(`Model ${namespace} not found`);
|
||||
}
|
||||
|
||||
const prop = model[nameProp];
|
||||
|
||||
if (typeof prop === 'function') {
|
||||
return prop.bind(model);
|
||||
}
|
||||
|
||||
return prop;
|
||||
},
|
||||
|
||||
set() {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
throw new Error('Models accessed via proxify are read-only, use the model instance directly to modify it.');
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function registerModel<TModel extends IBaseModel<any, any, any>>(
|
||||
name: string,
|
||||
instance: TModel | (() => TModel),
|
||||
overwriteExisting = true,
|
||||
): void {
|
||||
if (!overwriteExisting && (lazyModels.has(name) || models.has(name))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof instance === 'function') {
|
||||
lazyModels.set(name, instance);
|
||||
} else {
|
||||
models.set(name, instance);
|
||||
}
|
||||
}
|
||||
|
||||
export function proxify<T>(namespace: string): T {
|
||||
return new Proxy({}, handler(namespace)) as unknown as T;
|
||||
}
|
||||
18
packages/models/src/readSecondaryPreferred.ts
Normal file
18
packages/models/src/readSecondaryPreferred.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Db, ReadPreferenceLike } from 'mongodb';
|
||||
import { ReadPreference } from 'mongodb';
|
||||
|
||||
export function readSecondaryPreferred(db?: Db, tags: any[] = []): ReadPreferenceLike {
|
||||
const { readPreference } = db?.options || {};
|
||||
|
||||
if (tags.length) {
|
||||
return new ReadPreference(ReadPreference.SECONDARY_PREFERRED, tags);
|
||||
}
|
||||
if (readPreference && readPreference instanceof ReadPreference && readPreference.tags?.length) {
|
||||
return new ReadPreference(ReadPreference.SECONDARY_PREFERRED, readPreference.tags);
|
||||
}
|
||||
|
||||
// For some reason the new ReadPreference definition requires the tags parameter even not been
|
||||
// required by the code implementation and, for some reason, when passing an empty array it
|
||||
// doesn't ignore the tags and stop using the secondaries.
|
||||
return ReadPreference.SECONDARY_PREFERRED;
|
||||
}
|
||||
191
packages/models/src/updater.spec.ts
Normal file
191
packages/models/src/updater.spec.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
|
||||
import { UpdaterImpl } from './updater';
|
||||
|
||||
test('updater typings', () => {
|
||||
const updater = new UpdaterImpl<{
|
||||
_id: string;
|
||||
t: 'l';
|
||||
a: {
|
||||
b: string;
|
||||
};
|
||||
c?: number;
|
||||
|
||||
d?: {
|
||||
e: string;
|
||||
};
|
||||
e: string[];
|
||||
}>();
|
||||
|
||||
// @ts-expect-error: it should not allow any string to `t` only `l` is allowed
|
||||
updater.set('t', 'a');
|
||||
// `l` is allowed
|
||||
updater.set('t', 'l');
|
||||
|
||||
// `a` is { b: string }
|
||||
updater.set('a', { b: 'test' });
|
||||
updater.set('a.b', 'test');
|
||||
// @ts-expect-error: it should not allow strings to `a`, a is an object containing `b: string`
|
||||
updater.set('a', 'b');
|
||||
// @ts-expect-error: `a` is not optional so unset is not allowed
|
||||
updater.unset('a');
|
||||
// @ts-expect-error: strings cannot be incremented
|
||||
updater.inc('a', 1);
|
||||
|
||||
// `c` is number but it should be optional, so unset is allowed
|
||||
updater.unset('c');
|
||||
updater.set('c', 1);
|
||||
// @ts-expect-error: `c` is a number
|
||||
updater.set('c', 'b');
|
||||
// inc is allowed for numbers
|
||||
updater.inc('c', 1);
|
||||
|
||||
// `d` is { e: string } but it should be optional, so unset is allowed
|
||||
updater.unset('d');
|
||||
updater.set('d', { e: 'a' });
|
||||
// @ts-expect-error: `d` is an object
|
||||
updater.set('d', 'a');
|
||||
|
||||
// @ts-expect-error: it should not allow numbers, since e is a string
|
||||
updater.addToSet('e', 1);
|
||||
// @ts-expect-error: it should not allow strings, since a is an object
|
||||
updater.addToSet('a', 'b');
|
||||
|
||||
updater.addToSet('e', 'a');
|
||||
// @ts-expect-error: it should not allow `njame` its not specified in the model
|
||||
updater.set('njame', 1);
|
||||
|
||||
// `d` is { e: string } and also it should be optional, so unset is allowed
|
||||
updater.unset('d.e');
|
||||
// @ts-expect-error: `d` is an object cannot be incremented
|
||||
updater.inc('d', 1);
|
||||
|
||||
// `activity` is a string
|
||||
const omnichannel = new UpdaterImpl<IOmnichannelRoom>();
|
||||
omnichannel.addToSet('v.activity', 'asd');
|
||||
// @ts-expect-error: it should not allow numbers, since activity is a string
|
||||
omnichannel.addToSet('v.activity', 1);
|
||||
// @ts-expect-error: it should not allow objects, since activity is a string
|
||||
omnichannel.addToSet('v.activity', {
|
||||
asdas: 1,
|
||||
});
|
||||
// @ts-expect-error: it should not allow sub properties, since activity is a string
|
||||
omnichannel.addToSet('v.activity.asd', {
|
||||
asdas: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('updater supports nested paths up to 3 levels deep', () => {
|
||||
const updater = new UpdaterImpl<{
|
||||
_id: string;
|
||||
level1: {
|
||||
level2: {
|
||||
level3: string;
|
||||
};
|
||||
};
|
||||
}>();
|
||||
|
||||
// 1-level deep path
|
||||
updater.set('level1', { level2: { level3: 'test' } });
|
||||
// 2-level deep path
|
||||
updater.set('level1.level2', { level3: 'test' });
|
||||
// 3-level deep path
|
||||
updater.set('level1.level2.level3', 'test');
|
||||
});
|
||||
|
||||
test('updater validates keys on real schemas with nested paths', () => {
|
||||
const updater = new UpdaterImpl<IOmnichannelRoom>();
|
||||
|
||||
// valid 1-level paths
|
||||
updater.set('fname', 'test');
|
||||
// valid 2-level paths
|
||||
updater.set('responseBy.lastMessageTs', new Date());
|
||||
updater.set('v.activity', ['period1']);
|
||||
// valid 3-level paths
|
||||
updater.inc('metrics.response.total', 1);
|
||||
updater.set('metrics.response.avg', 42);
|
||||
updater.set('metrics.reaction.ft', 100);
|
||||
|
||||
// @ts-expect-error: non-existent top-level key
|
||||
updater.set('nonExistentField', 'hello');
|
||||
// @ts-expect-error: non-existent nested key
|
||||
updater.set('responseBy.nonExistent', new Date());
|
||||
// @ts-expect-error: 'fname' is a string, not a number — inc should reject it
|
||||
updater.inc('fname', 1);
|
||||
});
|
||||
|
||||
test('updater $set operations', async () => {
|
||||
const updater = new UpdaterImpl<{
|
||||
_id: string;
|
||||
t: 'l';
|
||||
a: {
|
||||
b: string;
|
||||
};
|
||||
c?: number;
|
||||
}>();
|
||||
|
||||
updater.set('a', {
|
||||
b: 'set',
|
||||
});
|
||||
|
||||
expect(updater.getUpdateFilter()).toEqual({ $set: { a: { b: 'set' } } });
|
||||
});
|
||||
|
||||
test('updater $unset operations', async () => {
|
||||
const updater = new UpdaterImpl<{
|
||||
_id: string;
|
||||
t: 'l';
|
||||
a: {
|
||||
b: string;
|
||||
};
|
||||
c?: number;
|
||||
}>();
|
||||
updater.unset('c');
|
||||
expect(updater.getUpdateFilter()).toEqual({ $unset: { c: 1 } });
|
||||
});
|
||||
|
||||
test('updater inc multiple operations', async () => {
|
||||
const updater = new UpdaterImpl<{
|
||||
_id: string;
|
||||
t: 'l';
|
||||
a: {
|
||||
b: string;
|
||||
};
|
||||
c?: number;
|
||||
}>();
|
||||
|
||||
updater.inc('c', 1);
|
||||
updater.inc('c', 1);
|
||||
|
||||
expect(updater.getUpdateFilter()).toEqual({ $inc: { c: 2 } });
|
||||
});
|
||||
|
||||
test('it should add items to array', async () => {
|
||||
const updater = new UpdaterImpl<{
|
||||
_id: string;
|
||||
a: string[];
|
||||
}>();
|
||||
|
||||
updater.addToSet('a', 'b');
|
||||
updater.addToSet('a', 'c');
|
||||
|
||||
expect(updater.getUpdateFilter()).toEqual({ $addToSet: { a: { $each: ['b', 'c'] } } });
|
||||
});
|
||||
|
||||
test('it should getUpdateFilter only once', async () => {
|
||||
const updater = new UpdaterImpl<{
|
||||
_id: string;
|
||||
t: 'l';
|
||||
a: {
|
||||
b: string;
|
||||
};
|
||||
c?: number;
|
||||
}>();
|
||||
|
||||
updater.set('a', {
|
||||
b: 'set',
|
||||
});
|
||||
|
||||
expect(updater.getUpdateFilter()).toEqual({ $set: { a: { b: 'set' } } });
|
||||
expect(() => updater.getUpdateFilter()).toThrow();
|
||||
});
|
||||
78
packages/models/src/updater.ts
Normal file
78
packages/models/src/updater.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Updater, SetProps, UnsetProps, IncProps, AddToSetProps } from '@rocket.chat/model-typings';
|
||||
import type { UpdateFilter } from 'mongodb';
|
||||
|
||||
type ArrayElementType<T> = T extends (infer E)[] ? E : T;
|
||||
|
||||
type Keys<T extends { _id: string }> = keyof SetProps<T>;
|
||||
|
||||
export class UpdaterImpl<T extends { _id: string }> implements Updater<T> {
|
||||
private _set: Map<Keys<T>, any> | undefined;
|
||||
|
||||
private _unset: Set<keyof UnsetProps<T>> | undefined;
|
||||
|
||||
private _inc: Map<keyof IncProps<T>, number> | undefined;
|
||||
|
||||
private _addToSet: Map<keyof AddToSetProps<T>, any[]> | undefined;
|
||||
|
||||
private dirty = false;
|
||||
|
||||
set<K extends keyof SetProps<T>>(key: K, value: SetProps<T>[K]) {
|
||||
this._set = this._set ?? new Map<Keys<T>, any>();
|
||||
this._set.set(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
unset<K extends keyof UnsetProps<T>>(key: K): Updater<T> {
|
||||
this._unset = this._unset ?? new Set<keyof UnsetProps<T>>();
|
||||
this._unset.add(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
inc<K extends keyof IncProps<T>>(key: K, value: number): Updater<T> {
|
||||
this._inc = this._inc ?? new Map<keyof IncProps<T>, number>();
|
||||
|
||||
const prev = this._inc.get(key) ?? 0;
|
||||
this._inc.set(key, prev + value);
|
||||
return this;
|
||||
}
|
||||
|
||||
addToSet<K extends keyof AddToSetProps<T>>(key: K, value: ArrayElementType<AddToSetProps<T>[K]>): Updater<T> {
|
||||
this._addToSet = this._addToSet ?? new Map<keyof AddToSetProps<T>, any[]>();
|
||||
|
||||
const prev = this._addToSet.get(key) ?? [];
|
||||
this._addToSet.set(key, [...prev, value]);
|
||||
return this;
|
||||
}
|
||||
|
||||
hasChanges() {
|
||||
const filter = this.getRawUpdateFilter();
|
||||
return this._hasChanges(filter);
|
||||
}
|
||||
|
||||
private _hasChanges(filter: UpdateFilter<T>) {
|
||||
return Object.keys(filter).length > 0;
|
||||
}
|
||||
|
||||
public getRawUpdateFilter() {
|
||||
return {
|
||||
...(this._set && { $set: Object.fromEntries(this._set) }),
|
||||
...(this._unset && { $unset: Object.fromEntries([...this._unset.values()].map((k) => [k, 1])) }),
|
||||
...(this._inc && { $inc: Object.fromEntries(this._inc) }),
|
||||
...(this._addToSet && { $addToSet: Object.fromEntries([...this._addToSet.entries()].map(([k, v]) => [k, { $each: v }])) }),
|
||||
} as unknown as UpdateFilter<T>;
|
||||
}
|
||||
|
||||
getUpdateFilter() {
|
||||
if (this.dirty) {
|
||||
throw new Error('Updater is dirty');
|
||||
}
|
||||
this.dirty = true;
|
||||
const filter = this.getRawUpdateFilter();
|
||||
if (!this._hasChanges(filter)) {
|
||||
throw new Error('No changes to update');
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
|
||||
export type { Updater };
|
||||
9
packages/models/tsconfig.json
Normal file
9
packages/models/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@rocket.chat/tsconfig/server.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user