diff --git a/.changeset/real-zoos-cover.md b/.changeset/real-zoos-cover.md new file mode 100644 index 0000000000000..ea52fed9b8e6e --- /dev/null +++ b/.changeset/real-zoos-cover.md @@ -0,0 +1,11 @@ +--- +'@rocket.chat/federation-matrix': patch +'@rocket.chat/meteor': patch +--- + +Fixes federation endpoints rejecting valid requests, which broke: + +- room history backfill +- image thumbnails +- room message pagination +- accepting an invite from another homeserver diff --git a/.changeset/wide-laws-teach.md b/.changeset/wide-laws-teach.md new file mode 100644 index 0000000000000..6b3b675e3e959 --- /dev/null +++ b/.changeset/wide-laws-teach.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes text rendering without line breaks when its length is higher than the value of the `MESSAGE_MAX_PARSE_LENGTH` environment variable diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 2a397e502b9ad..5cd6e7e365d8e 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -334,7 +334,7 @@ export class AppLivechatBridge extends LivechatBridge { return this.orch .getConverters() ?.get('visitors') - .convertVisitor(await LivechatVisitors.getVisitorByToken(token, {})); + .convertVisitor(await LivechatVisitors.getVisitorByToken(token, {})); } protected async findVisitorByPhoneNumber(phoneNumber: string, appId: string): Promise { @@ -380,7 +380,7 @@ export class AppLivechatBridge extends LivechatBridge { return this.orch .getConverters() ?.get('departments') - .convertDepartment(await LivechatDepartment.findOneByIdOrName(value, {})); + .convertDepartment(await LivechatDepartment.findOneByIdOrName(value, {})); } protected async findDepartmentsEnabledWithAgents(appId: string): Promise> { diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx index 4c80489748a1f..dd2a45ac1663d 100644 --- a/apps/meteor/client/components/MarkdownText.tsx +++ b/apps/meteor/client/components/MarkdownText.tsx @@ -1,3 +1,4 @@ +import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; import type { ComponentProps } from 'react'; @@ -16,10 +17,22 @@ type MarkdownTextParams = { export type MarkdownTextProps = Partial; +const preserveLineBreaks = css` + white-space: pre-line; +`; + const MarkdownText = ({ content, withTruncatedText = false, variant, preserveHtml, parseEmoji, ...boxProps }: MarkdownTextProps) => { if (content && content.length > getMarkdownParserLimit()) { + // `document` parses with `breaks: true`, so its line breaks have to survive the unparsed + // fallback; the inline variants collapse them on purpose, and truncation needs a single line. + const keepLineBreaks = (variant ?? 'document') === 'document' && !withTruncatedText; + return ( - + {content} ); diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx index b1e7c3d9a95be..c9d0dc6c5c74b 100644 --- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx @@ -15,6 +15,7 @@ import AttachmentContent from './structure/AttachmentContent'; import AttachmentDetails from './structure/AttachmentDetails'; import AttachmentInner from './structure/AttachmentInner'; import AttachmentMessageLink from './structure/AttachmentMessageLink'; +import { toPlainTextRoot } from '../../../../lib/toPlainTextRoot'; // TODO: remove this team collaboration const quoteStyles = css` @@ -74,7 +75,7 @@ export const QuoteAttachment = ({ attachment, source }: QuoteAttachmentProps) => /> )} - {attachment.md ? : attachment.text.substring(attachment.text.indexOf('\n') + 1)} + diff --git a/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx b/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx index 53946c262366b..06f4200307011 100644 --- a/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx +++ b/apps/meteor/client/components/message/variants/threadPreview/ThreadMessagePreviewBody.tsx @@ -5,18 +5,29 @@ import type { Root } from '@rocket.chat/message-parser'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { toPlainTextRoot } from '../../../../lib/toPlainTextRoot'; import GazzodownText from '../../../GazzodownText'; export type ThreadMessagePreviewBodyProps = { message: IMessage; }; +function getMdTokens(message: IMessage): Root | undefined { + if (message.md) { + return [...message.md]; + } + + if (message.msg) { + return toPlainTextRoot(message.msg); + } +} + const ThreadMessagePreviewBody = ({ message }: ThreadMessagePreviewBodyProps) => { const { t } = useTranslation(); const isEncryptedMessage = isE2EEMessage(message); const getMessage = () => { - const mdTokens: Root | undefined = message.md && [...message.md]; + const mdTokens = getMdTokens(message); if ( message.attachments && Array.isArray(message.attachments) && diff --git a/apps/meteor/client/lib/normalizeThreadMessage.spec.tsx b/apps/meteor/client/lib/normalizeThreadMessage.spec.tsx index 1dba5d344dded..396a260249aaf 100644 --- a/apps/meteor/client/lib/normalizeThreadMessage.spec.tsx +++ b/apps/meteor/client/lib/normalizeThreadMessage.spec.tsx @@ -1,6 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { parse } from '@rocket.chat/message-parser'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import type { TFunction } from 'i18next'; import { getMarkdownParserLimit } from './getMarkdownParserLimit'; @@ -52,6 +52,24 @@ describe('normalizeThreadMessage', () => { expect(container.textContent).toContain('This message is longer than the limit'); }); + it('should render one block per line when the message exceeds the limit', () => { + mockedGetMarkdownParserLimit.mockReturnValue(5); + + const message = { msg: 'line one\nline two', mentions: [], attachments: [] } as unknown as IMessage; + const result = normalizeThreadMessage(message, t); + + expect(mockedParse).not.toHaveBeenCalled(); + + render(<>{result}); + + // `getByText` matches an element whose own text equals the query, so these only pass if each + // line got its own block. A single text node holding the `\n` would normalize to + // "line one line two" and neither query would match, which is exactly the collapsed rendering + // this guards against. + expect(screen.getByText('line one')).toBeInTheDocument(); + expect(screen.getByText('line two')).toBeInTheDocument(); + }); + it('should return null when msg is empty and no attachments', () => { const message = { msg: '', mentions: [], attachments: undefined } as unknown as IMessage; expect(normalizeThreadMessage(message, t)).toBeNull(); diff --git a/apps/meteor/client/lib/normalizeThreadMessage.tsx b/apps/meteor/client/lib/normalizeThreadMessage.tsx index 57c4168fea240..442d9057ecf2b 100644 --- a/apps/meteor/client/lib/normalizeThreadMessage.tsx +++ b/apps/meteor/client/lib/normalizeThreadMessage.tsx @@ -6,12 +6,13 @@ import { MessageTypes } from '@rocket.chat/message-types'; import type { TFunction } from 'i18next'; import { getMarkdownParserLimit } from './getMarkdownParserLimit'; +import { toPlainTextRoot } from './toPlainTextRoot'; import { filterMarkdown } from '../../app/markdown/lib/markdown'; import GazzodownText from '../components/GazzodownText'; const tryParseWithLimit = (text: string): Root | undefined => { if (text.length > getMarkdownParserLimit()) { - return [{ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: text }] }] as Root; + return toPlainTextRoot(text); } const filtered = filterMarkdown(text); diff --git a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts index d79a1a4a61c91..49fd6c15fd060 100644 --- a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts +++ b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts @@ -640,4 +640,35 @@ describe('parser limit handling', () => { expect(result.md).toBe(existingMd); }); + + it('should return one block per line when the message exceeds the limit', () => { + mockedGetMarkdownParserLimit.mockReturnValue(10); + + const result = parseMessageTextToAstMarkdown({ ...baseMessage, msg: 'line one\nline two' }, parseOptions, autoTranslateOptions); + + expect(result.md).toStrictEqual([ + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'line one' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'line two' }] }, + ]); + }); + + it('should keep blank lines as line breaks when the message exceeds the limit', () => { + mockedGetMarkdownParserLimit.mockReturnValue(10); + + const result = parseMessageTextToAstMarkdown({ ...baseMessage, msg: 'line one\n\nline three' }, parseOptions, autoTranslateOptions); + + expect(result.md).toStrictEqual([ + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'line one' }] }, + { type: 'LINE_BREAK', value: undefined }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'line three' }] }, + ]); + }); + + it('should drop a leading line break past the limit, as the parser path does', () => { + mockedGetMarkdownParserLimit.mockReturnValue(10); + + const result = parseMessageTextToAstMarkdown({ ...baseMessage, msg: '\nline one\nline two' }, parseOptions, autoTranslateOptions); + + expect(result.md[0]).toStrictEqual({ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'line one' }] }); + }); }); diff --git a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts index 4be0ed060e2b8..bd40ff0461f65 100644 --- a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts +++ b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts @@ -11,6 +11,7 @@ import type { Options, Root } from '@rocket.chat/message-parser'; import { parse } from '@rocket.chat/message-parser'; import { getMarkdownParserLimit } from './getMarkdownParserLimit'; +import { toPlainTextRoot } from './toPlainTextRoot'; import type { AutoTranslateOptions } from '../views/room/MessageList/hooks/useAutoTranslate'; import { isParsedMessage } from '../views/room/MessageList/lib/isParsedMessage'; @@ -136,11 +137,7 @@ const textToMessageToken = (textOrRoot: string | Root, parseOptions: Options): R return textOrRoot; } - if (textOrRoot.length > getMarkdownParserLimit()) { - return [{ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: textOrRoot }] }]; - } - - const parsedMessage = parse(textOrRoot, parseOptions); + const parsedMessage = textOrRoot.length > getMarkdownParserLimit() ? toPlainTextRoot(textOrRoot) : parse(textOrRoot, parseOptions); const parsedMessageCleaned = parsedMessage[0].type !== 'LINE_BREAK' ? parsedMessage : (parsedMessage.slice(1) as Root); diff --git a/apps/meteor/client/lib/toPlainTextRoot.spec.ts b/apps/meteor/client/lib/toPlainTextRoot.spec.ts new file mode 100644 index 0000000000000..03f0cca00c3e7 --- /dev/null +++ b/apps/meteor/client/lib/toPlainTextRoot.spec.ts @@ -0,0 +1,72 @@ +import { parse } from '@rocket.chat/message-parser'; + +import { toPlainTextRoot } from './toPlainTextRoot'; + +const paragraph = (value: string) => ({ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value }] }); +const lineBreak = { type: 'LINE_BREAK', value: undefined }; + +describe('toPlainTextRoot', () => { + it('should return an empty root for an empty string', () => { + expect(toPlainTextRoot('')).toEqual([]); + }); + + it('should wrap a single line into a paragraph', () => { + expect(toPlainTextRoot('hello')).toEqual([paragraph('hello')]); + }); + + it('should emit one paragraph per line', () => { + expect(toPlainTextRoot('line one\nline two')).toEqual([paragraph('line one'), paragraph('line two')]); + }); + + it('should emit a line break for a blank line', () => { + expect(toPlainTextRoot('line one\n\nline three')).toEqual([paragraph('line one'), lineBreak, paragraph('line three')]); + }); + + it('should emit consecutive line breaks for consecutive blank lines', () => { + expect(toPlainTextRoot('a\n\n\nb')).toEqual([paragraph('a'), lineBreak, lineBreak, paragraph('b')]); + }); + + it('should keep markdown syntax as literal text', () => { + expect(toPlainTextRoot('**bold** and _italic_')).toEqual([paragraph('**bold** and _italic_')]); + }); + + it('should keep mentions and emojis as literal text', () => { + expect(toPlainTextRoot('hey @rocket.cat :smile:')).toEqual([paragraph('hey @rocket.cat :smile:')]); + }); + + describe('line ending normalization', () => { + it('should normalize CRLF line endings', () => { + expect(toPlainTextRoot('line one\r\nline two')).toEqual([paragraph('line one'), paragraph('line two')]); + }); + + it('should normalize lone CR line endings', () => { + expect(toPlainTextRoot('line one\rline two')).toEqual([paragraph('line one'), paragraph('line two')]); + }); + }); + + describe('trailing line breaks', () => { + it('should ignore a single trailing line break', () => { + expect(toPlainTextRoot('hello\n')).toEqual([paragraph('hello')]); + }); + + it('should keep a blank line before a trailing line break', () => { + expect(toPlainTextRoot('hello\n\n')).toEqual([paragraph('hello'), lineBreak]); + }); + + it('should keep a leading blank line', () => { + expect(toPlainTextRoot('\nhello')).toEqual([lineBreak, paragraph('hello')]); + }); + }); + + // The whole point of the fallback is to render like the parser minus the markup, so for text that + // carries no syntax both must produce the same tree. If one of these ever fails, the parser is the + // source of truth and this helper is what should change. + describe('parity with the parser', () => { + it.each([['single line'], ['line one\nline two'], ['line one\n\nline three'], ['line one\n']])( + 'should match the parser output for %j', + (text) => { + expect(toPlainTextRoot(text)).toEqual(parse(text)); + }, + ); + }); +}); diff --git a/apps/meteor/client/lib/toPlainTextRoot.ts b/apps/meteor/client/lib/toPlainTextRoot.ts new file mode 100644 index 0000000000000..3aa1d7c14613d --- /dev/null +++ b/apps/meteor/client/lib/toPlainTextRoot.ts @@ -0,0 +1,27 @@ +import type { Root } from '@rocket.chat/message-parser'; + +/** + * Builds a `Root` that renders `text` verbatim. + * + * Line breaks are not part of the text a renderer receives: the parser turns them into structure + * (one `PARAGRAPH` per line, a `LINE_BREAK` per blank line), and HTML collapses any `\n` left inside + * a text node. So messages that skip parsing (the ones past `MESSAGE_MAX_PARSE_LENGTH`) have to be + * mapped to the same structure, or they render as a single line. + */ +export const toPlainTextRoot = (text: string): Root => { + if (!text) { + return []; + } + + // `marked` and `message-parser` both normalize line endings before parsing; without this a message + // pasted from Windows keeps a stray `\r` at the end of every line. + const lines = text.replace(/\r\n?/g, '\n').split('\n'); + + if (lines.length > 1 && lines[lines.length - 1] === '') { + lines.pop(); + } + + return lines.map((line) => + line ? { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: line }] } : { type: 'LINE_BREAK', value: undefined }, + ) as Root; +}; diff --git a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx index dd95c8644420a..aaf93c26ee9d0 100644 --- a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx +++ b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx @@ -27,6 +27,7 @@ import UiKitMessageBlock from '../../../../components/message/uikit/UiKitMessage import { useFormatDate } from '../../../../hooks/useFormatDate'; import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; import { useFormatTime } from '../../../../hooks/useFormatTime'; +import { toPlainTextRoot } from '../../../../lib/toPlainTextRoot'; import MessageReportInfo from '../MessageReportInfo'; import useDeleteMessage from '../hooks/useDeleteMessage'; import { useDismissMessageAction } from '../hooks/useDismissMessageAction'; @@ -92,7 +93,9 @@ const ContextMessage = ({ {message.e2e === 'pending' && t('E2E_message_encrypted_placeholder')} ) : ( - message.msg + !!message.msg && ( + + ) )} {!!attachments && } diff --git a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx index c1ef003e72bc3..075da3d89214b 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx +++ b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx @@ -3,7 +3,6 @@ import { Message as MessageTemplate, MessageLeftContainer, MessageContainer, - MessageBody, MessageDivider, MessageName, MessageUsername, @@ -30,6 +29,7 @@ import Attachments from '../../../../components/message/content/Attachments'; import UiKitMessageBlock from '../../../../components/message/uikit/UiKitMessageBlock'; import { useFormatDate } from '../../../../hooks/useFormatDate'; import { useFormatTime } from '../../../../hooks/useFormatTime'; +import { toPlainTextRoot } from '../../../../lib/toPlainTextRoot'; export type ContactHistoryMessageProps = { message: IMessage; @@ -117,14 +117,14 @@ const ContactHistoryMessage = ({ message, sequential, isNewDay, showUserAvatar } )} {!!quotes?.length && } - {!message.blocks && - (message.md ? ( - - ) : ( - - {message.msg} - - ))} + {!message.blocks && ( + + )} {message.blocks && } {!!attachments && } diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.ts b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.ts index 0bcbbccece740..1ea2bf249ad5e 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.ts @@ -5,6 +5,7 @@ import { useMemo } from 'react'; import { useAutoLinkDomains } from './useAutoLinkDomains'; import { useMessageListAutoTranslate } from '../../../../components/message/list/MessageListContext'; import { parseMessageTextToAstMarkdown } from '../../../../lib/parseMessageTextToAstMarkdown'; +import { toPlainTextRoot } from '../../../../lib/toPlainTextRoot'; export const useMessageBody = (message: IMessage | undefined): string | Root => { const autoTranslateOptions = useMessageListAutoTranslate(); @@ -27,7 +28,7 @@ export const useMessageBody = (message: IMessage | undefined): string | Root => } if (message.msg) { - return message.msg; + return toPlainTextRoot(message.msg); } if (message.attachments) { diff --git a/apps/meteor/client/views/room/modals/ReportMessageModal/ReportMessageModal.tsx b/apps/meteor/client/views/room/modals/ReportMessageModal/ReportMessageModal.tsx index 5f4daadc0138f..799c3358090cf 100644 --- a/apps/meteor/client/views/room/modals/ReportMessageModal/ReportMessageModal.tsx +++ b/apps/meteor/client/views/room/modals/ReportMessageModal/ReportMessageModal.tsx @@ -9,6 +9,8 @@ import { useTranslation } from 'react-i18next'; import MarkdownText from '../../../../components/MarkdownText'; import MessageContentBody from '../../../../components/message/MessageContentBody'; +import { getMarkdownParserLimit } from '../../../../lib/getMarkdownParserLimit'; +import { toPlainTextRoot } from '../../../../lib/toPlainTextRoot'; type ReportMessageModalsFields = { description: string; @@ -40,6 +42,8 @@ const ReportMessageModal = ({ message, onClose }: ReportMessageModalProps) => { const { _id } = message; + const md = message.md ?? (message.msg.length > getMarkdownParserLimit() ? toPlainTextRoot(message.msg) : undefined); + const handleReportMessage = async ({ description }: ReportMessageModalsFields): Promise => { try { await reportMessage({ messageId: _id, description }); @@ -60,7 +64,7 @@ const ReportMessageModal = ({ message, onClose }: ReportMessageModalProps) => { confirmText={t('Report')} > - {message.md ? : } + {md ? : } diff --git a/apps/meteor/ee/server/api/abac/schemas.ts b/apps/meteor/ee/server/api/abac/schemas.ts index 34c46e36b9591..a76bd77ef7e68 100644 --- a/apps/meteor/ee/server/api/abac/schemas.ts +++ b/apps/meteor/ee/server/api/abac/schemas.ts @@ -108,7 +108,7 @@ const GetAbacAttributesResponse = { }; export const GETAbacAttributesResponseSchema = ajv.compile<{ - attributes: IAbacAttribute[]; + attributes: Pick[]; offset: number; count: number; total: number; diff --git a/apps/meteor/ee/server/apps/marketplace/appRequestNotifyUsers.ts b/apps/meteor/ee/server/apps/marketplace/appRequestNotifyUsers.ts index 5c6ec5f3cf585..c57cceae47104 100644 --- a/apps/meteor/ee/server/apps/marketplace/appRequestNotifyUsers.ts +++ b/apps/meteor/ee/server/apps/marketplace/appRequestNotifyUsers.ts @@ -22,7 +22,7 @@ const notifyBatchOfUsers = async (appName: string, learnMoreUrl: string, appRequ return acc; }, []); - const msgFn = (user: IUser): string => { + const msgFn = (user: Pick): string => { const defaultLang = user.language || 'en'; const msg = `${i18n.t('App_request_enduser_message', { appName, learnmore: learnMoreUrl, lng: defaultLang })}`; diff --git a/apps/meteor/ee/server/apps/storage/AppRealStorage.ts b/apps/meteor/ee/server/apps/storage/AppRealStorage.ts index eba729edce941..516b58295bfef 100644 --- a/apps/meteor/ee/server/apps/storage/AppRealStorage.ts +++ b/apps/meteor/ee/server/apps/storage/AppRealStorage.ts @@ -74,7 +74,9 @@ export class AppRealStorage extends AppMetadataStorage { updateQuery.$unset = { permissionsGranted: 1 }; } - return this.db.findOneAndUpdate({ _id }, updateQuery, { returnDocument: 'after' }); + // TODO need to change method return type to IAppStorageItem | null, because findOneAndUpdate can return null if the document is not found. + // But for now, we are asserting that it will always return a document. + return this.db.findOneAndUpdate({ _id }, updateQuery, { returnDocument: 'after' }) as Promise; } public async updateStatus(_id: string, status: AppStatus): Promise { diff --git a/apps/meteor/ee/server/lib/audit/functions.ts b/apps/meteor/ee/server/lib/audit/functions.ts index c00464cf25865..ee71ba97dad8f 100644 --- a/apps/meteor/ee/server/lib/audit/functions.ts +++ b/apps/meteor/ee/server/lib/audit/functions.ts @@ -42,7 +42,7 @@ const getRoomInfoByAuditParams = async ({ if (type === 'l') { const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId }); - const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId( + const rooms = await LivechatRooms.findByVisitorIdAndAgentId( visitor, agent, { @@ -176,7 +176,7 @@ export const auditGetOmnichannelMessagesMethod = async ( // No livechat.applyRoomRestrictions here (unlike the type 'l' path in getRoomInfoByAuditParams): this // mirrors the original DDP auditGetOmnichannelMessages. Access is gated by the can-audit permission and // audit is intended to span all omnichannel rooms, so unit/visibility restrictions are not applied. - const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId(visitor, agent, { + const rooms = await LivechatRooms.findByVisitorIdAndAgentId(visitor, agent, { projection: { _id: 1 }, }).toArray(); // keep rids an array — `$in: undefined` throws "$in needs an array" when no rooms match diff --git a/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.ts b/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.ts index 165acb5e6c12e..a50e8734cb5de 100644 --- a/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.ts +++ b/apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.ts @@ -8,8 +8,8 @@ import { settings } from '../../../../server/settings'; // debounced function by roomId, so multiple calls within 2 seconds to same roomId runs only once const list: Record = {}; -const debounceByRoomId = function (fn: (room: IRoom) => Promise) { - return function (this: unknown, room: IRoom) { +const debounceByRoomId = function >(fn: (room: T) => Promise) { + return function (this: unknown, room: T) { clearTimeout(list[room._id]); list[room._id] = setTimeout(() => { void fn.call(this, room); @@ -18,7 +18,7 @@ const debounceByRoomId = function (fn: (room: IRoom) => Promise) { }; }; -const updateMessages = debounceByRoomId(async ({ _id, lm }: IRoom) => { +const updateMessages = debounceByRoomId(async ({ _id, lm }: Pick) => { // @TODO maybe store firstSubscription in room object so we don't need to call the above update method const firstSubscription = await Subscriptions.getMinimumLastSeenByRoomId(_id); if (!firstSubscription?.ls) { diff --git a/apps/meteor/ee/server/lib/omnichannel/business-hour/lib/business-hour.ts b/apps/meteor/ee/server/lib/omnichannel/business-hour/lib/business-hour.ts index ceeeaab84ba53..cca7abe4ba277 100644 --- a/apps/meteor/ee/server/lib/omnichannel/business-hour/lib/business-hour.ts +++ b/apps/meteor/ee/server/lib/omnichannel/business-hour/lib/business-hour.ts @@ -30,7 +30,7 @@ export async function findBusinessHours(userId: string, { offset, count, sort }: const businessHoursWithDepartments = await Promise.all( businessHours.map(async (businessHour) => { const currentDepartments = await LivechatDepartment.findByBusinessHourId(businessHour._id, { - projection: { _id: 1 }, + projection: { _id: 1, name: 1 }, }).toArray(); if (currentDepartments.length) { diff --git a/apps/meteor/ee/server/models/LivechatUnit.ts b/apps/meteor/ee/server/models/LivechatUnit.ts index a80893d622c15..2485b3a7d02c4 100644 --- a/apps/meteor/ee/server/models/LivechatUnit.ts +++ b/apps/meteor/ee/server/models/LivechatUnit.ts @@ -3,5 +3,4 @@ import { registerModel } from '@rocket.chat/models'; import { LivechatUnitRaw } from './raw/LivechatUnit'; import { db } from '../../../server/database/utils'; -// @ts-expect-error - Overriding base types :) registerModel('ILivechatUnitModel', new LivechatUnitRaw(db)); diff --git a/apps/meteor/ee/server/models/raw/CannedResponse.ts b/apps/meteor/ee/server/models/raw/CannedResponse.ts index 54ea4560e566b..48f021bd91a41 100644 --- a/apps/meteor/ee/server/models/raw/CannedResponse.ts +++ b/apps/meteor/ee/server/models/raw/CannedResponse.ts @@ -1,7 +1,7 @@ import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; -import type { ICannedResponseModel } from '@rocket.chat/model-typings'; +import type { ICannedResponseModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import { BaseRaw } from '@rocket.chat/models'; -import type { Db, DeleteResult, FindCursor, FindOptions, IndexDescription, UpdateFilter } from 'mongodb'; +import type { Db, DeleteResult, FindCursor, IndexDescription, UpdateFilter, Document } from 'mongodb'; // TODO need to define type for CannedResponse object export class CannedResponseRaw extends BaseRaw implements ICannedResponseModel { @@ -64,27 +64,36 @@ export class CannedResponseRaw extends BaseRaw imple return Object.assign(record, { _id }); } - override findOneById(_id: string, options?: FindOptions): Promise { + override findOneById< + P extends Document = IOmnichannelCannedResponse, + O extends FindOptionsWithProjection

= FindOptionsWithProjection

, + >(_id: string, options?: O): Promise | null> { const query = { _id }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneByShortcut(shortcut: string, options?: FindOptions): Promise { + findOneByShortcut = FindOptionsWithProjection>( + shortcut: string, + options?: O, + ): Promise | null> { const query = { shortcut, }; - return this.findOne(query, options); + return this.findOne(query, options); } - findByDepartmentId(departmentId: string, options?: FindOptions): FindCursor { + findByDepartmentId< + T extends Document = IOmnichannelCannedResponse, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(departmentId: string, options?: O): FindCursor> { const query = { scope: 'department', departmentId, }; - return this.find(query, options); + return this.find(query, options); } // REMOVE diff --git a/apps/meteor/ee/server/models/raw/LivechatDepartment.ts b/apps/meteor/ee/server/models/raw/LivechatDepartment.ts index 902e9bb0e54f6..489b344c33287 100644 --- a/apps/meteor/ee/server/models/raw/LivechatDepartment.ts +++ b/apps/meteor/ee/server/models/raw/LivechatDepartment.ts @@ -1,5 +1,5 @@ import type { ILivechatDepartment, RocketChatRecordDeleted, LivechatDepartmentDTO } from '@rocket.chat/core-typings'; -import type { ILivechatDepartmentModel } from '@rocket.chat/model-typings'; +import type { ILivechatDepartmentModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import { LivechatDepartmentRaw } from '@rocket.chat/models'; import type { Collection, Document, FindCursor, FindOptions, UpdateResult, Db, AggregationCursor } from 'mongodb'; @@ -33,7 +33,10 @@ export class LivechatDepartmentEE extends LivechatDepartmentRaw implements ILive return this.updateMany({ parentId: id }, { $unset: { parentId: 1 }, $pull: { ancestors: id } }); } - override findActiveByUnitIds(unitIds: string[], options: FindOptions = {}): FindCursor { + override findActiveByUnitIds< + T extends Document = ILivechatDepartment, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(unitIds: string[], options?: O): FindCursor> { const query = { enabled: true, numAgents: { $gt: 0 }, @@ -43,7 +46,7 @@ export class LivechatDepartmentEE extends LivechatDepartmentRaw implements ILive }, }; - return this.find(query, options); + return this.find(query, options); } override findEnabledWithAgentsAndBusinessUnit( diff --git a/apps/meteor/ee/server/models/raw/LivechatUnit.ts b/apps/meteor/ee/server/models/raw/LivechatUnit.ts index 6b3c1529d0d70..75fbf07b63d19 100644 --- a/apps/meteor/ee/server/models/raw/LivechatUnit.ts +++ b/apps/meteor/ee/server/models/raw/LivechatUnit.ts @@ -1,7 +1,7 @@ import type { IOmnichannelBusinessUnit, ILivechatDepartment } from '@rocket.chat/core-typings'; -import type { FindPaginated, ILivechatUnitModel } from '@rocket.chat/model-typings'; +import type { FindPaginated, ILivechatUnitModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import { LivechatUnitMonitors, LivechatDepartment, LivechatRooms, BaseRaw } from '@rocket.chat/models'; -import type { FindOptions, Filter, FindCursor, Db, FilterOperators, UpdateResult, DeleteResult, Document, UpdateFilter } from 'mongodb'; +import type { Filter, FindCursor, Db, FilterOperators, UpdateResult, DeleteResult, Document, UpdateFilter } from 'mongodb'; const addQueryRestrictions = async (originalQuery: Filter = {}, unitsFromUser?: string[]) => { const query: FilterOperators = { ...originalQuery, type: 'u' }; @@ -21,32 +21,28 @@ export class LivechatUnitRaw extends BaseRaw implement super(db, 'livechat_department'); } - findPaginatedUnits( + findPaginatedUnits = FindOptionsWithProjection>( query: Filter, - options?: FindOptions, - ): FindPaginated> { - return super.findPaginated({ ...query, type: 'u' }, options); + options?: O, + ): FindPaginated>> { + return super.findPaginated({ ...query, type: 'u' }, options); } // @ts-expect-error - Overriding base types :) - async findOne

( + async findOne

= FindOptionsWithProjection

>( originalQuery: Filter, - options: FindOptions, + options?: O, extra?: Record, - ): Promise

{ + ): Promise | null> { const query = await addQueryRestrictions(originalQuery, extra?.unitsFromUser); - return this.col.findOne

(query, options); + return super.findOne(query, options); } - override async findOneById

( - _id: IOmnichannelBusinessUnit['_id'], - options: FindOptions, - extra?: Record, - ): Promise

{ - if (options) { - return this.findOne

({ _id }, options, extra); - } - return this.findOne

({ _id }, {}, extra); + override async findOneById< + P extends Document = IOmnichannelBusinessUnit, + O extends FindOptionsWithProjection

= FindOptionsWithProjection

, + >(_id: IOmnichannelBusinessUnit['_id'], options?: O, extra?: Record): Promise | null> { + return this.findOne({ _id }, options, extra); } async createOrUpdateUnit( @@ -162,7 +158,10 @@ export class LivechatUnitRaw extends BaseRaw implement return result; } - findOneByIdOrName(_idOrName: string, options: FindOptions): Promise { + findOneByIdOrName = FindOptionsWithProjection>( + _idOrName: string, + options?: O, + ): Promise | null> { const query = { $or: [ { @@ -174,7 +173,7 @@ export class LivechatUnitRaw extends BaseRaw implement ], }; - return this.findOne(query, options); + return this.findOne(query, options); } async findByMonitorId(monitorId: string): Promise { diff --git a/apps/meteor/ee/server/models/raw/ServiceLevelAgreements.ts b/apps/meteor/ee/server/models/raw/ServiceLevelAgreements.ts index 440e26bc0f164..d72578b35e591 100644 --- a/apps/meteor/ee/server/models/raw/ServiceLevelAgreements.ts +++ b/apps/meteor/ee/server/models/raw/ServiceLevelAgreements.ts @@ -1,5 +1,5 @@ import type { IOmnichannelServiceLevelAgreements } from '@rocket.chat/core-typings'; -import type { IOmnichannelServiceLevelAgreementsModel } from '@rocket.chat/model-typings/src'; +import type { IOmnichannelServiceLevelAgreementsModel } from '@rocket.chat/model-typings'; import { BaseRaw } from '@rocket.chat/models'; import type { Db, IndexDescription } from 'mongodb'; diff --git a/apps/meteor/ee/server/settings/settings.ts b/apps/meteor/ee/server/settings/settings.ts index 26f5afa6bc184..ab8aa6e1643b5 100644 --- a/apps/meteor/ee/server/settings/settings.ts +++ b/apps/meteor/ee/server/settings/settings.ts @@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { settings, SettingsEvents } from '../../../server/settings'; import { use } from '../../../server/settings/Middleware'; -export function changeSettingValue(record: ISetting): SettingValue { +export function changeSettingValue(record: Pick): SettingValue { if (!record.enterprise) { return record.value; } @@ -40,7 +40,7 @@ settings.set = use(settings.set, (context, next) => { return next({ ...record, value }); }); -SettingsEvents.on('fetch-settings', (settings: Array): void => { +SettingsEvents.on('fetch-settings', (settings): void => { for (const setting of settings) { const changedValue = changeSettingValue(setting); if (changedValue === undefined) { diff --git a/apps/meteor/ee/tests/unit/server/hooks/messages/BeforeSaveCannedResponse.tests.ts b/apps/meteor/ee/tests/unit/server/hooks/messages/BeforeSaveCannedResponse.tests.ts index dcb7da272fa56..3971e956369fc 100644 --- a/apps/meteor/ee/tests/unit/server/hooks/messages/BeforeSaveCannedResponse.tests.ts +++ b/apps/meteor/ee/tests/unit/server/hooks/messages/BeforeSaveCannedResponse.tests.ts @@ -39,7 +39,7 @@ class LivechatVisitorsModel extends BaseRaw { } class UsersModel extends BaseRaw { - override async findOneById() { + override async findOneById(): Promise { return { name: 'John Doe Agent', }; diff --git a/apps/meteor/server/api/lib/webdav.ts b/apps/meteor/server/api/lib/webdav.ts index db0c47a64e2d5..e6fcfb3841e75 100644 --- a/apps/meteor/server/api/lib/webdav.ts +++ b/apps/meteor/server/api/lib/webdav.ts @@ -1,7 +1,7 @@ -import type { IWebdavAccount } from '@rocket.chat/core-typings'; +import type { IWebdavAccountIntegration } from '@rocket.chat/core-typings'; import { WebdavAccounts } from '@rocket.chat/models'; -export async function findWebdavAccountsByUserId({ uid }: { uid: string }): Promise { +export async function findWebdavAccountsByUserId({ uid }: { uid: string }): Promise { return WebdavAccounts.findWithUserId(uid, { projection: { _id: 1, diff --git a/apps/meteor/server/api/v1/middlewares/authentication.ts b/apps/meteor/server/api/v1/middlewares/authentication.ts index fb6ee1df460ff..b74ad1d248191 100644 --- a/apps/meteor/server/api/v1/middlewares/authentication.ts +++ b/apps/meteor/server/api/v1/middlewares/authentication.ts @@ -25,7 +25,8 @@ export function authenticationMiddleware( const { 'x-user-id': userId, 'x-auth-token': authToken } = req.headers; if (userId && authToken) { - req.user = (await Users.findOneByIdAndLoginToken(userId as string, hashLoginToken(authToken as string))) || undefined; + const user = await Users.findOneByIdAndLoginToken(userId as string, hashLoginToken(authToken as string)); + req.user = user || undefined; } else { const { authorization } = req.headers; const accessToken = typeof req.query.access_token === 'string' ? req.query.access_token : undefined; diff --git a/apps/meteor/server/api/v1/omnichannel/lib/departments.ts b/apps/meteor/server/api/v1/omnichannel/lib/departments.ts index 7c66970975fe9..f18deedb849a2 100644 --- a/apps/meteor/server/api/v1/omnichannel/lib/departments.ts +++ b/apps/meteor/server/api/v1/omnichannel/lib/departments.ts @@ -141,7 +141,7 @@ export async function findDepartmentsToAutocomplete({ selector, onlyMyDepartments = false, showArchived = false, -}: FindDepartmentToAutocompleteParams): Promise<{ items: ILivechatDepartment[] }> { +}: FindDepartmentToAutocompleteParams): Promise<{ items: Pick[] }> { const { exceptions = [] } = selector; let { conditions = {} } = selector; diff --git a/apps/meteor/server/api/v1/omnichannel/lib/livechat.ts b/apps/meteor/server/api/v1/omnichannel/lib/livechat.ts index 6f13432f04f77..c161ea82b62e3 100644 --- a/apps/meteor/server/api/v1/omnichannel/lib/livechat.ts +++ b/apps/meteor/server/api/v1/omnichannel/lib/livechat.ts @@ -52,7 +52,9 @@ export function findGuest(token: string): Promise { return LivechatVisitors.getVisitorByToken(token); } -export function findGuestWithoutActivity(token: string): Promise { +export function findGuestWithoutActivity( + token: string, +): Promise | null> { return LivechatVisitors.getVisitorByToken(token, { projection: { name: 1, username: 1, token: 1, visitorEmails: 1, department: 1 } }); } diff --git a/apps/meteor/server/api/v1/omnichannel/lib/rooms.ts b/apps/meteor/server/api/v1/omnichannel/lib/rooms.ts index 98c0ac9eb888c..6c758f57699b2 100644 --- a/apps/meteor/server/api/v1/omnichannel/lib/rooms.ts +++ b/apps/meteor/server/api/v1/omnichannel/lib/rooms.ts @@ -73,7 +73,7 @@ export async function findRooms({ projection: { name: 1 }, }).toArray(); - rooms.forEach((room: IOmnichannelRoom & { department?: ILivechatDepartment }) => { + rooms.forEach((room: IOmnichannelRoom & { department?: Pick }) => { if (!room.departmentId) { return; } diff --git a/apps/meteor/server/api/v1/omnichannel/statistics.ts b/apps/meteor/server/api/v1/omnichannel/statistics.ts index cece06069173e..e3d6e3c67cb3c 100644 --- a/apps/meteor/server/api/v1/omnichannel/statistics.ts +++ b/apps/meteor/server/api/v1/omnichannel/statistics.ts @@ -49,7 +49,7 @@ API.v1.addRoute( throw new Error('invalid-chart-name'); } - const user = await Users.findOneById(this.userId, { projection: { _id: 1, utcOffset: 1 } }); + const user = await Users.findOneById(this.userId, { projection: { _id: 1, utcOffset: 1, language: 1 } }); const language = user?.language || settings.get('Language') || 'en'; return API.v1.success( diff --git a/apps/meteor/server/api/v1/settings.ts b/apps/meteor/server/api/v1/settings.ts index 7d7076e0c2038..a254056e609f7 100644 --- a/apps/meteor/server/api/v1/settings.ts +++ b/apps/meteor/server/api/v1/settings.ts @@ -32,6 +32,7 @@ import { disableCustomScripts } from '../../lib/shared/disableCustomScripts'; import { addOAuthServiceMethod } from '../../meteor-methods/auth/addOAuthService'; import { removeCustomOAuthSettings } from '../../meteor-methods/auth/removeOAuthService'; import { SettingsEvents, settings } from '../../settings'; +import type { FetchedSetting } from '../../settings/SettingsRegistry'; import { checkSettingValueBounds } from '../../settings/checkSettingValueBonds'; import { updateAuditedByUser } from '../../settings/lib/auditedSettingUpdates'; import { saveSettingsBulk } from '../../settings/lib/saveSettingsBulk'; @@ -45,7 +46,7 @@ async function fetchSettings( offset: FindOptions['skip'], count: FindOptions['limit'], fields: FindOptions['projection'], -): Promise<{ settings: ISetting[]; totalCount: number }> { +): Promise<{ settings: FetchedSetting[]; totalCount: number }> { const { cursor, totalCount } = Settings.findPaginated(query || {}, { sort: sort || { _id: 1 }, skip: offset, @@ -59,7 +60,7 @@ async function fetchSettings( return { settings: settingsList, totalCount: total }; } -const settingsPublicResponseSchema = ajv.compile<{ settings: ISetting[]; count: number; offset: number; total: number }>({ +const settingsPublicResponseSchema = ajv.compile<{ settings: FetchedSetting[]; count: number; offset: number; total: number }>({ type: 'object', properties: { settings: { type: 'array', items: { type: 'object' } }, @@ -89,7 +90,7 @@ const addCustomOAuthBodySchema = ajv.compile<{ name: string }>({ additionalProperties: false, }); -const settingsListResponseSchema = ajv.compile<{ settings: ISetting[]; count: number; offset: number; total: number }>({ +const settingsListResponseSchema = ajv.compile<{ settings: FetchedSetting[]; count: number; offset: number; total: number }>({ type: 'object', properties: { settings: { type: 'array', items: { type: 'object' } }, diff --git a/apps/meteor/server/hooks/messages/propagateDiscussionMetadata.ts b/apps/meteor/server/hooks/messages/propagateDiscussionMetadata.ts index 389d034b5f600..629426011d1f3 100644 --- a/apps/meteor/server/hooks/messages/propagateDiscussionMetadata.ts +++ b/apps/meteor/server/hooks/messages/propagateDiscussionMetadata.ts @@ -12,7 +12,10 @@ import { deleteRoom } from '../../lib/rooms/deleteRoom'; * have to be applied on top of the stored room, otherwise the discussion metadata would always * be left one message behind. */ -const withPendingRoomChanges = (room: IRoom, roomUpdater?: Updater): IRoom => { +const withPendingRoomChanges = ( + room: Pick, + roomUpdater?: Updater, +): Pick => { const { $inc, $set } = roomUpdater?.getRawUpdateFilter() ?? {}; const pendingMsgs = typeof $inc?.msgs === 'number' ? $inc.msgs : 0; const pendingLm = $set?.lm instanceof Date ? $set.lm : undefined; diff --git a/apps/meteor/server/lib/2fa/code/EmailCheck.ts b/apps/meteor/server/lib/2fa/code/EmailCheck.ts index fa00fafc0a7d2..4572a093f1b4f 100644 --- a/apps/meteor/server/lib/2fa/code/EmailCheck.ts +++ b/apps/meteor/server/lib/2fa/code/EmailCheck.ts @@ -1,10 +1,10 @@ -import { isOAuthUser, type IUser } from '@rocket.chat/core-typings'; +import { isOAuthUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import bcrypt from 'bcrypt'; import { Accounts } from 'meteor/accounts-base'; -import type { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; +import type { ICodeCheck, IProcessInvalidCodeResult, TwoFactorUser } from './ICodeCheck'; import { settings } from '../../../settings'; import { i18n } from '../../i18n'; import * as Mailer from '../../notifications/email/api'; @@ -12,14 +12,14 @@ import * as Mailer from '../../notifications/email/api'; export class EmailCheck implements ICodeCheck { public readonly name: string = 'email'; - private getUserVerifiedEmails(user: IUser): string[] { + private getUserVerifiedEmails(user: TwoFactorUser): string[] { if (!Array.isArray(user.emails)) { return []; } return user.emails.filter(({ verified }) => verified).map((e) => e.address); } - public isEnabled(user: IUser): boolean { + public isEnabled(user: TwoFactorUser): boolean { if (!settings.get('Accounts_TwoFactorAuthentication_By_Email_Enabled')) { return false; } @@ -35,7 +35,7 @@ export class EmailCheck implements ICodeCheck { return this.getUserVerifiedEmails(user).length > 0; } - private async send2FAEmail(address: string, random: string, user: IUser): Promise { + private async send2FAEmail(address: string, random: string, user: TwoFactorUser): Promise { const language = user.language || settings.get('Language') || 'en'; const t = i18n.getFixedT(language); @@ -68,7 +68,7 @@ ${t('If_you_didnt_try_to_login_in_your_account_please_ignore_this_email')} }); } - public async verify(user: IUser, codeFromEmail: string): Promise { + public async verify(user: TwoFactorUser, codeFromEmail: string): Promise { if (!this.isEnabled(user)) { return false; } @@ -96,7 +96,7 @@ ${t('If_you_didnt_try_to_login_in_your_account_please_ignore_this_email')} return false; } - public async sendEmailCode(user: IUser): Promise { + public async sendEmailCode(user: TwoFactorUser): Promise { const emails = this.getUserVerifiedEmails(user); const random = Random._randomString(6, '0123456789'); const encryptedRandom = await bcrypt.hash(random, Accounts._bcryptRounds()); @@ -112,7 +112,7 @@ ${t('If_you_didnt_try_to_login_in_your_account_please_ignore_this_email')} } } - public async processInvalidCode(user: IUser): Promise { + public async processInvalidCode(user: TwoFactorUser): Promise { await Users.removeExpiredEmailCodeOfUserId(user._id); // Generate new code if the there isn't any code with more than 5 minutes to expire @@ -143,7 +143,7 @@ ${t('If_you_didnt_try_to_login_in_your_account_please_ignore_this_email')} }; } - public async maxFaildedAttemtpsReached(user: IUser) { + public async maxFaildedAttemtpsReached(user: TwoFactorUser) { const maxAttempts = settings.get('Accounts_TwoFactorAuthentication_Max_Invalid_Email_Code_Attempts'); return Users.maxInvalidEmailCodeAttemptsReached(user._id, maxAttempts); } diff --git a/apps/meteor/server/lib/2fa/code/EmailCheckForOAuth.ts b/apps/meteor/server/lib/2fa/code/EmailCheckForOAuth.ts index 1b0c190b29792..83144ef4b29d5 100644 --- a/apps/meteor/server/lib/2fa/code/EmailCheckForOAuth.ts +++ b/apps/meteor/server/lib/2fa/code/EmailCheckForOAuth.ts @@ -1,21 +1,21 @@ -import type { IUser } from '@rocket.chat/core-typings'; import { TwoFactorChallenges } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { EmailCheck } from './EmailCheck'; +import type { TwoFactorUser } from './ICodeCheck'; export class EmailCheckForOAuth extends EmailCheck { public override readonly name = 'email-oauth'; public readonly method = 'email'; - public async sendTwoFactorChallenge(user: IUser): Promise { + public async sendTwoFactorChallenge(user: TwoFactorUser): Promise { const challengeId = await TwoFactorChallenges.createTwoFactorChallenge(user._id, 'email'); await this.sendEmailCode(user); return challengeId; } - public async verifyEmailTwoFactorChallenge(user: IUser, challengeId: string, code: string): Promise { + public async verifyEmailTwoFactorChallenge(user: TwoFactorUser, challengeId: string, code: string): Promise { const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId); if (!challenge) { return false; diff --git a/apps/meteor/server/lib/2fa/code/ICodeCheck.ts b/apps/meteor/server/lib/2fa/code/ICodeCheck.ts index 1cd1ba68e0db9..96908166ba770 100644 --- a/apps/meteor/server/lib/2fa/code/ICodeCheck.ts +++ b/apps/meteor/server/lib/2fa/code/ICodeCheck.ts @@ -1,5 +1,8 @@ import type { IUser } from '@rocket.chat/core-typings'; +/** The user fields the 2FA checks read; `getUserForCheck` projects exactly these. */ +export type TwoFactorUser = Pick; + export interface IProcessInvalidCodeResult { codeGenerated: boolean; codeExpires?: Date; @@ -9,11 +12,11 @@ export interface IProcessInvalidCodeResult { export interface ICodeCheck { readonly name: string; - isEnabled(user: IUser, force?: boolean): boolean; + isEnabled(user: TwoFactorUser, force?: boolean): boolean; - verify(user: IUser, code: string, force?: boolean): Promise; + verify(user: TwoFactorUser, code: string, force?: boolean): Promise; - processInvalidCode(user: IUser): Promise; + processInvalidCode(user: TwoFactorUser): Promise; - maxFaildedAttemtpsReached(user: IUser): Promise; + maxFaildedAttemtpsReached(user: TwoFactorUser): Promise; } diff --git a/apps/meteor/server/lib/2fa/code/PasswordCheckFallback.ts b/apps/meteor/server/lib/2fa/code/PasswordCheckFallback.ts index d99e6a8788456..7bc6948bb6615 100644 --- a/apps/meteor/server/lib/2fa/code/PasswordCheckFallback.ts +++ b/apps/meteor/server/lib/2fa/code/PasswordCheckFallback.ts @@ -1,14 +1,13 @@ -import type { IUser } from '@rocket.chat/core-typings'; import { Accounts } from 'meteor/accounts-base'; import type { Meteor } from 'meteor/meteor'; -import type { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; +import type { ICodeCheck, IProcessInvalidCodeResult, TwoFactorUser } from './ICodeCheck'; import { settings } from '../../../settings'; export class PasswordCheckFallback implements ICodeCheck { public readonly name = 'password'; - public isEnabled(user: IUser, force: boolean): boolean { + public isEnabled(user: TwoFactorUser, force: boolean): boolean { if (force) { return true; } @@ -20,7 +19,7 @@ export class PasswordCheckFallback implements ICodeCheck { return false; } - public async verify(user: IUser, code: string, force: boolean): Promise { + public async verify(user: TwoFactorUser, code: string, force: boolean): Promise { if (!this.isEnabled(user, force)) { return false; } @@ -43,7 +42,7 @@ export class PasswordCheckFallback implements ICodeCheck { }; } - public async maxFaildedAttemtpsReached(_user: IUser): Promise { + public async maxFaildedAttemtpsReached(_user: TwoFactorUser): Promise { return false; } } diff --git a/apps/meteor/server/lib/2fa/code/TOTPCheck.ts b/apps/meteor/server/lib/2fa/code/TOTPCheck.ts index 4624bdef4082b..cf9dd185b7035 100644 --- a/apps/meteor/server/lib/2fa/code/TOTPCheck.ts +++ b/apps/meteor/server/lib/2fa/code/TOTPCheck.ts @@ -1,13 +1,11 @@ -import type { IUser } from '@rocket.chat/core-typings'; - -import type { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; +import type { ICodeCheck, IProcessInvalidCodeResult, TwoFactorUser } from './ICodeCheck'; import { settings } from '../../../settings'; import { TOTP } from '../lib/totp'; export class TOTPCheck implements ICodeCheck { public readonly name: string = 'totp'; - public isEnabled(user: IUser): boolean { + public isEnabled(user: TwoFactorUser): boolean { if (!settings.get('Accounts_TwoFactorAuthentication_By_TOTP_Enabled')) { return false; } @@ -15,7 +13,7 @@ export class TOTPCheck implements ICodeCheck { return user.services?.totp?.enabled === true; } - public async verify(user: IUser, code: string): Promise { + public async verify(user: TwoFactorUser, code: string): Promise { if (!this.isEnabled(user)) { return false; } @@ -39,7 +37,7 @@ export class TOTPCheck implements ICodeCheck { }; } - public async maxFaildedAttemtpsReached(_user: IUser): Promise { + public async maxFaildedAttemtpsReached(_user: TwoFactorUser): Promise { return false; } } diff --git a/apps/meteor/server/lib/2fa/code/TOTPCheckForOAuth.ts b/apps/meteor/server/lib/2fa/code/TOTPCheckForOAuth.ts index 8197903a57a85..7620c77e21ddb 100644 --- a/apps/meteor/server/lib/2fa/code/TOTPCheckForOAuth.ts +++ b/apps/meteor/server/lib/2fa/code/TOTPCheckForOAuth.ts @@ -1,7 +1,7 @@ -import type { IUser } from '@rocket.chat/core-typings'; import { TwoFactorChallenges } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; +import type { TwoFactorUser } from './ICodeCheck'; import { TOTPCheck } from './TOTPCheck'; export class TOTPCheckForOAuth extends TOTPCheck { @@ -9,11 +9,11 @@ export class TOTPCheckForOAuth extends TOTPCheck { public readonly method = 'totp'; - public async sendTwoFactorChallenge(user: IUser): Promise { + public async sendTwoFactorChallenge(user: TwoFactorUser): Promise { return TwoFactorChallenges.createTwoFactorChallenge(user._id, 'totp'); } - public async verifyEmailTwoFactorChallenge(user: IUser, challengeId: string, code: string): Promise { + public async verifyEmailTwoFactorChallenge(user: TwoFactorUser, challengeId: string, code: string): Promise { const challenge = await TwoFactorChallenges.findOneByPendingChallengeId(challengeId); if (!challenge) { return false; diff --git a/apps/meteor/server/lib/2fa/code/index.ts b/apps/meteor/server/lib/2fa/code/index.ts index 6d924fb8e8cfe..619bd1065b816 100644 --- a/apps/meteor/server/lib/2fa/code/index.ts +++ b/apps/meteor/server/lib/2fa/code/index.ts @@ -6,7 +6,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { EmailCheck } from './EmailCheck'; -import type { ICodeCheck } from './ICodeCheck'; +import type { ICodeCheck, TwoFactorUser } from './ICodeCheck'; import { PasswordCheckFallback } from './PasswordCheckFallback'; import { TOTPCheck } from './TOTPCheck'; import { settings } from '../../../settings'; @@ -27,7 +27,7 @@ const checkMethods = new Map(); checkMethods.set(totpCheck.name, totpCheck); checkMethods.set(emailCheck.name, emailCheck); -function getMethodByNameOrFirstActiveForUser(user: IUser, name?: string): ICodeCheck | undefined { +function getMethodByNameOrFirstActiveForUser(user: TwoFactorUser, name?: string): ICodeCheck | undefined { if (name && checkMethods.has(name)) { return checkMethods.get(name); } @@ -35,7 +35,7 @@ function getMethodByNameOrFirstActiveForUser(user: IUser, name?: string): ICodeC return Array.from(checkMethods.values()).find((method) => method.isEnabled(user)); } -function getAvailableMethodNames(user: IUser): string[] { +function getAvailableMethodNames(user: TwoFactorUser): string[] { return ( Array.from(checkMethods) .filter(([, method]) => method.isEnabled(user)) @@ -43,9 +43,10 @@ function getAvailableMethodNames(user: IUser): string[] { ); } -export async function getUserForCheck(userId: string): Promise { +export async function getUserForCheck(userId: string): Promise { return Users.findOneById(userId, { projection: { + username: 1, emails: 1, language: 1, createdAt: 1, @@ -76,7 +77,7 @@ export function getRememberDate(from: Date = new Date()): Date | undefined { return expires; } -function isAuthorizedForToken(connection: IMethodConnection, user: IUser, options: ITwoFactorOptions): boolean { +function isAuthorizedForToken(connection: IMethodConnection, user: TwoFactorUser, options: ITwoFactorOptions): boolean { // Resolve the current login token from both transports: // - DDP: the login flow registers it in `Accounts._accountData`, read via `_getLoginToken`. // - REST: it is not registered in account data, so it is carried on `connection.token`. @@ -140,7 +141,7 @@ export async function rememberAuthorizationByToken(token: string, userId: IUser[ await Users.setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(user._id, token, getFingerprintFromConnection(connection), expires); } -async function rememberAuthorization(connection: IMethodConnection, user: IUser): Promise { +async function rememberAuthorization(connection: IMethodConnection, user: TwoFactorUser): Promise { // Same dual-transport resolution as `isAuthorizedForToken`: DDP reads from `Accounts._accountData` // via `_getLoginToken`, REST falls back to the token carried on `connection.token`. const currentToken = Accounts._getLoginToken(connection.id) || connection.token; @@ -163,14 +164,18 @@ async function rememberAuthorization(connection: IMethodConnection, user: IUser) } interface ICheckCodeForUser { - user: IUser | string; + user: TwoFactorUser | string; code?: string; method?: string; options?: ITwoFactorOptions; connection?: IMethodConnection; } -export const getSecondFactorMethod = (user: IUser, method: string | undefined, options: ITwoFactorOptions): ICodeCheck | undefined => { +export const getSecondFactorMethod = ( + user: TwoFactorUser, + method: string | undefined, + options: ITwoFactorOptions, +): ICodeCheck | undefined => { // try first getting one of the available methods or the one that was already provided const selectedMethod = getMethodByNameOrFirstActiveForUser(user, method); if (selectedMethod) { @@ -197,7 +202,7 @@ export async function checkCodeForUser({ user, code, method, options = {}, conne return true; } - let existingUser: IUser | null; + let existingUser: TwoFactorUser | null; if (typeof user === 'string') { existingUser = await getUserForCheck(user); } else { diff --git a/apps/meteor/server/lib/messaging/getHiddenSystemMessages.ts b/apps/meteor/server/lib/messaging/getHiddenSystemMessages.ts index 08f52620e0805..024197dbdf076 100644 --- a/apps/meteor/server/lib/messaging/getHiddenSystemMessages.ts +++ b/apps/meteor/server/lib/messaging/getHiddenSystemMessages.ts @@ -1,6 +1,6 @@ import type { MessageTypesValues, IRoom } from '@rocket.chat/core-typings'; -export const getHiddenSystemMessages = (room: IRoom, hiddenSystemMessages: MessageTypesValues[]): MessageTypesValues[] => { +export const getHiddenSystemMessages = (room: Pick, hiddenSystemMessages: MessageTypesValues[]): MessageTypesValues[] => { const hiddenTypes = hiddenSystemMessages.reduce((array, value): MessageTypesValues[] => { const newValue: MessageTypesValues[] = value === 'mute_unmute' ? ['user-muted', 'user-unmuted'] : [value]; return [...array, ...newValue]; diff --git a/apps/meteor/server/lib/omnichannel/QueueManager.ts b/apps/meteor/server/lib/omnichannel/QueueManager.ts index 16a8790d1b642..b73e2135357b4 100644 --- a/apps/meteor/server/lib/omnichannel/QueueManager.ts +++ b/apps/meteor/server/lib/omnichannel/QueueManager.ts @@ -58,7 +58,8 @@ export const saveQueueInquiry = async (inquiry: ILivechatInquiryRecord) => { * @deprecated */ export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent?: SelectedAgent) => { - const room = await LivechatRooms.findOneById(inquiry.rid, { projection: { v: 1 } }); + // No projection: requeueInquiry forwards the room into the routing pipeline, which expects a full room. + const room = await LivechatRooms.findOneById(inquiry.rid); if (!room) { await saveQueueInquiry(inquiry); diff --git a/apps/meteor/server/lib/omnichannel/analytics/dashboards.ts b/apps/meteor/server/lib/omnichannel/analytics/dashboards.ts index 619dfd1faf6db..942f4459bb009 100644 --- a/apps/meteor/server/lib/omnichannel/analytics/dashboards.ts +++ b/apps/meteor/server/lib/omnichannel/analytics/dashboards.ts @@ -36,7 +36,7 @@ const getProductivityMetricsAsync = async ({ start: string; end: string; departmentId?: string; - user: IUser; + user: Pick; }) => { if (!start || !end) { throw new Error('"start" and "end" must be provided'); @@ -83,7 +83,7 @@ const getAgentsProductivityMetricsAsync = async ({ start: string; end: string; departmentId?: string; - user: IUser; + user: Pick; }) => { if (!start || !end) { throw new Error('"start" and "end" must be provided'); @@ -230,7 +230,7 @@ const getConversationsMetricsAsync = async ({ start: string; end: string; departmentId?: string; - user: IUser; + user: Pick; }) => { if (!start || !end) { throw new Error('"start" and "end" must be provided'); diff --git a/apps/meteor/server/lib/omnichannel/business-hour/Helper.ts b/apps/meteor/server/lib/omnichannel/business-hour/Helper.ts index 7a431af506b24..237988f74c1ff 100644 --- a/apps/meteor/server/lib/omnichannel/business-hour/Helper.ts +++ b/apps/meteor/server/lib/omnichannel/business-hour/Helper.ts @@ -11,7 +11,7 @@ import { businessHourLogger } from '../logger'; export { filterBusinessHoursThatMustBeOpened }; export const filterBusinessHoursThatMustBeOpenedByDay = async ( - businessHours: ILivechatBusinessHour[], + businessHours: Pick[], day: string, // Format: moment.format('dddd') ): Promise[]> => { return filterBusinessHoursThatMustBeOpened( diff --git a/apps/meteor/server/lib/omnichannel/business-hour/filterBusinessHoursThatMustBeOpened.spec.ts b/apps/meteor/server/lib/omnichannel/business-hour/filterBusinessHoursThatMustBeOpened.spec.ts index acaa66426ba4b..0ca9924f1c199 100644 --- a/apps/meteor/server/lib/omnichannel/business-hour/filterBusinessHoursThatMustBeOpened.spec.ts +++ b/apps/meteor/server/lib/omnichannel/business-hour/filterBusinessHoursThatMustBeOpened.spec.ts @@ -9,7 +9,6 @@ describe('different timezones between server and business hours saturday ', () = const bh = await filterBusinessHoursThatMustBeOpened([ { _id: '65c40fa9052d6750ae25df83', - name: '', active: true, type: LivechatBusinessHourTypes.DEFAULT, workHours: [ @@ -41,11 +40,6 @@ describe('different timezones between server and business hours saturday ', () = code: '', }, ], - timezone: { - name: 'Asia/Kolkata', - utc: '+05:30', - }, - ts: new Date(), }, ]); @@ -62,8 +56,6 @@ describe('different timezones between server and business hours sunday ', () => _id: '68516f256ebb4bdceda2757e', active: true, type: LivechatBusinessHourTypes.DEFAULT, - ts: new Date(), - name: '', workHours: [ { day: 'Sunday', @@ -255,10 +247,6 @@ describe('different timezones between server and business hours sunday ', () => code: '', }, ], - timezone: { - name: 'Asia/Kolkata', - utc: '+05:30', - }, }, ]); @@ -275,8 +263,6 @@ describe('regular business hours', () => { _id: '68516f256ebb4bdceda2757e', active: true, type: LivechatBusinessHourTypes.DEFAULT, - ts: new Date(), - name: '', workHours: [ { day: 'Sunday', @@ -468,10 +454,6 @@ describe('regular business hours', () => { code: '', }, ], - timezone: { - name: 'America/Sao_Paulo', - utc: '-3', - }, }, ]); @@ -532,10 +514,8 @@ describe('finish time boundary', () => { const bh = await filterBusinessHoursThatMustBeOpened([ { _id: '68516f256ebb4bdceda2757f', - name: '', active: true, type: LivechatBusinessHourTypes.DEFAULT, - ts: new Date(), workHours: [ { day: 'Monday', @@ -553,10 +533,6 @@ describe('finish time boundary', () => { code: '', }, ], - timezone: { - name: 'UTC', - utc: '+00:00', - }, }, ]); diff --git a/apps/meteor/server/lib/omnichannel/business-hour/filterBusinessHoursThatMustBeOpened.ts b/apps/meteor/server/lib/omnichannel/business-hour/filterBusinessHoursThatMustBeOpened.ts index 0b89b6ddca177..b6dafe4f60951 100644 --- a/apps/meteor/server/lib/omnichannel/business-hour/filterBusinessHoursThatMustBeOpened.ts +++ b/apps/meteor/server/lib/omnichannel/business-hour/filterBusinessHoursThatMustBeOpened.ts @@ -2,7 +2,7 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import moment from 'moment'; export const filterBusinessHoursThatMustBeOpened = async ( - businessHours: Omit[], + businessHours: Pick[], ): Promise[]> => { const currentTime = moment(moment().format('dddd:HH:mm:ss'), 'dddd:HH:mm:ss'); diff --git a/apps/meteor/server/lib/omnichannel/custom-fields.ts b/apps/meteor/server/lib/omnichannel/custom-fields.ts index 9b46b4a8305e9..df9f763caf4d5 100644 --- a/apps/meteor/server/lib/omnichannel/custom-fields.ts +++ b/apps/meteor/server/lib/omnichannel/custom-fields.ts @@ -4,7 +4,10 @@ import { LivechatContacts, LivechatCustomField, LivechatRooms, LivechatVisitors import { livechatLogger } from './logger'; import { i18n } from '../../../app/utils/lib/i18n'; -export const validateRequiredCustomFields = (customFields: string[], livechatCustomFields: ILivechatCustomField[]) => { +export const validateRequiredCustomFields = ( + customFields: string[], + livechatCustomFields: Pick[], +) => { const errors: string[] = []; const requiredCustomFields = livechatCustomFields.filter((field) => field.required); @@ -103,14 +106,14 @@ export async function setMultipleVisitorCustomFields( value: string; overwrite: boolean; }[], - livechatCustomFields?: ILivechatCustomField[], + livechatCustomFields?: Pick[], ) { const keys = customFields.map((field) => field.key); livechatCustomFields ??= await LivechatCustomField.findByScope('visitor', { projection: { _id: 1, required: 1 } }, false).toArray(); validateRequiredCustomFields(keys, livechatCustomFields); - const matchingCustomFields = livechatCustomFields.filter((field: ILivechatCustomField) => keys.includes(field._id)); + const matchingCustomFields = livechatCustomFields.filter((field) => keys.includes(field._id)); const validCustomFields = customFields.filter((cf) => matchingCustomFields.find((mcf) => cf.key === mcf._id)); if (!validCustomFields.length) { return false; diff --git a/apps/meteor/server/lib/omnichannel/guests.ts b/apps/meteor/server/lib/omnichannel/guests.ts index 5cd25034829d7..25a9130e1dd92 100644 --- a/apps/meteor/server/lib/omnichannel/guests.ts +++ b/apps/meteor/server/lib/omnichannel/guests.ts @@ -123,7 +123,7 @@ async function cleanGuestHistory(_id: string) { await LivechatRooms.removeByVisitorId(_id); - const livechatInquiries = await LivechatInquiry.findIdsByVisitorId(_id).toArray(); + const livechatInquiries = await LivechatInquiry.findByVisitorIds([_id]).toArray(); await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id)); void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed'); } diff --git a/apps/meteor/server/lib/omnichannel/hooks.ts b/apps/meteor/server/lib/omnichannel/hooks.ts index 784eadc031e6c..ee25d141d5bc1 100644 --- a/apps/meteor/server/lib/omnichannel/hooks.ts +++ b/apps/meteor/server/lib/omnichannel/hooks.ts @@ -27,7 +27,7 @@ export async function afterAgentUserActivated(user: IUser) { callbacks.runAsync('livechat.onNewAgentCreated', user._id); } -export async function afterAgentAdded(user: IUser) { +export async function afterAgentAdded>(user: T): Promise { await setUserStatusLivechat(user._id, user.status !== 'offline' ? ILivechatAgentStatus.AVAILABLE : ILivechatAgentStatus.NOT_AVAILABLE); callbacks.runAsync('livechat.onNewAgentCreated', user._id); diff --git a/apps/meteor/server/lib/omnichannel/omni-users.ts b/apps/meteor/server/lib/omnichannel/omni-users.ts index bc57165816b65..baff92e6bff79 100644 --- a/apps/meteor/server/lib/omnichannel/omni-users.ts +++ b/apps/meteor/server/lib/omnichannel/omni-users.ts @@ -43,6 +43,8 @@ export async function addManager(username: string) { } export async function addAgent(username: string) { + // `status` is deliberately not projected: this endpoint has always turned the new agent available + // regardless of the user's presence, and `afterAgentAdded` relies on the field being absent to do so. const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); if (!user) { diff --git a/apps/meteor/server/lib/omnichannel/parseTranscriptRequest.ts b/apps/meteor/server/lib/omnichannel/parseTranscriptRequest.ts index 1a8fb1587ac23..338b31e88a20e 100644 --- a/apps/meteor/server/lib/omnichannel/parseTranscriptRequest.ts +++ b/apps/meteor/server/lib/omnichannel/parseTranscriptRequest.ts @@ -35,7 +35,9 @@ export const parseTranscriptRequest = async ( return options; } - const defOptions = { projection: { _id: 1, username: 1, name: 1 } }; + // `as const` keeps the projection statically readable so the return type narrows; + // `utcOffset` is part of the `requestedBy` contract. + const defOptions = { projection: { _id: 1, username: 1, name: 1, utcOffset: 1 } } as const; const requestedBy = user || (room.servedBy && (await Users.findOneById(room.servedBy._id, defOptions))) || diff --git a/apps/meteor/server/lib/omnichannel/sendTranscript.ts b/apps/meteor/server/lib/omnichannel/sendTranscript.ts index e3a045f2476bf..56c72211ea840 100644 --- a/apps/meteor/server/lib/omnichannel/sendTranscript.ts +++ b/apps/meteor/server/lib/omnichannel/sendTranscript.ts @@ -229,7 +229,8 @@ export async function requestTranscript({ subject: string; user: AtLeast; }) { - const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, transcriptRequest: 1 } }); + // `v` is required by the MAC-limit check below + const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, transcriptRequest: 1, v: 1 } }); if (!room?.open) { throw new Meteor.Error('error-invalid-room', 'Invalid room'); diff --git a/apps/meteor/server/lib/rooms/createDirectRoom.ts b/apps/meteor/server/lib/rooms/createDirectRoom.ts index 851ce50cd43d9..0b4f21c8e2f89 100644 --- a/apps/meteor/server/lib/rooms/createDirectRoom.ts +++ b/apps/meteor/server/lib/rooms/createDirectRoom.ts @@ -78,7 +78,8 @@ export async function createDirectRoom( const uids = roomMembers.map(({ _id }) => _id).sort(); // Deprecated: using users' _id to compose the room _id is deprecated - const room: IRoom | null = options?.forceNew ? null : await Rooms.findOneDirectRoomContainingAllUserIDs(uids, { projection: { _id: 1 } }); + // No projection: the full room is spread into the ICreatedRoom returned below. + const room: IRoom | null = options?.forceNew ? null : await Rooms.findOneDirectRoomContainingAllUserIDs(uids); const isNewRoom = !room; diff --git a/apps/meteor/server/lib/rooms/updateGroupDMsName.ts b/apps/meteor/server/lib/rooms/updateGroupDMsName.ts index a3aaf286e8e17..da3c8c5cf4196 100644 --- a/apps/meteor/server/lib/rooms/updateGroupDMsName.ts +++ b/apps/meteor/server/lib/rooms/updateGroupDMsName.ts @@ -5,17 +5,19 @@ import type { ClientSession } from 'mongodb'; import { notifyOnSubscriptionChangedByRoomId } from '../notifyListener'; -const getFname = (members: IUser[]): string => members.map(({ name, username }) => name || username).join(', '); -const getName = (members: IUser[]): string => members.map(({ username }) => username).join(','); +type GroupDMMember = Pick; -async function getUsersWhoAreInTheSameGroupDMsAs(user: IUser) { +const getFname = (members: GroupDMMember[]): string => members.map(({ name, username }) => name || username).join(', '); +const getName = (members: GroupDMMember[]): string => members.map(({ username }) => username).join(','); + +async function getUsersWhoAreInTheSameGroupDMsAs(user: GroupDMMember) { // add all users to single array so we can fetch details from them all at once if ((await Rooms.countGroupDMsByUids([user._id])) === 0) { return; } const userIds = new Set(); - const users = new Map(); + const users = new Map(); const rooms = Rooms.findGroupDMsByUids([user._id], { projection: { uids: 1 } }); await rooms.forEach((room) => { @@ -31,13 +33,13 @@ async function getUsersWhoAreInTheSameGroupDMsAs(user: IUser) { return users; } -function sortUsersAlphabetically(u1: IUser, u2: IUser): number { +function sortUsersAlphabetically(u1: GroupDMMember, u2: GroupDMMember): number { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return (u1.name! || u1.username!).localeCompare(u2.name! || u2.username!); } export const updateGroupDMsName = async ( - userThatChangedName: IUser, + userThatChangedName: GroupDMMember, options?: { session?: ClientSession; }, diff --git a/apps/meteor/server/lib/sendDirectMessageToUsers.ts b/apps/meteor/server/lib/sendDirectMessageToUsers.ts index 2c28ff269b077..d3c763815b3ab 100644 --- a/apps/meteor/server/lib/sendDirectMessageToUsers.ts +++ b/apps/meteor/server/lib/sendDirectMessageToUsers.ts @@ -8,7 +8,7 @@ import { executeSendMessage } from '../meteor-methods/messages/sendMessage'; export async function sendDirectMessageToUsers( fromId = 'rocket.cat', toIds: string[], - messageFn: (user: IUser) => string, + messageFn: (user: Pick) => string, ): Promise { const fromUser = await Users.findOneById(fromId, { projection: { _id: 1, username: 1 } }); if (!fromUser) { diff --git a/apps/meteor/server/lib/statistics/lib/statistics.ts b/apps/meteor/server/lib/statistics/lib/statistics.ts index 9375693a80fbe..154ae718e2297 100644 --- a/apps/meteor/server/lib/statistics/lib/statistics.ts +++ b/apps/meteor/server/lib/statistics/lib/statistics.ts @@ -3,7 +3,7 @@ import crypto from 'node:crypto'; import os from 'node:os'; import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services'; -import type { IRoom, IStats, ISetting } from '@rocket.chat/core-typings'; +import type { IStats, ISetting } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { @@ -278,36 +278,33 @@ export const statistics = { // Message statistics const channels = await Rooms.findByType('c', { projection: { msgs: 1, prid: 1 } }).toArray(); - const totalChannelDiscussionsMessages = channels.reduce(function _countChannelDiscussionsMessages(num: number, room: IRoom) { + const totalChannelDiscussionsMessages = channels.reduce(function _countChannelDiscussionsMessages(num: number, room) { return num + (room.prid ? room.msgs : 0); }, 0); statistics.totalChannelMessages = - channels.reduce(function _countChannelMessages(num: number, room: IRoom) { + channels.reduce(function _countChannelMessages(num: number, room) { return num + room.msgs; }, 0) - totalChannelDiscussionsMessages; const privateGroups = await Rooms.findByType('p', { projection: { msgs: 1, prid: 1 } }).toArray(); - const totalPrivateGroupsDiscussionsMessages = privateGroups.reduce(function _countPrivateGroupsDiscussionsMessages( - num: number, - room: IRoom, - ) { + const totalPrivateGroupsDiscussionsMessages = privateGroups.reduce(function _countPrivateGroupsDiscussionsMessages(num: number, room) { return num + (room.prid ? room.msgs : 0); }, 0); statistics.totalPrivateGroupMessages = - privateGroups.reduce(function _countPrivateGroupMessages(num: number, room: IRoom) { + privateGroups.reduce(function _countPrivateGroupMessages(num: number, room) { return num + room.msgs; }, 0) - totalPrivateGroupsDiscussionsMessages; statistics.totalDiscussionsMessages = totalPrivateGroupsDiscussionsMessages + totalChannelDiscussionsMessages; statistics.totalDirectMessages = (await Rooms.findByType('d', { projection: { msgs: 1 } }).toArray()).reduce( - function _countDirectMessages(num: number, room: IRoom) { + function _countDirectMessages(num: number, room) { return num + room.msgs; }, 0, ); statistics.totalLivechatMessages = (await Rooms.findByType('l', { projection: { msgs: 1 } }).toArray()).reduce( - function _countLivechatMessages(num: number, room: IRoom) { + function _countLivechatMessages(num: number, room) { return num + room.msgs; }, 0, diff --git a/apps/meteor/server/lib/users/getUserSingleOwnedRooms.ts b/apps/meteor/server/lib/users/getUserSingleOwnedRooms.ts index 49ad9510483d8..01c904602397d 100644 --- a/apps/meteor/server/lib/users/getUserSingleOwnedRooms.ts +++ b/apps/meteor/server/lib/users/getUserSingleOwnedRooms.ts @@ -1,4 +1,3 @@ -import type { IRoom } from '@rocket.chat/core-typings'; import { Rooms } from '@rocket.chat/models'; import type { SubscribedRoomsForUserWithDetails } from '../rooms/getRoomsWithSingleOwner'; @@ -17,7 +16,7 @@ export const getUserSingleOwnedRooms = async function (subscribedRooms: Subscrib shouldChangeOwner: [] as string[], }; - await rooms.forEach((room: IRoom) => { + await rooms.forEach((room) => { const name = room.fname || room.name; if (roomsThatWillBeRemoved.includes(room._id)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/apps/meteor/server/lib/users/setRealName.ts b/apps/meteor/server/lib/users/setRealName.ts index b5b4543e9ec79..3be8baaf69ec2 100644 --- a/apps/meteor/server/lib/users/setRealName.ts +++ b/apps/meteor/server/lib/users/setRealName.ts @@ -20,7 +20,7 @@ export const setRealName = async function ( return; } - const user = fullUser || (await Users.findOneById(userId, { session })); + const user = fullUser || (await Users.findOneById(userId, { session })); if (!user) { return; diff --git a/apps/meteor/server/lib/users/setUserActiveStatus.ts b/apps/meteor/server/lib/users/setUserActiveStatus.ts index 53fe21c09bc6b..c3b2ea1e3c590 100644 --- a/apps/meteor/server/lib/users/setUserActiveStatus.ts +++ b/apps/meteor/server/lib/users/setUserActiveStatus.ts @@ -1,4 +1,4 @@ -import type { IUser, IUserEmail } from '@rocket.chat/core-typings'; +import type { IUserEmail } from '@rocket.chat/core-typings'; import { isUserFederated, isDirectMessageRoom } from '@rocket.chat/core-typings'; import { Rooms, Users, Subscriptions, OAuthAccessTokens, OAuthRefreshTokens, OAuthAuthCodes } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; @@ -34,7 +34,7 @@ async function reactivateDirectConversations(userId: string) { }, []); const uniqueUserIds = [...new Set(userIds)]; const activeUsers = await Users.findActiveByUserIds(uniqueUserIds, { projection: { _id: 1 } }).toArray(); - const activeUserIds = activeUsers.map((u: IUser) => u._id); + const activeUserIds = activeUsers.map((u) => u._id); const roomsToReactivate = directConversations.reduce((acc: string[], room) => { const otherUserId = isDirectMessageRoom(room) ? room.uids.find((u: string) => u !== userId) : undefined; if (otherUserId && activeUserIds.includes(otherUserId)) { diff --git a/apps/meteor/server/lib/users/setUserAvatar.ts b/apps/meteor/server/lib/users/setUserAvatar.ts index 56d085c0ff74d..7101d29ab5788 100644 --- a/apps/meteor/server/lib/users/setUserAvatar.ts +++ b/apps/meteor/server/lib/users/setUserAvatar.ts @@ -40,7 +40,7 @@ export const setAvatarFromServiceWithValidation = async ( }); } - let user: IUser | null; + let user: Pick | null; if (targetUserId && targetUserId !== userId) { if (!(await hasPermissionAsync(userId, 'edit-other-user-avatar'))) { diff --git a/apps/meteor/server/lib/utils/lib/normalizeMessagesForUser.ts b/apps/meteor/server/lib/utils/lib/normalizeMessagesForUser.ts index b612c2397c86b..ff15e06b6c0a7 100644 --- a/apps/meteor/server/lib/utils/lib/normalizeMessagesForUser.ts +++ b/apps/meteor/server/lib/utils/lib/normalizeMessagesForUser.ts @@ -3,7 +3,9 @@ import { Users } from '@rocket.chat/models'; import { settings } from '../../../settings'; -const filterStarred = (message: T, uid?: string): T => { +type NormalizableMessage = Pick; + +const filterStarred = >(message: T, uid?: string): T => { // if Allow_anonymous_read is enabled, uid will be undefined if (!uid) return message; @@ -20,7 +22,7 @@ function getNameOfUsername(users: Map, username: string): string return users.get(username) || username; } -export const normalizeMessagesForUser = async (messages: T[], uid?: string): Promise => { +export const normalizeMessagesForUser = async (messages: T[], uid?: string): Promise => { // if not using real names, there is nothing else to do if (!settings.get('UI_Use_Real_Name')) { return messages.map((message) => filterStarred(message, uid)); @@ -58,7 +60,7 @@ export const normalizeMessagesForUser = async (me names.set(user.username, user.name); }); - messages.forEach((message: IMessage) => { + messages.forEach((message) => { if (!message.u) { return; } diff --git a/apps/meteor/server/meteor-methods/messages/createDirectMessage.ts b/apps/meteor/server/meteor-methods/messages/createDirectMessage.ts index bfbe7f244e160..d4f9c611f1d3d 100644 --- a/apps/meteor/server/meteor-methods/messages/createDirectMessage.ts +++ b/apps/meteor/server/meteor-methods/messages/createDirectMessage.ts @@ -55,7 +55,8 @@ export async function createDirectMessage( if ((await hasPermissionAsync(userId, 'view-d-room')) && !Object.keys(roomUsers).some((user) => typeof user === 'string')) { // Check if the direct room already exists, then return it const uids = (roomUsers as IUser[]).map(({ _id }) => _id).sort(); - const room = await Rooms.findOneDirectRoomContainingAllUserIDs(uids, { projection: { _id: 1 } }); + // No projection: the full room is spread into the ICreatedRoom-shaped return below. + const room = await Rooms.findOneDirectRoomContainingAllUserIDs(uids); if (room) { return { ...room, diff --git a/apps/meteor/server/meteor-methods/messages/createDiscussion.ts b/apps/meteor/server/meteor-methods/messages/createDiscussion.ts index 3d45e4842a6fa..6c09d496102d4 100644 --- a/apps/meteor/server/meteor-methods/messages/createDiscussion.ts +++ b/apps/meteor/server/meteor-methods/messages/createDiscussion.ts @@ -124,15 +124,11 @@ const create = async ({ } if (pmid) { - const discussionAlreadyExists = await Rooms.findOne( - { - prid, - pmid, - }, - { - projection: { _id: 1 }, - }, - ); + // No projection: the full room is spread into the returned discussion below. + const discussionAlreadyExists = await Rooms.findOne({ + prid, + pmid, + }); if (discussionAlreadyExists) { // do not allow multiple discussions to the same message'\ await addUserToRoom(discussionAlreadyExists._id, user); diff --git a/apps/meteor/server/meteor-methods/omnichannel/sendMessageLivechat.ts b/apps/meteor/server/meteor-methods/omnichannel/sendMessageLivechat.ts index d6166492b59b2..18d9b07a31b7e 100644 --- a/apps/meteor/server/meteor-methods/omnichannel/sendMessageLivechat.ts +++ b/apps/meteor/server/meteor-methods/omnichannel/sendMessageLivechat.ts @@ -43,14 +43,8 @@ export const sendMessageLivechat = async ({ }), ); - const guest = await LivechatVisitors.getVisitorByToken(token, { - projection: { - name: 1, - username: 1, - department: 1, - token: 1, - }, - }); + // No projection: the guest is forwarded into the message-sending pipeline, which expects a full visitor. + const guest = await LivechatVisitors.getVisitorByToken(token); if (!guest) { throw new Meteor.Error('invalid-token'); diff --git a/apps/meteor/server/services/ai-search/service.ts b/apps/meteor/server/services/ai-search/service.ts index ca2a17131bd8b..67cb473dd26c4 100644 --- a/apps/meteor/server/services/ai-search/service.ts +++ b/apps/meteor/server/services/ai-search/service.ts @@ -229,7 +229,7 @@ export class AISearchService extends ServiceClass implements IAISearchService { } } const msgIds = [...msgIdSet]; - let messageMap = new Map(); + let messageMap = new Map>(); if (msgIds.length > 0) { messageMap = new Map( diff --git a/apps/meteor/server/services/calendar/service.ts b/apps/meteor/server/services/calendar/service.ts index ffd17bf1813e3..b1a274a45e80d 100644 --- a/apps/meteor/server/services/calendar/service.ts +++ b/apps/meteor/server/services/calendar/service.ts @@ -236,9 +236,6 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe try { const eventsStartingNow = await CalendarEvent.findEventsStartingNow({ now: processTime, offset: 5000 }).toArray(); for await (const event of eventsStartingNow) { - if (event.busy === false) { - continue; - } await this.processEventStart(event); } } catch (err) { @@ -248,7 +245,7 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe await this.doSetupNextStatusChange(); } - private async processEventStart(event: ICalendarEvent): Promise { + private async processEventStart(event: Pick): Promise { // no endTime → no expiry to set, so the claim could never auto-clear if (!event.endTime) { return; diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index b7549b4bd86bc..03725c4ca792d 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -575,7 +575,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { throw new Error('user-not-on-private-team'); } - const teamRooms: (IRoom & { + const teamRooms: (Pick & { userCanDelete?: boolean; })[] = await Rooms.findByTeamId(teamId, { projection: { _id: 1, t: 1 }, @@ -783,9 +783,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { } const membersIds = members.map((m) => m.userId); - const usersToRemove = await Users.findByIds(membersIds, { - projection: { _id: 1, username: 1 }, - }).toArray(); + const usersToRemove = await Users.findByIds(membersIds).toArray(); const byUser = await Users.findOneById(uid); for await (const member of members) { diff --git a/apps/meteor/server/services/upload/service.ts b/apps/meteor/server/services/upload/service.ts index 790638e7d9d09..c2dd29f1d74bd 100644 --- a/apps/meteor/server/services/upload/service.ts +++ b/apps/meteor/server/services/upload/service.ts @@ -57,7 +57,11 @@ export class UploadService extends ServiceClassInternal implements IUploadServic return parseFileIntoMessageAttachments(file, roomId, user); } - async canDeleteFile(user: IUser, file: IUpload, msg: IMessage | null): Promise { + async canDeleteFile( + user: Pick, + file: Pick, + msg: IMessage | null, + ): Promise { if (msg) { return canDeleteMessageAsync(user, msg); } diff --git a/apps/meteor/server/settings/SettingsRegistry.ts b/apps/meteor/server/settings/SettingsRegistry.ts index 89b6ba1a5d0bb..64a10ccc25318 100644 --- a/apps/meteor/server/settings/SettingsRegistry.ts +++ b/apps/meteor/server/settings/SettingsRegistry.ts @@ -34,9 +34,12 @@ const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; * @deprecated * please do not use event emitter to mutate values */ +/** The fields the enterprise value-masking listener needs; `fetchSettings` projects exactly these. */ +export type FetchedSetting = Pick; + export const SettingsEvents = new Emitter<{ 'store-setting-value': [ISetting, { value: SettingValue }]; - 'fetch-settings': ISetting[]; + 'fetch-settings': FetchedSetting[]; 'remove-setting-value': ISetting; }>(); diff --git a/ee/packages/abac/src/index.ts b/ee/packages/abac/src/index.ts index c75f55f35df03..8a436e57522cb 100644 --- a/ee/packages/abac/src/index.ts +++ b/ee/packages/abac/src/index.ts @@ -350,7 +350,7 @@ export class AbacService extends ServiceClass implements IAbacService { filters?: { key?: string; values?: string; offset?: number; count?: number }, actor?: AbacActor, ): Promise<{ - attributes: IAbacAttribute[]; + attributes: Pick[]; offset: number; count: number; total: number; diff --git a/ee/packages/abac/src/pdp/LocalPDP.ts b/ee/packages/abac/src/pdp/LocalPDP.ts index 6d16d81e16997..5a2ab523fffa8 100644 --- a/ee/packages/abac/src/pdp/LocalPDP.ts +++ b/ee/packages/abac/src/pdp/LocalPDP.ts @@ -51,7 +51,7 @@ export class LocalPDP implements IPolicyDecisionPoint { return Users.find(query, { projection: { __rooms: 0 } }).toArray(); } - async onSubjectAttributesChanged(user: IUser, _next: IAbacAttributeDefinition[]): Promise { + async onSubjectAttributesChanged(user: IUser, _next: IAbacAttributeDefinition[]): Promise[]> { const roomIds = user.__rooms; // No attributes: no rooms :( diff --git a/ee/packages/abac/src/pdp/VirtruPDP.spec.ts b/ee/packages/abac/src/pdp/VirtruPDP.spec.ts index ec53e8bb3d596..0842c8ec252c4 100644 --- a/ee/packages/abac/src/pdp/VirtruPDP.spec.ts +++ b/ee/packages/abac/src/pdp/VirtruPDP.spec.ts @@ -387,7 +387,7 @@ describe('VirtruPDP.onSubjectAttributesChanged', () => { const apiCall = jest.fn(); const pdp = new VirtruPDP(mkClient({ apiCall })); const result = await pdp.onSubjectAttributesChanged(user({ __rooms: ['r1'], emails: [] }) as any, []); - expect(result).toEqual(rooms); + expect(result).toEqual([{ _id: 'r1' }]); expect(apiCall).not.toHaveBeenCalled(); }); @@ -405,7 +405,7 @@ describe('VirtruPDP.onSubjectAttributesChanged', () => { }); const pdp = new VirtruPDP(mkClient({ apiCall })); const result = await pdp.onSubjectAttributesChanged(user({ __rooms: ['rP', 'rD'] }) as any, []); - expect(result).toEqual([rooms[1]]); + expect(result).toEqual([{ _id: 'rD' }]); }); it('still treats DECISION_UNSPECIFIED as non-compliant (LDAP-driven path cannot yield inconclusive decisions)', async () => { @@ -416,7 +416,7 @@ describe('VirtruPDP.onSubjectAttributesChanged', () => { }); const pdp = new VirtruPDP(mkClient({ apiCall })); const result = await pdp.onSubjectAttributesChanged(user({ __rooms: ['r1'] }) as any, []); - expect(result).toEqual(rooms); + expect(result).toEqual([{ _id: 'r1' }]); }); it('splits >200 rooms into multiple decision batches', async () => { diff --git a/ee/packages/abac/src/pdp/VirtruPDP.ts b/ee/packages/abac/src/pdp/VirtruPDP.ts index d3d37d93aef4f..ff1cd5498da81 100644 --- a/ee/packages/abac/src/pdp/VirtruPDP.ts +++ b/ee/packages/abac/src/pdp/VirtruPDP.ts @@ -4,8 +4,6 @@ import { serverFetch } from '@rocket.chat/server-fetch'; import { isTruthy } from '@rocket.chat/tools'; import pLimit from 'p-limit'; -import { OnlyCompliantCanBeAddedToRoomError, PdpHealthCheckError } from '../errors'; -import { logger } from '../logger'; import type { IPolicyDecisionPoint, IGetDecisionBulkRequest, @@ -17,6 +15,8 @@ import type { import { HEALTH_CHECK_TIMEOUT } from '../clients/virtru/VirtruClient'; import type { VirtruClient } from '../clients/virtru/VirtruClient'; import { buildEntityIdentifier, buildAttributeFqns, getUserEntityKey } from '../clients/virtru/identity'; +import { OnlyCompliantCanBeAddedToRoomError, PdpHealthCheckError } from '../errors'; +import { logger } from '../logger'; const pdpLogger = logger.section('VirtruPDP'); @@ -266,9 +266,7 @@ export class VirtruPDP implements IPolicyDecisionPoint { return []; } - const users = Users.findActiveByRoomIds([room._id], { - projection: { _id: 1, emails: 1, username: 1 }, - }); + const users = Users.findActiveByRoomIds([room._id]); const config = this.client.getConfig(); const nonCompliantUsers: IUser[] = []; @@ -374,7 +372,7 @@ export class VirtruPDP implements IPolicyDecisionPoint { projection: { _id: 1, abacAttributes: 1 }, }); - const abacRoomById = new Map(); + const abacRoomById = new Map>(); for await (const room of abacRoomCursor) { abacRoomById.set(room._id, room); } @@ -389,7 +387,7 @@ export class VirtruPDP implements IPolicyDecisionPoint { return this.evaluateUserRooms(entries); } - async onSubjectAttributesChanged(user: IUser, _next: IAbacAttributeDefinition[]): Promise { + async onSubjectAttributesChanged(user: IUser, _next: IAbacAttributeDefinition[]): Promise[]> { const roomIds = user.__rooms; if (!roomIds?.length) { return []; @@ -410,7 +408,7 @@ export class VirtruPDP implements IPolicyDecisionPoint { msg: 'User has no entity key for Virtru PDP evaluation, treating as non-compliant for all ABAC rooms', userId: user._id, }); - return abacRooms; + return abacRooms.map(({ _id }) => ({ _id })); } const decisionRequests = abacRooms.map((room) => ({ @@ -430,12 +428,12 @@ export class VirtruPDP implements IPolicyDecisionPoint { const responses = await this.getDecisionBulk(decisionRequests); - const nonCompliantRooms: IRoom[] = []; + const nonCompliantRooms: Pick[] = []; responses.forEach((resp, index) => { const permitted = resp?.resourceDecisions?.length && resp.resourceDecisions.every((rd) => rd.decision === 'DECISION_PERMIT'); if (!permitted && abacRooms[index]) { - nonCompliantRooms.push(abacRooms[index]); + nonCompliantRooms.push({ _id: abacRooms[index]._id }); } }); diff --git a/ee/packages/abac/src/pdp/types.ts b/ee/packages/abac/src/pdp/types.ts index 4749d45f8f7e5..97482351077d0 100644 --- a/ee/packages/abac/src/pdp/types.ts +++ b/ee/packages/abac/src/pdp/types.ts @@ -52,7 +52,7 @@ export interface IPolicyDecisionPoint { newAttributes: IAbacAttributeDefinition[], ): Promise; - onSubjectAttributesChanged(user: IUser, next: IAbacAttributeDefinition[]): Promise; + onSubjectAttributesChanged(user: IUser, next: IAbacAttributeDefinition[]): Promise[]>; evaluateUserRooms( entries: Array<{ diff --git a/ee/packages/abac/src/store/VirtruAttributeStore.ts b/ee/packages/abac/src/store/VirtruAttributeStore.ts index 6ff21cecf9053..4b7c2d62ac13b 100644 --- a/ee/packages/abac/src/store/VirtruAttributeStore.ts +++ b/ee/packages/abac/src/store/VirtruAttributeStore.ts @@ -1,5 +1,5 @@ import type { AbacActor } from '@rocket.chat/core-services'; -import type { IAbacAttribute, IAbacAttributeDefinition, IRoom, IRoomAbacRedaction } from '@rocket.chat/core-typings'; +import type { IAbacAttributeDefinition, IRoom, IRoomAbacRedaction } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import mem from 'mem'; @@ -96,7 +96,7 @@ export class VirtruAttributeStore implements IAttributeStore { const count = opts?.count ?? total; const slice = attributes.slice(offset, offset + count); return { - attributes: slice.map((a) => ({ _id: a.key, ...a })) as IAbacAttribute[], + attributes: slice.map((a) => ({ _id: a.key, ...a })), offset, count: slice.length, total, diff --git a/ee/packages/abac/src/store/types.ts b/ee/packages/abac/src/store/types.ts index b9703ae155e91..a45299145b382 100644 --- a/ee/packages/abac/src/store/types.ts +++ b/ee/packages/abac/src/store/types.ts @@ -5,7 +5,12 @@ export type AttributeEntitlements = Map>; export type ListAttributesOptions = { key?: string; values?: string; offset?: number; count?: number }; -export type ListAttributesResult = { attributes: IAbacAttribute[]; offset: number; count: number; total: number }; +export type ListAttributesResult = { + attributes: Pick[]; + offset: number; + count: number; + total: number; +}; export interface IAttributeStore { list(actor: AbacActor | undefined, opts?: ListAttributesOptions): Promise; diff --git a/ee/packages/federation-matrix/src/api/_matrix/client/media.ts b/ee/packages/federation-matrix/src/api/_matrix/client/media.ts index e465acaa6143f..e3530b269f55d 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/client/media.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/client/media.ts @@ -22,10 +22,12 @@ const isMediaParamsProps = ajv.compile(MediaParamsSchema); const ThumbnailQuerySchema = { type: 'object', properties: { - width: { oneOf: [{ type: 'number' }, { type: 'string' }] }, - height: { oneOf: [{ type: 'number' }, { type: 'string' }] }, + // union type lists rather than `oneOf`: ajvQuery coerces between number and string, so both + // `oneOf` branches would match a numeric value and fail validation + width: { type: ['number', 'string'] }, + height: { type: ['number', 'string'] }, method: { type: 'string', enum: ['crop', 'scale'] }, - timeout_ms: { oneOf: [{ type: 'number' }, { type: 'string' }] }, + timeout_ms: { type: ['number', 'string'] }, }, }; diff --git a/ee/packages/federation-matrix/src/api/_matrix/client/rooms-messaging.ts b/ee/packages/federation-matrix/src/api/_matrix/client/rooms-messaging.ts index d724fe859f16d..3ebb047904288 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/client/rooms-messaging.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/client/rooms-messaging.ts @@ -57,7 +57,9 @@ const MessagesQuerySchema = { from: { type: 'string' }, to: { type: 'string' }, dir: { type: 'string', enum: ['b', 'f'] }, - limit: { oneOf: [{ type: 'number' }, { type: 'string' }] }, + // a union type list rather than `oneOf`: ajvQuery coerces between number and string, so both + // `oneOf` branches would match a numeric value and fail validation + limit: { type: ['number', 'string'] }, filter: { type: 'string' }, }, }; diff --git a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts index c65fc73ff83e4..f127559ef3c4e 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/send-join.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/send-join.ts @@ -1,4 +1,3 @@ -import type { EventID } from '@rocket.chat/federation-sdk'; import { federationSDK } from '@rocket.chat/federation-sdk'; import { Router } from '@rocket.chat/http-router'; import { ajv } from '@rocket.chat/rest-typings/dist/v1/Ajv'; @@ -29,12 +28,6 @@ const TimestampSchema = { description: 'Unix timestamp in milliseconds', }; -const DepthSchema = { - type: 'number', - minimum: 0, - description: 'Event depth', -}; - const ServerNameSchema = { type: 'string', description: 'Matrix server name', @@ -51,137 +44,42 @@ const SendJoinParamsSchema = { const isSendJoinParamsProps = ajv.compile(SendJoinParamsSchema); -const EventHashSchema = { - type: 'object', - properties: { - sha256: { - type: 'string', - description: 'SHA256 hash of the event', - }, - }, - required: ['sha256'], -}; - -const EventSignatureSchema = { - type: 'object', - description: 'Event signatures by server and key ID', -}; - -const MembershipEventContentSchema = { - type: 'object', - properties: { - membership: { - type: 'string', - enum: ['join', 'leave', 'invite', 'ban', 'knock'], - description: 'Membership state', - }, - displayname: { - type: 'string', - nullable: true, - }, - avatar_url: { - type: 'string', - nullable: true, - }, - join_authorised_via_users_server: { - type: 'string', - nullable: true, - }, - is_direct: { - type: 'boolean', - nullable: true, - }, - reason: { - type: 'string', - description: 'Reason for membership change', - nullable: true, - }, - }, - required: ['membership'], -}; - -const EventBaseSchema = { +const SendJoinEventSchema = { type: 'object', properties: { type: { type: 'string', - description: 'Event type', - }, - content: { - type: 'object', - description: 'Event content', + const: 'm.room.member', }, - sender: UsernameSchema, - room_id: RoomIdSchema, - origin_server_ts: TimestampSchema, - depth: DepthSchema, - prev_events: { - type: 'array', - items: { - type: 'string', - }, - description: 'Previous events in the room', + state_key: { + ...UsernameSchema, + description: 'Matrix user ID of the joining member', }, - auth_events: { - type: 'array', - items: { - type: 'string', - }, - description: 'Authorization events', + sender: { + ...UsernameSchema, + description: 'Matrix user ID of the joining member', }, origin: { - type: 'string', - description: 'Origin server', - }, - hashes: { - ...EventHashSchema, - nullable: true, + ...ServerNameSchema, + description: 'The name of the joining homeserver', }, - signatures: { - ...EventSignatureSchema, - nullable: true, - }, - unsigned: { - type: 'object', - description: 'Unsigned data', - nullable: true, - }, - }, - required: ['type', 'content', 'sender', 'room_id', 'origin_server_ts', 'depth', 'prev_events', 'auth_events', 'origin'], -}; - -const SendJoinEventSchema = { - type: 'object', - allOf: [ - EventBaseSchema, - { + origin_server_ts: TimestampSchema, + content: { type: 'object', properties: { - type: { + membership: { type: 'string', - const: 'm.room.member', + const: 'join', }, - content: { - type: 'object', - allOf: [ - MembershipEventContentSchema, - { - type: 'object', - properties: { - membership: { - type: 'string', - const: 'join', - }, - }, - required: ['membership'], - }, - ], + join_authorised_via_users_server: { + ...UsernameSchema, + description: 'User ID of a resident server member authorizing the join into a restricted room', }, - state_key: UsernameSchema, }, - required: ['type', 'content', 'state_key'], + required: ['membership'], }, - ], + }, + required: ['type', 'state_key', 'sender', 'origin', 'origin_server_ts', 'content'], }; const isSendJoinEventProps = ajv.compile(SendJoinEventSchema); @@ -235,7 +133,7 @@ export const getMatrixSendJoinRoutes = () => { const { roomId, stateKey } = c.req.param(); const body = await c.req.json(); - const response = await federationSDK.sendJoin(roomId, stateKey as EventID, body); + const response = await federationSDK.sendJoin(roomId, stateKey, body); return { body: response, diff --git a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts index d0b001cdc111a..3881118cfd87b 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/transactions.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/transactions.ts @@ -279,7 +279,10 @@ const BackfillQuerySchema = { description: 'Maximum number of events to retrieve', }, v: { - oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + // a string branch here would be redundant: ajvQuery coerces a single `?v=` into a + // one-element array, and in a `oneOf` both branches would match and fail validation + type: 'array', + items: { type: 'string' }, description: 'Event ID(s) to backfill from', }, }, @@ -289,7 +292,7 @@ const BackfillQuerySchema = { const isBackfillQueryProps = ajvQuery.compile<{ limit: number; - v: string | string[]; + v: string[]; }>(BackfillQuerySchema); const BackfillResponseSchema = { diff --git a/packages/core-services/src/types/IAbacService.ts b/packages/core-services/src/types/IAbacService.ts index 3eac9e59b8351..c2d9b433c9bc9 100644 --- a/packages/core-services/src/types/IAbacService.ts +++ b/packages/core-services/src/types/IAbacService.ts @@ -22,7 +22,7 @@ export interface IAbacService { count?: number; }, actor?: AbacActor, - ): Promise<{ attributes: IAbacAttribute[]; offset: number; count: number; total: number }>; + ): Promise<{ attributes: Pick[]; offset: number; count: number; total: number }>; listAbacRooms( filters?: { offset?: number; diff --git a/packages/core-services/src/types/IUploadService.ts b/packages/core-services/src/types/IUploadService.ts index 9b3a1504fd2d4..ea6233134e068 100644 --- a/packages/core-services/src/types/IUploadService.ts +++ b/packages/core-services/src/types/IUploadService.ts @@ -30,7 +30,11 @@ export interface IUploadService { getFileBuffer({ file }: { file: IUpload }): Promise; extractMetadata(file: IUpload): Promise<{ height?: number; width?: number; format?: string }>; parseFileIntoMessageAttachments(file: Partial, roomId: string, user: IUser): Promise; - canDeleteFile(user: IUser, file: IUpload, msg: IMessage | null): Promise; + canDeleteFile( + user: Pick, + file: Pick, + msg: IMessage | null, + ): Promise; deleteFile(user: IUser, fileId: IUpload['_id'], msg: IMessage | null): Promise<{ deletedFiles: IUpload['_id'][] }>; streamUploadedFile({ file, diff --git a/packages/core-typings/src/ILivechatBusinessHour.ts b/packages/core-typings/src/ILivechatBusinessHour.ts index e2f705f784b31..166754226ce21 100644 --- a/packages/core-typings/src/ILivechatBusinessHour.ts +++ b/packages/core-typings/src/ILivechatBusinessHour.ts @@ -37,5 +37,6 @@ export interface ILivechatBusinessHour extends IRocketChatRecord { timezone: IBusinessHourTimezone; ts: Date; workHours: IBusinessHourWorkHour[]; - departments?: ILivechatDepartment[]; + /** Populated on the fly by the business-hours endpoints; never persisted with the record. */ + departments?: Pick[]; } diff --git a/packages/core-typings/src/ISetting.ts b/packages/core-typings/src/ISetting.ts index 69d13ed175a94..602aafca6c156 100644 --- a/packages/core-typings/src/ISetting.ts +++ b/packages/core-typings/src/ISetting.ts @@ -167,7 +167,11 @@ export const isSetting = (setting: any): setting is ISetting => export const isSettingEnterprise = (setting: ISettingBase): setting is ISettingEnterprise => setting.enterprise === true; -export const isSettingColor = (setting: ISettingBase): setting is ISettingColor => setting.type === 'color'; +export function isSettingColor(setting: ISettingBase): setting is ISettingColor; +export function isSettingColor>(setting: T): setting is T & { type: 'color' }; +export function isSettingColor(setting: Pick): boolean { + return setting.type === 'color'; +} export const isSettingCode = (setting: ISettingBase): setting is ISettingCode => setting.type === 'code'; diff --git a/packages/core-typings/src/ISubscription.ts b/packages/core-typings/src/ISubscription.ts index a40c50bad0404..2b4f721fea898 100644 --- a/packages/core-typings/src/ISubscription.ts +++ b/packages/core-typings/src/ISubscription.ts @@ -96,6 +96,8 @@ export interface IBannedSubscription extends ISubscription { status: 'BANNED'; } -export const isBannedSubscription = (subscription: ISubscription): subscription is IBannedSubscription => { +export const isBannedSubscription = >( + subscription: T, +): subscription is T & { status: 'BANNED' } => { return subscription?.status === 'BANNED'; }; diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index 6cf492d6fb5d0..2a72a97467f26 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -140,12 +140,13 @@ const userServiceKeys: IUserService[] = ['emailCode', 'email2fa', 'totp', 'resum const isUserServiceKey = (key: string): key is IUserService => userServiceKeys.includes(key as IUserService) || defaultOAuthKeys.includes(key as IOAuthService); -const isDefaultOAuthUser = (user: IUser): boolean => +const isDefaultOAuthUser = (user: Pick): boolean => !!user.services && Object.keys(user.services).some((key) => defaultOAuthKeys.includes(key as IOAuthService)); -const isCustomOAuthUser = (user: IUser): boolean => !!user.services && Object.keys(user.services).some((key) => !isUserServiceKey(key)); +const isCustomOAuthUser = (user: Pick): boolean => + !!user.services && Object.keys(user.services).some((key) => !isUserServiceKey(key)); -export const isOAuthUser = (user: IUser): boolean => isDefaultOAuthUser(user) || isCustomOAuthUser(user); +export const isOAuthUser = (user: Pick): boolean => isDefaultOAuthUser(user) || isCustomOAuthUser(user); export interface IUserEmail { address: string; @@ -252,7 +253,9 @@ export interface IRegisterUser extends IUser { name: string; } -export const isRegisterUser = (user: IUser): user is IRegisterUser => user.username !== undefined && user.name !== undefined; +export const isRegisterUser = >( + user: T, +): user is T & Required> => user.username !== undefined && user.name !== undefined; export const isUserFederated = (user: Partial | Partial>) => 'federated' in user && user.federated === true; diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index b39cd6dad854c..15ea64ba55bfa 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -8,7 +8,8 @@ "/dist" ], "scripts": { - "build": "rm -rf dist && tsc -p tsconfig.json", + "build": "rm -rf dist && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit -p tsconfig.json", "lint": "eslint .", "lint:fix": "eslint --fix ." }, diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 13922815f45aa..cb8eb20ff2da7 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -76,6 +76,7 @@ export type * from './models/IMigrationsModel'; export type * from './models/IModerationReportsModel'; export type * from './models/IMediaCallsModel'; export type * from './models/IMediaCallNegotiationsModel'; +export type * from './types/DocumentWithProjection'; export type * from './updater'; export type * from './models/IWorkspaceCredentialsModel'; export type * from './models/ICallHistoryModel'; diff --git a/packages/model-typings/src/models/IAbacAttributesModel.ts b/packages/model-typings/src/models/IAbacAttributesModel.ts index 9cb313276760a..87ff46e79dd1b 100644 --- a/packages/model-typings/src/models/IAbacAttributesModel.ts +++ b/packages/model-typings/src/models/IAbacAttributesModel.ts @@ -1,9 +1,13 @@ import type { IAbacAttribute } from '@rocket.chat/core-typings'; -import type { FindOptions } from 'mongodb'; +import type { Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IAbacAttributesModel extends IBaseModel { - findOneByKey(key: string, options?: FindOptions): Promise; + findOneByKey = FindOptionsWithProjection>( + key: string, + options?: O, + ): Promise | null>; countTotalValues(): Promise; } diff --git a/packages/model-typings/src/models/IBannersDismissModel.ts b/packages/model-typings/src/models/IBannersDismissModel.ts index 043421e1886b9..9c37146daa6b7 100644 --- a/packages/model-typings/src/models/IBannersDismissModel.ts +++ b/packages/model-typings/src/models/IBannersDismissModel.ts @@ -17,6 +17,6 @@ export interface IBannersDismissModel extends IBaseModel { findByUserIdAndBannerId

( userId: string, bannerIds: string[], - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor; } diff --git a/packages/model-typings/src/models/IBannersModel.ts b/packages/model-typings/src/models/IBannersModel.ts index fd54f4fd97621..1eddda669350a 100644 --- a/packages/model-typings/src/models/IBannersModel.ts +++ b/packages/model-typings/src/models/IBannersModel.ts @@ -1,12 +1,18 @@ import type { BannerPlatform, IBanner, Optional } from '@rocket.chat/core-typings'; -import type { Document, FindCursor, FindOptions, UpdateResult, InsertOneResult } from 'mongodb'; +import type { Document, FindCursor, UpdateResult, InsertOneResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IBannersModel extends IBaseModel { create(doc: Optional): Promise>; - findActiveByRoleOrId(roles: string[], platform: BannerPlatform, bannerId?: string, options?: FindOptions): FindCursor; + findActiveByRoleOrId = FindOptionsWithProjection>( + roles: string[], + platform: BannerPlatform, + bannerId?: string, + options?: O, + ): FindCursor>; disable(bannerId: string): Promise; diff --git a/packages/model-typings/src/models/IBaseModel.ts b/packages/model-typings/src/models/IBaseModel.ts index abf1b3dede0e2..14d4dcd07c256 100644 --- a/packages/model-typings/src/models/IBaseModel.ts +++ b/packages/model-typings/src/models/IBaseModel.ts @@ -12,7 +12,6 @@ import type { Filter, FindCursor, FindOneAndDeleteOptions, - FindOneAndUpdateOptions, FindOptions, InsertManyResult, InsertOneOptions, @@ -24,14 +23,22 @@ import type { WithId, } from 'mongodb'; +import type { + ApplyProjection, + DocumentWithDriverProjection, + DocumentWithProjection, + FindOneAndUpdateOptionsWithProjection, + FindOptionsWithProjection, + ProjectionSpec, +} from '../types/DocumentWithProjection'; import type { Updater } from '../updater'; export type DefaultFields = Partial> | Partial> | void; -export type ResultFields = Defaults extends void +export type ResultFields = Defaults extends void | undefined ? Base - : Defaults[keyof Defaults] extends 1 - ? Pick - : Omit; + : Defaults extends ProjectionSpec + ? ApplyProjection + : Base; export type InsertionModel = EnhancedOmit, '_updatedAt'> & { _updatedAt?: Date; @@ -55,24 +62,43 @@ export interface IBaseModel< getUpdater(): Updater; updateFromUpdater(query: Filter, updater: Updater, options?: UpdateOptions): Promise; - findOneAndDelete(filter: Filter, options?: FindOneAndDeleteOptions): Promise>; - findOneAndDeleteById(_id: T['_id'], options?: FindOneAndDeleteOptions): Promise>; - findOneAndUpdate(query: Filter, update: UpdateFilter | T, options?: FindOneAndUpdateOptions): Promise>; - - findOneById(_id: T['_id'], options?: FindOptions | undefined): Promise; - findOneById

(_id: T['_id'], options?: FindOptions

): Promise

; - - findOne(query?: Filter | T['_id'], options?: undefined): Promise; - findOne

(query: Filter | T['_id'], options?: FindOptions

): Promise

; + /** + * 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. + */ + findOneAndDelete(filter: Filter, options?: FindOneAndDeleteOptions): Promise | null>; + findOneAndDeleteById(_id: T['_id'], options?: FindOneAndDeleteOptions): Promise | null>; + findOneAndUpdate

( + query: Filter, + update: UpdateFilter | T, + options?: O, + ): Promise | null>; + + findOneById(_id: T['_id'], options?: undefined): Promise | null>; + findOneById

= FindOptionsWithProjection

>( + _id: T['_id'], + options?: O, + ): Promise | null>; + + findOne(query?: Filter | T['_id'], options?: undefined): Promise | null>; + findOne

= FindOptionsWithProjection

>( + query: Filter | T['_id'], + options?: O, + ): Promise | null>; find(query?: Filter): FindCursor>; - find

(query: Filter, options: FindOptions

): FindCursor

; - find

( + find

= FindOptionsWithProjection

>( query: Filter | undefined, - options?: FindOptions

, - ): FindCursor> | FindCursor>; + options?: O, + ): FindCursor>; - findPaginated

(query: Filter, options?: FindOptions

): FindPaginated>>; + findPaginated(query?: Filter): FindPaginated>>; + findPaginated

= FindOptionsWithProjection

>( + query: Filter | undefined, + options?: O, + ): FindPaginated>>; update( filter: Filter, diff --git a/packages/model-typings/src/models/IBaseUploadsModel.ts b/packages/model-typings/src/models/IBaseUploadsModel.ts index be8698e2bef84..ec95da8b7ff1d 100644 --- a/packages/model-typings/src/models/IBaseUploadsModel.ts +++ b/packages/model-typings/src/models/IBaseUploadsModel.ts @@ -1,7 +1,8 @@ import type { EncryptedContent, IUpload } from '@rocket.chat/core-typings'; -import type { DeleteResult, UpdateResult, ClientSession, Document, InsertOneResult, WithId, FindCursor, FindOptions } from 'mongodb'; +import type { DeleteResult, UpdateResult, ClientSession, Document, InsertOneResult, WithId, FindCursor } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IBaseUploadsModel extends IBaseModel { insertFileInit(userId: string, store: string, file: { name: string }, extra: object): Promise>>; @@ -10,19 +11,29 @@ export interface IBaseUploadsModel extends IBaseModel { confirmTemporaryFile(fileId: string, userId: string): Promise | undefined; - findByIds(_ids: string[], options?: FindOptions): FindCursor; + findByIds = FindOptionsWithProjection>( + _ids: string[], + options?: O, + ): FindCursor>; findOneByName(name: string, options?: { session?: ClientSession }): Promise; findOneByRoomId(rid: string): Promise; - findExpiredTemporaryFiles(options?: FindOptions): FindCursor; + findExpiredTemporaryFiles = FindOptionsWithProjection>( + options?: O, + ): FindCursor>; updateFileNameById(fileId: string, name: string): Promise; deleteFile(fileId: string, options?: { session?: ClientSession }): Promise; - findOneByIdAndUserIdAndRoomId(fileId: string, userId: string, rid: string, options?: FindOptions): Promise; + findOneByIdAndUserIdAndRoomId = FindOptionsWithProjection>( + fileId: string, + userId: string, + rid: string, + options?: O, + ): Promise | null>; updateFileMetadata( fileId: string, diff --git a/packages/model-typings/src/models/ICalendarEventModel.ts b/packages/model-typings/src/models/ICalendarEventModel.ts index 9636185a90cfc..0574d0091ef1f 100644 --- a/packages/model-typings/src/models/ICalendarEventModel.ts +++ b/packages/model-typings/src/models/ICalendarEventModel.ts @@ -14,6 +14,12 @@ export interface ICalendarEventModel extends IBaseModel { uid: ICalendarEvent['uid'], ): Promise; findOverlappingEvents(eventId: ICalendarEvent['_id'], uid: IUser['_id'], startTime: Date, endTime: Date): FindCursor; - findNextFutureEvent(startTime: Date): Promise; - findEventsStartingNow({ now, offset }: { now: Date; offset?: number }): FindCursor; + findNextFutureEvent(startTime: Date): Promise | null>; + findEventsStartingNow({ + now, + offset, + }: { + now: Date; + offset?: number; + }): FindCursor>; } diff --git a/packages/model-typings/src/models/ICallHistoryModel.ts b/packages/model-typings/src/models/ICallHistoryModel.ts index 750ee20a1a0e1..38467eedbb95f 100644 --- a/packages/model-typings/src/models/ICallHistoryModel.ts +++ b/packages/model-typings/src/models/ICallHistoryModel.ts @@ -1,22 +1,26 @@ import type { CallHistoryItem, IRegisterUser, IUser } from '@rocket.chat/core-typings'; -import type { FindCursor, FindOptions } from 'mongodb'; +import type { FindCursor, Document } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ICallHistoryModel extends IBaseModel { - findOneByIdAndUid( + findOneByIdAndUid = FindOptionsWithProjection>( _id: CallHistoryItem['_id'], uid: CallHistoryItem['uid'], - options?: FindOptions, - ): Promise; + options?: O, + ): Promise | null>; - findOneByCallIdAndUid( + findOneByCallIdAndUid = FindOptionsWithProjection>( callId: CallHistoryItem['callId'], uid: CallHistoryItem['uid'], - options?: FindOptions, - ): Promise; + options?: O, + ): Promise | null>; - findAllByUserIdAndSearchFilters( + findAllByUserIdAndSearchFilters< + T extends Document = CallHistoryItem, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( uid: IUser['_id'], filters: { type?: CallHistoryItem['type']; @@ -24,8 +28,8 @@ export interface ICallHistoryModel extends IBaseModel { direction?: CallHistoryItem['direction']; inStates?: CallHistoryItem['state'][]; }, - options: FindOptions, - ): FindPaginated>; + options: O, + ): FindPaginated>>; updateUserReferences(userId: IRegisterUser['_id'], username: IRegisterUser['username'], name?: IRegisterUser['name']): Promise; } diff --git a/packages/model-typings/src/models/ICannedResponseModel.ts b/packages/model-typings/src/models/ICannedResponseModel.ts index 3a0c9a8e0f5a8..a8385c12494b0 100644 --- a/packages/model-typings/src/models/ICannedResponseModel.ts +++ b/packages/model-typings/src/models/ICannedResponseModel.ts @@ -1,12 +1,21 @@ import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; -import type { FindOptions, FindCursor, DeleteResult, UpdateResult, Document } from 'mongodb'; +import type { FindCursor, DeleteResult, UpdateResult, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ICannedResponseModel extends IBaseModel { - findOneById(_id: string, options?: FindOptions): Promise; - findOneByShortcut(shortcut: string, options?: FindOptions): Promise; - findByDepartmentId(departmentId: string, options?: FindOptions): FindCursor; + findOneByShortcut = FindOptionsWithProjection>( + shortcut: string, + options?: O, + ): Promise | null>; + findByDepartmentId< + T extends Document = IOmnichannelCannedResponse, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( + departmentId: string, + options?: O, + ): FindCursor>; removeById(_id: string): Promise; createCannedResponse({ shortcut, diff --git a/packages/model-typings/src/models/ICustomSoundsModel.ts b/packages/model-typings/src/models/ICustomSoundsModel.ts index b30a5104895bb..2a00363690cef 100644 --- a/packages/model-typings/src/models/ICustomSoundsModel.ts +++ b/packages/model-typings/src/models/ICustomSoundsModel.ts @@ -1,11 +1,20 @@ import type { ICustomSound } from '@rocket.chat/core-typings'; -import type { FindCursor, FindOptions, InsertOneResult, UpdateResult, WithId } from 'mongodb'; +import type { FindCursor, InsertOneResult, UpdateResult, WithId, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ICustomSoundsModel extends IBaseModel { - findByName(name: string, exceptId?: string, options?: FindOptions): FindCursor; - findOneByName(name: string, exceptId?: string, options?: FindOptions): Promise; + findByName = FindOptionsWithProjection>( + name: string, + exceptId?: string, + options?: O, + ): FindCursor>; + findOneByName = FindOptionsWithProjection>( + name: string, + exceptId?: string, + options?: O, + ): Promise | null>; create(data: Omit): Promise>>; updateById(_id: string, data: Partial>): Promise; } diff --git a/packages/model-typings/src/models/ICustomUserStatusModel.ts b/packages/model-typings/src/models/ICustomUserStatusModel.ts index 4e55a29992f1c..9f33acf4e0351 100644 --- a/packages/model-typings/src/models/ICustomUserStatusModel.ts +++ b/packages/model-typings/src/models/ICustomUserStatusModel.ts @@ -1,14 +1,28 @@ import type { ICustomUserStatus } from '@rocket.chat/core-typings'; -import type { FindCursor, FindOptions, InsertOneResult, UpdateResult, WithId } from 'mongodb'; +import type { FindCursor, InsertOneResult, UpdateResult, WithId, Document } from 'mongodb'; import type { IBaseModel, InsertionModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ICustomUserStatusModel extends IBaseModel { - findOneByName(name: string, options?: undefined): Promise; - findOneByName(name: string, options?: FindOptions): Promise; - findOneByNameExceptId(name: string, except: string, options?: FindOptions): Promise; - findByName(name: string, options?: FindOptions): FindCursor; - findByNameExceptId(name: string, except: string, options?: FindOptions): FindCursor; + findOneByName = FindOptionsWithProjection>( + name: string, + options?: O, + ): Promise | null>; + findOneByNameExceptId = FindOptionsWithProjection>( + name: string, + except: string, + options?: O, + ): Promise | null>; + findByName = FindOptionsWithProjection>( + name: string, + options?: O, + ): FindCursor>; + findByNameExceptId = FindOptionsWithProjection>( + name: string, + except: string, + options?: O, + ): FindCursor>; setName(_id: string, name: string): Promise; setStatusType(_id: string, statusType: string): Promise; create(data: InsertionModel): Promise>>; diff --git a/packages/model-typings/src/models/IEmojiCustomModel.ts b/packages/model-typings/src/models/IEmojiCustomModel.ts index e41bedd14fc57..6a914bb606425 100644 --- a/packages/model-typings/src/models/IEmojiCustomModel.ts +++ b/packages/model-typings/src/models/IEmojiCustomModel.ts @@ -1,17 +1,32 @@ import type { IEmojiCustom } from '@rocket.chat/core-typings'; -import type { FindCursor, FindOptions, InsertOneResult, UpdateResult, WithId } from 'mongodb'; +import type { FindCursor, InsertOneResult, UpdateResult, WithId, Document } from 'mongodb'; import type { IBaseModel, InsertionModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IEmojiCustomModel extends IBaseModel { - findByNameOrAlias(emojiName: string, options?: FindOptions): FindCursor; - findOneByNamesOrAliases(names: string[], exceptId?: string, options?: FindOptions): Promise; - findByNameOrAliasExceptID(name: string, except: string, options?: FindOptions): FindCursor; + findByNameOrAlias = FindOptionsWithProjection>( + emojiName: string, + options?: O, + ): FindCursor>; + findOneByNamesOrAliases = FindOptionsWithProjection>( + names: string[], + exceptId?: string, + options?: O, + ): Promise | null>; + findByNameOrAliasExceptID = FindOptionsWithProjection>( + name: string, + except: string, + options?: O, + ): FindCursor>; setName(_id: string, name: string): Promise; setAliases(_id: string, aliases: string[]): Promise; setExtension(_id: string, extension: string): Promise; setETagByName(name: string, etag: string): Promise; create(data: InsertionModel): Promise>>; countByNameOrAlias(name: string): Promise; - findOneByName(name: string, options?: FindOptions): Promise; + findOneByName = FindOptionsWithProjection>( + name: string, + options?: O, + ): Promise | null>; } diff --git a/packages/model-typings/src/models/IIntegrationsModel.ts b/packages/model-typings/src/models/IIntegrationsModel.ts index 4fc3e12765ecf..d0da18ac35845 100644 --- a/packages/model-typings/src/models/IIntegrationsModel.ts +++ b/packages/model-typings/src/models/IIntegrationsModel.ts @@ -1,7 +1,8 @@ import type { IIntegration, IUser } from '@rocket.chat/core-typings'; -import type { AggregateOptions, FindCursor, FindOptions } from 'mongodb'; +import type { AggregateOptions, FindCursor, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export type IntegrationsStatistics = { totalIntegrations: number; @@ -20,10 +21,10 @@ export interface IIntegrationsModel extends IBaseModel { removeByIdAndCreatedByIfExists(params: { _id: IIntegration['_id']; createdBy?: IUser['_id'] }): Promise; findOneByUrl(url: string): Promise; updateRoomName(oldRoomName: string, newRoomName: string): ReturnType['updateMany']>; - findOneByIdAndToken

( + findOneByIdAndToken

= FindOptionsWithProjection

>( id: IIntegration['_id'], token: string, - options?: FindOptions

, - ): Promise

; + options?: O, + ): Promise | null>; getStatistics(options?: AggregateOptions): Promise; } diff --git a/packages/model-typings/src/models/ILivechatBusinessHoursModel.ts b/packages/model-typings/src/models/ILivechatBusinessHoursModel.ts index 99599d2c3dba5..9786dcf759631 100644 --- a/packages/model-typings/src/models/ILivechatBusinessHoursModel.ts +++ b/packages/model-typings/src/models/ILivechatBusinessHoursModel.ts @@ -1,7 +1,8 @@ import type { ILivechatBusinessHour, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; -import type { Document, FindOptions } from 'mongodb'; +import type { Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IWorkHoursCronJobsItem { day: string; @@ -14,14 +15,25 @@ export interface IWorkHoursCronJobsWrapper { } export interface ILivechatBusinessHoursModel extends IBaseModel { - findActiveBusinessHours(options?: FindOptions): Promise; - findOneDefaultBusinessHour(options?: undefined): Promise; - findOneDefaultBusinessHour(options: FindOptions): Promise; - findOneDefaultBusinessHour

( - options: FindOptions

, - ): Promise

; - findOneDefaultBusinessHour

(options?: any): Promise; - findActiveAndOpenBusinessHoursByDay(day: string, options?: FindOptions): Promise; + findActiveBusinessHours< + T extends Document = ILivechatBusinessHour, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( + options?: O, + ): Promise[]>; + findOneDefaultBusinessHour< + P extends Document = ILivechatBusinessHour, + O extends FindOptionsWithProjection

= FindOptionsWithProjection

, + >( + options?: O, + ): Promise | null>; + findActiveAndOpenBusinessHoursByDay< + T extends Document = ILivechatBusinessHour, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( + day: string, + options?: O, + ): Promise[]>; findDefaultActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise; insertOne(data: Omit): Promise; findHoursToScheduleJobs(): Promise; diff --git a/packages/model-typings/src/models/ILivechatContactsModel.ts b/packages/model-typings/src/models/ILivechatContactsModel.ts index 37f289f4a273e..52eabec21cc30 100644 --- a/packages/model-typings/src/models/ILivechatContactsModel.ts +++ b/packages/model-typings/src/models/ILivechatContactsModel.ts @@ -5,19 +5,11 @@ import type { ILivechatContactVisitorAssociation, ILivechatVisitor, } from '@rocket.chat/core-typings'; -import type { - AggregationCursor, - Document, - FindCursor, - FindOneAndUpdateOptions, - FindOptions, - UpdateFilter, - UpdateOptions, - UpdateResult, -} from 'mongodb'; +import type { AggregationCursor, Document, FindCursor, FindOneAndUpdateOptions, UpdateFilter, UpdateOptions, UpdateResult } from 'mongodb'; import type { Updater } from '../updater'; import type { FindPaginated, IBaseModel, InsertionModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ILivechatContactsModel extends IBaseModel { insertContact( @@ -33,10 +25,10 @@ export interface ILivechatContactsModel extends IBaseModel { ): Promise; updateById(contactId: string, update: UpdateFilter, options?: UpdateOptions): Promise; addChannel(contactId: string, channel: ILivechatContactChannel): Promise; - findPaginatedContacts( + findPaginatedContacts = FindOptionsWithProjection>( search: { searchText?: string; unknown?: boolean }, - options?: FindOptions, - ): FindPaginated>; + options?: O, + ): FindPaginated>>; updateLastChatById( contactId: string, visitor: ILivechatContactVisitorAssociation, @@ -44,21 +36,21 @@ export interface ILivechatContactsModel extends IBaseModel { ): Promise; findContactMatchingVisitor(visitor: AtLeast): Promise; findContactByEmailAndContactManager(email: string): Promise | null>; - findOneByVisitor( + findOneByVisitor = FindOptionsWithProjection>( visitor: ILivechatContactVisitorAssociation, - options?: FindOptions, - ): Promise; + options?: O, + ): Promise | null>; isChannelBlocked(visitor: ILivechatContactVisitorAssociation): Promise; updateFromUpdaterByAssociation( visitor: ILivechatContactVisitorAssociation, contactUpdater: Updater, options?: UpdateOptions, ): Promise; - findSimilarVerifiedContacts( + findSimilarVerifiedContacts = FindOptionsWithProjection>( channel: Pick, originalContactId: string, - options?: FindOptions, - ): Promise; + options?: O, + ): Promise[]>; findAllByVisitorId(visitorId: string): FindCursor; addEmail(contactId: string, email: string): Promise; isContactActiveOnPeriod(visitor: ILivechatContactVisitorAssociation, period: string): Promise; @@ -77,7 +69,8 @@ export interface ILivechatContactsModel extends IBaseModel { getStatistics(): AggregationCursor<{ totalConflicts: number; avgChannelsPerContact: number }>; disableByVisitorId(visitorId: string): Promise; disableByContactId(contactId: string): Promise; - findOneEnabledById(_id: ILivechatContact['_id'], options?: FindOptions): Promise; - findOneEnabledById

(_id: P['_id'], options?: FindOptions

): Promise

; - findOneEnabledById(_id: ILivechatContact['_id'], options?: any): Promise; + findOneEnabledById

= FindOptionsWithProjection

>( + _id: ILivechatContact['_id'], + options?: O, + ): Promise | null>; } diff --git a/packages/model-typings/src/models/ILivechatCustomFieldModel.ts b/packages/model-typings/src/models/ILivechatCustomFieldModel.ts index 527601f9ff8cf..56977c037b339 100644 --- a/packages/model-typings/src/models/ILivechatCustomFieldModel.ts +++ b/packages/model-typings/src/models/ILivechatCustomFieldModel.ts @@ -1,30 +1,32 @@ import type { ILivechatCustomField } from '@rocket.chat/core-typings'; -import type { FindOptions, FindCursor, Document } from 'mongodb'; +import type { FindCursor, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ILivechatCustomFieldModel extends IBaseModel { - findByScope( + findByScope = FindOptionsWithProjection>( scope: ILivechatCustomField['scope'], - options?: FindOptions, + options?: O, includeHidden?: boolean, - ): FindCursor; - findByScope( - scope: ILivechatCustomField['scope'], - options?: FindOptions, - includeHidden?: boolean, - ): FindCursor; - findMatchingCustomFields( + ): FindCursor>; + findMatchingCustomFields< + T extends Document = ILivechatCustomField, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( scope: ILivechatCustomField['scope'], searchable: boolean, - options?: FindOptions, - ): FindCursor; - findMatchingCustomFieldsByIds( + options?: O, + ): FindCursor>; + findMatchingCustomFieldsByIds< + T extends Document = ILivechatCustomField, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( ids: ILivechatCustomField['_id'][], scope: ILivechatCustomField['scope'], searchable: boolean, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; createOrUpdateCustomField( _id: string | null, field: string, diff --git a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts index 52928bf0ef295..534104e59f808 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts @@ -1,38 +1,38 @@ import type { AvailableAgentsAggregation, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; -import type { DeleteResult, FindCursor, FindOptions, Document, UpdateResult, Filter, AggregationCursor } from 'mongodb'; +import type { DeleteResult, FindCursor, Document, UpdateResult, Filter, AggregationCursor } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ILivechatDepartmentAgentsModel extends IBaseModel { - findByAgentId(agentId: string, options?: FindOptions): FindCursor; - - findAgentsByDepartmentId(departmentId: string): FindPaginated>; - - findAgentsByDepartmentId( - departmentId: string, - options: FindOptions, - ): FindPaginated>; - - findAgentsByDepartmentId

( - departmentId: string, - options: FindOptions

, - ): FindPaginated>; + findByAgentId = FindOptionsWithProjection>( + agentId: string, + options?: O, + ): FindCursor>; - findAgentsByDepartmentId( + findAgentsByDepartmentId< + P extends Document = ILivechatDepartmentAgents, + O extends FindOptionsWithProjection

= FindOptionsWithProjection

, + >( departmentId: string, - options?: undefined | FindOptions, - ): FindPaginated>; + options?: O, + ): FindPaginated>>; findByDepartmentIds(departmentIds: string[], options?: Record): FindCursor; setDepartmentEnabledByDepartmentId(departmentId: string, departmentEnabled: boolean): Promise; removeByDepartmentId(departmentId: string): Promise; - findByDepartmentId(departmentId: string, options?: FindOptions): FindCursor; - findOneByAgentIdAndDepartmentId( + findByDepartmentId = FindOptionsWithProjection>( + departmentId: string, + options?: O, + ): FindCursor>; + findOneByAgentIdAndDepartmentId< + T extends Document = ILivechatDepartmentAgents, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( agentId: string, departmentId: string, - options?: FindOptions, - ): Promise; - findOneByAgentIdAndDepartmentId(agentId: string, departmentId: string): Promise; + options?: O, + ): Promise | null>; saveAgent(agent: Omit): Promise; removeByAgentId(agentId: string): Promise; getNextAgentForDepartment( @@ -52,11 +52,17 @@ export interface ILivechatDepartmentAgentsModel extends IBaseModel; enableAgentsByDepartmentId(departmentId: string): Promise; findAllAgentsConnectedToListOfDepartments(departmentIds: string[]): Promise; - findByAgentIds(agentIds: string[], options?: FindOptions): FindCursor; - findByAgentsAndDepartmentId( + findByAgentIds = FindOptionsWithProjection>( + agentIds: string[], + options?: O, + ): FindCursor>; + findByAgentsAndDepartmentId< + T extends Document = ILivechatDepartmentAgents, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( agentsIds: ILivechatDepartmentAgents['agentId'][], departmentId: ILivechatDepartmentAgents['departmentId'], - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; findDepartmentsOfAgent(agentId: string, enabled?: boolean): AggregationCursor; } diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index fe1a9b2831857..cd8dac020bce6 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -2,23 +2,44 @@ import type { ILivechatDepartment, LivechatDepartmentDTO } from '@rocket.chat/co import type { FindOptions, FindCursor, Filter, UpdateResult, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ILivechatDepartmentModel extends IBaseModel { countTotal(): Promise; - findInIds(departmentsIds: string[], options: FindOptions): FindCursor; - findByNameRegexWithExceptionsAndConditions( + findInIds = FindOptionsWithProjection>( + departmentsIds: string[], + options: O, + ): FindCursor>; + findByNameRegexWithExceptionsAndConditions< + T extends Document = ILivechatDepartment, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( searchTerm: string, exceptions: string[], conditions: Filter, - options: FindOptions, - ): FindCursor; + options: O, + ): FindCursor>; - findByBusinessHourId(businessHourId: string, options: FindOptions): FindCursor; + findByBusinessHourId = FindOptionsWithProjection>( + businessHourId: string, + options: O, + ): FindCursor>; countByBusinessHourIdExcludingDepartmentId(businessHourId: string, departmentId: string): Promise; - findEnabledByBusinessHourId(businessHourId: string, options: FindOptions): FindCursor; + findEnabledByBusinessHourId< + T extends Document = ILivechatDepartment, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( + businessHourId: string, + options: O, + ): FindCursor>; - findActiveDepartmentsWithoutBusinessHour(options: FindOptions): FindCursor; + findActiveDepartmentsWithoutBusinessHour< + T extends Document = ILivechatDepartment, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( + options: O, + ): FindCursor>; addBusinessHourToDepartmentsByIds(ids: string[], businessHourId: string): Promise; @@ -39,11 +60,22 @@ export interface ILivechatDepartmentModel extends IBaseModel['projection'], ): FindCursor; - findOneByIdOrName(_idOrName: string, options?: FindOptions): Promise; - findByUnitIds(unitIds: string[], options?: FindOptions): FindCursor; + findOneByIdOrName = FindOptionsWithProjection>( + _idOrName: string, + options?: O, + ): Promise | null>; + findByUnitIds = FindOptionsWithProjection>( + unitIds: string[], + options?: O, + ): FindCursor>; countDepartmentsInUnit(unitId: string): Promise; - findActiveByUnitIds(unitIds: string[], options?: FindOptions): FindCursor; - findNotArchived(options?: FindOptions): FindCursor; + findActiveByUnitIds = FindOptionsWithProjection>( + unitIds: string[], + options?: O, + ): FindCursor>; + findNotArchived = FindOptionsWithProjection>( + options?: O, + ): FindCursor>; getBusinessHoursWithDepartmentStatuses(): Promise< { _id: string; @@ -53,7 +85,10 @@ export interface ILivechatDepartmentModel extends IBaseModel; checkIfMonitorIsMonitoringDepartmentById(monitorId: string, departmentId: string): Promise; countArchived(): Promise; - findEnabledInIds(departmentsIds: string[], options?: FindOptions): FindCursor; + findEnabledInIds = FindOptionsWithProjection>( + departmentsIds: string[], + options?: O, + ): FindCursor>; archiveDepartment(_id: string): Promise; unarchiveDepartment(_id: string): Promise; addDepartmentToUnit(_id: string, unitId: string, ancestors: string[]): Promise; diff --git a/packages/model-typings/src/models/ILivechatInquiryModel.ts b/packages/model-typings/src/models/ILivechatInquiryModel.ts index 89614bf26fd59..d32c2ffe08bff 100644 --- a/packages/model-typings/src/models/ILivechatInquiryModel.ts +++ b/packages/model-typings/src/models/ILivechatInquiryModel.ts @@ -2,12 +2,13 @@ import type { IMessage, ILivechatInquiryRecord, LivechatInquiryStatus, SelectedA import type { FindOptions, Document, UpdateResult, DeleteResult, FindCursor, DeleteOptions, AggregateOptions } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ILivechatInquiryModel extends IBaseModel { - findOneByRoomId( + findOneByRoomId = FindOptionsWithProjection>( rid: string, - options?: FindOptions, - ): Promise; + options?: O, + ): Promise | null>; getDistinctQueuedDepartments(options: AggregateOptions): Promise<{ _id: string | null }[]>; setDepartmentByInquiryId(inquiryId: string, department: string): Promise; setLastMessageByRoomId(rid: ILivechatInquiryRecord['rid'], message: IMessage): Promise; @@ -18,14 +19,15 @@ export interface ILivechatInquiryModel extends IBaseModel; unlock(inquiryId: string): Promise; unlockAll(): Promise; - findIdsByVisitorId(_id: ILivechatInquiryRecord['v']['_id']): FindCursor; getCurrentSortedQueueAsync(props: { inquiryId?: string; department?: string; queueSortBy: FindOptions['sort']; }): Promise<(Pick & { position: number })[]>; removeByRoomId(rid: string, options?: DeleteOptions): Promise; - getQueuedInquiries(options?: FindOptions): FindCursor; + getQueuedInquiries = FindOptionsWithProjection>( + options?: O, + ): FindCursor>; takeInquiry(inquiryId: string, lockedAt?: Date): Promise; queueInquiry(inquiryId: string, lastMessage?: IMessage, defaultAgent?: SelectedAgent | null): Promise; queueInquiryAndRemoveDefaultAgent(inquiryId: string): Promise; @@ -40,5 +42,8 @@ export interface ILivechatInquiryModel extends IBaseModel; setStatusById(inquiryId: string, status: LivechatInquiryStatus): Promise; updateNameByVisitorIds(visitorIds: string[], name: string): Promise; - findByVisitorIds(visitorIds: string[], options?: FindOptions): FindCursor; + findByVisitorIds = FindOptionsWithProjection>( + visitorIds: string[], + options?: O, + ): FindCursor>; } diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 5753262e1490e..586470351da69 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -13,6 +13,7 @@ import type { FindCursor, UpdateResult, AggregationCursor, Document, FindOptions import type { FindPaginated } from '..'; import type { Updater } from '../updater'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; type Period = { start: any; @@ -139,7 +140,10 @@ export interface ILivechatRoomsModel extends IBaseModel { closeRoomById(roomId: string, closeInfo: IOmnichannelRoomClosingInfo, options?: UpdateOptions): Promise; bulkRemoveDepartmentAndUnitsFromRooms(departmentId: string): Promise; - findOneByIdOrName(_idOrName: string, options?: FindOptions): Promise; + findOneByIdOrName = FindOptionsWithProjection>( + _idOrName: string, + options?: O, + ): Promise | null>; updateSurveyFeedbackById(_id: string, surveyFeedback: unknown): Promise; updateDataByToken(token: string, key: string, value: string, overwrite?: boolean): Promise; saveRoomById( @@ -155,57 +159,78 @@ export interface ILivechatRoomsModel extends IBaseModel { visitorToken: string, fields?: FindOptions['projection'], ): Promise; - findOneByVisitorTokenAndEmailThreadAndDepartment( + findOneByVisitorTokenAndEmailThreadAndDepartment< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( visitorToken: string, emailThread: string[], departmentId?: string, - options?: FindOptions, - ): Promise; + options?: O, + ): Promise | null>; updateEmailThreadByRoomId(roomId: string, threadIds: string[] | string): Promise; - findOneLastServedAndClosedByVisitorToken(visitorToken: string, options?: FindOptions): Promise; + findOneLastServedAndClosedByVisitorToken< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( + visitorToken: string, + options?: O, + ): Promise | null>; findOneByVisitorToken(visitorToken: string, fields?: FindOptions['projection']): Promise; - findOpenByVisitorToken( + findOpenByVisitorToken = FindOptionsWithProjection>( visitorToken: string, - options?: FindOptions, + options?: O, extraQuery?: Filter, - ): FindCursor; - findOneOpenByContactChannelVisitor( + ): FindCursor>; + findOneOpenByContactChannelVisitor< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( association: ILivechatContactVisitorAssociation, - options?: FindOptions, - ): Promise; - findOneOpenByVisitorToken( + options?: O, + ): Promise | null>; + findOneOpenByVisitorToken = FindOptionsWithProjection>( visitorToken: string, - options?: FindOptions, + options?: O, extraQuery?: Filter, - ): Promise; - findOneOpenByVisitorTokenAndDepartmentIdAndSource( + ): Promise | null>; + findOneOpenByVisitorTokenAndDepartmentIdAndSource< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( visitorToken: string, departmentId?: string, source?: string, - options?: FindOptions, - ): Promise; - findOpenByVisitorTokenAndDepartmentId( + options?: O, + ): Promise | null>; + findOpenByVisitorTokenAndDepartmentId< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( visitorToken: string, departmentId: string, - options?: FindOptions, + options?: O, extraQuery?: Filter, - ): FindCursor; - findByVisitorIdAndAgentId( + ): FindCursor>; + findByVisitorIdAndAgentId = FindOptionsWithProjection>( visitorId?: string, agentId?: string, - options?: FindOptions, + options?: O, extraQuery?: Filter, - ): FindCursor; - findOneOpenByRoomIdAndVisitorToken( + ): FindCursor>; + findOneOpenByRoomIdAndVisitorToken< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( roomId: string, visitorToken: string, - options?: FindOptions, - ): Promise; - findClosedRooms( + options?: O, + ): Promise | null>; + findClosedRooms = FindOptionsWithProjection>( departmentIds?: string[], - options?: FindOptions, + options?: O, extraQuery?: Filter, - ): FindCursor; + ): FindCursor>; getResponseByRoomIdUpdateQuery( responseBy: IOmnichannelRoom['responseBy'], updater?: Updater, @@ -241,11 +266,11 @@ export interface ILivechatRoomsModel extends IBaseModel { date: { gte: Date; lte: Date }, data?: { departmentId: string }, ): AggregationCursor>; - findOpenByAgent( + findOpenByAgent = FindOptionsWithProjection>( userId: string, extraQuery?: Filter, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; countOpenByAgent(userId: string, extraQuery?: Filter): Promise; changeAgentByRoomId(roomId: string, newAgent: { agentId: string; username: string }): Promise; changeDepartmentIdByRoomId(roomId: string, departmentId: string): Promise; @@ -279,6 +304,9 @@ export interface ILivechatRoomsModel extends IBaseModel { oldContactId: ILivechatContact['_id'], contact: Partial>, ): Promise; - findOpenByContactId(contactId: ILivechatContact['_id'], options?: FindOptions): FindCursor; - checkContactOpenRooms(contactId: ILivechatContact['_id']): Promise; + findOpenByContactId = FindOptionsWithProjection>( + contactId: ILivechatContact['_id'], + options?: O, + ): FindCursor>; + checkContactOpenRooms(contactId: ILivechatContact['_id']): Promise | null>; } diff --git a/packages/model-typings/src/models/ILivechatUnitModel.ts b/packages/model-typings/src/models/ILivechatUnitModel.ts index 2e447dc5724bf..47810a646ef32 100644 --- a/packages/model-typings/src/models/ILivechatUnitModel.ts +++ b/packages/model-typings/src/models/ILivechatUnitModel.ts @@ -1,25 +1,27 @@ import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings'; -import type { FindOptions, Filter, FindCursor, DeleteResult, UpdateResult, Document } from 'mongodb'; +import type { Filter, FindCursor, DeleteResult, UpdateResult, Document } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; -// @ts-expect-error - Overriding base types :) export interface ILivechatUnitModel extends IBaseModel { // - findPaginatedUnits( + findPaginatedUnits = FindOptionsWithProjection>( query: Filter, - options?: FindOptions, - ): FindPaginated>; - findOne( + options?: O, + ): FindPaginated>>; + // `extra` carries the unit restrictions applied to the query before it reaches `BaseRaw.findOne`, + // so the projection is rewritten as usual and inference behaves like every other model + findOne

= FindOptionsWithProjection

>( originalQuery: Filter, - options: FindOptions, + options?: O, extra?: Record, - ): Promise; - findOneById

( + ): Promise | null>; + findOneById

= FindOptionsWithProjection

>( _id: IOmnichannelBusinessUnit['_id'], - options: FindOptions, + options?: O, extra?: Record, - ): Promise

; + ): Promise | null>; createOrUpdateUnit( _id: string | null, { name, visibility }: { name: string; visibility: IOmnichannelBusinessUnit['visibility'] }, @@ -32,7 +34,10 @@ export interface ILivechatUnitModel extends IBaseModel decrementDepartmentsCount(_id: string): Promise; removeById(_id: string): Promise; removeByIdAndUnit(_id: string, unitsFromUser?: string[]): Promise; - findOneByIdOrName(_idOrName: string, options: FindOptions): Promise; + findOneByIdOrName = FindOptionsWithProjection>( + _idOrName: string, + options: O, + ): Promise | null>; findByMonitorId(monitorId: string): Promise; findMonitoredDepartmentsByMonitorId(monitorId: string, includeDisabled: boolean): Promise; countUnits(): Promise; diff --git a/packages/model-typings/src/models/ILivechatVisitorsModel.ts b/packages/model-typings/src/models/ILivechatVisitorsModel.ts index 47744a29abe13..68a4bda527dc5 100644 --- a/packages/model-typings/src/models/ILivechatVisitorsModel.ts +++ b/packages/model-typings/src/models/ILivechatVisitorsModel.ts @@ -12,11 +12,21 @@ import type { } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ILivechatVisitorsModel extends IBaseModel { - findById(_id: string, options?: FindOptions): FindCursor; - findByIds(ids: string[], options?: FindOptions): FindCursor; - getVisitorByToken(token: string, options?: FindOptions): Promise; + findById = FindOptionsWithProjection>( + _id: string, + options?: O, + ): FindCursor>; + findByIds = FindOptionsWithProjection>( + ids: string[], + options?: O, + ): FindCursor>; + getVisitorByToken = FindOptionsWithProjection>( + token: string, + options?: O, + ): Promise | null>; findByNameRegexWithExceptionsAndConditions

( searchTerm: string, exceptions: string[], @@ -28,12 +38,15 @@ export interface ILivechatVisitorsModel extends IBaseModel { } >; - findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField( + findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField< + T extends Document = ILivechatVisitor, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( emailOrPhone?: string, nameOrUsername?: RegExp, allowedCustomFields?: string[], - options?: FindOptions, - ): Promise>>; + options?: O, + ): Promise>>>; findOneByEmailAndPhoneAndCustomField( email: string | null | undefined, @@ -73,11 +86,17 @@ export interface ILivechatVisitorsModel extends IBaseModel { saveGuestEmailPhoneById(_id: string, emails: string[], phones: string[]): Promise; - findOneEnabledById(_id: string, options?: FindOptions): Promise; + findOneEnabledById = FindOptionsWithProjection>( + _id: string, + options?: O, + ): Promise | null>; disableById(_id: string): Promise; - findEnabled(query: Filter, options?: FindOptions): FindCursor; + findEnabled = FindOptionsWithProjection>( + query: Filter, + options?: O, + ): FindCursor>; saveGuestById( _id: string, diff --git a/packages/model-typings/src/models/ILoginServiceConfigurationModel.ts b/packages/model-typings/src/models/ILoginServiceConfigurationModel.ts index 6a1a564c45d9f..48699814032fa 100644 --- a/packages/model-typings/src/models/ILoginServiceConfigurationModel.ts +++ b/packages/model-typings/src/models/ILoginServiceConfigurationModel.ts @@ -1,7 +1,8 @@ import type { LoginServiceConfiguration } from '@rocket.chat/core-typings'; -import type { Document, FindOptions } from 'mongodb'; +import type { Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ILoginServiceConfigurationModel extends IBaseModel { createOrUpdateService( @@ -9,8 +10,8 @@ export interface ILoginServiceConfigurationModel extends IBaseModel, ): Promise; removeByService(serviceName: LoginServiceConfiguration['service']): Promise | null>; - findOneByService

( + findOneByService

= FindOptionsWithProjection

>( serviceName: LoginServiceConfiguration['service'], - options?: FindOptions

, - ): Promise

; + options?: O, + ): Promise | null>; } diff --git a/packages/model-typings/src/models/IMediaCallNegotiationsModel.ts b/packages/model-typings/src/models/IMediaCallNegotiationsModel.ts index 1fd96f09f7110..6ac8a7deef4b5 100644 --- a/packages/model-typings/src/models/IMediaCallNegotiationsModel.ts +++ b/packages/model-typings/src/models/IMediaCallNegotiationsModel.ts @@ -1,13 +1,14 @@ import type { IMediaCallNegotiation, MediaCallNegotiationStream, RTCSessionDescriptionInit } from '@rocket.chat/core-typings'; -import type { Document, FindOptions, UpdateResult } from 'mongodb'; +import type { Document, UpdateResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IMediaCallNegotiationsModel extends IBaseModel { - findLatestByCallId( + findLatestByCallId = FindOptionsWithProjection>( callId: IMediaCallNegotiation['callId'], - options?: FindOptions, - ): Promise; + options?: O, + ): Promise | null>; setOfferById(id: string, offer: RTCSessionDescriptionInit, offerStreams?: MediaCallNegotiationStream[]): Promise; setAnswerById(id: string, answer: RTCSessionDescriptionInit, answerStreams?: MediaCallNegotiationStream[]): Promise; setStableById(id: string): Promise; diff --git a/packages/model-typings/src/models/IMediaCallsModel.ts b/packages/model-typings/src/models/IMediaCallsModel.ts index 2221a5335d226..d6e08613a4e38 100644 --- a/packages/model-typings/src/models/IMediaCallsModel.ts +++ b/packages/model-typings/src/models/IMediaCallsModel.ts @@ -6,29 +6,35 @@ import type { MediaCallContact, MediaCallSignedContact, } from '@rocket.chat/core-typings'; -import type { Document, FindCursor, FindOptions, UpdateResult } from 'mongodb'; +import type { Document, FindCursor, UpdateResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IMediaCallsModel extends IBaseModel { - findOneByIdAndCallee( + findOneByIdAndCallee = FindOptionsWithProjection>( id: IMediaCall['_id'], callee: MediaCallActor, - options?: FindOptions, - ): Promise; - findOneByCallerRequestedId( + options?: O, + ): Promise | null>; + findOneByCallerRequestedId = FindOptionsWithProjection>( id: Required['callerRequestedId'], caller: { type: MediaCallActorType; id: string }, - options?: FindOptions, - ): Promise; + options?: O, + ): Promise | null>; startRingingById(callId: string, expiresAt: Date): Promise; acceptCallById(callId: string, data: { calleeContractId: string; supportedFeatures: string[] }, expiresAt: Date): Promise; activateCallById(callId: string, expiresAt: Date): Promise; setExpiresAtById(callId: string, expiresAt: Date): Promise; hangupCallById(callId: string, params: { endedBy?: IMediaCall['endedBy']; reason?: string } | undefined): Promise; transferCallById(callId: string, params: { by: MediaCallSignedContact; to: MediaCallContact }): Promise; - findAllExpiredCalls(options: FindOptions | undefined): FindCursor; - findAllNotOverByUid(uid: IUser['_id'], options?: FindOptions): FindCursor; + findAllExpiredCalls = FindOptionsWithProjection>( + options?: O, + ): FindCursor>; + findAllNotOverByUid = FindOptionsWithProjection>( + uid: IUser['_id'], + options?: O, + ): FindCursor>; hasUnfinishedCalls(): Promise; hasUnfinishedCallsByUid(uid: IUser['_id'], exceptCallId?: string): Promise; } diff --git a/packages/model-typings/src/models/IMessagesModel.ts b/packages/model-typings/src/models/IMessagesModel.ts index 184d016e2655a..98d0778549add 100644 --- a/packages/model-typings/src/models/IMessagesModel.ts +++ b/packages/model-typings/src/models/IMessagesModel.ts @@ -22,6 +22,7 @@ import type { } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; type PaginatedRequest = { count?: number; @@ -31,23 +32,38 @@ type PaginatedRequest = { query?: string; }; export interface IMessagesModel extends IBaseModel { - findPaginatedVisibleByMentionAndRoomId( + findPaginatedVisibleByMentionAndRoomId< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( username: IUser['username'], rid: IRoom['_id'], - options?: FindOptions, - ): FindPaginated>; + options?: O, + ): FindPaginated>>; - findVisibleByMentionAndRoomId(username: IUser['username'], rid: IRoom['_id'], options?: FindOptions): FindCursor; + findVisibleByMentionAndRoomId = FindOptionsWithProjection>( + username: IUser['username'], + rid: IRoom['_id'], + options?: O, + ): FindCursor>; - findStarredByUserAtRoom(userId: IUser['_id'], roomId: IRoom['_id'], options?: FindOptions): FindPaginated>; + findStarredByUserAtRoom = FindOptionsWithProjection>( + userId: IUser['_id'], + roomId: IRoom['_id'], + options?: O, + ): FindPaginated>>; - findPaginatedByRoomIdAndType( + findPaginatedByRoomIdAndType = FindOptionsWithProjection>( roomId: IRoom['_id'], type: IMessage['t'], - options?: FindOptions, - ): FindPaginated>; + options?: O, + ): FindPaginated>>; - findDiscussionsByRoomAndText(rid: IRoom['_id'], text: string, options?: FindOptions): FindPaginated>; + findDiscussionsByRoomAndText = FindOptionsWithProjection>( + rid: IRoom['_id'], + text: string, + options?: O, + ): FindPaginated>>; findAllNumberOfTransferredRooms(p: { start: Date; @@ -67,13 +83,17 @@ export interface IMessagesModel extends IBaseModel { getTotalOfMessagesSentByDate(params: { start: Date; end: Date; options?: any }): Promise; - findLivechatClosedMessages(rid: IRoom['_id'], searchTerm?: string, options?: FindOptions): FindPaginated>; - findLivechatMessagesWithoutTypes( + findLivechatClosedMessages = FindOptionsWithProjection>( + rid: IRoom['_id'], + searchTerm?: string, + options?: O, + ): FindPaginated>>; + findLivechatMessagesWithoutTypes = FindOptionsWithProjection>( rid: IRoom['_id'], ignoredTypes: IMessage['t'][], showSystemMessages: boolean, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; countRoomsWithStarredMessages(options: AggregateOptions): Promise; countRoomsWithPinnedMessages(options: AggregateOptions): Promise; @@ -84,7 +104,10 @@ export interface IMessagesModel extends IBaseModel { countByType(type: IMessage['t'], options: CountDocumentsOptions): Promise; - findPaginatedPinnedByRoom(roomId: IMessage['rid'], options?: FindOptions): FindPaginated>; + findPaginatedPinnedByRoom = FindOptionsWithProjection>( + roomId: IMessage['rid'], + options?: O, + ): FindPaginated>>; setFederationReactionEventId(username: string, _id: string, reaction: string, federationEventId: string): Promise; @@ -98,16 +121,22 @@ export interface IMessagesModel extends IBaseModel { removeByRoomId(roomId: IRoom['_id']): Promise; - findVisibleByRoomIdNotContainingTypesBeforeTs( + findVisibleByRoomIdNotContainingTypesBeforeTs< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( roomId: IRoom['_id'], types: IMessage['t'][], ts: Date, showSystemMessages: boolean, - options?: FindOptions, + options?: O, showThreadMessages?: boolean, - ): FindCursor; + ): FindCursor>; - findLivechatClosingMessage(rid: IRoom['_id'], options?: FindOptions): Promise; + findLivechatClosingMessage = FindOptionsWithProjection>( + rid: IRoom['_id'], + options?: O, + ): Promise | null>; setReactions(messageId: string, reactions: IMessage['reactions']): Promise; setRoomIdByToken(token: string, rid: string): Promise; @@ -129,59 +158,86 @@ export interface IMessagesModel extends IBaseModel { ): Promise; countVisibleByRoomIdBetweenTimestampsInclusive(roomId: string, afterTimestamp: Date, beforeTimestamp: Date): Promise; - findByMention(username: string, options?: FindOptions): FindCursor; - findVisibleThreadByThreadId(tmid: string, options?: FindOptions): FindCursor; + findByMention = FindOptionsWithProjection>( + username: string, + options?: O, + ): FindCursor>; + findVisibleThreadByThreadId = FindOptionsWithProjection>( + tmid: string, + options?: O, + ): FindCursor>; findFilesByUserId(userId: string, options?: FindOptions): FindCursor>; - findVisibleByIds(ids: string[], options?: FindOptions): FindCursor; - findVisibleByRoomIdNotContainingTypes( + findVisibleByIds = FindOptionsWithProjection>( + ids: string[], + options?: O, + ): FindCursor>; + findVisibleByRoomIdNotContainingTypes< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( roomId: string, types: MessageTypesValues[], - options?: FindOptions, + options?: O, showThreadMessages?: boolean, - ): FindCursor; + ): FindCursor>; countVisibleByRoomIdContainingTypes(roomId: string, types: MessageTypesValues[]): Promise; - findFilesByRoomIdPinnedTimestampAndUsers( + findFilesByRoomIdPinnedTimestampAndUsers< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( rid: string, excludePinned: boolean, ignoreDiscussion: boolean, ts: Filter['ts'], users: string[], ignoreThreads: boolean, - options?: FindOptions, - ): FindCursor; - findVisibleByRoomId(rid: string, options?: FindOptions): FindCursor; - findDiscussionByRoomIdPinnedTimestampAndUsers( + options?: O, + ): FindCursor>; + findVisibleByRoomId = FindOptionsWithProjection>( + rid: string, + options?: O, + ): FindCursor>; + findDiscussionByRoomIdPinnedTimestampAndUsers< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( rid: string, excludePinned: boolean, ts: Filter['ts'], users: string[], - options?: FindOptions, - ): FindCursor; - findVisibleByRoomIdAfterTimestamp( + options?: O, + ): FindCursor>; + findVisibleByRoomIdAfterTimestamp = FindOptionsWithProjection>( roomId: string, timestamp: Date, showThreadMessages?: boolean, - options?: FindOptions, - ): FindCursor; - findVisibleByRoomIdBeforeTimestampNotContainingTypes( + options?: O, + ): FindCursor>; + findVisibleByRoomIdBeforeTimestampNotContainingTypes< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( roomId: string, timestamp: Date, types: MessageTypesValues[], - options?: FindOptions, + options?: O, showThreadMessages?: boolean, inclusive?: boolean, - ): FindCursor; + ): FindCursor>; - findVisibleByRoomIdBetweenTimestampsNotContainingTypes( + findVisibleByRoomIdBetweenTimestampsNotContainingTypes< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( roomId: string, afterTimestamp: Date, beforeTimestamp: Date, types: MessageTypesValues[], - options?: FindOptions, + options?: O, showThreadMessages?: boolean, inclusive?: boolean, - ): FindCursor; + ): FindCursor>; countVisibleByRoomIdBetweenTimestampsNotContainingTypes( roomId: string, afterTimestamp: Date, @@ -190,20 +246,24 @@ export interface IMessagesModel extends IBaseModel { showThreadMessages?: boolean, inclusive?: boolean, ): Promise; - findVisibleByRoomIdBeforeTimestamp( + findVisibleByRoomIdBeforeTimestamp = FindOptionsWithProjection>( roomId: string, timestamp: Date, showThreadMessages?: boolean, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; getLastTimestamp(options?: FindOptions): Promise; findOneBySlackBotIdAndSlackTs(slackBotId: string, slackTs: Date): Promise; - findByRoomIdAndMessageIds(rid: string, messageIds: string[], options?: FindOptions): FindCursor; - findForUpdates( + findByRoomIdAndMessageIds = FindOptionsWithProjection>( + rid: string, + messageIds: string[], + options?: O, + ): FindCursor>; + findForUpdates = FindOptionsWithProjection>( roomId: IMessage['rid'], { updatedAt, minTs }: { updatedAt: { $lt: Date } | { $gt: Date }; minTs?: Date }, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; updateUsernameOfEditByUserId(userId: string, username: string): Promise; updateAllUsernamesByUserId(userId: string, username: string): Promise; @@ -225,7 +285,11 @@ export interface IMessagesModel extends IBaseModel { pinned?: boolean, pinnedAt?: Date, ): Promise; - findOneByRoomIdAndMessageId(rid: string, messageId: string, options?: FindOptions): Promise; + findOneByRoomIdAndMessageId = FindOptionsWithProjection>( + rid: string, + messageId: string, + options?: O, + ): Promise | null>; updateUserStarById(_id: string, userId: string, starred?: boolean): Promise; updateUsernameAndMessageOfMentionByIdAndOldUsername( @@ -241,10 +305,13 @@ export interface IMessagesModel extends IBaseModel { removeByRoomIds(rids: string[]): Promise; - findThreadsByRoomIdPinnedTimestampAndUsers( + findThreadsByRoomIdPinnedTimestampAndUsers< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( data: { rid: string; pinned: boolean; ignoreDiscussion?: boolean; ts: Filter['ts']; users: string[] }, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; removeByIdPinnedTimestampLimitAndUsers( rid: string, diff --git a/packages/model-typings/src/models/INpsVoteModel.ts b/packages/model-typings/src/models/INpsVoteModel.ts index 0b764299fb76f..fee403f14c463 100644 --- a/packages/model-typings/src/models/INpsVoteModel.ts +++ b/packages/model-typings/src/models/INpsVoteModel.ts @@ -1,12 +1,25 @@ import type { INpsVote, INpsVoteStatus } from '@rocket.chat/core-typings'; -import type { Document, FindCursor, FindOptions, UpdateResult } from 'mongodb'; +import type { Document, FindCursor, UpdateResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface INpsVoteModel extends IBaseModel { - findNotSentByNpsId(npsId: string, options?: FindOptions): FindCursor; - findByNpsIdAndStatus(npsId: string, status: INpsVoteStatus, options?: FindOptions): FindCursor; - findByNpsId(npsId: string, options?: FindOptions): FindCursor; + // `sort` and `limit` are branded away because the implementation overwrites both on the cursor; + // a caller passing them would have them silently dropped + findNotSentByNpsId = FindOptionsWithProjection>( + npsId: string, + options?: O & { sort?: never; limit?: never }, + ): FindCursor>; + findByNpsIdAndStatus = FindOptionsWithProjection>( + npsId: string, + status: INpsVoteStatus, + options?: O, + ): FindCursor>; + findByNpsId = FindOptionsWithProjection>( + npsId: string, + options?: O, + ): FindCursor>; save(vote: Omit): Promise; updateVotesToSent(voteIds: string[]): Promise; updateOldSendingToNewByNpsId(npsId: string): Promise; diff --git a/packages/model-typings/src/models/IOAuthAccessTokensModel.ts b/packages/model-typings/src/models/IOAuthAccessTokensModel.ts index 5a3ac3ebda02f..722ca897bcebc 100644 --- a/packages/model-typings/src/models/IOAuthAccessTokensModel.ts +++ b/packages/model-typings/src/models/IOAuthAccessTokensModel.ts @@ -1,11 +1,18 @@ import type { IOAuthAccessToken } from '@rocket.chat/core-typings'; -import type { DeleteResult, FindOptions } from 'mongodb'; +import type { DeleteResult, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IOAuthAccessTokensModel extends IBaseModel { - findOneByAccessToken(accessToken: string, options?: FindOptions): Promise; - findOneByRefreshToken(refreshToken: string, options?: FindOptions): Promise; + findOneByAccessToken = FindOptionsWithProjection>( + accessToken: string, + options?: O, + ): Promise | null>; + findOneByRefreshToken = FindOptionsWithProjection>( + refreshToken: string, + options?: O, + ): Promise | null>; deleteByUserId(userId: string): Promise; deleteByUserIds(userIds: string[]): Promise; } diff --git a/packages/model-typings/src/models/IOAuthAppsModel.ts b/packages/model-typings/src/models/IOAuthAppsModel.ts index 3a696465d2ca3..32a2549427b0a 100644 --- a/packages/model-typings/src/models/IOAuthAppsModel.ts +++ b/packages/model-typings/src/models/IOAuthAppsModel.ts @@ -1,29 +1,36 @@ import type { IOAuthApps } from '@rocket.chat/core-typings'; -import type { FindOptions } from 'mongodb'; +import type { Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IOAuthAppsModel extends IBaseModel { - findOneAuthAppByIdOrClientId( + findOneAuthAppByIdOrClientId = FindOptionsWithProjection>( props: | { clientId: string } | { appId: string } | { _id: string; }, - options?: FindOptions, - ): Promise; + options?: O, + ): Promise | null>; - findOneActiveByClientId(clientId: string, options?: FindOptions): Promise; + findOneActiveByClientId = FindOptionsWithProjection>( + clientId: string, + options?: O, + ): Promise | null>; updateById( _id: IOAuthApps['_id'], data: Partial>, ): Promise; - findOneActiveByClientIdAndClientSecret( + findOneActiveByClientIdAndClientSecret< + T extends Document = IOAuthApps, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( clientId: string, clientSecret: string, - options?: FindOptions, - ): Promise; + options?: O, + ): Promise | null>; } diff --git a/packages/model-typings/src/models/IOAuthAuthCodesModel.ts b/packages/model-typings/src/models/IOAuthAuthCodesModel.ts index 01c3da630e632..7a7fd1dd30b78 100644 --- a/packages/model-typings/src/models/IOAuthAuthCodesModel.ts +++ b/packages/model-typings/src/models/IOAuthAuthCodesModel.ts @@ -1,10 +1,14 @@ import type { IOAuthAuthCode } from '@rocket.chat/core-typings'; -import type { DeleteResult, FindOptions } from 'mongodb'; +import type { DeleteResult, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IOAuthAuthCodesModel extends IBaseModel { - findOneByAuthCode(authCode: string, options?: FindOptions): Promise; + findOneByAuthCode = FindOptionsWithProjection>( + authCode: string, + options?: O, + ): Promise | null>; deleteByUserId(userId: string): Promise; deleteByUserIds(userIds: string[]): Promise; } diff --git a/packages/model-typings/src/models/IOAuthRefreshTokensModel.ts b/packages/model-typings/src/models/IOAuthRefreshTokensModel.ts index e3692b844df6a..e1df6c6553e9c 100644 --- a/packages/model-typings/src/models/IOAuthRefreshTokensModel.ts +++ b/packages/model-typings/src/models/IOAuthRefreshTokensModel.ts @@ -1,10 +1,14 @@ import type { IOAuthRefreshToken } from '@rocket.chat/core-typings'; -import type { DeleteResult, FindOptions } from 'mongodb'; +import type { DeleteResult, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IOAuthRefreshTokensModel extends IBaseModel { - findOneByRefreshToken(refreshToken: string, options?: FindOptions): Promise; + findOneByRefreshToken = FindOptionsWithProjection>( + refreshToken: string, + options?: O, + ): Promise | null>; deleteByUserId(userId: string): Promise; deleteByUserIds(userIds: string[]): Promise; } diff --git a/packages/model-typings/src/models/IPushTokenModel.ts b/packages/model-typings/src/models/IPushTokenModel.ts index 4b907ce47ee24..287cea1e84603 100644 --- a/packages/model-typings/src/models/IPushTokenModel.ts +++ b/packages/model-typings/src/models/IPushTokenModel.ts @@ -1,20 +1,27 @@ import type { AtLeast, IPushToken, IUser } from '@rocket.chat/core-typings'; -import type { DeleteResult, FindOptions, InsertOneResult, UpdateResult, FindCursor } from 'mongodb'; +import type { DeleteResult, InsertOneResult, UpdateResult, FindCursor, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IPushTokenModel extends IBaseModel { countTokensByUserId(userId: IUser['_id']): Promise; countGcmTokens(): Promise; countApnTokens(): Promise; findOneByTokenAndAppName(token: IPushToken['token'], appName: IPushToken['appName']): Promise; - findFirstByUserId(userId: IUser['_id'], options?: FindOptions): Promise; - findAllTokensByUserId(userId: IUser['_id'], options?: FindOptions): FindCursor; - findTokensByUserIdExceptId( + findFirstByUserId = FindOptionsWithProjection>( + userId: IUser['_id'], + options?: O, + ): Promise | null>; + findAllTokensByUserId = FindOptionsWithProjection>( + userId: IUser['_id'], + options?: O, + ): FindCursor>; + findTokensByUserIdExceptId = FindOptionsWithProjection>( userId: IUser['_id'], idToIgnore: IPushToken['_id'], - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; insertToken(data: AtLeast): Promise>; refreshTokenById( diff --git a/packages/model-typings/src/models/IRolesModel.ts b/packages/model-typings/src/models/IRolesModel.ts index 70f81277e8ee7..53d5da53df60a 100644 --- a/packages/model-typings/src/models/IRolesModel.ts +++ b/packages/model-typings/src/models/IRolesModel.ts @@ -1,28 +1,29 @@ import type { IRole, IUser, IRoom } from '@rocket.chat/core-typings'; -import type { FindCursor, FindOptions, CountDocumentsOptions } from 'mongodb'; +import type { FindCursor, FindOptions, CountDocumentsOptions, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IRolesModel extends IBaseModel { - findByUpdatedDate(updatedAfterDate: Date, options?: FindOptions): FindCursor; + findByUpdatedDate = FindOptionsWithProjection>( + updatedAfterDate: Date, + options?: O, + ): FindCursor>; isUserInRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: IRoom['_id']): Promise; - findOneByIdOrName(_idOrName: IRole['_id'] | IRole['name'], options?: undefined): Promise; - - findOneByIdOrName(_idOrName: IRole['_id'] | IRole['name'], options: FindOptions): Promise; - - findOneByIdOrName

( - _idOrName: IRole['_id'] | IRole['name'], - options: FindOptions

, - ): Promise

; - - findOneByIdOrName

(_idOrName: IRole['_id'] | IRole['name'], options?: any): Promise; + findOneByIdOrName

= FindOptionsWithProjection

>( + _idOrName: IRole['_id'], + options?: O, + ): Promise | null>; findOneByName

(name: IRole['name'], options?: any): Promise; findInIds

(ids: IRole['_id'][], options?: FindOptions): P extends Pick ? FindCursor

: FindCursor; findInIdsOrNames

( - _idsOrNames: IRole['_id'][] | IRole['name'][], + _idsOrNames: IRole['_id'][], options?: FindOptions, ): P extends Pick ? FindCursor

: FindCursor; - findByScope(scope: IRole['scope'], options?: FindOptions): FindCursor; + findByScope = FindOptionsWithProjection>( + scope: IRole['scope'], + options?: O, + ): FindCursor>; updateById( _id: IRole['_id'], name: IRole['name'], @@ -41,13 +42,11 @@ export interface IRolesModel extends IBaseModel { ): Promise>; /** @deprecated function getUsersInRole should be used instead */ - findUsersInRole

( - roleId: IRole['_id'], - scope: IRoom['_id'] | undefined, - options?: any | undefined, - ): Promise | FindCursor

>; + findUsersInRole

(roleId: IRole['_id'], scope: IRoom['_id'] | undefined, options?: any): Promise | FindCursor

>; - findCustomRoles(options?: FindOptions): FindCursor; + findCustomRoles = FindOptionsWithProjection>( + options?: O, + ): FindCursor>; createWithRandomId( name: IRole['name'], diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 2fb1d1b783a5c..1052872412584 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -23,6 +23,7 @@ import type { import type { Updater } from '../updater'; import type { FindPaginated, IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IChannelsWithNumberOfMessagesBetweenDate { room: { @@ -39,22 +40,32 @@ export interface IChannelsWithNumberOfMessagesBetweenDate { } export interface IRoomsModel extends IBaseModel { - findAllByTypesAndDiscussionAndTeam( + findAllByTypesAndDiscussionAndTeam = FindOptionsWithProjection>( filters?: { types?: Array; discussions?: boolean; teams?: boolean; }, - findOptions?: FindOptions, - ): FindCursor; + findOptions?: O, + ): FindCursor>; isAbacAttributeInUse(key: string, values: string[]): Promise; - findOneByRoomIdAndUserId(rid: IRoom['_id'], uid: IUser['_id'], options?: FindOptions): Promise; + findOneByRoomIdAndUserId = FindOptionsWithProjection>( + rid: IRoom['_id'], + uid: IUser['_id'], + options?: O, + ): Promise | null>; - findManyByRoomIds(roomIds: Array, options?: FindOptions): FindCursor; + findManyByRoomIds = FindOptionsWithProjection>( + roomIds: Array, + options?: O, + ): FindCursor>; - findManyArchivedByRoomIds(roomIds: Array, options?: FindOptions): FindCursor; + findManyArchivedByRoomIds = FindOptionsWithProjection>( + roomIds: Array, + options?: O, + ): FindCursor>; findPaginatedByIds( roomIds: Array, @@ -63,49 +74,71 @@ export interface IRoomsModel extends IBaseModel { getMostRecentAverageChatDurationTime(numberMostRecentChats: number, department?: string): Promise; - findByNameOrFnameContainingAndTypes( + findByNameOrFnameContainingAndTypes = FindOptionsWithProjection>( name: NonNullable, types: Array, discussion?: boolean, teams?: boolean, - options?: FindOptions, - ): FindPaginated>; + options?: O, + ): FindPaginated>>; - findPrivateRoomsAndTeamsPaginated(name: NonNullable, options?: FindOptions): FindPaginated>; + findPrivateRoomsAndTeamsPaginated = FindOptionsWithProjection>( + name: NonNullable, + options?: O, + ): FindPaginated>>; - findByTeamId(teamId: ITeam['_id'], options?: FindOptions): FindCursor; + findByTeamId = FindOptionsWithProjection>( + teamId: ITeam['_id'], + options?: O, + ): FindCursor>; countByTeamId(teamId: ITeam['_id']): Promise; - findPaginatedByTeamIdContainingNameAndDefault( + findPaginatedByTeamIdContainingNameAndDefault< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( teamId: ITeam['_id'], name: IRoom['name'], teamDefault: boolean, ids: Array | undefined, - options?: FindOptions, - ): FindPaginated>; + options?: O, + ): FindPaginated>>; - findByTeamIdAndRoomsId(teamId: ITeam['_id'], rids: Array, options?: FindOptions): FindCursor; + findByTeamIdAndRoomsId = FindOptionsWithProjection>( + teamId: ITeam['_id'], + rids: Array, + options?: O, + ): FindCursor>; - findRoomsByNameOrFnameStarting(name: NonNullable, options?: FindOptions): FindCursor; + findRoomsByNameOrFnameStarting = FindOptionsWithProjection>( + name: NonNullable, + options?: O, + ): FindCursor>; - findRoomsWithoutDiscussionsByRoomIds( + findRoomsWithoutDiscussionsByRoomIds = FindOptionsWithProjection>( name: NonNullable, roomIds: Array, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; - findPaginatedRoomsWithoutDiscussionsByRoomIds( + findPaginatedRoomsWithoutDiscussionsByRoomIds< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( name: NonNullable, roomIds: Array, - options?: FindOptions, - ): FindPaginated>; + options?: O, + ): FindPaginated>>; - findChannelAndGroupListWithoutTeamsByNameStartingByOwner( + findChannelAndGroupListWithoutTeamsByNameStartingByOwner< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( name: IRoom['name'], groupsToAccept: Array, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; unsetTeamId(teamId: ITeam['_id'], options?: UpdateOptions): Promise; @@ -123,9 +156,16 @@ export interface IRoomsModel extends IBaseModel { incUsersCountByIds(ids: Array, inc: number, options?: UpdateOptions): Promise; - findOneByNameOrFname(name: NonNullable, options?: FindOptions): Promise; + findOneByNameOrFname = FindOptionsWithProjection>( + name: NonNullable, + options?: O, + ): Promise | null>; - findOneByJoinCodeAndId(joinCode: string, rid: IRoom['_id'], options?: FindOptions): Promise; + findOneByJoinCodeAndId = FindOptionsWithProjection>( + joinCode: string, + rid: IRoom['_id'], + options?: O, + ): Promise | null>; findOneByNonValidatedName(name: NonNullable, options?: FindOptions): Promise; @@ -137,30 +177,49 @@ export interface IRoomsModel extends IBaseModel { setFnameById(_id: IRoom['_id'], fname: IRoom['fname']): Promise; - findE2ERoomById(roomId: IRoom['_id'], options?: FindOptions): Promise; + findE2ERoomById = FindOptionsWithProjection>( + roomId: IRoom['_id'], + options?: O, + ): Promise | null>; countRoomsInsideTeams(autoJoin?: boolean): Promise; - findOneDirectRoomContainingAllUserIDs(uid: IDirectMessageRoom['uids'], options?: FindOptions): Promise; + findOneDirectRoomContainingAllUserIDs = FindOptionsWithProjection>( + uid: IDirectMessageRoom['uids'], + options?: O, + ): Promise | null>; countByType(t: IRoom['t']): Promise; - findPaginatedByNameOrFNameAndRoomIdsIncludingTeamRooms( + findPaginatedByNameOrFNameAndRoomIdsIncludingTeamRooms< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( searchTerm: RegExp | null, teamIds: Array, roomIds: Array, - options?: FindOptions, - ): FindPaginated>; + options?: O, + ): FindPaginated>>; - findPaginatedContainingNameOrFNameInIdsAsTeamMain( + findPaginatedContainingNameOrFNameInIdsAsTeamMain< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( searchTerm: RegExp | null, rids: Array, - options?: FindOptions, - ): FindPaginated>; + options?: O, + ): FindPaginated>>; - findPaginatedByTypeAndIds(type: IRoom['t'], ids: Array, options?: FindOptions): FindPaginated>; + findPaginatedByTypeAndIds = FindOptionsWithProjection>( + type: IRoom['t'], + ids: Array, + options?: O, + ): FindPaginated>>; - findOneFederatedByMrid(mrid: string, options?: FindOptions): Promise; + findOneFederatedByMrid = FindOptionsWithProjection>( + mrid: string, + options?: O, + ): Promise | null>; findBiggestFederatedRoomInNumberOfUsers(options?: FindOptions): Promise; @@ -175,7 +234,10 @@ export interface IRoomsModel extends IBaseModel { ): Promise; getIncMsgCountUpdateQuery(inc: number, roomUpdater: Updater): Updater; decreaseMessageCountById(rid: string, dec: number): Promise; - findOneByIdOrName(_idOrName: string, options?: FindOptions): Promise; + findOneByIdOrName = FindOptionsWithProjection>( + _idOrName: string, + options?: O, + ): Promise | null>; setReactionsInLastMessage(roomId: string, reactions: NonNullable['reactions']): Promise; unsetReactionsInLastMessage(roomId: string): Promise; unsetAllImportIds(): Promise; @@ -191,7 +253,10 @@ export interface IRoomsModel extends IBaseModel { readOnly: NonNullable, reactWhenReadOnly: NonNullable, ): Promise; - getDirectConversationsByUserId(userId: string, options?: FindOptions): FindCursor; + getDirectConversationsByUserId = FindOptionsWithProjection>( + userId: string, + options?: O, + ): FindCursor>; setAllowReactingWhenReadOnlyById( roomId: string, allowReactingWhenReadOnly: NonNullable, @@ -204,44 +269,108 @@ export interface IRoomsModel extends IBaseModel { e2eKeyId: string, options?: Omit, ): Promise; - findOneByImportId(importId: string, options?: FindOptions): Promise; + findOneByImportId = FindOptionsWithProjection>( + importId: string, + options?: O, + ): Promise | null>; findOneByNameAndNotId(name: string, rid: string): Promise; - findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options?: FindOptions): Promise; - findOneByDisplayName(displayName: string, options?: FindOptions): Promise; - findOneByNameAndType( + findOneByIdAndType = FindOptionsWithProjection>( + roomId: IRoom['_id'], + type: IRoom['t'], + options?: O, + ): Promise | null>; + findOneByDisplayName = FindOptionsWithProjection>( + displayName: string, + options?: O, + ): Promise | null>; + findOneByNameAndType = FindOptionsWithProjection>( name: string, type: IRoom['t'], - options?: FindOptions, + options?: O, includeFederatedRooms?: boolean, - ): Promise; - findById(rid: string, options?: FindOptions): Promise; - findByIds(rids: string[], options?: FindOptions): FindCursor; - findByType(type: IRoom['t'], options?: FindOptions): FindCursor; - findByTypeInIds(type: IRoom['t'], ids: string[], options?: FindOptions): FindCursor; - findPrivateRoomsByIdsWithAbacAttributes(ids: string[], options?: FindOptions): FindCursor; - findAllPrivateRoomsWithAbacAttributes(options?: FindOptions): FindCursor; - findBySubscriptionUserId(userId: string, options?: FindOptions): Promise>; - findBySubscriptionUserIdUpdatedAfter(userId: string, updatedAfter: Date, options?: FindOptions): Promise>; - findByNameAndTypeNotDefault( + ): Promise | null>; + findById = FindOptionsWithProjection>( + rid: string, + options?: O, + ): Promise | null>; + findByIds = FindOptionsWithProjection>( + rids: string[], + options?: O, + ): FindCursor>; + findByType = FindOptionsWithProjection>( + type: IRoom['t'], + options?: O, + ): FindCursor>; + findByTypeInIds = FindOptionsWithProjection>( + type: IRoom['t'], + ids: string[], + options?: O, + ): FindCursor>; + findPrivateRoomsByIdsWithAbacAttributes< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( + ids: string[], + options?: O, + ): FindCursor>; + findAllPrivateRoomsWithAbacAttributes = FindOptionsWithProjection>( + options?: O, + ): FindCursor>; + findBySubscriptionUserId = FindOptionsWithProjection>( + userId: string, + options?: O, + ): Promise>>; + findBySubscriptionUserIdUpdatedAfter = FindOptionsWithProjection>( + userId: string, + updatedAfter: Date, + options?: O, + ): Promise>>; + findByNameAndTypeNotDefault = FindOptionsWithProjection>( name: IRoom['name'] | RegExp, type: IRoom['t'], - options?: FindOptions, + options?: O, includeFederatedRooms?: boolean, - ): FindCursor; - findByNameOrFNameAndTypesNotInIds( + ): FindCursor>; + findByNameOrFNameAndTypesNotInIds = FindOptionsWithProjection>( name: IRoom['name'] | RegExp, types: IRoom['t'][], ids: string[], - options?: FindOptions, + options?: O, includeFederatedRooms?: boolean, - ): FindCursor; - findByDefaultAndTypes(defaultValue: boolean, types: IRoom['t'][], options?: FindOptions): FindCursor; - findDirectRoomContainingAllUsernames(usernames: string[], options?: FindOptions): Promise; - findByTypeAndNameOrId(type: IRoom['t'], name: string, options?: FindOptions): Promise; - findByTypeAndNameContaining(type: IRoom['t'], name: string, options?: FindOptions): FindCursor; - findByTypeInIdsAndNameContaining(type: IRoom['t'], ids: string[], name: string, options?: FindOptions): FindCursor; - findGroupDMsByUids(uids: string[], options?: FindOptions): FindCursor; - find1On1ByUserId(userId: string, options?: FindOptions): FindCursor; + ): FindCursor>; + findByDefaultAndTypes = FindOptionsWithProjection>( + defaultValue: boolean, + types: IRoom['t'][], + options?: O, + ): FindCursor>; + findDirectRoomContainingAllUsernames = FindOptionsWithProjection>( + usernames: string[], + options?: O, + ): Promise | null>; + findByTypeAndNameOrId = FindOptionsWithProjection>( + type: IRoom['t'], + name: string, + options?: O, + ): Promise | null>; + findByTypeAndNameContaining = FindOptionsWithProjection>( + type: IRoom['t'], + name: string, + options?: O, + ): FindCursor>; + findByTypeInIdsAndNameContaining = FindOptionsWithProjection>( + type: IRoom['t'], + ids: string[], + name: string, + options?: O, + ): FindCursor>; + findGroupDMsByUids = FindOptionsWithProjection>( + uids: string[], + options?: O, + ): FindCursor>; + find1On1ByUserId = FindOptionsWithProjection>( + userId: string, + options?: O, + ): FindCursor>; findByUsernamesOrUids(uids: IRoom['u']['_id'][], usernames: IRoom['u']['username'][]): FindCursor; findDMsByUids(uids: IRoom['u']['_id'][]): FindCursor; addImportIds(rid: string, importIds: string[]): Promise; @@ -318,9 +447,16 @@ export interface IRoomsModel extends IBaseModel { unsetAbacAttributesById(rid: IRoom['_id']): Promise; unsetAllAbacAttributes(): Promise; updateSingleAbacAttributeValuesById(rid: IRoom['_id'], key: string, values: string[]): Promise; - insertAbacAttributeIfNotExistsById(rid: IRoom['_id'], key: string, values: string[]): Promise; + insertAbacAttributeIfNotExistsById( + rid: IRoom['_id'], + key: string, + values: string[], + ): Promise | null>; updateAbacAttributeValuesArrayFilteredById(rid: IRoom['_id'], key: string, values: string[]): Promise; removeAbacAttributeByRoomIdAndKey(rid: IRoom['_id'], key: string): Promise; removeUserReferenceFromDMsById(roomId: string, username: string, userId: string): Promise; - findFederatedByIds(ids: Array, options?: FindOptions): FindCursor; + findFederatedByIds = FindOptionsWithProjection>( + ids: Array, + options?: O, + ): FindCursor>; } diff --git a/packages/model-typings/src/models/ISessionsModel.ts b/packages/model-typings/src/models/ISessionsModel.ts index b85c41659b78d..1a074866ff32a 100644 --- a/packages/model-typings/src/models/ISessionsModel.ts +++ b/packages/model-typings/src/models/ISessionsModel.ts @@ -7,9 +7,10 @@ import type { DeviceManagementPopulatedSession, DeviceManagementSession, } from '@rocket.chat/core-typings'; -import type { BulkWriteResult, Document, FindOptions, UpdateResult, FindCursor, OptionalId } from 'mongodb'; +import type { BulkWriteResult, Document, UpdateResult, FindCursor, OptionalId } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export type DestructuredDate = { year: number; month: number; day: number }; export type DestructuredDateWithType = { @@ -143,9 +144,9 @@ export interface ISessionsModel extends IBaseModel { updateAllSessionsByDateToComputed({ start, end }: DestructuredRange): Promise; - getLoggedInByUserIdAndSessionId( + getLoggedInByUserIdAndSessionId = FindOptionsWithProjection>( userId: string, sessionId: string, - options?: FindOptions, - ): Promise; + options?: O, + ): Promise | null>; } diff --git a/packages/model-typings/src/models/ISettingsModel.ts b/packages/model-typings/src/models/ISettingsModel.ts index ded3ead204e1d..6869a1a28d28d 100644 --- a/packages/model-typings/src/models/ISettingsModel.ts +++ b/packages/model-typings/src/models/ISettingsModel.ts @@ -1,16 +1,8 @@ import type { ISetting, ISettingColor, ISettingSelectOption, SettingValue } from '@rocket.chat/core-typings'; -import type { - FindCursor, - UpdateFilter, - UpdateResult, - Document, - FindOptions, - FindOneAndUpdateOptions, - WithId, - UpdateOptions, -} from 'mongodb'; +import type { FindCursor, UpdateFilter, UpdateResult, Document, FindOneAndUpdateOptions, WithId, UpdateOptions } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ISettingsModel extends IBaseModel { getValueById(_id: string): Promise; @@ -19,7 +11,10 @@ export interface ISettingsModel extends IBaseModel { findOneNotHiddenById(_id: string): Promise; - findByIds(_id?: string[] | string, options?: FindOptions): FindCursor; + findByIds = FindOptionsWithProjection>( + _id?: string[] | string, + options?: O, + ): FindCursor>; updateValueById( _id: string, diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index c40d60179496f..22b4d17a8f43f 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -17,20 +17,38 @@ import type { } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; -import type { DocumentWithProjection } from '../types/DocumentWithProjection'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ISubscriptionsModel extends IBaseModel { getBadgeCount(uid: string): Promise; - findOneByRoomIdAndUserId(rid: string, uid: string, options?: FindOptions): Promise; + findOneByRoomIdAndUserId = FindOptionsWithProjection>( + rid: string, + uid: string, + options?: O, + ): Promise | null>; - findByUserIdAndRoomIds(userId: string, roomIds: Array, options?: FindOptions): FindCursor; + findByUserIdAndRoomIds = FindOptionsWithProjection>( + userId: string, + roomIds: Array, + options?: O, + ): FindCursor>; - findByRoomId(roomId: string, options?: FindOptions): FindCursor; + findByRoomId = FindOptionsWithProjection>( + roomId: string, + options?: O, + ): FindCursor>; - findUnarchivedByRoomId(roomId: string, options?: FindOptions): FindCursor; + findUnarchivedByRoomId = FindOptionsWithProjection>( + roomId: string, + options?: O, + ): FindCursor>; - findByRoomIdAndNotUserId(roomId: string, userId: string, options?: FindOptions): FindCursor; + findByRoomIdAndNotUserId = FindOptionsWithProjection>( + roomId: string, + userId: string, + options?: O, + ): FindCursor>; countByRoomIdAndUserId(rid: string, uid: string | undefined, includeInvitations?: boolean): Promise; @@ -50,41 +68,44 @@ export interface ISubscriptionsModel extends IBaseModel { removeRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid: IRoom['_id']): Promise; - findUsersInRoles(roles: IRole['_id'][], rid: string | undefined): Promise>; - - findUsersInRoles(roles: IRole['_id'][], rid: string | undefined, options: FindOptions): Promise>; - - findUsersInRoles

( + findUsersInRoles

= FindOptionsWithProjection

>( roles: IRole['_id'][], rid: string | undefined, - options: FindOptions

, - ): Promise>; - - findUsersInRoles

( - roles: IRole['_id'][], - rid: IRoom['_id'] | undefined, - options?: FindOptions

, - ): Promise>; + options?: O, + ): Promise>>; addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid?: IRoom['_id']): Promise; isUserInRoleScope(uid: IUser['_id'], rid?: IRoom['_id']): Promise; - findByRolesAndRoomId({ roles, rid }: { roles: string; rid?: string }, options?: FindOptions): FindCursor; + findByRolesAndRoomId = FindOptionsWithProjection>( + { roles, rid }: { roles: string; rid?: string }, + options?: O, + ): FindCursor>; - findByUserIdAndTypes(userId: string, types: ISubscription['t'][], options?: FindOptions): FindCursor; + findByUserIdAndTypes = FindOptionsWithProjection>( + userId: string, + types: ISubscription['t'][], + options?: O, + ): FindCursor>; - findOpenByVisitorIds(visitorIds: string[], options?: FindOptions): FindCursor; + findOpenByVisitorIds = FindOptionsWithProjection>( + visitorIds: string[], + options?: O, + ): FindCursor>; - findByRoomIdAndNotAlertOrOpenExcludingUserIds( + findByRoomIdAndNotAlertOrOpenExcludingUserIds< + T extends Document = ISubscription, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( filter: { roomId: ISubscription['rid']; uidsExclude?: ISubscription['u']['_id'][]; uidsInclude?: ISubscription['u']['_id'][]; onlyRead: boolean; }, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; removeByRoomId(roomId: ISubscription['rid'], options?: DeleteOptions & { onTrash: (doc: ISubscription) => void }): Promise; @@ -151,80 +172,121 @@ export interface ISubscriptionsModel extends IBaseModel { getAutoTranslateLanguagesByRoomAndNotUser(rid: string, userId: string): Promise<(string | undefined)[]>; - findByRidWithoutE2EKey(rid: string, options: FindOptions): FindCursor; + findByRidWithoutE2EKey = FindOptionsWithProjection>( + rid: string, + options: O, + ): FindCursor>; findUsersWithPublicE2EKeyByRids( rids: IRoom['_id'][], excludeUserId: IUser['_id'], usersLimit?: number, ): AggregationCursor<{ rid: IRoom['_id']; users: { _id: IUser['_id']; public_key: string }[] }>; - findByUserId(userId: string, options?: FindOptions): FindCursor; + findByUserId = FindOptionsWithProjection>( + userId: string, + options?: O, + ): FindCursor>; updateAutoTranslateById(_id: string, autoTranslate: boolean): Promise; setAutoTranslateByUserId(userId: IUser['_id'], language: string | null): Promise; - findByAutoTranslateAndUserId( + findByAutoTranslateAndUserId = FindOptionsWithProjection>( userId: ISubscription['u']['_id'], autoTranslate?: ISubscription['autoTranslate'], - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; - findByUserIdAndRoomType( + findByUserIdAndRoomType = FindOptionsWithProjection>( userId: ISubscription['u']['_id'], type: ISubscription['t'], - options?: FindOptions, - ): FindCursor; - findByNameAndRoomType( + options?: O, + ): FindCursor>; + findByNameAndRoomType = FindOptionsWithProjection>( filter: Partial>, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise; - findByUserIdWithoutE2E(userId: string, options?: FindOptions): FindCursor; + findByUserIdWithoutE2E = FindOptionsWithProjection>( + userId: string, + options?: O, + ): FindCursor>; resetUserE2EKey(userId: string): Promise; - findOneByRoomIdAndUsername(roomId: string, username: string, options: FindOptions): Promise; + findOneByRoomIdAndUsername = FindOptionsWithProjection>( + roomId: string, + username: string, + options: O, + ): Promise | null>; - findByTypeAndUserId(type: ISubscription['t'], userId: string, options?: FindOptions): FindCursor; + findByTypeAndUserId = FindOptionsWithProjection>( + type: ISubscription['t'], + userId: string, + options?: O, + ): FindCursor>; - findByType(types: ISubscription['t'][], options?: FindOptions): FindCursor; + findByType = FindOptionsWithProjection>( + types: ISubscription['t'][], + options?: O, + ): FindCursor>; - findByUserIdAndRoles(userId: string, roles: string[], options?: FindOptions): FindCursor; + findByUserIdAndRoles = FindOptionsWithProjection>( + userId: string, + roles: string[], + options?: O, + ): FindCursor>; getLastSeen(options?: FindOptions): Promise; - findByRoomWithUserHighlights(roomId: string, options?: FindOptions): FindCursor; - findByUserIdAndType(userId: string, type: ISubscription['t'], options?: FindOptions): FindCursor; - findByUserIdExceptType( + findByRoomWithUserHighlights = FindOptionsWithProjection>( + roomId: string, + options?: O, + ): FindCursor>; + findByUserIdAndType = FindOptionsWithProjection>( + userId: string, + type: ISubscription['t'], + options?: O, + ): FindCursor>; + findByUserIdExceptType = FindOptionsWithProjection>( userId: string, typeException: ISubscription['t'], - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; - findByRoomIdAndRoles

= FindOptions

>( + findByRoomIdAndRoles

= FindOptionsWithProjection

>( roomId: string, roles: string[], options?: O, ): FindCursor>; - - findByRoomIdAndRoles(roomId: string, roles: string[], options?: FindOptions): FindCursor; - findByRoomIdAndUserIds( + findByRoomIdAndUserIds = FindOptionsWithProjection>( roomId: ISubscription['rid'], userIds: ISubscription['u']['_id'][], - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; - getMinimumLastSeenByRoomId(rid: string): Promise; + getMinimumLastSeenByRoomId(rid: string): Promise | null>; setAsUnreadByRoomIdAndUserId(roomId: string, userId: string, firstMessageUnreadTimestamp: Date): Promise; archiveByRoomId(roomId: string): Promise; - findArchivedByRoomId(roomId: string, options?: FindOptions): FindCursor; - findArchivedByUserId(userId: string, options?: FindOptions): FindCursor; + findArchivedByRoomId = FindOptionsWithProjection>( + roomId: string, + options?: O, + ): FindCursor>; + findArchivedByUserId = FindOptionsWithProjection>( + userId: string, + options?: O, + ): FindCursor>; unarchiveByIds(ids: string[]): Promise; updateNameAndAlertByRoomId(roomId: string, name: string, fname: string): Promise; - findByRoomIdWhenUsernameExists(rid: string, options?: FindOptions): FindCursor; + findByRoomIdWhenUsernameExists = FindOptionsWithProjection>( + rid: string, + options?: O, + ): FindCursor>; setCustomFieldsDirectMessagesByUserId(userId: string, fields: Record): Promise; setFavoriteByRoomIdAndUserId(roomId: string, userId: string, favorite?: boolean): Promise; hideByRoomIdAndUserId(roomId: string, userId: string): Promise; - findByRoomIdWhenUserIdExists(rid: string, options?: FindOptions): FindCursor; + findByRoomIdWhenUserIdExists = FindOptionsWithProjection>( + rid: string, + options?: O, + ): FindCursor>; updateNameAndFnameById(_id: string, name: string, fname: string, options?: { session?: ClientSession }): Promise; setUserUsernameByUserId(userId: string, username: string): Promise; updateFnameByRoomId(rid: string, fname: string): Promise; @@ -267,12 +329,12 @@ export interface ISubscriptionsModel extends IBaseModel { notificationField: keyof ISubscription, notificationOriginField: keyof ISubscription, ): Promise; - findByUserPreferences( + findByUserPreferences = FindOptionsWithProjection>( userId: string, notificationOriginField: keyof ISubscription, originFieldNotEqualValue: 'user' | 'subscription', - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; clearNotificationUserPreferences( userId: string, notificationField: string, @@ -298,11 +360,11 @@ export interface ISubscriptionsModel extends IBaseModel { removeUnreadThreadByRoomIdAndUserId(rid: string, userId: string, tmid: string, clearAlert?: boolean): Promise; removeUnreadThreadsByRoomId(rid: string, tunread: string[]): Promise; - findUnreadThreadsByRoomId( + findUnreadThreadsByRoomId = FindOptionsWithProjection>( rid: ISubscription['rid'], tunread: ISubscription['tunread'], - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; countByRoomIdAndRoles(roomId: string, roles: string[]): Promise; countByRoomId(roomId: string, options?: CountDocumentsOptions): Promise; @@ -318,5 +380,8 @@ export interface ISubscriptionsModel extends IBaseModel { banByRoomIdAndUserId(roomId: string, userId: string): Promise; unbanToInvitedById(subId: string, inviter: Required> & Pick): Promise; setAbacLastTimeCheckedByUserIdAndRoomId(userId: string, roomId: string, time: Date): Promise; - findJoinedByUserId(userId: ISubscription['u']['_id'], options?: FindOptions): FindCursor; + findJoinedByUserId = FindOptionsWithProjection>( + userId: ISubscription['u']['_id'], + options?: O, + ): FindCursor>; } diff --git a/packages/model-typings/src/models/ITeamMemberModel.ts b/packages/model-typings/src/models/ITeamMemberModel.ts index 8f9e49ae25407..fab8f57aa0b67 100644 --- a/packages/model-typings/src/models/ITeamMemberModel.ts +++ b/packages/model-typings/src/models/ITeamMemberModel.ts @@ -12,7 +12,7 @@ export interface ITeamMemberModel extends IBaseModel { findByUserId

( userId: string, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor; findOneByUserIdAndTeamId(userId: string, teamId: string): Promise; @@ -24,7 +24,7 @@ export interface ITeamMemberModel extends IBaseModel { findOneByUserIdAndTeamId

( userId: string, teamId: string, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): Promise

; findByTeamId(teamId: string): FindCursor; @@ -35,7 +35,7 @@ export interface ITeamMemberModel extends IBaseModel { findByTeamId

( teamId: string, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor; countByTeamId(teamId: string): Promise; @@ -47,7 +47,7 @@ export interface ITeamMemberModel extends IBaseModel { findByTeamIds

( teamIds: Array, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor; countByTeamIdAndRole(teamId: string, role: IRole['_id']): Promise; @@ -59,7 +59,7 @@ export interface ITeamMemberModel extends IBaseModel { limit: number, skip: number, query?: Filter, - ): FindPaginated>; + ): FindPaginated>>; updateOneByUserIdAndTeamId(userId: string, teamId: string, update: Partial): Promise; createOneByTeamIdAndUserId( diff --git a/packages/model-typings/src/models/ITeamModel.ts b/packages/model-typings/src/models/ITeamModel.ts index ef2cde1cdaa14..35666ccebbdb7 100644 --- a/packages/model-typings/src/models/ITeamModel.ts +++ b/packages/model-typings/src/models/ITeamModel.ts @@ -1,7 +1,8 @@ import type { ITeam, TeamType } from '@rocket.chat/core-typings'; -import type { FindOptions, FindCursor, UpdateResult, DeleteResult, Filter, Document } from 'mongodb'; +import type { FindCursor, UpdateResult, DeleteResult, Filter, Document, FindOptions } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ITeamModel extends IBaseModel { findByNames(names: Array): FindCursor; @@ -12,26 +13,20 @@ export interface ITeamModel extends IBaseModel { findByNames

( names: Array, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor; - findByIds(ids: Array, query?: Filter): FindCursor; - - findByIds(ids: Array, options: FindOptions, query?: Filter): FindCursor; - - findByIds

( + findByIds

= FindOptionsWithProjection

>( ids: Array, - options: FindOptions

, + options?: O, query?: Filter, - ): FindCursor

; + ): FindCursor>; - findByIds

( + findByIdsPaginated = FindOptionsWithProjection>( ids: Array, - options?: undefined | FindOptions | FindOptions

, + options?: O, query?: Filter, - ): FindCursor

| FindCursor; - - findByIdsPaginated(ids: Array, options?: undefined | FindOptions, query?: Filter): FindPaginated>; + ): FindPaginated>>; findByIdsAndType(ids: Array, type: TeamType): FindCursor; @@ -46,7 +41,7 @@ export interface ITeamModel extends IBaseModel { findByIdsAndType

( ids: Array, type: TeamType, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor; findByType(type: number): FindCursor; @@ -57,7 +52,7 @@ export interface ITeamModel extends IBaseModel { findByType

( type: number, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor | FindCursor

; findByNameAndTeamIds(name: string | RegExp, teamIds: Array): FindCursor; @@ -73,7 +68,7 @@ export interface ITeamModel extends IBaseModel { findByNameAndTeamIds

( name: string | RegExp, teamIds: Array, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor; findOneByName(name: string | RegExp): Promise; @@ -84,7 +79,7 @@ export interface ITeamModel extends IBaseModel { findOneByName

( name: string | RegExp, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): Promise

| Promise; findOneByMainRoomId(roomId: string): Promise; @@ -95,7 +90,7 @@ export interface ITeamModel extends IBaseModel { findOneByMainRoomId

( roomId: string, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): Promise

| Promise; updateMainRoomForTeam(id: string, roomId: string): Promise; diff --git a/packages/model-typings/src/models/ITwoFactorChallengesModel.ts b/packages/model-typings/src/models/ITwoFactorChallengesModel.ts index e0f523ae49104..1bef1bca704eb 100644 --- a/packages/model-typings/src/models/ITwoFactorChallengesModel.ts +++ b/packages/model-typings/src/models/ITwoFactorChallengesModel.ts @@ -1,10 +1,17 @@ import type { ITwoFactorChallenge } from '@rocket.chat/core-typings'; -import type { DeleteResult, FindOptions } from 'mongodb'; +import type { DeleteResult, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface ITwoFactorChallengesModel extends IBaseModel { - findOneByPendingChallengeId(id: string, options?: FindOptions): Promise; + findOneByPendingChallengeId< + T extends Document = ITwoFactorChallenge, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( + id: string, + options?: O, + ): Promise | null>; removeByPendingChallengeId(id: string): Promise; createTwoFactorChallenge(userId: string, method: ITwoFactorChallenge['method']): Promise; } diff --git a/packages/model-typings/src/models/IUploadsModel.ts b/packages/model-typings/src/models/IUploadsModel.ts index decda2d2680a3..ecd15a8062356 100644 --- a/packages/model-typings/src/models/IUploadsModel.ts +++ b/packages/model-typings/src/models/IUploadsModel.ts @@ -1,8 +1,9 @@ import type { IRoom, IUpload } from '@rocket.chat/core-typings'; -import type { FindCursor, WithId, Filter, FindOptions, UpdateResult } from 'mongodb'; +import type { FindCursor, WithId, Filter, FindOptions, UpdateResult, Document } from 'mongodb'; import type { FindPaginated } from './IBaseModel'; import type { IBaseUploadsModel } from './IBaseUploadsModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IUploadsModel extends IBaseUploadsModel { findPaginatedWithoutThumbs(query: Filter, options?: any): FindPaginated>>; @@ -19,5 +20,8 @@ export interface IUploadsModel extends IBaseUploadsModel { setFederationRoomInfo(fileId: IUpload['_id'], rid: IRoom['_id'], mrid: string): Promise; - findAllByOriginalFileId(originalFileId: string, options?: FindOptions): FindCursor; + findAllByOriginalFileId = FindOptionsWithProjection>( + originalFileId: string, + options?: O, + ): FindCursor>; } diff --git a/packages/model-typings/src/models/IUserDataFilesModel.ts b/packages/model-typings/src/models/IUserDataFilesModel.ts index 22471ea081dc6..27653243739b9 100644 --- a/packages/model-typings/src/models/IUserDataFilesModel.ts +++ b/packages/model-typings/src/models/IUserDataFilesModel.ts @@ -1,8 +1,12 @@ import type { IUserDataFile } from '@rocket.chat/core-typings'; -import type { FindOptions } from 'mongodb'; +import type { Document } from 'mongodb'; import type { IBaseUploadsModel } from './IBaseUploadsModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IUserDataFilesModel extends IBaseUploadsModel { - findLastFileByUser(userId: string, options?: FindOptions): Promise; + findLastFileByUser = FindOptionsWithProjection>( + userId: string, + options?: O, + ): Promise | null>; } diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index b7734f0c23721..a621addd07b2a 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -26,83 +26,138 @@ import type { } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; -import type { DocumentWithProjection } from '../types/DocumentWithProjection'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IUsersModel extends IBaseModel { addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][]): Promise; - findUsersInRoles = FindOptions>( + findUsersInRoles = FindOptionsWithProjection>( roles: IRole['_id'][] | IRole['_id'], _scope?: null, options?: O, ): FindCursor>; - findPaginatedUsersInRoles(roles: IRole['_id'][] | IRole['_id'], options?: FindOptions): FindPaginated>; - findOneByIdWithEmailAddress(uid: IUser['_id'], options?: FindOptions): Promise; - findOneByUsername(username: string, options?: FindOptions): Promise; - findOneAgentById(_id: IUser['_id'], options?: FindOptions): Promise; - findUsersInRolesWithQuery(roles: IRole['_id'][] | IRole['_id'], query: Filter, options?: FindOptions): FindCursor; - findPaginatedUsersInRolesWithQuery( + findPaginatedUsersInRoles = FindOptionsWithProjection>( + roles: IRole['_id'][] | IRole['_id'], + options?: O, + ): FindPaginated>>; + findOneByIdWithEmailAddress = FindOptionsWithProjection>( + uid: IUser['_id'], + options?: O, + ): Promise | null>; + findOneByUsername = FindOptionsWithProjection>( + username: string, + options?: O, + ): Promise | null>; + findOneAgentById = FindOptionsWithProjection>( + _id: IUser['_id'], + options?: O, + ): Promise | null>; + findUsersInRolesWithQuery = FindOptionsWithProjection>( roles: IRole['_id'][] | IRole['_id'], query: Filter, - options?: FindOptions, - ): FindPaginated>>; - findOneByUsernameAndRoomIgnoringCase(username: string | RegExp, rid: string, options?: FindOptions): Promise; - findOneByIdAndLoginHashedToken(_id: IUser['_id'], token: string, options?: FindOptions): Promise; - findByActiveUsersExcept( + options?: O, + ): FindCursor>; + findPaginatedUsersInRolesWithQuery = FindOptionsWithProjection>( + roles: IRole['_id'][] | IRole['_id'], + query: Filter, + options?: O, + ): FindPaginated>>; + findOneByUsernameAndRoomIgnoringCase = FindOptionsWithProjection>( + username: string | RegExp, + rid: string, + options?: O, + ): Promise | null>; + findOneByIdAndLoginHashedToken = FindOptionsWithProjection>( + _id: IUser['_id'], + token: string, + options?: O, + ): Promise | null>; + findByActiveUsersExcept = FindOptionsWithProjection>( searchTerm: string, exceptions: string[], - options?: FindOptions, + options?: O, searchFields?: string[], extraQuery?: Filter[], extra?: { startsWith: boolean; endsWith: boolean }, - ): FindCursor; - findPaginatedByActiveUsersExcept( + ): FindCursor>; + findPaginatedByActiveUsersExcept = FindOptionsWithProjection>( searchTerm: string, exceptions?: string[], - options?: FindOptions, + options?: O, searchFields?: string[], extraQuery?: Filter[], extra?: { startsWith?: boolean; endsWith?: boolean }, - ): FindPaginated>>; + ): FindPaginated>>; - findPaginatedByActiveLocalUsersExcept( + findPaginatedByActiveLocalUsersExcept = FindOptionsWithProjection>( searchTerm: string, exceptions?: string[], - options?: FindOptions, + options?: O, forcedSearchFields?: string[], localDomain?: string, - ): FindPaginated>>; + ): FindPaginated>>; - findPaginatedByActiveExternalUsersExcept( + findPaginatedByActiveExternalUsersExcept< + T extends Document = IUser, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( searchTerm: string, exceptions?: string[], - options?: FindOptions, + options?: O, forcedSearchFields?: string[], localDomain?: string, - ): FindPaginated>>; + ): FindPaginated>>; - findActive(query: Filter, options?: FindOptions): FindCursor; + findActive = FindOptionsWithProjection>( + query: Filter, + options?: O, + ): FindCursor>; - findActiveByIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; + findActiveByIds = FindOptionsWithProjection>( + userIds: IUser['_id'][], + options?: O, + ): FindCursor>; - findByIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; + findByIds = FindOptionsWithProjection>( + userIds: IUser['_id'][], + options?: O, + ): FindCursor>; - findOneByUsernameIgnoringCase(username: IUser['username'], options?: FindOptions): Promise; + findOneByUsernameIgnoringCase = FindOptionsWithProjection>( + username: IUser['username'], + options?: O, + ): Promise | null>; - findOneWithoutLDAPByUsernameIgnoringCase(username: string, options?: FindOptions): Promise; + findOneWithoutLDAPByUsernameIgnoringCase< + T extends Document = IUser, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( + username: string, + options?: O, + ): Promise | null>; findOneByLDAPId(id: string, attribute?: string): Promise; - findOneByAppId(appId: string, options?: FindOptions): Promise; - findUsersByIdentifiers( + findOneByAppId = FindOptionsWithProjection>( + appId: string, + options?: O, + ): Promise | null>; + findUsersByIdentifiers = FindOptionsWithProjection>( params: { usernames?: string[]; ids?: string[]; emails?: string[]; ldapIds?: string[] }, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; - findLDAPUsers(options?: FindOptions): FindCursor; + findLDAPUsers = FindOptionsWithProjection>( + options?: O, + ): FindCursor>; - findActiveLDAPUsersExceptIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; + findActiveLDAPUsersExceptIds = FindOptionsWithProjection>( + userIds: IUser['_id'][], + options?: O, + ): FindCursor>; - findConnectedLDAPUsers(options?: FindOptions): FindCursor; + findConnectedLDAPUsers = FindOptionsWithProjection>( + options?: O, + ): FindCursor>; isUserInRole(userId: IUser['_id'], roleId: IRole['_id']): Promise | null>; @@ -145,12 +200,15 @@ export interface IUsersModel extends IBaseModel { findAllResumeTokensByUserId(userId: IUser['_id']): Promise<{ tokens: IMeteorLoginToken[] }[]>; - findActiveByUsernameOrNameRegexWithExceptionsAndConditions( + findActiveByUsernameOrNameRegexWithExceptionsAndConditions< + T extends Document = IUser, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( termRegex: { $regex: string; $options: string } | RegExp, exceptions?: string[], conditions?: Filter, - options?: FindOptions, - ): FindCursor; + options?: O, + ): FindCursor>; countAllAgentsStatus({ departmentId, @@ -168,7 +226,10 @@ export interface IUsersModel extends IBaseModel { setAbacAttributesById(userId: IUser['_id'], attributes: NonNullable): Promise; unsetAbacAttributesById(userId: IUser['_id']): Promise; - findActiveByRoomIds(roomIds: IRoom['_id'][], options?: FindOptions): FindCursor; + findActiveByRoomIds = FindOptionsWithProjection>( + roomIds: IRoom['_id'][], + options?: O, + ): FindCursor>; setCasExternalIdByUsername(username: string): Promise; updateStatusText(_id: IUser['_id'], statusText: string, options?: UpdateOptions): Promise; @@ -240,7 +301,10 @@ export interface IUsersModel extends IBaseModel { countActiveUsersEmail2faEnable(options: any): Promise; - findActiveByIdsOrUsernames(userIds: IUser['_id'][], options?: FindOptions): FindCursor; + findActiveByIdsOrUsernames = FindOptionsWithProjection>( + userIds: IUser['_id'][], + options?: O, + ): FindCursor>; setAsFederated(userId: string): any; @@ -268,12 +332,12 @@ export interface IUsersModel extends IBaseModel { isLivechatEnabledWhenIdle?: boolean, acceptChatsWithNoAgents?: boolean, ): Promise[]>; - findOneOnlineAgentByUserList( + findOneOnlineAgentByUserList = FindOptionsWithProjection>( userList: string[] | string, - options?: FindOptions, + options?: O, isLivechatEnabledWhenAgentIdle?: boolean, acceptChatsWithNoAgents?: boolean, - ): Promise; + ): Promise | null>; findBotAgents(usernameList?: string | string[]): FindCursor; countBotAgents(usernameList?: string | string[]): Promise; @@ -334,49 +398,122 @@ export interface IUsersModel extends IBaseModel { update2FABackupCodesByUserId(userId: string, codes: string[]): Promise; enableEmail2FAByUserId(userId: string): Promise; disableEmail2FAByUserId(userId: string): Promise; - findByIdsWithPublicE2EKey(userIds: string[], options?: FindOptions): FindCursor; + findByIdsWithPublicE2EKey = FindOptionsWithProjection>( + userIds: string[], + options?: O, + ): FindCursor>; resetE2EKey(userId: string): Promise; removeExpiredEmailCodeOfUserId(userId: string): Promise; maxInvalidEmailCodeAttemptsReached(userId: string, maxAttemtps: number): Promise; addEmailCodeByUserId(userId: string, code: string, expire: Date): Promise; - findActiveUsersInRoles(roles: string[], options?: FindOptions): FindCursor; + findActiveUsersInRoles = FindOptionsWithProjection>( + roles: string[], + options?: O, + ): FindCursor>; countActiveUsersInRoles(roles: string[], options?: FindOptions): Promise; - findOneByUsernameAndServiceNameIgnoringCase( + findOneByUsernameAndServiceNameIgnoringCase< + T extends Document = IUser, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( username: string, userId: string, serviceName: string, - options?: FindOptions, - ): Promise; - findOneByEmailAddressAndServiceNameIgnoringCase( + options?: O, + ): Promise | null>; + findOneByEmailAddressAndServiceNameIgnoringCase< + T extends Document = IUser, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( emailAddress: string, userId: string, serviceName: string, - options?: FindOptions, - ): Promise; - findOneByEmailAddress(emailAddress: string, options?: FindOptions): Promise; - findOneWithoutLDAPByEmailAddress(emailAddress: string, options?: FindOptions): Promise; - findOneAdmin(userId: string, options?: FindOptions): Promise; - findOneByIdAndLoginToken(userId: string, loginToken: string, options?: FindOptions): Promise; - findOneActiveById(userId: string, options?: FindOptions): Promise; - findOneByIdOrUsername(userId: string, options?: FindOptions): Promise; - findOneByRolesAndType(roles: IRole['_id'][], type: string, options?: FindOptions): Promise; - findPresenceUsersByIds(userIds: string[], options?: FindOptions): FindCursor; - findUsersNotOffline(options?: FindOptions): FindCursor; + options?: O, + ): Promise | null>; + findOneByEmailAddress = FindOptionsWithProjection>( + emailAddress: string, + options?: O, + ): Promise | null>; + findOneWithoutLDAPByEmailAddress = FindOptionsWithProjection>( + emailAddress: string, + options?: O, + ): Promise | null>; + findOneAdmin = FindOptionsWithProjection>( + userId: string, + options?: O, + ): Promise | null>; + findOneByIdAndLoginToken = FindOptionsWithProjection>( + userId: string, + loginToken: string, + options?: O, + ): Promise | null>; + findOneActiveById = FindOptionsWithProjection>( + userId: string, + options?: O, + ): Promise | null>; + findOneByIdOrUsername = FindOptionsWithProjection>( + userId: string, + options?: O, + ): Promise | null>; + findOneByRolesAndType = FindOptionsWithProjection>( + roles: IRole['_id'][], + type: string, + options?: O, + ): Promise | null>; + findPresenceUsersByIds = FindOptionsWithProjection>( + userIds: string[], + options?: O, + ): FindCursor>; + findUsersNotOffline = FindOptionsWithProjection>( + options?: O, + ): FindCursor>; countUsersNotOffline(options?: FindOptions): Promise; - findNotIdUpdatedFrom(userId: string, updatedFrom: Date, options?: FindOptions): FindCursor; - findByRoomId(roomId: string, options?: FindOptions): Promise>; - findByUsernames(usernames: string[], options?: FindOptions): FindCursor; - findByUsernamesIgnoringCase(usernames: string[], options?: FindOptions): FindCursor; - findActiveByUserIds(userIds: string[], options?: FindOptions): FindCursor; + findNotIdUpdatedFrom = FindOptionsWithProjection>( + userId: string, + updatedFrom: Date, + options?: O, + ): FindCursor>; + findByRoomId = FindOptionsWithProjection>( + roomId: string, + options?: O, + ): Promise>>; + findByUsernames = FindOptionsWithProjection>( + usernames: string[], + options?: O, + ): FindCursor>; + findByUsernamesIgnoringCase = FindOptionsWithProjection>( + usernames: string[], + options?: O, + ): FindCursor>; + findActiveByUserIds = FindOptionsWithProjection>( + userIds: string[], + options?: O, + ): FindCursor>; countActiveLocalGuests(idsExceptions: string[]): Promise; - findCrowdUsers(options?: FindOptions): FindCursor; + findCrowdUsers = FindOptionsWithProjection>( + options?: O, + ): FindCursor>; getLastLogin(options?: FindOptions): Promise; - findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; - findUsersByIds(userIds: string[], options?: FindOptions): FindCursor; - getOldest(options?: FindOptions): Promise; + findUsersByUsernames = FindOptionsWithProjection>( + usernames: string[], + options?: O, + ): FindCursor>; + findUsersByIds = FindOptionsWithProjection>( + userIds: string[], + options?: O, + ): FindCursor>; + getOldest = FindOptionsWithProjection>( + options?: O, + ): Promise | null>; getSAMLByIdAndSAMLProvider(userId: string, samlProvider: string): Promise; - findBySAMLNameIdOrIdpSession(samlNameId: string, idpSession: string, options?: FindOptions): FindCursor; - findBySAMLInResponseTo(inResponseTo: string, options?: FindOptions): FindCursor; + findBySAMLNameIdOrIdpSession = FindOptionsWithProjection>( + samlNameId: string, + idpSession: string, + options?: O, + ): FindCursor>; + findBySAMLInResponseTo = FindOptionsWithProjection>( + inResponseTo: string, + options?: O, + ): FindCursor>; addImportIds(userId: string, importIds: string | string[]): Promise; updateInviteToken(userId: string, token: string): Promise; updateLastLoginById(userId: string): Promise; @@ -392,7 +529,11 @@ export interface IUsersModel extends IBaseModel { unsetAvatarData(userId: string): Promise; setUserActive(userId: string, active: boolean): Promise; setActiveNotLoggedInAfterWithRole(latestLastLoginDate: Date, role?: string, active?: boolean): Promise; - findActiveNotLoggedInAfterWithRole(latestLastLoginDate: Date, role?: string, options?: FindOptions): FindCursor; + findActiveNotLoggedInAfterWithRole = FindOptionsWithProjection>( + latestLastLoginDate: Date, + role?: string, + options?: O, + ): FindCursor>; unsetRequirePasswordChange(userId: string): Promise; resetPasswordAndSetRequirePasswordChange( userId: string, @@ -424,7 +565,10 @@ export interface IUsersModel extends IBaseModel { findAllUsersWithPendingAvatar(): FindCursor; updateCustomFieldsById(userId: string, customFields: Record): Promise; countRoomMembers(roomId: string): Promise; - findOneByImportId(_id: IUser['_id'], options?: FindOptions): Promise; + findOneByImportId = FindOptionsWithProjection>( + _id: IUser['_id'], + options?: O, + ): Promise | null>; removeAgent(_id: string): Promise; findAgentsWithDepartments( role: IRole['_id'][] | IRole['_id'], @@ -437,12 +581,22 @@ export interface IUsersModel extends IBaseModel { findOnlineButNotAvailableAgents(userIds?: IUser['_id'][]): FindCursor; findAgentsAvailableWithoutBusinessHours(userIds?: IUser['_id'][]): FindCursor>; updateLivechatStatusByAgentIds(userIds: string[], status: ILivechatAgentStatus): Promise; - findOneByFreeSwitchExtension(freeSwitchExtension: string, options?: FindOptions): Promise; + findOneByFreeSwitchExtension = FindOptionsWithProjection>( + freeSwitchExtension: string, + options?: O, + ): Promise | null>; countUsersInRoles(roles: IRole['_id'][]): Promise; countAllUsersWithPendingAvatar(): Promise; - findOneByIdAndRole(userId: IUser['_id'], role: string, options: FindOptions): Promise; + findOneByIdAndRole = FindOptionsWithProjection>( + userId: IUser['_id'], + role: string, + options?: O, + ): Promise | null>; countActiveUsersInNonDMRoom(rid: string): Promise; countActiveUsersInDMRoom(rid: string): Promise; verifyEmailByAddress(_id: IUser['_id'], emailAddress: string): Promise; - findOneByEmailVerificationToken(token: string, options?: FindOptions): Promise; + findOneByEmailVerificationToken = FindOptionsWithProjection>( + token: string, + options?: O, + ): Promise | null>; } diff --git a/packages/model-typings/src/models/IUsersSessionsModel.ts b/packages/model-typings/src/models/IUsersSessionsModel.ts index 68a11b0dddd98..16e04e4f835cf 100644 --- a/packages/model-typings/src/models/IUsersSessionsModel.ts +++ b/packages/model-typings/src/models/IUsersSessionsModel.ts @@ -1,7 +1,8 @@ import type { IUserSession, IUserSessionConnection } from '@rocket.chat/core-typings'; -import type { FindCursor, FindOptions } from 'mongodb'; +import type { FindCursor, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IUsersSessionsModel extends IBaseModel { updateConnectionStatusById(uid: string, connectionId: string, status: string): ReturnType['updateOne']>; @@ -12,6 +13,9 @@ export interface IUsersSessionsModel extends IBaseModel { userId: string, { id, instanceId, status }: Pick, ): ReturnType['updateOne']>; - findByOtherInstanceIds(instanceIds: string[], options?: FindOptions): FindCursor; + findByOtherInstanceIds = FindOptionsWithProjection>( + instanceIds: string[], + options?: O, + ): FindCursor>; removeConnectionsFromOtherInstanceIds(instanceIds: string[]): ReturnType['updateMany']>; } diff --git a/packages/model-typings/src/models/IWebdavAccountsModel.ts b/packages/model-typings/src/models/IWebdavAccountsModel.ts index a044747986373..d14943500fe43 100644 --- a/packages/model-typings/src/models/IWebdavAccountsModel.ts +++ b/packages/model-typings/src/models/IWebdavAccountsModel.ts @@ -1,11 +1,19 @@ import type { IWebdavAccount } from '@rocket.chat/core-typings'; -import type { FindOptions, FindCursor, DeleteResult } from 'mongodb'; +import type { FindCursor, DeleteResult, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { DocumentWithProjection, FindOptionsWithProjection } from '../types/DocumentWithProjection'; export interface IWebdavAccountsModel extends IBaseModel { - findOneByIdAndUserId(_id: string, userId: string, options: FindOptions): Promise; - findOneByUserIdServerUrlAndUsername( + findOneByIdAndUserId = FindOptionsWithProjection>( + _id: string, + userId: string, + options: O, + ): Promise | null>; + findOneByUserIdServerUrlAndUsername< + T extends Document = IWebdavAccount, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( { userId, serverURL, @@ -15,10 +23,13 @@ export interface IWebdavAccountsModel extends IBaseModel { serverURL: string; username: string; }, - options: FindOptions, - ): Promise; + options: O, + ): Promise | null>; - findWithUserId(userId: string, options: FindOptions): FindCursor; + findWithUserId = FindOptionsWithProjection>( + userId: string, + options: O, + ): FindCursor>; removeByUserAndId(_id: string, userId: string): Promise; } diff --git a/packages/model-typings/src/types/DocumentWithProjection.ts b/packages/model-typings/src/types/DocumentWithProjection.ts index 45610528ac6c4..b336f6846c36a 100644 --- a/packages/model-typings/src/types/DocumentWithProjection.ts +++ b/packages/model-typings/src/types/DocumentWithProjection.ts @@ -1,15 +1,94 @@ -import type { FindOptions } from 'mongodb'; +import type { Document, FindOneAndUpdateOptions, FindOptions } from 'mongodb'; type Prettify = { [K in keyof T]: T[K]; } & {}; -export type DocumentWithProjection, O extends FindOptions['projection']> = O extends { - projection: infer P; -} - ? P extends FindOptions['projection'] - ? keyof P extends keyof T - ? Prettify> - : T +export type ProjectionValue = 0 | 1 | boolean; + +/** Projection operators (`$slice`, `$elemMatch`, `$meta`, positional `$`) hold plain documents. */ +export type ProjectionSpec = Record; + +/** + * `FindOptions` with a projection type that keeps `0`/`1` as literal types when the options object + * is inferred into a generic parameter. The driver's own `FindOptions['projection']` is `Document` + * (`{ [key: string]: any }`), which contains no literal types, so `0` and `1` widen to `number` and + * inclusion becomes indistinguishable from exclusion. + * + * `ProjectionSpec | Document` looks redundant, but both members are load-bearing: + * - `ProjectionSpec` supplies the literal contextual type that keeps `0`/`1` narrow; + * - `Document` keeps assignability identical to the driver's `FindOptions`, so interface-typed + * projections (which get no implicit index signature) keep compiling. + * Do not collapse the union. + */ +export type WithProjectionSpec = Omit & { + projection?: ProjectionSpec | Document; +}; + +export type FindOptionsWithProjection = WithProjectionSpec>; + +export type FindOneAndUpdateOptionsWithProjection = WithProjectionSpec; + +/** + * `Extract` would miss documents with a string index signature (`keyof T` collapses + * to `string | number`, which `'_id'` is not a member of), so probe with `extends` instead. + */ +type IdKey = '_id' extends keyof T ? '_id' : never; + +type InclusionKeys

= { [K in keyof P]-?: P[K] extends 1 | true ? K : never }[keyof P]; + +type ExclusionKeys

= { [K in keyof P]-?: P[K] extends 0 | false ? K : never }[keyof P]; + +/** + * Applies a projection `P` to a document type `T`, mirroring what `BaseRaw` actually sends to the + * server — see `doNotMixInclusionAndExclusionFields`, which strips every exclusion key (`0` or + * `false`) as soon as one key is an inclusion, so a mixed projection behaves as inclusion-only and + * still returns `_id`. + * + * Bails out to `T` whenever the projection cannot be read statically: dotted paths, `$`-operators, + * computed keys, or values that are not `0`/`1`/`false`/`true` literals. + */ +export type ApplyProjection = [keyof P] extends [keyof T] + ? [keyof P] extends [InclusionKeys

| ExclusionKeys

] + ? [InclusionKeys

] extends [never] + ? Omit & keyof T> + : Prettify & keyof T) | IdKey>> + : T + : T; + +/** + * `NoInfer` blocks contextual inference through the return type: when a call sits in a typed + * position (argument, object literal property), the document type param would otherwise fall back + * to its `Document` constraint instead of its default, degenerating the result to `{ key: any }`. + * The bail-out branches must stay a bare `T` so the no-projection case reduces to exactly the + * document type — implementations that forward a legacy `` generic rely on that identity. + */ +export type DocumentWithProjection = O extends { projection: infer P } + ? P extends ProjectionSpec + ? ApplyProjection, P> + : T + : T; + +/** + * Applies a projection the way the **driver** sees it. `findOneAndUpdate` / `findOneAndDelete` go + * straight to the collection, so they get none of `BaseRaw`'s rewriting. Two consequences: + * - `_id` comes back unless the projection excludes it explicitly, so `{ a: 1, _id: 0 }` really does + * drop `_id` here, where the `find` path would have kept it; + * - mixing inclusion with any other exclusion is a server error, so there is nothing useful to + * describe and we fall back to `T`. + */ +type ApplyDriverProjection = [keyof P] extends [keyof T] + ? [keyof P] extends [InclusionKeys

| ExclusionKeys

] + ? [InclusionKeys

] extends [never] + ? Omit & keyof T> + : [Exclude, '_id'>] extends [never] + ? Prettify & keyof T) | Exclude, ExclusionKeys

>>> + : T + : T + : T; + +export type DocumentWithDriverProjection = O extends { projection: infer P } + ? P extends ProjectionSpec + ? ApplyDriverProjection, P> : T : T; diff --git a/packages/model-typings/src/types/DocumentWithProjection.typetest.ts b/packages/model-typings/src/types/DocumentWithProjection.typetest.ts new file mode 100644 index 0000000000000..a146c65909365 --- /dev/null +++ b/packages/model-typings/src/types/DocumentWithProjection.typetest.ts @@ -0,0 +1,120 @@ +/** + * Compile-time assertions for {@link DocumentWithProjection}. Nothing here is meant to run; it + * exists so that `tsc -p tsconfig.json` fails if the projection inference regresses. + */ +import type { Document } from 'mongodb'; + +import type { DocumentWithDriverProjection, DocumentWithProjection, FindOptionsWithProjection } from './DocumentWithProjection'; + +type Expect = T; + +/** Strict type identity. */ +type Equal = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; + +/** Mutual assignability — tolerant of `Prettify`/`Pick` representation differences. */ +type Equivalent = [A] extends [B] ? ([B] extends [A] ? true : false) : false; + +type Doc = { + _id: string; + username?: string; + password: string; + name: string; + roles: string[]; +}; + +type Project

= DocumentWithProjection; + +/** + * The invariant the whole backward-compatibility story rests on: `projection` is optional in + * `FindOptionsWithProjection`, so the default `O` does not match `{ projection: infer P }` and the + * result collapses to the document type. Making `projection` required would silently change the + * meaning of every call site that passes an explicit generic. + */ +export type NoProjectionCollapsesToDocument = Expect>, Doc>>; + +export type UndefinedOptionsCollapsesToDocument = Expect, Doc>>; + +export type Inclusion = Expect, Pick>>; + +export type InclusionKeepsIdImplicitly = Expect, Pick>>; + +export type BooleanInclusion = Expect, Pick>>; + +export type Exclusion = Expect, Omit>>; + +export type BooleanExclusion = Expect, Omit>>; + +export type ExclusionCanDropId = Expect, Omit>>; + +/** `doNotMixInclusionAndExclusionFields` drops the exclusion keys at runtime, so `_id` survives a mix. */ +export type MixedBehavesAsInclusion = Expect, Pick>>; + +export type MixedBooleanBehavesAsInclusion = Expect, Pick>>; + +export type MixedNotationBehavesAsInclusion = Expect, Pick>>; + +export type MixedNotationExclusionStaysExclusion = Expect< + Equivalent, Omit> +>; + +export type MixedKeepsIdEvenWhenExcluded = Expect, Pick>>; + +export type MixedKeepsIdEvenWhenExcludedWithBoolean = Expect, Pick>>; + +// Everything below must bail out to the full document rather than guess. + +export type DottedPathBailsOut = Expect, Doc>>; + +export type OperatorBailsOut = Expect, Doc>>; + +export type MetaBailsOut = Expect, Doc>>; + +export type NonLiteralValueBailsOut = Expect, Doc>>; + +export type UnknownKeyBailsOut = Expect, Doc>>; + +export type WideProjectionBailsOut = Expect>, Doc>>; + +// Documents with a string index signature collapse `keyof T` to `string | number`, which used to +// make the implicit `_id` disappear from inclusion projections. + +type IndexedDoc = { + _id: string; + name: string; + [k: string]: any; +}; + +export type IndexSignatureKeepsId = Expect< + Equivalent, { _id: string; name: string }> +>; + +// When a call sits in a contextually typed position (argument, object literal property), inference +// through the return type would drive the document type param to its `Document` constraint instead +// of its default. The `NoInfer` inside `DocumentWithProjection` blocks that. + +declare const contextualProbe: { + findOne

= FindOptionsWithProjection

>( + options?: O, + ): DocumentWithProjection | null; +}; + +export const contextualInferenceKeepsDefault: Pick | null = contextualProbe.findOne({ + projection: { username: 1 }, +}); + +// `findOneAndUpdate` / `findOneAndDelete` reach the driver directly, so they get none of BaseRaw's +// rewriting. The two rules below are where driver semantics diverge from the `find` path. + +type DriverProject

= DocumentWithDriverProjection; + +export type DriverInclusion = Expect, Pick>>; + +export type DriverExclusion = Expect, Omit>>; + +/** `find` keeps `_id` here because BaseRaw strips the `0`; the driver really drops it. */ +export type DriverExplicitIdExclusionDropsId = Expect, Pick>>; + +/** Mixing inclusion with a non-`_id` exclusion is a server error, so there is nothing to describe. */ +export type DriverGenuineMixBailsOut = Expect, Doc>>; + +export type DriverNoProjectionCollapses = Expect, Doc>>; diff --git a/packages/model-typings/tsconfig.build.json b/packages/model-typings/tsconfig.build.json new file mode 100644 index 0000000000000..e6f4bdb0850b6 --- /dev/null +++ b/packages/model-typings/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + // `*.typetest.ts` holds compile-time assertions only — `yarn typecheck` covers them, the build must not emit them. + "exclude": ["node_modules", "${configDir}/**/*.spec.ts", "${configDir}/**/*.typetest.ts"] +} diff --git a/packages/model-typings/tsconfig.json b/packages/model-typings/tsconfig.json index e00a45b253fa4..0648749de8d2a 100644 --- a/packages/model-typings/tsconfig.json +++ b/packages/model-typings/tsconfig.json @@ -5,5 +5,8 @@ "rootDir": "./src", "outDir": "./dist" }, - "include": ["./src/**/*"] + "include": ["./src/**/*"], + // restates the inherited exclusions: a child `exclude` replaces the parent's rather than adding to it. + // keeping only `node_modules` lets ESLint and `yarn typecheck` cover the type tests; the build config drops them. + "exclude": ["node_modules"] } diff --git a/packages/models/src/dummy/BaseDummy.ts b/packages/models/src/dummy/BaseDummy.ts index 5b4363a93b475..723cdea00563f 100644 --- a/packages/models/src/dummy/BaseDummy.ts +++ b/packages/models/src/dummy/BaseDummy.ts @@ -1,5 +1,14 @@ import type { RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { DefaultFields, FindPaginated, IBaseModel, InsertionModel, ResultFields } from '@rocket.chat/model-typings'; +import type { + DefaultFields, + DocumentWithProjection, + FindOptionsWithProjection, + FindPaginated, + IBaseModel, + InsertionModel, + DocumentWithDriverProjection, + FindOneAndUpdateOptionsWithProjection, +} from '@rocket.chat/model-typings'; import type { BulkWriteOptions, ChangeStream, @@ -61,40 +70,38 @@ export class BaseDummy< return null; } - async findOneAndUpdate(): Promise | null> { + async findOneAndUpdate< + P extends Document = T, + O extends FindOneAndUpdateOptionsWithProjection = FindOneAndUpdateOptionsWithProjection, + >(): Promise | null> { return null; } - findOneById(_id: T['_id'], options?: FindOptions | undefined): Promise; - - findOneById

(_id: T['_id'], options?: FindOptions

): Promise

; - - async findOneById(_id: T['_id'], _options?: any): Promise { + async findOneById

= FindOptionsWithProjection

>( + _id: T['_id'], + _options?: O, + ): Promise | null> { return null; } - findOne(query?: Filter | T['_id'], options?: undefined): Promise; - - findOne

(query: Filter | T['_id'], options: FindOptions

): Promise

; - - async findOne

(_query: Filter | T['_id'], _options?: any): Promise | WithId

| null> { + async findOne

= FindOptionsWithProjection

>( + _query?: Filter | T['_id'], + _options?: O, + ): Promise | null> { return null; } - find(query?: Filter): FindCursor>; - - find

(query: Filter, options: FindOptions

): FindCursor

; - - find

( - _query: Filter | undefined, - _options?: FindOptions

, - ): FindCursor> | FindCursor> { + find

= FindOptionsWithProjection

>( + _query?: Filter, + _options?: O, + ): FindCursor> { return undefined as any; } - findPaginated

(query: Filter, options?: FindOptions

): FindPaginated>>; - - findPaginated(_query: Filter, _options?: any): FindPaginated>> { + findPaginated

= FindOptionsWithProjection

>( + _query?: Filter, + _options?: O, + ): FindPaginated>> { return { cursor: undefined as any, totalCount: Promise.resolve(0), @@ -168,7 +175,7 @@ export class BaseDummy< _query: Filter, _options?: FindOptions

, ): FindCursor> | undefined { - return undefined as any; + return undefined; } trashFindOneById(_id: TDeleted['_id']): Promise; diff --git a/packages/models/src/models/Banners.ts b/packages/models/src/models/Banners.ts index ffad398c162d0..5dd7da13cdb68 100644 --- a/packages/models/src/models/Banners.ts +++ b/packages/models/src/models/Banners.ts @@ -1,7 +1,7 @@ import type { IBanner, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import { BannerPlatform } from '@rocket.chat/core-typings'; -import type { IBannersModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, FindOptions, IndexDescription, InsertOneResult, UpdateResult } from 'mongodb'; +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'; @@ -34,7 +34,12 @@ export class BannersRaw extends BaseRaw implements IBannersModel { }); } - findActiveByRoleOrId(roles: string[], platform: BannerPlatform, bannerId?: string, options?: FindOptions): FindCursor { + findActiveByRoleOrId = FindOptionsWithProjection>( + roles: string[], + platform: BannerPlatform, + bannerId?: string, + options?: O, + ): FindCursor> { const today = new Date(); const query = { @@ -46,7 +51,7 @@ export class BannersRaw extends BaseRaw implements IBannersModel { $or: [{ roles: { $in: roles } }, { roles: { $exists: false } }], }; - return this.find(query, options); + return this.find(query, options); } disable(bannerId: string): Promise { diff --git a/packages/models/src/models/BannersDismiss.ts b/packages/models/src/models/BannersDismiss.ts index 5fabf466e8d12..50930f98a4cd0 100644 --- a/packages/models/src/models/BannersDismiss.ts +++ b/packages/models/src/models/BannersDismiss.ts @@ -1,6 +1,6 @@ import type { IBannerDismiss, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { IBannersDismissModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, FindOptions, IndexDescription } from 'mongodb'; +import type { Collection, FindCursor, Db, IndexDescription, Document, FindOptions } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -26,7 +26,7 @@ export class BannersDismissRaw extends BaseRaw implements IBanne findByUserIdAndBannerId

( userId: string, bannerIds: string[], - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor { const query = { userId, diff --git a/packages/models/src/models/BaseRaw.spec.ts b/packages/models/src/models/BaseRaw.spec.ts new file mode 100644 index 0000000000000..cbb093a89c4df --- /dev/null +++ b/packages/models/src/models/BaseRaw.spec.ts @@ -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): 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)); + + 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 }); + }); +}); diff --git a/packages/models/src/models/BaseRaw.ts b/packages/models/src/models/BaseRaw.ts index 6c572b9fa70d2..2c3dba7700cb3 100644 --- a/packages/models/src/models/BaseRaw.ts +++ b/packages/models/src/models/BaseRaw.ts @@ -1,5 +1,15 @@ import type { RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IBaseModel, DefaultFields, ResultFields, FindPaginated, InsertionModel } from '@rocket.chat/model-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 { @@ -135,15 +145,14 @@ export abstract class BaseRaw< } const projection: Record = optionsDef?.projection; - const keys = Object.keys(projection); - const removeKeys = keys.filter((key) => projection[key] === 0); - if (keys.length > removeKeys.length) { - removeKeys.forEach((key) => delete projection[key]); - } + 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, - projection, + // a mixed projection gets its exclusions dropped into a new object; `projection` may be owned by the caller + projection: isMixed ? Object.fromEntries(inclusionEntries) : projection, }; } @@ -171,7 +180,19 @@ export abstract class BaseRaw< }; } - public findOneAndUpdate(query: Filter, update: UpdateFilter | T, options?: FindOneAndUpdateOptions): Promise | null> { + /* + * Every finder below asserts its result. `this.col` is a `Collection` and yields `WithId`, + * while the signatures return `DocumentWithProjection` — 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

( + query: Filter, + update: UpdateFilter | T, + options?: O, + ): Promise | null> { this.setUpdatedAt(update); if (options?.upsert && !('_id' in update || (update.$set && '_id' in update.$set)) && !('_id' in query)) { @@ -181,56 +202,49 @@ export abstract class BaseRaw< } as Partial & { _id: string }; } - return this.col.findOneAndUpdate(query, update, options || {}); - } - - async findOneById(_id: T['_id'], options?: FindOptions): Promise; + const result = this.col.findOneAndUpdate(query, update, (options || {}) as FindOneAndUpdateOptions); - async findOneById

(_id: T['_id'], options?: FindOptions

): Promise

; - - async findOneById(_id: T['_id'], options?: any): Promise { - const query: Filter = { _id } as Filter; - if (options) { - return this.findOne(query, options); - } - return this.findOne(query); + return result as Promise | null>; } - async findOne(query?: Filter | T['_id'], options?: undefined): Promise; - - async findOne

(query: Filter | T['_id'], options?: FindOptions

): Promise

; + async findOneById

= FindOptionsWithProjection

>( + _id: T['_id'], + options?: O, + ): Promise | null> { + return this.findOne({ _id } as Filter, options); + } - async findOne

(query: Filter | T['_id'] = {}, options?: any): Promise | WithId

| null> { + async findOne

= FindOptionsWithProjection

>( + query: Filter | T['_id'] = {}, + options?: O, + ): Promise | null> { const q: Filter = typeof query === 'string' ? ({ _id: query } as Filter) : query; const optionsDef = this.doNotMixInclusionAndExclusionFields(options); if (optionsDef) { - return this.col.findOne(q, optionsDef); + return this.col.findOne(q, optionsDef) as Promise | null>; } - return this.col.findOne(q); + return this.col.findOne(q) as Promise | null>; } - find(query?: Filter): FindCursor>; - - find

(query: Filter, options?: FindOptions

): FindCursor

; - - find

( + find

= FindOptionsWithProjection

>( query: Filter = {}, - options?: FindOptions

, - ): FindCursor> | FindCursor> { + options?: O, + ): FindCursor> { const optionsDef = this.doNotMixInclusionAndExclusionFields(options); - return this.col.find(query, optionsDef); + return this.col.find(query, optionsDef) as unknown as FindCursor>; } - findPaginated

(query: Filter, options?: FindOptions

): FindPaginated>>; - - findPaginated(query: Filter = {}, options?: any): FindPaginated>> { + findPaginated

= FindOptionsWithProjection

>( + query: Filter = {}, + options?: O, + ): FindPaginated>> { 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: cursor as unknown as FindCursor>, totalCount, }; } @@ -324,9 +338,13 @@ export abstract class BaseRaw< } 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, { $set: trash } as UpdateFilter, { - upsert: true, - }); + await this.trash?.updateOne( + { _id } as Filter, + { $set: trash }, + { + upsert: true, + }, + ); } if (options) { @@ -335,6 +353,12 @@ export abstract class BaseRaw< 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, options?: FindOneAndDeleteOptions): Promise | null> { if (!this.trash) { return this.col.findOneAndDelete(filter, options || {}); @@ -352,9 +376,13 @@ export abstract class BaseRaw< __collection__: this.name, } as unknown as TDeleted; - await this.trash?.updateOne({ _id } as Filter, { $set: trash } as UpdateFilter, { - upsert: true, - }); + await this.trash?.updateOne( + { _id } as Filter, + { $set: trash }, + { + upsert: true, + }, + ); try { await this.col.deleteOne({ _id } as Filter); @@ -390,13 +418,17 @@ export abstract class BaseRaw< __collection__: this.name, } as unknown as TDeleted; - ids.push(_id as T['_id']); + 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, { $set: trash } as UpdateFilter, { - upsert: true, - session: options?.session, - }); + await this.trash?.updateOne( + { _id } as Filter, + { $set: trash }, + { + upsert: true, + session: options?.session, + }, + ); void options?.onTrash?.(doc); } diff --git a/packages/models/src/models/BaseUploadModel.ts b/packages/models/src/models/BaseUploadModel.ts index a9a8483c231ee..01d0b20274b15 100644 --- a/packages/models/src/models/BaseUploadModel.ts +++ b/packages/models/src/models/BaseUploadModel.ts @@ -1,5 +1,5 @@ import type { EncryptedContent, IUpload } from '@rocket.chat/core-typings'; -import type { IBaseUploadsModel } from '@rocket.chat/model-typings'; +import type { IBaseUploadsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import type { DeleteResult, IndexDescription, @@ -8,7 +8,6 @@ import type { InsertOneResult, WithId, Filter, - FindOptions, FindCursor, ClientSession, } from 'mongodb'; @@ -92,14 +91,17 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.updateOne(filter, update); } - findByIds(_ids: string[], options?: FindOptions): FindCursor { + findByIds = FindOptionsWithProjection>( + _ids: string[], + options?: O, + ): FindCursor> { const query = { _id: { $in: _ids, }, }; - return this.find(query, options); + return this.find(query, options); } async findOneByName(name: string, options?: { session?: ClientSession }): Promise { @@ -110,8 +112,10 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.findOne({ rid }); } - findExpiredTemporaryFiles(options?: FindOptions): FindCursor { - return this.find( + findExpiredTemporaryFiles = FindOptionsWithProjection>( + options?: O, + ): FindCursor> { + return this.find( { expiresAt: { $lte: new Date(), @@ -135,8 +139,13 @@ export abstract class BaseUploadModelRaw extends BaseRaw implements IBaseUplo return this.deleteOne({ _id: fileId }, { session: options?.session }); } - async findOneByIdAndUserIdAndRoomId(fileId: string, userId: string, rid: string, options?: FindOptions): Promise { - return this.findOne({ _id: fileId, userId, rid }, options); + async findOneByIdAndUserIdAndRoomId = FindOptionsWithProjection>( + fileId: string, + userId: string, + rid: string, + options?: O, + ): Promise | null> { + return this.findOne({ _id: fileId, userId, rid }, options); } async updateFileMetadata( diff --git a/packages/models/src/models/CalendarEvent.ts b/packages/models/src/models/CalendarEvent.ts index 2c7a567080f2e..308905feb20fc 100644 --- a/packages/models/src/models/CalendarEvent.ts +++ b/packages/models/src/models/CalendarEvent.ts @@ -145,7 +145,7 @@ export class CalendarEventRaw extends BaseRaw implements ICalend }); } - public async findNextFutureEvent(startTime: Date): Promise { + public async findNextFutureEvent(startTime: Date): Promise | null> { return this.findOne( { startTime: { $gte: startTime }, @@ -161,7 +161,13 @@ export class CalendarEventRaw extends BaseRaw implements ICalend ); } - public findEventsStartingNow({ now, offset = 1000 }: { now: Date; offset?: number }): FindCursor { + public findEventsStartingNow({ + now, + offset = 1000, + }: { + now: Date; + offset?: number; + }): FindCursor> { return this.find( { startTime: { diff --git a/packages/models/src/models/CallHistory.ts b/packages/models/src/models/CallHistory.ts index c038c99e9d928..2c11f0ad36e46 100644 --- a/packages/models/src/models/CallHistory.ts +++ b/packages/models/src/models/CallHistory.ts @@ -1,7 +1,7 @@ import type { CallHistoryItem, IRegisterUser, IUser } from '@rocket.chat/core-typings'; -import type { FindPaginated, ICallHistoryModel } from '@rocket.chat/model-typings'; +import type { FindPaginated, ICallHistoryModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/tools'; -import type { Db, Filter, FindCursor, FindOptions, IndexDescription } from 'mongodb'; +import type { Db, Filter, FindCursor, IndexDescription, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -14,20 +14,20 @@ export class CallHistoryRaw extends BaseRaw implements ICallHis return [{ key: { uid: 1, callId: 1 }, unique: true }, { key: { uid: 1, ts: -1 } }]; } - async findOneByIdAndUid( + async findOneByIdAndUid = FindOptionsWithProjection>( _id: CallHistoryItem['_id'], uid: CallHistoryItem['uid'], - options?: FindOptions, - ): Promise { - return this.findOne({ _id, uid }, options); + options?: O, + ): Promise | null> { + return this.findOne({ _id, uid }, options); } - async findOneByCallIdAndUid( + async findOneByCallIdAndUid = FindOptionsWithProjection>( callId: CallHistoryItem['callId'], uid: CallHistoryItem['uid'], - options?: FindOptions, - ): Promise { - return this.findOne({ callId, uid }, options); + options?: O, + ): Promise | null> { + return this.findOne({ callId, uid }, options); } public async updateUserReferences( @@ -48,7 +48,10 @@ export class CallHistoryRaw extends BaseRaw implements ICallHis ); } - public findAllByUserIdAndSearchFilters( + public findAllByUserIdAndSearchFilters< + T extends Document = CallHistoryItem, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( uid: IUser['_id'], filters: { type?: CallHistoryItem['type']; @@ -56,8 +59,8 @@ export class CallHistoryRaw extends BaseRaw implements ICallHis direction?: CallHistoryItem['direction']; inStates?: CallHistoryItem['state'][]; }, - options: FindOptions, - ): FindPaginated> { + options?: O, + ): FindPaginated>> { const { type, direction, inStates, searchTerm } = filters; const textSearch = searchTerm ? { $regex: escapeRegExp(searchTerm), $options: 'i' } : null; @@ -84,6 +87,6 @@ export class CallHistoryRaw extends BaseRaw implements ICallHis }), }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } } diff --git a/packages/models/src/models/CustomSounds.ts b/packages/models/src/models/CustomSounds.ts index 98781868f0c81..f1deef4539f34 100644 --- a/packages/models/src/models/CustomSounds.ts +++ b/packages/models/src/models/CustomSounds.ts @@ -1,6 +1,6 @@ import type { ICustomSound, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { ICustomSoundsModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, FindOptions, IndexDescription, InsertOneResult, UpdateResult, WithId } from 'mongodb'; +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'; @@ -14,22 +14,30 @@ export class CustomSoundsRaw extends BaseRaw implements ICustomSou } // find - findByName(name: string, exceptId?: string, options?: FindOptions): FindCursor { + findByName = FindOptionsWithProjection>( + name: string, + exceptId?: string, + options?: O, + ): FindCursor> { const query = { name, ...(exceptId && { _id: { $nin: [exceptId] } }), }; - return this.find(query, options); + return this.find(query, options); } - findOneByName(name: string, exceptId?: string, options?: FindOptions): Promise { + findOneByName = FindOptionsWithProjection>( + name: string, + exceptId?: string, + options?: O, + ): Promise | null> { const query = { name, ...(exceptId && { _id: { $nin: [exceptId] } }), }; - return this.findOne(query, options); + return this.findOne(query, options); } // INSERT diff --git a/packages/models/src/models/CustomUserStatus.ts b/packages/models/src/models/CustomUserStatus.ts index c0bcbfee0e778..3eeda9fe17294 100644 --- a/packages/models/src/models/CustomUserStatus.ts +++ b/packages/models/src/models/CustomUserStatus.ts @@ -1,6 +1,6 @@ import type { ICustomUserStatus, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { ICustomUserStatusModel, InsertionModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, FindOptions, IndexDescription, InsertOneResult, UpdateResult, WithId } from 'mongodb'; +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'; @@ -13,39 +13,49 @@ export class CustomUserStatusRaw extends BaseRaw implements I return [{ key: { name: 1 } }]; } - // find one by name - - async findOneByName(name: string, options?: undefined): Promise; - - async findOneByName(name: string, options?: FindOptions): Promise { - return options ? this.findOne({ name }, options) : this.findOne({ name }); + async findOneByName = FindOptionsWithProjection>( + name: string, + options?: O, + ): Promise | null> { + return options ? this.findOne({ name }, options) : this.findOne({ name }); } - findOneByNameExceptId(name: string, except: string, options?: FindOptions): Promise { + findOneByNameExceptId = FindOptionsWithProjection>( + name: string, + except: string, + options?: O, + ): Promise | null> { const query = { _id: { $nin: [except] }, name, }; - return this.findOne(query, options); + return this.findOne(query, options); } // find - findByName(name: string, options?: FindOptions): FindCursor { + findByName = FindOptionsWithProjection>( + name: string, + options?: O, + ): FindCursor> { const query = { name, }; - return this.find(query, options); + return this.find(query, options); } - findByNameExceptId(name: string, except: string, options?: FindOptions): FindCursor { + findByNameExceptId = FindOptionsWithProjection>( + name: string, + except: string, + options?: O, + ): FindCursor> { const query = { _id: { $nin: [except] }, name, }; - return this.find(query, options); + return this.find(query, options); } // update diff --git a/packages/models/src/models/EmailInbox.ts b/packages/models/src/models/EmailInbox.ts index a4d27f7a9d844..4667a2bd5a87c 100644 --- a/packages/models/src/models/EmailInbox.ts +++ b/packages/models/src/models/EmailInbox.ts @@ -26,7 +26,7 @@ export class EmailInboxRaw extends BaseRaw implements IEmailInboxMo return this.findOneAndUpdate({ _id: id }, data, { returnDocument: 'after', projection: { _id: 1 }, - }) as unknown as Promise>>; + }); } findActive(): FindCursor { diff --git a/packages/models/src/models/EmojiCustom.ts b/packages/models/src/models/EmojiCustom.ts index 011ccbe623310..e2b483d5b8f95 100644 --- a/packages/models/src/models/EmojiCustom.ts +++ b/packages/models/src/models/EmojiCustom.ts @@ -1,6 +1,6 @@ import type { IEmojiCustom, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IEmojiCustomModel, InsertionModel } from '@rocket.chat/model-typings'; -import type { Collection, Filter, FindCursor, Db, FindOptions, IndexDescription, InsertOneResult, UpdateResult, WithId } from 'mongodb'; +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'; @@ -14,7 +14,10 @@ export class EmojiCustomRaw extends BaseRaw implements IEmojiCusto } // find - findByNameOrAlias(emojiName: string, options?: FindOptions): FindCursor { + findByNameOrAlias = FindOptionsWithProjection>( + emojiName: string, + options?: O, + ): FindCursor> { let name = emojiName; if (typeof emojiName === 'string') { @@ -25,25 +28,33 @@ export class EmojiCustomRaw extends BaseRaw implements IEmojiCusto $or: [{ name }, { aliases: name }], }; - return this.find(query, options); + return this.find(query, options); } - findOneByNamesOrAliases(names: string[], exceptId?: string, options?: FindOptions): Promise { + findOneByNamesOrAliases = FindOptionsWithProjection>( + names: string[], + exceptId?: string, + options?: O, + ): Promise | null> { const query: Filter = { ...(exceptId && { _id: { $nin: [exceptId] } }), $or: [{ name: { $in: names } }, { aliases: { $in: names } }], }; - return this.findOne(query, options); + return this.findOne(query, options); } - findByNameOrAliasExceptID(name: string, except: string, options?: FindOptions): FindCursor { + findByNameOrAliasExceptID = FindOptionsWithProjection>( + name: string, + except: string, + options?: O, + ): FindCursor> { const query = { _id: { $nin: [except] }, $or: [{ name }, { aliases: name }], }; - return this.find(query, options); + return this.find(query, options); } // update @@ -101,7 +112,10 @@ export class EmojiCustomRaw extends BaseRaw implements IEmojiCusto } // TODO: convert name: string to branded type using to enforce validation also replace this type cross the models/apis - findOneByName(name: string, options?: FindOptions): Promise { - return this.findOne({ name }, options); + findOneByName = FindOptionsWithProjection>( + name: string, + options?: O, + ): Promise | null> { + return this.findOne({ name }, options); } } diff --git a/packages/models/src/models/Integrations.ts b/packages/models/src/models/Integrations.ts index d680c57db4262..1a8bea1ec8aab 100644 --- a/packages/models/src/models/Integrations.ts +++ b/packages/models/src/models/Integrations.ts @@ -1,6 +1,12 @@ import type { IIntegration, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IBaseModel, IIntegrationsModel, IntegrationsStatistics } from '@rocket.chat/model-typings'; -import type { AggregateOptions, Collection, Db, FindCursor, FindOptions, IndexDescription } from 'mongodb'; +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'; @@ -65,12 +71,12 @@ export class IntegrationsRaw extends BaseRaw implements IIntegrati return this.find({ channel: { $in: channels } }); } - findOneByIdAndToken

( + findOneByIdAndToken

= FindOptionsWithProjection

>( id: IIntegration['_id'], token: string, - options?: FindOptions

, - ): Promise

{ - return this.findOne

({ _id: id, token }, options); + options?: O, + ): Promise | null> { + return this.findOne({ _id: id, token }, options); } async getStatistics(options?: AggregateOptions): Promise { diff --git a/packages/models/src/models/LivechatBusinessHours.ts b/packages/models/src/models/LivechatBusinessHours.ts index 7cd53eab3f385..cf00978beb48f 100644 --- a/packages/models/src/models/LivechatBusinessHours.ts +++ b/packages/models/src/models/LivechatBusinessHours.ts @@ -1,7 +1,7 @@ import type { ILivechatBusinessHour, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; -import type { ILivechatBusinessHoursModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, Document, FindOptions } from 'mongodb'; +import type { ILivechatBusinessHoursModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { Collection, Db, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -20,20 +20,18 @@ export class LivechatBusinessHoursRaw extends BaseRaw imp super(db, 'livechat_business_hours', trash); } - async findOneDefaultBusinessHour(options?: undefined): Promise; - - async findOneDefaultBusinessHour(options: FindOptions): Promise; - - async findOneDefaultBusinessHour

( - options: FindOptions

, - ): Promise

; - - findOneDefaultBusinessHour

(options?: any): Promise { - return this.findOne({ type: LivechatBusinessHourTypes.DEFAULT }, options); + findOneDefaultBusinessHour< + P extends Document = ILivechatBusinessHour, + O extends FindOptionsWithProjection

= FindOptionsWithProjection

, + >(options?: O): Promise | null> { + return this.findOne({ type: LivechatBusinessHourTypes.DEFAULT }, options); } - findActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise { - return this.find( + findActiveAndOpenBusinessHoursByDay< + T extends Document = ILivechatBusinessHour, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(day: string, options?: O): Promise[]> { + return this.find( { active: true, workHours: { @@ -47,8 +45,11 @@ export class LivechatBusinessHoursRaw extends BaseRaw imp ).toArray(); } - findActiveBusinessHours(options: FindOptions = {}): Promise { - return this.find( + findActiveBusinessHours< + T extends Document = ILivechatBusinessHour, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(options?: O): Promise[]> { + return this.find( { active: true, }, diff --git a/packages/models/src/models/LivechatContacts.ts b/packages/models/src/models/LivechatContacts.ts index 6ba5227e7c062..130c7adbe7138 100644 --- a/packages/models/src/models/LivechatContacts.ts +++ b/packages/models/src/models/LivechatContacts.ts @@ -6,7 +6,14 @@ import type { ILivechatVisitor, RocketChatRecordDeleted, } from '@rocket.chat/core-typings'; -import type { FindPaginated, ILivechatContactsModel, InsertionModel, Updater } from '@rocket.chat/model-typings'; +import type { + FindPaginated, + ILivechatContactsModel, + InsertionModel, + Updater, + DocumentWithProjection, + FindOptionsWithProjection, +} from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/tools'; import type { Document, @@ -14,7 +21,6 @@ import type { Db, RootFilterOperators, Filter, - FindOptions, FindCursor, IndexDescription, UpdateResult, @@ -143,10 +149,10 @@ export class LivechatContactsRaw extends BaseRaw implements IL return this.updateOne({ _id: contactId, enabled: { $ne: false } }, update, options); } - findPaginatedContacts( + findPaginatedContacts = FindOptionsWithProjection>( search: { searchText?: string; unknown?: boolean }, - options?: FindOptions, - ): FindPaginated> { + options?: O, + ): FindPaginated>> { const { searchText, unknown = false } = search; const searchRegex = escapeRegExp(searchText || ''); const match: Filter> = { @@ -159,13 +165,11 @@ export class LivechatContactsRaw extends BaseRaw implements IL enabled: { $ne: false }, }; - return this.findPaginated( - { ...match }, - { - allowDiskUse: true, - ...options, - }, - ); + // safe to merge into `O`: only `O['projection']` feeds the return type, and it survives the spread + return this.findPaginated({ ...match }, { + allowDiskUse: true, + ...options, + } as unknown as O); } async findContactMatchingVisitor(visitor: AtLeast): Promise { @@ -216,11 +220,11 @@ export class LivechatContactsRaw extends BaseRaw implements IL }; } - async findOneByVisitor( + async findOneByVisitor = FindOptionsWithProjection>( visitor: ILivechatContactVisitorAssociation, - options: FindOptions = {}, - ): Promise { - return this.findOne(this.makeQueryForVisitor(visitor), options); + options?: O, + ): Promise | null> { + return this.findOne(this.makeQueryForVisitor(visitor), options); } async addChannel(contactId: string, channel: ILivechatContactChannel): Promise { @@ -278,12 +282,15 @@ export class LivechatContactsRaw extends BaseRaw implements IL return this.updateFromUpdater(this.makeQueryForVisitor(visitor), contactUpdater, options); } - async findSimilarVerifiedContacts( + async findSimilarVerifiedContacts< + T extends Document = ILivechatContact, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( { field, value }: Pick, originalContactId: string, - options?: FindOptions, - ): Promise { - return this.find( + options?: O, + ): Promise[]> { + return this.find( { channels: { $elemMatch: { @@ -304,12 +311,11 @@ export class LivechatContactsRaw extends BaseRaw implements IL }); } - async findOneEnabledById(_id: ILivechatContact['_id'], options?: FindOptions): Promise; - - async findOneEnabledById

(_id: P['_id'], options?: FindOptions

): Promise

; - - async findOneEnabledById(_id: ILivechatContact['_id'], options?: any): Promise { - return this.findOne({ _id, enabled: { $ne: false } }, options); + async findOneEnabledById

= FindOptionsWithProjection

>( + _id: ILivechatContact['_id'], + options?: O, + ): Promise | null> { + return this.findOne({ _id, enabled: { $ne: false } }, options); } disableByVisitorId(visitorId: string): Promise { diff --git a/packages/models/src/models/LivechatCustomField.ts b/packages/models/src/models/LivechatCustomField.ts index 1068b1d87d610..803f55e0b7081 100644 --- a/packages/models/src/models/LivechatCustomField.ts +++ b/packages/models/src/models/LivechatCustomField.ts @@ -1,6 +1,6 @@ import type { ILivechatCustomField, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { ILivechatCustomFieldModel } from '@rocket.chat/model-typings'; -import type { Db, Collection, IndexDescription, FindOptions, FindCursor } from 'mongodb'; +import type { ILivechatCustomFieldModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { Db, Collection, IndexDescription, FindCursor, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -13,40 +13,42 @@ export class LivechatCustomFieldRaw extends BaseRaw implem return [{ key: { scope: 1 } }]; } - findByScope( + findByScope = FindOptionsWithProjection>( scope: ILivechatCustomField['scope'], - options?: FindOptions, + options?: O, includeHidden = true, - ): FindCursor { - return this.find({ scope, ...(includeHidden === true ? {} : { visibility: { $ne: 'hidden' } }) }, options); + ): FindCursor> { + return this.find({ scope, ...(includeHidden === true ? {} : { visibility: { $ne: 'hidden' } }) }, options); } - findMatchingCustomFields( - scope: ILivechatCustomField['scope'], - searchable = true, - options?: FindOptions, - ): FindCursor { + findMatchingCustomFields< + T extends Document = ILivechatCustomField, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(scope: ILivechatCustomField['scope'], searchable = true, options?: O): FindCursor> { const query = { scope, searchable, }; - return this.find(query, options); + return this.find(query, options); } - findMatchingCustomFieldsByIds( + findMatchingCustomFieldsByIds< + T extends Document = ILivechatCustomField, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( ids: ILivechatCustomField['_id'][], scope: ILivechatCustomField['scope'], searchable = true, - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query = { _id: { $in: ids }, scope, searchable, }; - return this.find(query, options); + return this.find(query, options); } async createOrUpdateCustomField( diff --git a/packages/models/src/models/LivechatDepartment.ts b/packages/models/src/models/LivechatDepartment.ts index 0c4ff59f039fa..fe144bf4adcc2 100644 --- a/packages/models/src/models/LivechatDepartment.ts +++ b/packages/models/src/models/LivechatDepartment.ts @@ -1,5 +1,5 @@ import type { ILivechatDepartment, LivechatDepartmentDTO, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { ILivechatDepartmentModel } from '@rocket.chat/model-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'; @@ -65,17 +65,23 @@ export class LivechatDepartmentRaw extends BaseRaw implemen return this.estimatedDocumentCount(); } - findInIds(departmentsIds: string[], options: FindOptions): FindCursor { + findInIds = FindOptionsWithProjection>( + departmentsIds: string[], + options?: O, + ): FindCursor> { const query = { _id: { $in: departmentsIds } }; - return this.find(query, options); + return this.find(query, options); } - findByNameRegexWithExceptionsAndConditions( + findByNameRegexWithExceptionsAndConditions< + T extends Document = ILivechatDepartment, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( searchTerm: string, exceptions: string[] = [], conditions: Filter = {}, - options: FindOptions = {}, - ): FindCursor { + options?: O, + ): FindCursor> { if (!Array.isArray(exceptions)) { exceptions = [exceptions]; } @@ -90,12 +96,15 @@ export class LivechatDepartmentRaw extends BaseRaw implemen ...conditions, }; - return this.find(query, options); + return this.find(query, options); } - findByBusinessHourId(businessHourId: string, options: FindOptions): FindCursor { + findByBusinessHourId = FindOptionsWithProjection>( + businessHourId: string, + options?: O, + ): FindCursor> { const query = { businessHourId }; - return this.find(query, options); + return this.find(query, options); } countByBusinessHourIdExcludingDepartmentId(businessHourId: string, departmentId: string): Promise { @@ -103,22 +112,31 @@ export class LivechatDepartmentRaw extends BaseRaw implemen return this.countDocuments(query); } - findEnabledByBusinessHourId(businessHourId: string, options: FindOptions): FindCursor { + findEnabledByBusinessHourId< + T extends Document = ILivechatDepartment, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(businessHourId: string, options?: O): FindCursor> { const query = { businessHourId, enabled: true }; - return this.find(query, options); + return this.find(query, options); } - findActiveDepartmentsWithoutBusinessHour(options: FindOptions): FindCursor { + findActiveDepartmentsWithoutBusinessHour< + T extends Document = ILivechatDepartment, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(options?: O): FindCursor> { const query = { enabled: true, businessHourId: { $exists: false }, }; - return this.find(query, options); + return this.find(query, options); } - findEnabledInIds(departmentsIds: string[], options?: FindOptions): FindCursor { + findEnabledInIds = FindOptionsWithProjection>( + departmentsIds: string[], + options?: O, + ): FindCursor> { const query = { _id: { $in: departmentsIds }, enabled: true }; - return this.find(query, options); + return this.find(query, options); } addBusinessHourToDepartmentsByIds(ids: string[] = [], businessHourId: string): Promise { @@ -262,7 +280,10 @@ export class LivechatDepartmentRaw extends BaseRaw implemen return this.findEnabledWithAgents(projection); } - findOneByIdOrName(_idOrName: string, options: FindOptions = {}): Promise { + findOneByIdOrName = FindOptionsWithProjection>( + _idOrName: string, + options?: O, + ): Promise | null> { const query = { $or: [ { @@ -274,10 +295,13 @@ export class LivechatDepartmentRaw extends BaseRaw implemen ], }; - return this.findOne(query, options); + return this.findOne(query, options); } - findByUnitIds(unitIds: string[], options: FindOptions = {}): FindCursor { + findByUnitIds = FindOptionsWithProjection>( + unitIds: string[], + options?: O, + ): FindCursor> { const query = { parentId: { $exists: true, @@ -285,21 +309,26 @@ export class LivechatDepartmentRaw extends BaseRaw implemen }, }; - return this.find(query, options); + return this.find(query, options); } countDepartmentsInUnit(unitId: string): Promise { return this.countDocuments({ parentId: unitId }); } - findActiveByUnitIds(_unitIds: string[], _options: FindOptions = {}): FindCursor { + findActiveByUnitIds = FindOptionsWithProjection>( + _unitIds: string[], + _options?: O, + ): FindCursor> { throw new Error('not-implemented'); } - findNotArchived(options: FindOptions = {}): FindCursor { + findNotArchived = FindOptionsWithProjection>( + options?: O, + ): FindCursor> { const query = { archived: { $ne: false } }; - return this.find(query, options); + return this.find(query, options); } getBusinessHoursWithDepartmentStatuses(): Promise< diff --git a/packages/models/src/models/LivechatDepartmentAgents.ts b/packages/models/src/models/LivechatDepartmentAgents.ts index 46d0aba7f434d..3170cf4a768f8 100644 --- a/packages/models/src/models/LivechatDepartmentAgents.ts +++ b/packages/models/src/models/LivechatDepartmentAgents.ts @@ -1,11 +1,15 @@ import type { AvailableAgentsAggregation, ILivechatDepartmentAgents, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { FindPaginated, ILivechatDepartmentAgentsModel } from '@rocket.chat/model-typings'; +import type { + FindPaginated, + ILivechatDepartmentAgentsModel, + DocumentWithProjection, + FindOptionsWithProjection, +} from '@rocket.chat/model-typings'; import type { Collection, FindCursor, Db, Filter, - FindOptions, Document, UpdateResult, DeleteResult, @@ -47,37 +51,31 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw): FindCursor { - return this.find({ agentId: { $in: agentIds } }, options); + findByAgentIds = FindOptionsWithProjection>( + agentIds: string[], + options?: O, + ): FindCursor> { + return this.find({ agentId: { $in: agentIds } }, options); } - findByAgentId(agentId: string, options?: FindOptions): FindCursor { - return this.find({ agentId }, options); + findByAgentId = FindOptionsWithProjection>( + agentId: string, + options?: O, + ): FindCursor> { + return this.find({ agentId }, options); } - findAgentsByDepartmentId(departmentId: string): FindPaginated>; - - findAgentsByDepartmentId( - departmentId: string, - options: FindOptions, - ): FindPaginated>; - - findAgentsByDepartmentId

( - departmentId: string, - options: FindOptions

, - ): FindPaginated>; - - findAgentsByDepartmentId( - departmentId: string, - options?: undefined | FindOptions, - ): FindPaginated> { + findAgentsByDepartmentId< + P extends Document = ILivechatDepartmentAgents, + O extends FindOptionsWithProjection

= FindOptionsWithProjection

, + >(departmentId: string, options?: O): FindPaginated>> { const query = { departmentId }; if (options === undefined) { - return this.findPaginated(query); + return this.findPaginated(query); } - return this.findPaginated(query, options); + return this.findPaginated(query, options); } findByDepartmentIds(departmentIds: string[], options = {}): FindCursor { @@ -92,16 +90,18 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw): FindCursor { - return this.find({ departmentId }, options); + findByDepartmentId = FindOptionsWithProjection>( + departmentId: string, + options?: O, + ): FindCursor> { + return this.find({ departmentId }, options); } - findOneByAgentIdAndDepartmentId( - agentId: string, - departmentId: string, - options?: FindOptions, - ): Promise { - return this.findOne({ agentId, departmentId }, options); + findOneByAgentIdAndDepartmentId< + T extends Document = ILivechatDepartmentAgents, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(agentId: string, departmentId: string, options?: O): Promise | null> { + return this.findOne({ agentId, departmentId }, options); } saveAgent(agent: { @@ -185,7 +185,7 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw = FindOptionsWithProjection, + >( agentsIds: ILivechatDepartmentAgents['agentId'][], departmentId: ILivechatDepartmentAgents['departmentId'], - options?: FindOptions, - ): FindCursor { - return this.find({ agentId: { $in: agentsIds }, departmentId }, options); + options?: O, + ): FindCursor> { + return this.find({ agentId: { $in: agentsIds }, departmentId }, options); } findDepartmentsOfAgent(agentId: string, enabled = false): AggregationCursor { diff --git a/packages/models/src/models/LivechatInquiry.ts b/packages/models/src/models/LivechatInquiry.ts index 519fac8a9a511..6a0e773dafdd4 100644 --- a/packages/models/src/models/LivechatInquiry.ts +++ b/packages/models/src/models/LivechatInquiry.ts @@ -6,7 +6,7 @@ import type { SelectedAgent, } from '@rocket.chat/core-typings'; import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; -import type { ILivechatInquiryModel } from '@rocket.chat/model-typings'; +import type { ILivechatInquiryModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import type { Collection, Db, @@ -107,18 +107,14 @@ export class LivechatInquiryRaw extends BaseRaw implemen ]; } - findOneByRoomId( + findOneByRoomId = FindOptionsWithProjection>( rid: string, - options?: FindOptions, - ): Promise { + options?: O, + ): Promise | null> { const query = { rid, }; - return this.findOne(query, options); - } - - findIdsByVisitorId(_id: ILivechatInquiryRecord['v']['_id']): FindCursor { - return this.find({ 'v._id': _id }, { projection: { _id: 1 } }); + return this.findOne(query, options); } getDistinctQueuedDepartments(options: AggregateOptions): Promise<{ _id: string | null }[]> { @@ -288,8 +284,10 @@ export class LivechatInquiryRaw extends BaseRaw implemen return this.deleteOne({ rid }, options); } - getQueuedInquiries(options?: FindOptions): FindCursor { - return this.find({ status: LivechatInquiryStatus.QUEUED }, options); + getQueuedInquiries = FindOptionsWithProjection>( + options?: O, + ): FindCursor> { + return this.find({ status: LivechatInquiryStatus.QUEUED }, options); } takeInquiry(inquiryId: string, lockedAt?: Date): Promise { @@ -459,7 +457,10 @@ export class LivechatInquiryRaw extends BaseRaw implemen return this.updateMany(query, update); } - findByVisitorIds(visitorIds: string[], options?: FindOptions): FindCursor { - return this.find({ 'v._id': { $in: visitorIds } }, options); + findByVisitorIds = FindOptionsWithProjection>( + visitorIds: string[], + options?: O, + ): FindCursor> { + return this.find({ 'v._id': { $in: visitorIds } }, options); } } diff --git a/packages/models/src/models/LivechatRooms.ts b/packages/models/src/models/LivechatRooms.ts index 59077c7bd3ac6..43a4a365479d2 100644 --- a/packages/models/src/models/LivechatRooms.ts +++ b/packages/models/src/models/LivechatRooms.ts @@ -13,7 +13,7 @@ import type { AtLeast, } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; -import type { FindPaginated, ILivechatRoomsModel } from '@rocket.chat/model-typings'; +import type { FindPaginated, ILivechatRoomsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/tools'; import type { Db, @@ -1660,7 +1660,10 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.updateMany({ departmentId }, { $unset: { departmentId: 1, departmentAncestors: 1 } }); } - findOneByIdOrName(_idOrName: string, options: FindOptions) { + findOneByIdOrName = FindOptionsWithProjection>( + _idOrName: string, + options?: O, + ): Promise | null> { const query: Filter = { t: 'l', $or: [ @@ -1673,7 +1676,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ], }; - return this.findOne(query, options); + return this.findOne(query, options); } updateSurveyFeedbackById(_id: string, surveyFeedback: string) { @@ -1814,12 +1817,10 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.findOne(query, options); } - findOneByVisitorTokenAndEmailThreadAndDepartment( - visitorToken: string, - emailThread: string[], - departmentId: string, - options: FindOptions, - ) { + findOneByVisitorTokenAndEmailThreadAndDepartment< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(visitorToken: string, emailThread: string[], departmentId: string, options?: O): Promise | null> { const query: Filter = { 't': 'l', 'v.token': visitorToken, @@ -1830,7 +1831,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...(departmentId && { departmentId }), }; - return this.findOne(query, options); + return this.findOne(query, options); } updateEmailThreadByRoomId(roomId: string, threadIds: string[]) { @@ -1843,7 +1844,10 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.updateOne({ _id: roomId }, query); } - findOneLastServedAndClosedByVisitorToken(visitorToken: string, options: FindOptions = {}) { + findOneLastServedAndClosedByVisitorToken< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(visitorToken: string, options?: O): Promise | null> { const query: Filter = { 't': 'l', 'v.token': visitorToken, @@ -1851,8 +1855,9 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive 'servedBy': { $exists: true }, }; - options.sort = { closedAt: -1 }; - return this.findOne(query, options); + // safe to merge into `O`: only `O['projection']` feeds the return type, and it survives the spread. + // note the model's `sort` wins over a caller-supplied one. + return this.findOne(query, { ...options, sort: { closedAt: -1 } } as unknown as O); } findOneByVisitorToken(visitorToken: string, fields: FindOptions['projection']) { @@ -1870,7 +1875,11 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.findOne(query, options); } - findOpenByVisitorToken(visitorToken: string, options: FindOptions = {}, extraQuery: Filter = {}) { + findOpenByVisitorToken = FindOptionsWithProjection>( + visitorToken: string, + options?: O, + extraQuery: Filter = {}, + ): FindCursor> { const query: Filter = { 't': 'l', 'open': true, @@ -1878,13 +1887,13 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...extraQuery, }; - return this.find(query, options); + return this.find(query, options); } - findOneOpenByContactChannelVisitor( - association: ILivechatContactVisitorAssociation, - options: FindOptions = {}, - ): Promise { + findOneOpenByContactChannelVisitor< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(association: ILivechatContactVisitorAssociation, options?: O): Promise | null> { const query: Filter = { 't': 'l', 'open': true, @@ -1893,10 +1902,14 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...(association.source.id ? { 'source.id': association.source.id } : {}), }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneOpenByVisitorToken(visitorToken: string, options: FindOptions = {}, extraQuery: Filter = {}) { + findOneOpenByVisitorToken = FindOptionsWithProjection>( + visitorToken: string, + options?: O, + extraQuery: Filter = {}, + ): Promise | null> { const query: Filter = { 't': 'l', 'open': true, @@ -1904,15 +1917,13 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...extraQuery, }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneOpenByVisitorTokenAndDepartmentIdAndSource( - visitorToken: string, - departmentId?: string, - source?: string, - options: FindOptions = {}, - ) { + findOneOpenByVisitorTokenAndDepartmentIdAndSource< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(visitorToken: string, departmentId?: string, source?: string, options?: O): Promise | null> { const query: Filter = { 't': 'l', 'open': true, @@ -1921,15 +1932,18 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...(source && { 'source.type': source }), }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOpenByVisitorTokenAndDepartmentId( + findOpenByVisitorTokenAndDepartmentId< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( visitorToken: string, departmentId: string, - options: FindOptions = {}, + options?: O, extraQuery: Filter = {}, - ) { + ): FindCursor> { const query: Filter = { 't': 'l', 'open': true, @@ -1938,15 +1952,15 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...extraQuery, }; - return this.find(query, options); + return this.find(query, options); } - findByVisitorIdAndAgentId( + findByVisitorIdAndAgentId = FindOptionsWithProjection>( visitorId?: string, agentId?: string, - options: FindOptions = {}, + options?: O, extraQuery: Filter = {}, - ) { + ): FindCursor> { const query: Filter = { t: 'l', ...(visitorId && { 'v._id': visitorId }), @@ -1954,7 +1968,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...extraQuery, }; - return this.find(query, options); + return this.find(query, options); } async findNewestByContactVisitorAssociation( @@ -1974,7 +1988,10 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive }); } - findOneOpenByRoomIdAndVisitorToken(roomId: string, visitorToken: string, options: FindOptions = {}) { + findOneOpenByRoomIdAndVisitorToken< + T extends Document = IOmnichannelRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(roomId: string, visitorToken: string, options?: O): Promise | null> { const query: Filter = { 't': 'l', '_id': roomId, @@ -1982,10 +1999,14 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive 'v.token': visitorToken, }; - return this.findOne(query, options); + return this.findOne(query, options); } - findClosedRooms(departmentIds?: string[], options: FindOptions = {}, extraQuery: Filter = {}) { + findClosedRooms = FindOptionsWithProjection>( + departmentIds?: string[], + options?: O, + extraQuery: Filter = {}, + ): FindCursor> { const query: Filter = { t: 'l', open: { $exists: false }, @@ -1994,7 +2015,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...extraQuery, }; - return this.find(query, options); + return this.find(query, options); } getResponseByRoomIdUpdateQuery(responseBy: IOmnichannelRoom['responseBy'], updater: Updater = this.getUpdater()) { @@ -2275,7 +2296,11 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.countDocuments(query); } - findOpenByAgent(userId: string, extraQuery: Filter = {}, options: FindOptions = {}) { + findOpenByAgent = FindOptionsWithProjection>( + userId: string, + extraQuery: Filter = {}, + options?: O, + ): FindCursor> { const query: Filter = { 't': 'l', 'open': true, @@ -2283,7 +2308,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...extraQuery, }; - return this.find(query, options); + return this.find(query, options); } changeAgentByRoomId(roomId: string, newAgent: { agentId: string; username: string; ts?: Date }) { @@ -2733,11 +2758,14 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive throw new Error('Method not implemented.'); } - findOpenByContactId(contactId: ILivechatContact['_id'], options?: FindOptions): FindCursor { - return this.find({ open: true, contactId }, options); + findOpenByContactId = FindOptionsWithProjection>( + contactId: ILivechatContact['_id'], + options?: O, + ): FindCursor> { + return this.find({ open: true, contactId }, options); } - checkContactOpenRooms(contactId: ILivechatContact['_id']): Promise { + checkContactOpenRooms(contactId: ILivechatContact['_id']): Promise | null> { return this.findOne({ contactId, open: true }, { projection: { _id: 1 } }); } } diff --git a/packages/models/src/models/LivechatVisitors.ts b/packages/models/src/models/LivechatVisitors.ts index ce0eee598c93b..3864faa72e04f 100644 --- a/packages/models/src/models/LivechatVisitors.ts +++ b/packages/models/src/models/LivechatVisitors.ts @@ -1,5 +1,5 @@ import type { IVisitorExternalIdentifier, ILivechatVisitor, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { FindPaginated, ILivechatVisitorsModel } from '@rocket.chat/model-typings'; +import type { FindPaginated, ILivechatVisitorsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/tools'; import type { AggregationCursor, @@ -111,16 +111,22 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL * Find visitors by _id * @param {string} token - Visitor token */ - findById(_id: string, options: FindOptions): FindCursor { + findById = FindOptionsWithProjection>( + _id: string, + options?: O, + ): FindCursor> { const query = { _id, }; - return this.find(query, options); + return this.find(query, options); } - findEnabled(query: Filter, options?: FindOptions): FindCursor { - return this.find( + findEnabled = FindOptionsWithProjection>( + query: Filter, + options?: O, + ): FindCursor> { + return this.find( { ...query, disabled: { $ne: true }, @@ -129,21 +135,27 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL ); } - findOneEnabledById(_id: string, options?: FindOptions): Promise { + findOneEnabledById = FindOptionsWithProjection>( + _id: string, + options?: O, + ): Promise | null> { const query = { _id, disabled: { $ne: true }, }; - return this.findOne(query, options); + return this.findOne(query, options); } - getVisitorByToken(token: string, options: FindOptions): Promise { + getVisitorByToken = FindOptionsWithProjection>( + token: string, + options?: O, + ): Promise | null> { const query = { token, }; - return this.findOne(query, options); + return this.findOne(query, options); } countVisitorsBetweenDate({ start, end, department }: { start: Date; end: Date; department?: string }): Promise { @@ -216,14 +228,17 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL /** * Find visitors by their email or phone or username or name */ - async findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField( + async findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField< + T extends Document = ILivechatVisitor, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( emailOrPhone?: string, nameOrUsername?: RegExp, allowedCustomFields: string[] = [], - options?: FindOptions, - ): Promise>> { + options?: O, + ): Promise>>> { if (!emailOrPhone && !nameOrUsername && allowedCustomFields.length === 0) { - return this.findPaginated({ disabled: { $ne: true } }, options); + return this.findPaginated({ disabled: { $ne: true } }, options); } const query: Filter = { @@ -253,7 +268,7 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL disabled: { $ne: true }, }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } async findOneByEmailAndPhoneAndCustomField( @@ -498,11 +513,14 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL return this.findOneAndUpdate({ _id }, { $set: { department } }, { returnDocument: 'after' }); } - findByIds(ids: string[], options?: FindOptions): FindCursor { + findByIds = FindOptionsWithProjection>( + ids: string[], + options?: O, + ): FindCursor> { const query = { _id: { $in: ids }, }; - return this.find(query, options); + return this.find(query, options); } } diff --git a/packages/models/src/models/LoginServiceConfiguration.ts b/packages/models/src/models/LoginServiceConfiguration.ts index 721d63ff21b40..7e87dc59d8ae2 100644 --- a/packages/models/src/models/LoginServiceConfiguration.ts +++ b/packages/models/src/models/LoginServiceConfiguration.ts @@ -1,6 +1,6 @@ import type { LoginServiceConfiguration, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { ILoginServiceConfigurationModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, Document, FindOptions } from 'mongodb'; +import type { ILoginServiceConfigurationModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { Collection, Db, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -48,10 +48,10 @@ export class LoginServiceConfigurationRaw extends BaseRaw( - serviceName: LoginServiceConfiguration['service'], - options?: FindOptions

, - ): Promise

{ - return this.findOne({ service: serviceName.toLowerCase() }, options); + async findOneByService< + P extends Document = LoginServiceConfiguration, + O extends FindOptionsWithProjection

= FindOptionsWithProjection

, + >(serviceName: LoginServiceConfiguration['service'], options?: O): Promise | null> { + return this.findOne({ service: serviceName.toLowerCase() }, options); } } diff --git a/packages/models/src/models/MediaCallNegotiations.ts b/packages/models/src/models/MediaCallNegotiations.ts index 206473cca9476..4f95866aab3fd 100644 --- a/packages/models/src/models/MediaCallNegotiations.ts +++ b/packages/models/src/models/MediaCallNegotiations.ts @@ -4,8 +4,8 @@ import type { MediaCallNegotiationStream, RTCSessionDescriptionInit, } from '@rocket.chat/core-typings'; -import type { IMediaCallNegotiationsModel } from '@rocket.chat/model-typings'; -import type { IndexDescription, Collection, Db, FindOptions, Document, UpdateResult } from 'mongodb'; +import type { IMediaCallNegotiationsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { IndexDescription, Collection, Db, Document, UpdateResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -18,21 +18,23 @@ export class MediaCallNegotiationsRaw extends BaseRaw imp return [{ key: { callId: 1, requestTimestamp: -1 }, unique: false }]; } - public async findLatestByCallId( - callId: IMediaCallNegotiation['callId'], - options?: FindOptions, - ): Promise { - return this.findOne( + public async findLatestByCallId< + T extends Document = IMediaCallNegotiation, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(callId: IMediaCallNegotiation['callId'], options?: O): Promise | null> { + return this.findOne( { 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, ); } diff --git a/packages/models/src/models/MediaCalls.ts b/packages/models/src/models/MediaCalls.ts index 858ea9a9680ec..5ec6e414af0c4 100644 --- a/packages/models/src/models/MediaCalls.ts +++ b/packages/models/src/models/MediaCalls.ts @@ -7,18 +7,8 @@ import type { IUser, MediaCallActor, } from '@rocket.chat/core-typings'; -import type { IMediaCallsModel } from '@rocket.chat/model-typings'; -import type { - IndexDescription, - Collection, - Db, - UpdateFilter, - UpdateOptions, - UpdateResult, - FindOptions, - Document, - FindCursor, -} from 'mongodb'; +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'; @@ -38,12 +28,12 @@ export class MediaCallsRaw extends BaseRaw implements IMediaCallsMod ]; } - public async findOneByIdAndCallee( + public async findOneByIdAndCallee = FindOptionsWithProjection>( id: IMediaCall['_id'], callee: MediaCallActor, - options?: FindOptions, - ): Promise { - return this.findOne( + options?: O, + ): Promise | null> { + return this.findOne( { '_id': id, 'callee.type': callee.type, @@ -54,12 +44,15 @@ export class MediaCallsRaw extends BaseRaw implements IMediaCallsMod ); } - public async findOneByCallerRequestedId( + public async findOneByCallerRequestedId< + T extends Document = IMediaCall, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( id: Required['callerRequestedId'], caller: { type: MediaCallActorType; id: string }, - options?: FindOptions, - ): Promise { - return this.findOne( + options?: O, + ): Promise | null> { + return this.findOne( { 'caller.type': caller.type, 'caller.id': caller.id, @@ -184,8 +177,10 @@ export class MediaCallsRaw extends BaseRaw implements IMediaCallsMod ); } - public findAllExpiredCalls(options?: FindOptions): FindCursor { - return this.find( + public findAllExpiredCalls = FindOptionsWithProjection>( + options?: O, + ): FindCursor> { + return this.find( { ended: false, expiresAt: { @@ -196,8 +191,11 @@ export class MediaCallsRaw extends BaseRaw implements IMediaCallsMod ); } - public findAllNotOverByUid(uid: IUser['_id'], options?: FindOptions): FindCursor { - return this.find( + public findAllNotOverByUid = FindOptionsWithProjection>( + uid: IUser['_id'], + options?: O, + ): FindCursor> { + return this.find( { ended: false, expiresAt: { diff --git a/packages/models/src/models/Messages.ts b/packages/models/src/models/Messages.ts index 4a288560e912e..f0aeccb19d7c2 100644 --- a/packages/models/src/models/Messages.ts +++ b/packages/models/src/models/Messages.ts @@ -9,7 +9,7 @@ import type { IMessageWithPendingFileImport, DeepWritable, } from '@rocket.chat/core-typings'; -import type { FindPaginated, IMessagesModel } from '@rocket.chat/model-typings'; +import type { FindPaginated, IMessagesModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import type { PaginatedRequest } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/tools'; import type { @@ -78,65 +78,72 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { ]; } - findVisibleByMentionAndRoomId(username: IUser['username'], rid: IRoom['_id'], options?: FindOptions): FindCursor { + findVisibleByMentionAndRoomId = FindOptionsWithProjection>( + username: IUser['username'], + rid: IRoom['_id'], + options?: O, + ): FindCursor> { const query: Filter = { '_hidden': { $ne: true }, 'mentions.username': username, rid, }; - return this.find(query, options); + return this.find(query, options); } - findPaginatedVisibleByMentionAndRoomId( - username: IUser['username'], - rid: IRoom['_id'], - options?: FindOptions, - ): FindPaginated> { + findPaginatedVisibleByMentionAndRoomId< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(username: IUser['username'], rid: IRoom['_id'], options?: O): FindPaginated>> { const query: Filter = { '_hidden': { $ne: true }, 'mentions.username': username, rid, }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findStarredByUserAtRoom( + findStarredByUserAtRoom = FindOptionsWithProjection>( userId: IUser['_id'], roomId: IRoom['_id'], - options?: FindOptions, - ): FindPaginated> { + options?: O, + ): FindPaginated>> { const query: Filter = { '_hidden': { $ne: true }, 'starred._id': userId, 'rid': roomId, }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findPaginatedByRoomIdAndType( + findPaginatedByRoomIdAndType = FindOptionsWithProjection>( roomId: IRoom['_id'], type: IMessage['t'], - options: FindOptions = {}, - ): FindPaginated> { + options?: O, + ): FindPaginated>> { const query = { rid: roomId, t: type, }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findDiscussionsByRoomAndText(rid: IRoom['_id'], text: string, options?: FindOptions): FindPaginated> { + findDiscussionsByRoomAndText = FindOptionsWithProjection>( + rid: IRoom['_id'], + text: string, + options?: O, + ): FindPaginated>> { const query: Filter = { rid, drid: { $exists: true }, msg: new RegExp(escapeRegExp(text), 'i'), }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } findAllNumberOfTransferredRooms({ @@ -320,8 +327,12 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { return this.col.aggregate(params, { allowDiskUse: true, readPreference: readSecondaryPreferred() }).toArray(); } - findLivechatClosedMessages(rid: IRoom['_id'], searchTerm?: string, options?: FindOptions): FindPaginated> { - return this.findPaginated( + findLivechatClosedMessages = FindOptionsWithProjection>( + rid: IRoom['_id'], + searchTerm?: string, + options?: O, + ): FindPaginated>> { + return this.findPaginated( { rid, $or: [{ t: { $exists: false } }, { t: 'livechat-close' }], @@ -331,8 +342,11 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { ); } - findLivechatClosingMessage(rid: IRoom['_id'], options?: FindOptions): Promise { - return this.findOne( + findLivechatClosingMessage = FindOptionsWithProjection>( + rid: IRoom['_id'], + options?: O, + ): Promise | null> { + return this.findOne( { rid, t: 'livechat-close', @@ -341,14 +355,17 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { ); } - findVisibleByRoomIdNotContainingTypesBeforeTs( + findVisibleByRoomIdNotContainingTypesBeforeTs< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( roomId: IRoom['_id'], types: IMessage['t'][], ts: Date, showSystemMessages: boolean, - options?: FindOptions, + options?: O, showThreadMessages = true, - ): FindCursor { + ): FindCursor> { const query: Filter = { _hidden: { $ne: true, @@ -375,15 +392,15 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { query.t = { $exists: false }; } - return this.find(query, options); + return this.find(query, options); } - findLivechatMessagesWithoutTypes( + findLivechatMessagesWithoutTypes = FindOptionsWithProjection>( rid: IRoom['_id'], ignoredTypes: IMessage['t'][], showSystemMessages: boolean, - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query: Filter = { rid, }; @@ -396,7 +413,7 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { query.t = { $exists: false }; } - return this.find(query, options); + return this.find(query, options); } async setBlocksById(_id: string, blocks: Required['blocks']): Promise { @@ -484,7 +501,10 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { return this.countDocuments(query, options); } - findPaginatedPinnedByRoom(roomId: IMessage['rid'], options?: FindOptions): FindPaginated> { + findPaginatedPinnedByRoom = FindOptionsWithProjection>( + roomId: IMessage['rid'], + options?: O, + ): FindPaginated>> { const query: Filter = { t: { $ne: 'rm' }, _hidden: { $ne: true }, @@ -492,7 +512,7 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { rid: roomId, }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } countStarred(options?: CountDocumentsOptions): Promise { @@ -636,10 +656,13 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { } // FIND - findByMention(username: string, options?: FindOptions): FindCursor { + findByMention = FindOptionsWithProjection>( + username: string, + options?: O, + ): FindCursor> { const query = { 'mentions.username': username }; - return this.find(query, options); + return this.find(query, options); } findFilesByUserId(userId: string, options: FindOptions = {}): FindCursor> { @@ -650,15 +673,18 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { return this.find(query, { projection: { 'file._id': 1, 'files._id': 1 }, ...options }); } - findFilesByRoomIdPinnedTimestampAndUsers( + findFilesByRoomIdPinnedTimestampAndUsers< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( rid: string, excludePinned: boolean, ignoreDiscussion = true, ts: Filter['ts'], users: string[] = [], ignoreThreads = true, - options: FindOptions = {}, - ): FindCursor { + options?: O, + ): FindCursor> { const query: Filter = { rid, ts, @@ -676,16 +702,19 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { ...(users.length ? { 'u.username': { $in: users } } : {}), }; - return this.find(query, options); + return this.find(query, options); } - findDiscussionByRoomIdPinnedTimestampAndUsers( + findDiscussionByRoomIdPinnedTimestampAndUsers< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( rid: string, excludePinned: boolean, ts: Filter['ts'], users: string[] = [], - options: FindOptions = {}, - ): FindCursor { + options?: O, + ): FindCursor> { const query: Filter = { rid, ts, @@ -694,10 +723,13 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { ...(users.length ? { 'u.username': { $in: users } } : {}), }; - return this.find(query, options); + return this.find(query, options); } - findVisibleByRoomId(rid: string, options?: FindOptions): FindCursor { + findVisibleByRoomId = FindOptionsWithProjection>( + rid: string, + options?: O, + ): FindCursor> { const query = { _hidden: { $ne: true, @@ -706,10 +738,13 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { rid, }; - return this.find(query, options); + return this.find(query, options); } - findVisibleByIds(ids: string[], options?: FindOptions): FindCursor { + findVisibleByIds = FindOptionsWithProjection>( + ids: string[], + options?: O, + ): FindCursor> { const query = { _id: { $in: ids }, _hidden: { @@ -717,10 +752,13 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { }, }; - return this.find(query, options); + return this.find(query, options); } - findVisibleThreadByThreadId(tmid: string, options?: FindOptions): FindCursor { + findVisibleThreadByThreadId = FindOptionsWithProjection>( + tmid: string, + options?: O, + ): FindCursor> { const query = { _hidden: { $ne: true, @@ -729,15 +767,13 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { tmid, }; - return this.find(query, options); + return this.find(query, options); } - findVisibleByRoomIdNotContainingTypes( - roomId: string, - types: MessageTypesValues[], - options?: FindOptions, - showThreadMessages = true, - ): FindCursor { + findVisibleByRoomIdNotContainingTypes< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(roomId: string, types: MessageTypesValues[], options?: O, showThreadMessages = true): FindCursor> { const query: Filter = { _hidden: { $ne: true, @@ -759,7 +795,7 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { }), }; - return this.find(query, options); + return this.find(query, options); } countVisibleByRoomIdContainingTypes(roomId: string, types: MessageTypesValues[]): Promise { @@ -774,12 +810,12 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { return this.countDocuments(query); } - findVisibleByRoomIdAfterTimestamp( + findVisibleByRoomIdAfterTimestamp = FindOptionsWithProjection>( roomId: string, timestamp: Date, showThreadMessages = true, - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query = { _hidden: { $ne: true, @@ -800,14 +836,14 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { }), }; - return this.find(query, options); + return this.find(query, options); } - findForUpdates( + findForUpdates = FindOptionsWithProjection>( roomId: IMessage['rid'], { updatedAt, minTs }: { updatedAt: { $lt: Date } | { $gt: Date }; minTs?: Date }, - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query = { rid: roomId, _hidden: { $ne: true }, @@ -815,15 +851,15 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { ...(minTs && { ts: { $gte: minTs } }), }; - return this.find(query, options); + return this.find(query, options); } - findVisibleByRoomIdBeforeTimestamp( + findVisibleByRoomIdBeforeTimestamp = FindOptionsWithProjection>( roomId: string, timestamp: Date, showThreadMessages = true, - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query = { _hidden: { $ne: true, @@ -844,17 +880,20 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { }), }; - return this.find(query, options); + return this.find(query, options); } - findVisibleByRoomIdBeforeTimestampNotContainingTypes( + findVisibleByRoomIdBeforeTimestampNotContainingTypes< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( roomId: string, timestamp: Date, types: MessageTypesValues[], - options?: FindOptions, + options?: O, showThreadMessages = true, inclusive = false, - ): FindCursor { + ): FindCursor> { const query = { _hidden: { $ne: true, @@ -879,18 +918,21 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { }), }; - return this.find(query, options); + return this.find(query, options); } - findVisibleByRoomIdBetweenTimestampsNotContainingTypes( + findVisibleByRoomIdBetweenTimestampsNotContainingTypes< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( roomId: string, afterTimestamp: Date, beforeTimestamp: Date, types: MessageTypesValues[], - options: FindOptions = {}, + options?: O, showThreadMessages = true, inclusive = false, - ): FindCursor { + ): FindCursor> { const query = { _hidden: { $ne: true, @@ -916,7 +958,7 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { }), }; - return this.find(query, options); + return this.find(query, options); } countVisibleByRoomIdBetweenTimestampsNotContainingTypes( @@ -962,7 +1004,11 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { return message?.ts; } - findByRoomIdAndMessageIds(rid: string, messageIds: string[], options?: FindOptions): FindCursor { + findByRoomIdAndMessageIds = FindOptionsWithProjection>( + rid: string, + messageIds: string[], + options?: O, + ): FindCursor> { const query = { rid, _id: { @@ -970,7 +1016,7 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { }, }; - return this.find(query, options); + return this.find(query, options); } findOneBySlackBotIdAndSlackTs(slackBotId: string, slackTs: Date): Promise { @@ -988,13 +1034,17 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { return this.findOne(query); } - findOneByRoomIdAndMessageId(rid: string, messageId: string, options?: FindOptions): Promise { + findOneByRoomIdAndMessageId = FindOptionsWithProjection>( + rid: string, + messageId: string, + options?: O, + ): Promise | null> { const query = { rid, _id: messageId, }; - return this.findOne(query, options); + return this.findOne(query, options); } getLastVisibleUserMessageSentByRoomId(rid: string, messageId?: string): Promise { @@ -1319,7 +1369,10 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { return this.deleteMany({ rid: { $in: rids } }); } - findThreadsByRoomIdPinnedTimestampAndUsers( + findThreadsByRoomIdPinnedTimestampAndUsers< + T extends Document = IMessage, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( { rid, pinned, @@ -1327,8 +1380,8 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { ts, users = [], }: { rid: string; pinned: boolean; ignoreDiscussion?: boolean; ts: Filter['ts']; users: string[] }, - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query: Filter = { rid, ts, @@ -1345,7 +1398,7 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { query.drid = { $exists: false }; } - return this.find(query, options); + return this.find(query, options); } async findByIdPinnedTimestampLimitAndUsers( diff --git a/packages/models/src/models/NpsVote.ts b/packages/models/src/models/NpsVote.ts index 4bf83b2342f8e..55e5246012e69 100644 --- a/packages/models/src/models/NpsVote.ts +++ b/packages/models/src/models/NpsVote.ts @@ -1,7 +1,7 @@ import type { INpsVote, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import { INpsVoteStatus } from '@rocket.chat/core-typings'; -import type { INpsVoteModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, Document, FindOptions, IndexDescription, UpdateResult } from 'mongodb'; +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'; @@ -15,35 +15,47 @@ export class NpsVoteRaw extends BaseRaw implements INpsVoteModel { return [{ key: { npsId: 1, status: 1, sentAt: 1 } }, { key: { npsId: 1, identifier: 1 }, unique: true }]; } - findNotSentByNpsId(npsId: string, options?: Omit, 'sort' | 'limit'>): FindCursor { + // `sort` and `limit` are branded away because the cursor below overwrites both; a caller passing + // them would have them silently dropped + findNotSentByNpsId = FindOptionsWithProjection>( + npsId: string, + options?: O & { sort?: never; limit?: never }, + ): FindCursor> { const query = { npsId, status: INpsVoteStatus.NEW, }; - const cursor = options ? this.find(query, options) : this.find(query); + const cursor = options ? this.find(query, options) : this.find(query); return cursor.sort({ ts: 1 }).limit(1000); } - findByNpsIdAndStatus(npsId: string, status: INpsVoteStatus, options?: FindOptions): FindCursor { + findByNpsIdAndStatus = FindOptionsWithProjection>( + npsId: string, + status: INpsVoteStatus, + options?: O, + ): FindCursor> { const query = { npsId, status, }; if (options) { - return this.find(query, options); + return this.find(query, options); } - return this.find(query); + return this.find(query); } - findByNpsId(npsId: string, options?: FindOptions): FindCursor { + findByNpsId = FindOptionsWithProjection>( + npsId: string, + options?: O, + ): FindCursor> { const query = { npsId, }; if (options) { - return this.find(query, options); + return this.find(query, options); } - return this.find(query); + return this.find(query); } save(vote: Omit): Promise { diff --git a/packages/models/src/models/OAuthAccessTokens.ts b/packages/models/src/models/OAuthAccessTokens.ts index efcf37b40a73e..f06fa7c6f07bf 100644 --- a/packages/models/src/models/OAuthAccessTokens.ts +++ b/packages/models/src/models/OAuthAccessTokens.ts @@ -1,6 +1,6 @@ import type { IOAuthAccessToken, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IOAuthAccessTokensModel } from '@rocket.chat/model-typings'; -import type { Db, Collection, DeleteResult, FindOptions, IndexDescription } from 'mongodb'; +import type { IOAuthAccessTokensModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { Db, Collection, DeleteResult, IndexDescription, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -19,18 +19,24 @@ export class OAuthAccessTokensRaw extends BaseRaw implements ]; } - async findOneByAccessToken(accessToken: string, options?: FindOptions): Promise { + async findOneByAccessToken = FindOptionsWithProjection>( + accessToken: string, + options?: O, + ): Promise | null> { if (typeof accessToken !== 'string' || !accessToken) { return null; } - return this.findOne({ accessToken }, options); + return this.findOne({ accessToken }, options); } - async findOneByRefreshToken(refreshToken: string, options?: FindOptions): Promise { + async findOneByRefreshToken< + T extends Document = IOAuthAccessToken, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(refreshToken: string, options?: O): Promise | null> { if (typeof refreshToken !== 'string' || !refreshToken) { return null; } - return this.findOne({ refreshToken }, options); + return this.findOne({ refreshToken }, options); } async deleteByUserId(userId: string): Promise { diff --git a/packages/models/src/models/OAuthApps.ts b/packages/models/src/models/OAuthApps.ts index b6d63688331e0..c2835dc84d93f 100644 --- a/packages/models/src/models/OAuthApps.ts +++ b/packages/models/src/models/OAuthApps.ts @@ -1,6 +1,6 @@ import type { IOAuthApps, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IOAuthAppsModel } from '@rocket.chat/model-typings'; -import type { Db, Collection, FindOptions, IndexDescription } from 'mongodb'; +import type { IOAuthAppsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { Db, Collection, IndexDescription, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -13,11 +13,11 @@ export class OAuthAppsRaw extends BaseRaw implements IOAuthAppsModel return [{ key: { clientId: 1, clientSecret: 1 } }, { key: { appId: 1 } }]; } - findOneAuthAppByIdOrClientId( + findOneAuthAppByIdOrClientId = FindOptionsWithProjection>( props: { clientId: string } | { appId: string } | { _id: string }, - options?: FindOptions, - ): Promise { - return this.findOne( + options?: O, + ): Promise | null> { + return this.findOne( { ...('_id' in props && { _id: props._id }), ...('appId' in props && { _id: props.appId }), @@ -27,11 +27,14 @@ export class OAuthAppsRaw extends BaseRaw implements IOAuthAppsModel ); } - findOneActiveByClientId(clientId: string, options?: FindOptions): Promise { + findOneActiveByClientId = FindOptionsWithProjection>( + clientId: string, + options?: O, + ): Promise | null> { if (typeof clientId !== 'string' || !clientId) { return Promise.resolve(null); } - return this.findOne( + return this.findOne( { active: true, clientId, @@ -47,15 +50,14 @@ export class OAuthAppsRaw extends BaseRaw implements IOAuthAppsModel return this.findOneAndUpdate({ _id }, { $set: data }, { returnDocument: 'after' }); } - findOneActiveByClientIdAndClientSecret( - clientId: string, - clientSecret: string, - options?: FindOptions, - ): Promise { + findOneActiveByClientIdAndClientSecret< + T extends Document = IOAuthApps, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(clientId: string, clientSecret: string, options?: O): Promise | null> { if (typeof clientId !== 'string' || !clientId || typeof clientSecret !== 'string' || !clientSecret) { return Promise.resolve(null); } - return this.findOne( + return this.findOne( { active: true, clientId, diff --git a/packages/models/src/models/OAuthAuthCodes.ts b/packages/models/src/models/OAuthAuthCodes.ts index e2eef02a39587..994aab75e5a77 100644 --- a/packages/models/src/models/OAuthAuthCodes.ts +++ b/packages/models/src/models/OAuthAuthCodes.ts @@ -1,6 +1,6 @@ import type { IOAuthAuthCode, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IOAuthAuthCodesModel } from '@rocket.chat/model-typings'; -import type { Db, Collection, DeleteResult, FindOptions, IndexDescription } from 'mongodb'; +import type { IOAuthAuthCodesModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { Db, Collection, DeleteResult, IndexDescription, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -13,11 +13,14 @@ export class OAuthAuthCodesRaw extends BaseRaw implements IOAuth return [{ key: { authCode: 1 } }, { key: { userId: 1 } }, { key: { expires: 1 }, expireAfterSeconds: 60 * 5 }]; } - findOneByAuthCode(authCode: string, options?: FindOptions): Promise { + findOneByAuthCode = FindOptionsWithProjection>( + authCode: string, + options?: O, + ): Promise | null> { if (typeof authCode !== 'string' || !authCode) { return Promise.resolve(null); } - return this.findOne({ authCode }, options); + return this.findOne({ authCode }, options); } async deleteByUserId(userId: string): Promise { diff --git a/packages/models/src/models/OAuthRefreshTokens.ts b/packages/models/src/models/OAuthRefreshTokens.ts index 7b48c0ddec7b1..b13bcb7507d16 100644 --- a/packages/models/src/models/OAuthRefreshTokens.ts +++ b/packages/models/src/models/OAuthRefreshTokens.ts @@ -1,6 +1,6 @@ import type { IOAuthRefreshToken, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IOAuthRefreshTokensModel } from '@rocket.chat/model-typings'; -import type { Db, Collection, DeleteResult, FindOptions, IndexDescription } from 'mongodb'; +import type { IOAuthRefreshTokensModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { Db, Collection, DeleteResult, IndexDescription, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -13,11 +13,14 @@ export class OAuthRefreshTokensRaw extends BaseRaw implement return [{ key: { refreshToken: 1 } }, { key: { userId: 1 } }, { key: { expires: 1 }, expireAfterSeconds: 60 * 60 * 24 * 30 }]; } - findOneByRefreshToken(refreshToken: string, options?: FindOptions): Promise { + findOneByRefreshToken = FindOptionsWithProjection>( + refreshToken: string, + options?: O, + ): Promise | null> { if (typeof refreshToken !== 'string' || !refreshToken) { return Promise.resolve(null); } - return this.findOne({ refreshToken }, options); + return this.findOne({ refreshToken }, options); } async deleteByUserId(userId: string): Promise { diff --git a/packages/models/src/models/PushToken.ts b/packages/models/src/models/PushToken.ts index 22c11f111855f..8209a2f53e1bf 100644 --- a/packages/models/src/models/PushToken.ts +++ b/packages/models/src/models/PushToken.ts @@ -1,6 +1,6 @@ import type { IPushToken, IUser, AtLeast } from '@rocket.chat/core-typings'; -import type { IPushTokenModel } from '@rocket.chat/model-typings'; -import type { Db, DeleteResult, FindOptions, IndexDescription, InsertOneResult, UpdateResult, FindCursor } from 'mongodb'; +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'; @@ -42,12 +42,18 @@ export class PushTokenRaw extends BaseRaw implements IPushTokenModel return this.countDocuments(query); } - async findFirstByUserId(userId: IUser['_id'], options: FindOptions = {}): Promise { - return this.findOne({ userId }, options); + async findFirstByUserId = FindOptionsWithProjection>( + userId: IUser['_id'], + options?: O, + ): Promise | null> { + return this.findOne({ userId }, options); } - findAllTokensByUserId(userId: IUser['_id'], options?: FindOptions): FindCursor { - return this.find( + findAllTokensByUserId = FindOptionsWithProjection>( + userId: IUser['_id'], + options?: O, + ): FindCursor> { + return this.find( { userId, $or: [{ 'token.apn': { $exists: true } }, { 'token.gcm': { $exists: true } }], @@ -56,12 +62,12 @@ export class PushTokenRaw extends BaseRaw implements IPushTokenModel ); } - findTokensByUserIdExceptId( + findTokensByUserIdExceptId = FindOptionsWithProjection>( userId: IUser['_id'], idToIgnore: IPushToken['_id'], - options?: FindOptions, - ): FindCursor { - return this.find( + options?: O, + ): FindCursor> { + return this.find( { _id: { $ne: idToIgnore }, userId, diff --git a/packages/models/src/models/Roles.ts b/packages/models/src/models/Roles.ts index 0e74093f30eab..9717f096d03e5 100644 --- a/packages/models/src/models/Roles.ts +++ b/packages/models/src/models/Roles.ts @@ -1,5 +1,5 @@ import type { IRole, IRoom, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IRolesModel } from '@rocket.chat/model-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'; @@ -10,12 +10,15 @@ export class RolesRaw extends BaseRaw implements IRolesModel { super(db, 'roles', trash); } - findByUpdatedDate(updatedAfterDate: Date, options?: FindOptions): FindCursor { + findByUpdatedDate = FindOptionsWithProjection>( + updatedAfterDate: Date, + options?: O, + ): FindCursor> { const query = { _updatedAt: { $gte: new Date(updatedAfterDate) }, }; - return options ? this.find(query, options) : this.find(query); + return options ? this.find(query, options) : this.find(query); } async isUserInRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: IRoom['_id']): Promise { @@ -46,16 +49,10 @@ export class RolesRaw extends BaseRaw implements IRolesModel { return false; } - async findOneByIdOrName(_idOrName: IRole['_id'], options?: undefined): Promise; - - async findOneByIdOrName(_idOrName: IRole['_id'], options: FindOptions): Promise; - - async findOneByIdOrName

( + findOneByIdOrName

= FindOptionsWithProjection

>( _idOrName: IRole['_id'], - options: FindOptions

, - ): Promise

; - - findOneByIdOrName

(_idOrName: IRole['_id'], options?: any): Promise { + options?: O, + ): Promise | null> { const query: Filter = { $or: [ { @@ -67,7 +64,7 @@ export class RolesRaw extends BaseRaw implements IRolesModel { ], }; - return this.findOne(query, options); + return this.findOne(query, options); } async findOneByName

(name: IRole['name'], options?: any): Promise { @@ -75,7 +72,7 @@ export class RolesRaw extends BaseRaw implements IRolesModel { name, }; - return this.findOne(query, options); + return this.findOne(query, options); } findInIds

(ids: IRole['_id'][], options?: FindOptions): P extends Pick ? FindCursor

: FindCursor { @@ -110,12 +107,15 @@ export class RolesRaw extends BaseRaw implements IRolesModel { return this.find(query, options || {}) as P extends Pick ? FindCursor

: FindCursor; } - findByScope(scope: IRole['scope'], options?: FindOptions): FindCursor { + findByScope = FindOptionsWithProjection>( + scope: IRole['scope'], + options?: O, + ): FindCursor> { const query = { scope, }; - return this.find(query, options || {}); + return this.find(query, options); } countByScope(scope: IRole['scope'], options?: CountDocumentsOptions): Promise { @@ -126,12 +126,14 @@ export class RolesRaw extends BaseRaw implements IRolesModel { return this.countDocuments(query, options); } - findCustomRoles(options?: FindOptions): FindCursor { + findCustomRoles = FindOptionsWithProjection>( + options?: O, + ): FindCursor> { const query: Filter = { protected: false, }; - return this.find(query, options || {}); + return this.find(query, options); } countCustomRoles(options?: CountDocumentsOptions): Promise { diff --git a/packages/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index bfb103484c489..68584ba65a210 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -9,7 +9,7 @@ import type { IUser, RocketChatRecordDeleted, } from '@rocket.chat/core-typings'; -import type { FindPaginated, IRoomsModel } from '@rocket.chat/model-typings'; +import type { FindPaginated, IRoomsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/tools'; import type { AggregationCursor, @@ -137,26 +137,36 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return !!room; } - findOneByRoomIdAndUserId(rid: IRoom['_id'], uid: IUser['_id'], options: FindOptions = {}): Promise { + findOneByRoomIdAndUserId = FindOptionsWithProjection>( + rid: IRoom['_id'], + uid: IUser['_id'], + options?: O, + ): Promise | null> { const query: Filter = { '_id': rid, 'u._id': uid, }; - return this.findOne(query, options); + return this.findOne(query, options); } - findManyByRoomIds(roomIds: Array, options: FindOptions = {}): FindCursor { + findManyByRoomIds = FindOptionsWithProjection>( + roomIds: Array, + options?: O, + ): FindCursor> { const query: Filter = { _id: { $in: roomIds, }, }; - return this.find(query, options); + return this.find(query, options); } - findManyArchivedByRoomIds(roomIds: Array, options: FindOptions = {}): FindCursor { + findManyArchivedByRoomIds = FindOptionsWithProjection>( + roomIds: Array, + options?: O, + ): FindCursor> { const query: Filter = { _id: { $in: roomIds, @@ -164,7 +174,7 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { archived: true, }; - return this.find(query, options); + return this.find(query, options); } findPaginatedByIds( @@ -209,13 +219,13 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return statistic; } - findByNameOrFnameContainingAndTypes( + findByNameOrFnameContainingAndTypes = FindOptionsWithProjection>( name: NonNullable, types: Array, discussion = false, teams = false, - options: FindOptions = {}, - ): FindPaginated> { + options?: O, + ): FindPaginated>> { const nameRegex = new RegExp(escapeRegExp(name).trim(), 'i'); const nameCondition: Filter = { @@ -250,10 +260,13 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ...(!teams ? { teamMain: { $exists: false } } : {}), }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findPrivateRoomsAndTeamsPaginated(name: NonNullable, options: FindOptions = {}): FindPaginated> { + findPrivateRoomsAndTeamsPaginated = FindOptionsWithProjection>( + name: NonNullable, + options?: O, + ): FindPaginated>> { const nameRegex = new RegExp(escapeRegExp(name).trim(), 'i'); const nameCondition: Filter = { @@ -270,10 +283,13 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { prid: { $exists: false }, }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findByTeamId(teamId: ITeam['_id'], options: FindOptions = {}): FindCursor { + findByTeamId = FindOptionsWithProjection>( + teamId: ITeam['_id'], + options?: O, + ): FindCursor> { const query: Filter = { teamId, teamMain: { @@ -281,7 +297,7 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { }, }; - return this.find(query, options); + return this.find(query, options); } countByTeamId(teamId: ITeam['_id']): Promise { @@ -295,13 +311,16 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.countDocuments(query); } - findPaginatedByTeamIdContainingNameAndDefault( + findPaginatedByTeamIdContainingNameAndDefault< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( teamId: ITeam['_id'], name: IRoom['name'], teamDefault: boolean, ids: Array | undefined, - options: FindOptions = {}, - ): FindPaginated> { + options?: O, + ): FindPaginated>> { const query: Filter = { teamId, teamMain: { @@ -312,10 +331,14 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ...(ids ? { $or: [{ t: 'c' }, { _id: { $in: ids } }] } : {}), }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findByTeamIdAndRoomsId(teamId: ITeam['_id'], rids: Array, options: FindOptions = {}): FindCursor { + findByTeamIdAndRoomsId = FindOptionsWithProjection>( + teamId: ITeam['_id'], + rids: Array, + options?: O, + ): FindCursor> { const query: Filter = { teamId, _id: { @@ -323,10 +346,13 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { }, }; - return this.find(query, options); + return this.find(query, options); } - findRoomsByNameOrFnameStarting(name: NonNullable, options: FindOptions = {}): FindCursor { + findRoomsByNameOrFnameStarting = FindOptionsWithProjection>( + name: NonNullable, + options?: O, + ): FindCursor> { const nameRegex = new RegExp(`^${escapeRegExp(name).trim()}`, 'i'); const query: Filter = { @@ -343,14 +369,14 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ], }; - return this.find(query, options); + return this.find(query, options); } - findRoomsWithoutDiscussionsByRoomIds( + findRoomsWithoutDiscussionsByRoomIds = FindOptionsWithProjection>( name: NonNullable, roomIds: Array, - options: FindOptions = {}, - ): FindCursor { + options?: O, + ): FindCursor> { const nameRegex = new RegExp(`^${escapeRegExp(name).trim()}`, 'i'); const query: Filter = { @@ -379,14 +405,13 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { prid: { $exists: false }, }; - return this.find(query, options); + return this.find(query, options); } - findPaginatedRoomsWithoutDiscussionsByRoomIds( - name: NonNullable, - roomIds: Array, - options: FindOptions = {}, - ): FindPaginated> { + findPaginatedRoomsWithoutDiscussionsByRoomIds< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(name: NonNullable, roomIds: Array, options?: O): FindPaginated>> { const nameRegex = new RegExp(`^${escapeRegExp(name).trim()}`, 'i'); const query: Filter = { @@ -416,14 +441,13 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { $and: [{ $or: [{ federated: { $exists: false } }, { federated: false }] }], }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findChannelAndGroupListWithoutTeamsByNameStartingByOwner( - name: IRoom['name'], - groupsToAccept: Array, - options: FindOptions = {}, - ): FindCursor { + findChannelAndGroupListWithoutTeamsByNameStartingByOwner< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(name: IRoom['name'], groupsToAccept: Array, options?: O): FindCursor> { const nameRegex = name && new RegExp(`^${escapeRegExp(name).trim()}`, 'i'); const query: Filter = { @@ -439,7 +463,7 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ...(name && { name: nameRegex }), $and: [{ $or: [{ federated: { $exists: false } }, { federated: false }] }], }; - return this.find(query, options); + return this.find(query, options); } unsetTeamId(teamId: ITeam['_id'], options: UpdateOptions = {}): Promise { @@ -475,7 +499,10 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.updateOne({ _id: rid }, { $set: { teamDefault } }, options); } - findOneByNameOrFname(name: NonNullable, options: FindOptions = {}): Promise { + findOneByNameOrFname = FindOptionsWithProjection>( + name: NonNullable, + options?: O, + ): Promise | null> { const query = { $or: [ { @@ -487,16 +514,20 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ], }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneByJoinCodeAndId(joinCode: string, rid: IRoom['_id'], options: FindOptions = {}): Promise { + findOneByJoinCodeAndId = FindOptionsWithProjection>( + joinCode: string, + rid: IRoom['_id'], + options?: O, + ): Promise | null> { const query: Filter = { _id: rid, joinCode, }; - return this.findOne(query, options); + return this.findOne(query, options); } async findOneByNonValidatedName(name: NonNullable, options: FindOptions = {}) { @@ -591,8 +622,11 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ); } - findE2ERoomById(roomId: IRoom['_id'], options: FindOptions = {}): Promise { - return this.findOne( + findE2ERoomById = FindOptionsWithProjection>( + roomId: IRoom['_id'], + options?: O, + ): Promise | null> { + return this.findOne( { _id: roomId, encrypted: true, @@ -613,12 +647,15 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.countDocuments({ t }); } - findPaginatedByNameOrFNameAndRoomIdsIncludingTeamRooms( + findPaginatedByNameOrFNameAndRoomIdsIncludingTeamRooms< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( searchTerm: RegExp | null, teamIds: Array, roomIds: Array, - options: FindOptions = {}, - ): FindPaginated> { + options?: O, + ): FindPaginated>> { const query: Filter = { $and: [ { teamMain: { $exists: false } }, @@ -661,14 +698,13 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ], }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findPaginatedContainingNameOrFNameInIdsAsTeamMain( - searchTerm: RegExp | null, - rids: Array, - options: FindOptions = {}, - ): FindPaginated> { + findPaginatedContainingNameOrFNameInIdsAsTeamMain< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(searchTerm: RegExp | null, rids: Array, options?: O): FindPaginated>> { const query: Filter = { teamMain: true, $and: [ @@ -701,14 +737,14 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { }); } - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findPaginatedByTypeAndIds( + findPaginatedByTypeAndIds = FindOptionsWithProjection>( type: IRoom['t'], ids: Array, - options: FindOptions = {}, - ): FindPaginated> { + options?: O, + ): FindPaginated>> { const query: Filter = { t: type, _id: { @@ -716,40 +752,49 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { }, }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findOneDirectRoomContainingAllUserIDs(uid: IDirectMessageRoom['uids'], options: FindOptions = {}): Promise { + findOneDirectRoomContainingAllUserIDs = FindOptionsWithProjection>( + uid: IDirectMessageRoom['uids'], + options?: O, + ): Promise | null> { const query: Filter = { t: 'd', uids: { $size: uid.length, $all: uid }, }; - return this.findOne(query, { + return this.findOne(query, { ...options, sort: { federated: 1, ts: 1, }, - }); + } as unknown as O); } - findFederatedByIds(ids: Array, options: FindOptions = {}): FindCursor { + findFederatedByIds = FindOptionsWithProjection>( + ids: Array, + options?: O, + ): FindCursor> { const query = { _id: { $in: ids }, federated: true, }; - return this.find(query, options); + return this.find(query, options); } - findOneFederatedByMrid(mrid: string, options: FindOptions = {}): Promise { + findOneFederatedByMrid = FindOptionsWithProjection>( + mrid: string, + options?: O, + ): Promise | null> { const query: Filter = { 'federated': true, 'federation.mrid': mrid, }; - return this.findOne(query, options); + return this.findOne(query, options); } async findBiggestFederatedRoomInNumberOfUsers(options?: FindOptions): Promise { @@ -819,7 +864,10 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.incMsgCountById(_id, -count); } - findOneByIdOrName(_idOrName: IRoom['_id'] | IRoom['name'], options: FindOptions = {}): Promise { + findOneByIdOrName = FindOptionsWithProjection>( + _idOrName: IRoom['_id'] | IRoom['name'], + options?: O, + ): Promise | null> { const query: Filter = { $or: [ { @@ -831,11 +879,15 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ], }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options: FindOptions = {}): Promise { - return this.findOne({ _id: roomId, t: type }, options); + findOneByIdAndType = FindOptionsWithProjection>( + roomId: IRoom['_id'], + type: IRoom['t'], + options?: O, + ): Promise | null> { + return this.findOne({ _id: roomId, t: type }, options); } setReactionsInLastMessage(roomId: IRoom['_id'], reactions: IMessage['reactions']): Promise { @@ -965,8 +1017,11 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.updateMany(query, update); } - getDirectConversationsByUserId(_id: IRoom['_id'], options: FindOptions = {}): FindCursor { - return this.find({ t: 'd', uids: { $size: 2, $in: [_id] } }, options); + getDirectConversationsByUserId = FindOptionsWithProjection>( + _id: IRoom['_id'], + options?: O, + ): FindCursor> { + return this.find({ t: 'd', uids: { $size: 2, $in: [_id] } }, options); } // 2 @@ -1043,10 +1098,13 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.findOneAndUpdate(query, update, { returnDocument: 'after', ...options }); } - findOneByImportId(_id: IRoom['_id'], options: FindOptions = {}): Promise { + findOneByImportId = FindOptionsWithProjection>( + _id: IRoom['_id'], + options?: O, + ): Promise | null> { const query: Filter = { importIds: _id }; - return this.findOne(query, options); + return this.findOne(query, options); } findOneByNameAndNotId(name: NonNullable, rid: IRoom['_id']): Promise { @@ -1058,18 +1116,21 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.findOne(query); } - findOneByDisplayName(fname: IRoom['fname'], options: FindOptions = {}): Promise { + findOneByDisplayName = FindOptionsWithProjection>( + fname: IRoom['fname'], + options?: O, + ): Promise | null> { const query: Filter = { fname }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneByNameAndType( + findOneByNameAndType = FindOptionsWithProjection>( name: NonNullable, type: IRoom['t'], - options: FindOptions = {}, + options?: O, includeFederatedRooms = false, - ): Promise { + ): Promise | null> { const query: Filter = { t: type, teamId: { @@ -1080,25 +1141,37 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { : { $or: [{ federated: { $exists: false } }, { federated: false }], name }), }; - return this.findOne(query, options); + return this.findOne(query, options); } - // FIND - findById(roomId: IRoom['_id'], options: FindOptions = {}): Promise { - return this.findOne({ _id: roomId }, options); + findById = FindOptionsWithProjection>( + roomId: IRoom['_id'], + options?: O, + ): Promise | null> { + return this.findOne({ _id: roomId }, options); } - findByIds(roomIds: Array, options: FindOptions = {}): FindCursor { - return this.find({ _id: { $in: roomIds } }, options); + findByIds = FindOptionsWithProjection>( + roomIds: Array, + options?: O, + ): FindCursor> { + return this.find({ _id: { $in: roomIds } }, options); } - findByType(type: IRoom['t'], options: FindOptions = {}): FindCursor { + findByType = FindOptionsWithProjection>( + type: IRoom['t'], + options?: O, + ): FindCursor> { const query: Filter = { t: type }; - return this.find(query, options); + return this.find(query, options); } - findByTypeInIds(type: IRoom['t'], ids: Array, options: FindOptions = {}): FindCursor { + findByTypeInIds = FindOptionsWithProjection>( + type: IRoom['t'], + ids: Array, + options?: O, + ): FindCursor> { const query: Filter = { _id: { $in: ids, @@ -1106,29 +1179,37 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { t: type, }; - return this.find(query, options); + return this.find(query, options); } - findPrivateRoomsByIdsWithAbacAttributes(ids: Array, options: FindOptions = {}): FindCursor { + findPrivateRoomsByIdsWithAbacAttributes< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(ids: Array, options?: O): FindCursor> { const query: Filter = { _id: { $in: ids }, t: 'p', abacAttributes: { $exists: true, $ne: [] }, }; - return this.find(query, options); + return this.find(query, options); } - findAllPrivateRoomsWithAbacAttributes(options: FindOptions = {}): FindCursor { + findAllPrivateRoomsWithAbacAttributes = FindOptionsWithProjection>( + options?: O, + ): FindCursor> { const query: Filter = { t: 'p', abacAttributes: { $exists: true, $ne: [] }, }; - return this.find(query, options); + return this.find(query, options); } - async findBySubscriptionUserId(userId: IUser['_id'], options: FindOptions = {}): Promise> { + async findBySubscriptionUserId = FindOptionsWithProjection>( + userId: IUser['_id'], + options?: O, + ): Promise>> { const data = (await Subscriptions.findByUserId(userId, { projection: { rid: 1 } }).toArray()).map((item) => item.rid); const query: Filter = { @@ -1152,14 +1233,13 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ], }; - return this.find(query, options); + return this.find(query, options); } - async findBySubscriptionUserIdUpdatedAfter( - userId: IUser['_id'], - _updatedAt: IRoom['_updatedAt'], - options: FindOptions = {}, - ): Promise> { + async findBySubscriptionUserIdUpdatedAfter< + T extends Document = IRoom, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(userId: IUser['_id'], _updatedAt: IRoom['_updatedAt'], options?: O): Promise>> { const ids = (await Subscriptions.findByUserId(userId, { projection: { rid: 1 } }).toArray()).map((item) => item.rid); const query: Filter = { @@ -1186,15 +1266,15 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ], }; - return this.find(query, options); + return this.find(query, options); } - findByNameAndTypeNotDefault( + findByNameAndTypeNotDefault = FindOptionsWithProjection>( name: IRoom['name'] | RegExp, type: IRoom['t'], - options: FindOptions = {}, + options?: O, includeFederatedRooms = false, - ): FindCursor { + ): FindCursor> { const query: Filter = { t: type, default: { @@ -1222,17 +1302,16 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { }; // do not use cache - return this.find(query, options); + return this.find(query, options); } - // 3 - findByNameOrFNameAndTypesNotInIds( + findByNameOrFNameAndTypesNotInIds = FindOptionsWithProjection>( name: IRoom['name'] | RegExp, types: Array, ids: Array, - options: FindOptions = {}, + options?: O, includeFederatedRooms = false, - ): FindCursor { + ): FindCursor> { const nameCondition: Filter = { $or: [{ name }, { fname: name }], }; @@ -1279,10 +1358,14 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { }; // do not use cache - return this.find(query, options); + return this.find(query, options); } - findByDefaultAndTypes(defaultValue: boolean, types: Array, options: FindOptions = {}): FindCursor { + findByDefaultAndTypes = FindOptionsWithProjection>( + defaultValue: boolean, + types: Array, + options?: O, + ): FindCursor> { const query: Filter = { t: { $in: types, @@ -1290,36 +1373,40 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ...(defaultValue ? { default: true } : { default: { $ne: true } }), }; - return this.find(query, options); + return this.find(query, options); } - findDirectRoomContainingAllUsernames( + findDirectRoomContainingAllUsernames = FindOptionsWithProjection>( usernames: NonNullable, - options: FindOptions = {}, - ): Promise { + options?: O, + ): Promise | null> { const query: Filter = { t: 'd', usernames: { $size: usernames.length, $all: usernames }, usersCount: usernames.length, }; - return this.findOne(query, options); + return this.findOne(query, options); } - findByTypeAndNameOrId( + findByTypeAndNameOrId = FindOptionsWithProjection>( type: IRoom['t'], identifier: NonNullable, - options: FindOptions = {}, - ): Promise { + options?: O, + ): Promise | null> { const query: Filter = { t: type, $or: [{ name: identifier }, { _id: identifier }], }; - return this.findOne(query, options); + return this.findOne(query, options); } - findByTypeAndNameContaining(type: IRoom['t'], name: NonNullable, options: FindOptions = {}): FindCursor { + findByTypeAndNameContaining = FindOptionsWithProjection>( + type: IRoom['t'], + name: NonNullable, + options?: O, + ): FindCursor> { const nameRegex = new RegExp(escapeRegExp(name).trim(), 'i'); const query: Filter = { @@ -1327,15 +1414,15 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { t: type, }; - return this.find(query, options); + return this.find(query, options); } - findByTypeInIdsAndNameContaining( + findByTypeInIdsAndNameContaining = FindOptionsWithProjection>( type: IRoom['t'], ids: Array, name: NonNullable, - options: FindOptions = {}, - ): FindCursor { + options?: O, + ): FindCursor> { const nameRegex = new RegExp(escapeRegExp(name).trim(), 'i'); const query: Filter = { @@ -1346,11 +1433,14 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { t: type, }; - return this.find(query, options); + return this.find(query, options); } - findGroupDMsByUids(uids: NonNullable, options: FindOptions = {}): FindCursor { - return this.find( + findGroupDMsByUids = FindOptionsWithProjection>( + uids: NonNullable, + options?: O, + ): FindCursor> { + return this.find( { usersCount: { $gt: 2 }, uids: { $in: uids }, @@ -1366,8 +1456,11 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { }); } - find1On1ByUserId(userId: IRoom['_id'], options: FindOptions = {}): FindCursor { - return this.find( + find1On1ByUserId = FindOptionsWithProjection>( + userId: IRoom['_id'], + options?: O, + ): FindCursor> { + return this.find( { uids: userId, usersCount: 2, @@ -1812,7 +1905,11 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.updateOne(query, update); } - insertAbacAttributeIfNotExistsById(_id: IRoom['_id'], key: string, values: string[]): Promise { + insertAbacAttributeIfNotExistsById( + _id: IRoom['_id'], + key: string, + values: string[], + ): Promise | null> { return this.findOneAndUpdate( { _id, 'abacAttributes.key': { $ne: key } }, { $push: { abacAttributes: { key, values } } }, @@ -2101,14 +2198,14 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { ]); } - findAllByTypesAndDiscussionAndTeam( + findAllByTypesAndDiscussionAndTeam = FindOptionsWithProjection>( filters: { types?: Array; discussions?: boolean; teams?: boolean; } = {}, - options: FindOptions = {}, - ): FindCursor { + options?: O, + ): FindCursor> { const { types, discussions, teams } = filters; const query: Filter = {}; @@ -2125,7 +2222,7 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { query.teamMain = { $exists: teams }; } - return this.find(query, options); + return this.find(query, options); } resetRoomKeyAndSetE2EEQueueByRoomId( diff --git a/packages/models/src/models/ServerEvents.ts b/packages/models/src/models/ServerEvents.ts index 9ec4358dc5b4f..265da0498f039 100644 --- a/packages/models/src/models/ServerEvents.ts +++ b/packages/models/src/models/ServerEvents.ts @@ -89,7 +89,7 @@ export class ServerEventsRaw extends BaseRaw implements IServerEve t: key, ts: new Date(), actor, - data: Object.entries(data).map(([key, value]) => ({ key, value })) as E['data'], + 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 } }), diff --git a/packages/models/src/models/Sessions.ts b/packages/models/src/models/Sessions.ts index 79fe10ce2a409..27f39eb483837 100644 --- a/packages/models/src/models/Sessions.ts +++ b/packages/models/src/models/Sessions.ts @@ -11,7 +11,7 @@ import type { IUser, RocketChatRecordDeleted, } from '@rocket.chat/core-typings'; -import type { ISessionsModel } from '@rocket.chat/model-typings'; +import type { ISessionsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import type { PaginatedResult, WithItemCount } from '@rocket.chat/rest-typings'; import type { AggregationCursor, @@ -25,7 +25,6 @@ import type { IndexDescription, UpdateResult, OptionalId, - FindOptions, } from 'mongodb'; import { getCollectionName } from '../index'; @@ -1572,11 +1571,10 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { ); } - async getLoggedInByUserIdAndSessionId( - userId: string, - sessionId: string, - options?: FindOptions, - ): Promise { - return this.findOne({ userId, sessionId, logoutAt: { $exists: false } }, options); + async getLoggedInByUserIdAndSessionId< + T extends Document = ISession, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(userId: string, sessionId: string, options?: O): Promise | null> { + return this.findOne({ userId, sessionId, logoutAt: { $exists: false } }, options); } } diff --git a/packages/models/src/models/Settings.ts b/packages/models/src/models/Settings.ts index b8c25115f6f98..d7b0ae873fa7e 100644 --- a/packages/models/src/models/Settings.ts +++ b/packages/models/src/models/Settings.ts @@ -1,5 +1,5 @@ import type { ISetting, ISettingColor, ISettingSelectOption, RocketChatRecordDeleted, SettingValue } from '@rocket.chat/core-typings'; -import type { ISettingsModel } from '@rocket.chat/model-typings'; +import type { ISettingsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import type { Collection, FindCursor, @@ -8,7 +8,6 @@ import type { UpdateFilter, UpdateResult, Document, - FindOptions, FindOneAndUpdateOptions, WithId, UpdateOptions, @@ -16,6 +15,10 @@ import type { import { BaseRaw } from './BaseRaw'; +type PublicSettingFields = T extends ISettingColor + ? Pick + : Pick; + export class SettingsRaw extends BaseRaw implements ISettingsModel { constructor(db: Db, trash?: Collection>) { super(db, 'settings', trash); @@ -48,7 +51,10 @@ export class SettingsRaw extends BaseRaw implements ISettingsModel { return this.findOne(query); } - findByIds(_id: string[] | string = [], options?: FindOptions): FindCursor { + findByIds = FindOptionsWithProjection>( + _id: string[] | string = [], + options?: O, + ): FindCursor> { if (typeof _id === 'string') { _id = [_id]; } @@ -59,7 +65,7 @@ export class SettingsRaw extends BaseRaw implements ISettingsModel { }, }; - return this.find(query, options); + return this.find(query, options); } updateValueById( @@ -174,13 +180,7 @@ export class SettingsRaw extends BaseRaw implements ISettingsModel { return this.updateOne(query, update); } - findNotHiddenPublic( - ids: ISetting['_id'][] = [], - ): FindCursor< - T extends ISettingColor - ? Pick - : Pick - > { + findNotHiddenPublic(ids: ISetting['_id'][] = []): FindCursor> { const filter: Filter = { hidden: { $ne: true }, public: true, @@ -190,6 +190,8 @@ export class SettingsRaw extends BaseRaw implements ISettingsModel { 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, @@ -200,7 +202,7 @@ export class SettingsRaw extends BaseRaw implements ISettingsModel { modules: 1, requiredOnWizard: 1, }, - }); + }) as unknown as FindCursor>; } findSetupWizardSettings(): FindCursor { diff --git a/packages/models/src/models/Subscriptions.ts b/packages/models/src/models/Subscriptions.ts index 2ad8856e6a4bc..d015c6049f908 100644 --- a/packages/models/src/models/Subscriptions.ts +++ b/packages/models/src/models/Subscriptions.ts @@ -1,5 +1,5 @@ import type { AtLeast, IRole, IRoom, ISubscription, IUser, RocketChatRecordDeleted, SpotlightUser } from '@rocket.chat/core-typings'; -import type { ISubscriptionsModel } from '@rocket.chat/model-typings'; +import type { ISubscriptionsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/tools'; import { compact } from 'lodash'; import type { @@ -77,16 +77,24 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return result?.total || 0; } - findOneByRoomIdAndUserId(rid: string, uid: string, options: FindOptions = {}): Promise { + findOneByRoomIdAndUserId = FindOptionsWithProjection>( + rid: string, + uid: string, + options?: O, + ): Promise | null> { const query = { rid, 'u._id': uid, }; - return this.findOne(query, options); + return this.findOne(query, options); } - findByUserIdAndRoomIds(userId: string, roomIds: Array, options: FindOptions = {}): FindCursor { + findByUserIdAndRoomIds = FindOptionsWithProjection>( + userId: string, + roomIds: Array, + options?: O, + ): FindCursor> { const query = { 'u._id': userId, 'rid': { @@ -94,28 +102,38 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri }, }; - return this.find(query, options); + return this.find(query, options); } - findByRoomId(roomId: string, options: FindOptions = {}): FindCursor { + findByRoomId = FindOptionsWithProjection>( + roomId: string, + options?: O, + ): FindCursor> { const query = { rid: roomId, }; - return this.find(query, options); + return this.find(query, options); } - findUnarchivedByRoomId(roomId: string, options: FindOptions = {}): FindCursor { + findUnarchivedByRoomId = FindOptionsWithProjection>( + roomId: string, + options?: O, + ): FindCursor> { const query = { 'rid': roomId, 'archived': { $ne: true }, 'u._id': { $exists: true }, }; - return this.find(query, options); + return this.find(query, options); } - findByRoomIdAndNotUserId(roomId: string, userId: string, options: FindOptions = {}): FindCursor { + findByRoomIdAndNotUserId = FindOptionsWithProjection>( + roomId: string, + userId: string, + options?: O, + ): FindCursor> { const query = { 'rid': roomId, 'u._id': { @@ -123,7 +141,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri }, }; - return this.find(query, options); + return this.find(query, options); } countByRoomIdAndUserId(rid: string, uid: string | undefined, includeInvitations = false): Promise { @@ -218,32 +236,23 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateOne(query, update); } - findUsersInRoles(roles: IRole['_id'][], rid: string | undefined): Promise>; - - findUsersInRoles(roles: IRole['_id'][], rid: string | undefined, options: FindOptions): Promise>; - - findUsersInRoles

( - roles: IRole['_id'][], - rid: string | undefined, - options: FindOptions

, - ): Promise>; - - async findUsersInRoles

( + async findUsersInRoles

= FindOptionsWithProjection

>( roles: IRole['_id'][], rid: IRoom['_id'] | undefined, - options?: FindOptions

, - ): Promise> { + options?: O, + ): Promise>> { const query = { roles: { $in: roles }, ...(rid && { rid }), }; + // this projection is internal to the lookup below, so it must not be typed against the caller's `O` const subscriptions = await this.find(query, { projection: { 'u._id': 1 } }).toArray(); const users = compact(subscriptions.map((subscription) => subscription.u?._id).filter(Boolean)); // TODO remove dependency to other models - this logic should be inside a function/service - return Users.find

({ _id: { $in: users } }, options || {}); + return Users.find({ _id: { $in: users } }, options); } async countUsersInRoles(roles: IRole['_id'][], rid: IRoom['_id'] | undefined): Promise { @@ -294,17 +303,24 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return !!found; } - findByRolesAndRoomId({ roles, rid }: { roles: string; rid?: string }, options?: FindOptions): FindCursor { - return this.find( + findByRolesAndRoomId = FindOptionsWithProjection>( + { roles, rid }: { roles: string; rid?: string }, + options?: O, + ): FindCursor> { + return this.find( { roles, ...(rid && { rid }), }, - options || {}, + options, ); } - findByUserIdAndTypes(userId: string, types: ISubscription['t'][], options?: FindOptions): FindCursor { + findByUserIdAndTypes = FindOptionsWithProjection>( + userId: string, + types: ISubscription['t'][], + options?: O, + ): FindCursor> { const query = { 'u._id': userId, 't': { @@ -312,19 +328,25 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri }, }; - return this.find(query, options || {}); + return this.find(query, options); } - findOpenByVisitorIds(visitorIds: string[], options?: FindOptions): FindCursor { + findOpenByVisitorIds = FindOptionsWithProjection>( + visitorIds: string[], + options?: O, + ): FindCursor> { const query = { 'open': true, 'v._id': { $in: visitorIds }, }; - return this.find(query, options || {}); + return this.find(query, options); } - findByRoomIdAndNotAlertOrOpenExcludingUserIds( + findByRoomIdAndNotAlertOrOpenExcludingUserIds< + T extends Document = ISubscription, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( { roomId, uidsExclude, @@ -336,8 +358,8 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri uidsInclude?: ISubscription['u']['_id'][]; onlyRead: boolean; }, - options?: FindOptions, - ) { + options?: O, + ): FindCursor> { const query = { rid: roomId, ...(uidsExclude?.length && { @@ -348,7 +370,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri }), }; - return this.find(query, options || {}); + return this.find(query, options); } async removeByRoomId( @@ -667,17 +689,17 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany({ 'u._id': userId, 'autoTranslate': true }, { $unset: { autoTranslate: 1, autoTranslateLanguage: 1 } }); } - findByAutoTranslateAndUserId( + findByAutoTranslateAndUserId = FindOptionsWithProjection>( userId: ISubscription['u']['_id'], autoTranslate: ISubscription['autoTranslate'] = true, - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query = { 'u._id': userId, autoTranslate, }; - return this.find(query, options); + return this.find(query, options); } disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise { @@ -711,7 +733,10 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.col.distinct('autoTranslateLanguage', query); } - findByRidWithoutE2EKey(rid: string, options: FindOptions): FindCursor { + findByRidWithoutE2EKey = FindOptionsWithProjection>( + rid: string, + options?: O, + ): FindCursor> { const query = { rid, E2EKey: { @@ -719,7 +744,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri }, }; - return this.find(query, options); + return this.find(query, options); } findUsersWithPublicE2EKeyByRids( @@ -934,7 +959,10 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri ); } - findByUserIdWithoutE2E(userId: string, options?: FindOptions): FindCursor { + findByUserIdWithoutE2E = FindOptionsWithProjection>( + userId: string, + options?: O, + ): FindCursor> { const query = { 'u._id': userId, 'E2EKey': { @@ -942,45 +970,56 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri }, }; - return this.find(query, options); + return this.find(query, options); } - findOneByRoomIdAndUsername(roomId: string, username: string, options: FindOptions): Promise { + findOneByRoomIdAndUsername = FindOptionsWithProjection>( + roomId: string, + username: string, + options?: O, + ): Promise | null> { const query = { 'rid': roomId, 'u.username': username, }; - return this.findOne(query, options); + return this.findOne(query, options); } // FIND - findByUserId(userId: string, options?: FindOptions): FindCursor { + findByUserId = FindOptionsWithProjection>( + userId: string, + options?: O, + ): FindCursor> { const query: Filter = { 'u._id': userId, 'status': { $ne: 'BANNED' as const } }; - return this.find(query, options); + return this.find(query, options); } - findByUserIdExceptType( + findByUserIdExceptType = FindOptionsWithProjection>( userId: string, typeException: ISubscription['t'], - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query: Filter = { 'u._id': userId, 't': { $ne: typeException }, }; - return this.find(query, options); + return this.find(query, options); } - findByUserIdAndType(userId: string, type: ISubscription['t'], options?: FindOptions): FindCursor { + findByUserIdAndType = FindOptionsWithProjection>( + userId: string, + type: ISubscription['t'], + options?: O, + ): FindCursor> { const query: Filter = { 'u._id': userId, 't': type, }; - return this.find(query, options); + return this.find(query, options); } /** @@ -988,33 +1027,35 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri * @param {IRole['_id'][]} roles * @param {any} options */ - findByUserIdAndRoles(userId: string, roles: string[], options?: FindOptions): FindCursor { + findByUserIdAndRoles = FindOptionsWithProjection>( + userId: string, + roles: string[], + options?: O, + ): FindCursor> { const query = { 'u._id': userId, 'roles': { $in: roles }, }; - return this.find(query, options); + return this.find(query, options); } /** * @param {string} roomId * @param {IRole['_id'][]} roles the list of roles - * @param {any} options */ - findByRoomIdAndRoles: ISubscriptionsModel['findByRoomIdAndRoles'] = ( + findByRoomIdAndRoles

= FindOptionsWithProjection

>( roomId: string, roles: string[], - options?: FindOptions, - ) => { - const rolesArray = ([] as string[]).concat(roles); + options?: O, + ): FindCursor> { const query = { rid: roomId, - roles: { $in: rolesArray }, + roles: { $in: ([] as string[]).concat(roles) }, }; - return this.find(query, options); - }; + return this.find(query, options); + } countByRoomIdAndRoles(roomId: string, roles: string[]): Promise { roles = ([] as string[]).concat(roles); @@ -1047,32 +1088,42 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.countDocuments(query); } - findByType(types: ISubscription['t'][], options?: FindOptions): FindCursor { + findByType = FindOptionsWithProjection>( + types: ISubscription['t'][], + options?: O, + ): FindCursor> { const query: Filter = { t: { $in: types, }, }; - return this.find(query, options); + return this.find(query, options); } - findByTypeAndUserId(type: ISubscription['t'], userId: string, options?: FindOptions): FindCursor { + findByTypeAndUserId = FindOptionsWithProjection>( + type: ISubscription['t'], + userId: string, + options?: O, + ): FindCursor> { const query: Filter = { 't': type, 'u._id': userId, }; - return this.find(query, options); + return this.find(query, options); } - findByRoomWithUserHighlights(roomId: string, options?: FindOptions): FindCursor { + findByRoomWithUserHighlights = FindOptionsWithProjection>( + roomId: string, + options?: O, + ): FindCursor> { const query = { 'rid': roomId, 'userHighlights.0': { $exists: true }, }; - return this.find(query, options); + return this.find(query, options); } async getLastSeen(options: FindOptions = { projection: { _id: 0, ls: 1 } }): Promise { @@ -1082,11 +1133,11 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return subscription?.ls; } - findByRoomIdAndUserIds( + findByRoomIdAndUserIds = FindOptionsWithProjection>( roomId: ISubscription['rid'], userIds: ISubscription['u']['_id'][], - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query = { 'rid': roomId, 'u._id': { @@ -1094,19 +1145,25 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri }, }; - return this.find(query, options); + return this.find(query, options); } - findByRoomIdWhenUserIdExists(rid: string, options?: FindOptions): FindCursor { + findByRoomIdWhenUserIdExists = FindOptionsWithProjection>( + rid: string, + options?: O, + ): FindCursor> { const query = { rid, 'u._id': { $exists: true } }; - return this.find(query, options); + return this.find(query, options); } - findByRoomIdWhenUsernameExists(rid: string, options?: FindOptions): FindCursor { + findByRoomIdWhenUsernameExists = FindOptionsWithProjection>( + rid: string, + options?: O, + ): FindCursor> { const query = { rid, 'u.username': { $exists: true } }; - return this.find(query, options); + return this.find(query, options); } countByRoomIdWhenUsernameExists(rid: string): Promise { @@ -1115,7 +1172,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.countDocuments(query); } - getMinimumLastSeenByRoomId(rid: string): Promise { + getMinimumLastSeenByRoomId(rid: string): Promise | null> { return this.findOne( { rid, @@ -1147,12 +1204,18 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } - findArchivedByRoomId(roomId: string, options?: FindOptions): FindCursor { - return this.find({ rid: roomId, archived: true }, options); + findArchivedByRoomId = FindOptionsWithProjection>( + roomId: string, + options?: O, + ): FindCursor> { + return this.find({ rid: roomId, archived: true }, options); } - findArchivedByUserId(userId: string, options?: FindOptions): FindCursor { - return this.find({ 'u._id': userId, 'archived': true }, options); + findArchivedByUserId = FindOptionsWithProjection>( + userId: string, + options?: O, + ): FindCursor> { + return this.find({ 'u._id': userId, 'archived': true }, options); } unarchiveByIds(ids: string[]): Promise { @@ -1213,23 +1276,23 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } - findByUserIdAndRoomType( + findByUserIdAndRoomType = FindOptionsWithProjection>( userId: ISubscription['u']['_id'], type: ISubscription['t'], - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query = { 'u._id': userId, 't': type, }; - return this.find(query, options); + return this.find(query, options); } - findByNameAndRoomType( + findByNameAndRoomType = FindOptionsWithProjection>( filter: Partial>, - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { if (!filter.name && !filter.t) { throw new Error('invalid filter'); } @@ -1237,7 +1300,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri ...(filter.name && { name: filter.name }), ...(filter.t && { t: filter.t }), }; - return this.find(query, options); + return this.find(query, options); } setFavoriteByRoomIdAndUserId(roomId: string, userId: string, favorite?: boolean): Promise { @@ -1635,12 +1698,12 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } - findByUserPreferences( + findByUserPreferences = FindOptionsWithProjection>( userId: string, notificationOriginField: keyof ISubscription, notificationOriginValue: 'user' | 'subscription', - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const value = notificationOriginValue === 'user' ? 'user' : { $ne: 'subscription' }; const query: Filter = { @@ -1648,7 +1711,7 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri [notificationOriginField]: value, }; - return this.find(query, options); + return this.find(query, options); } updateUserHighlights(userId: string, userHighlights: any): Promise { @@ -1850,17 +1913,17 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateMany(query, update); } - findUnreadThreadsByRoomId( + findUnreadThreadsByRoomId = FindOptionsWithProjection>( rid: ISubscription['rid'], tunread: ISubscription['tunread'], - options?: FindOptions, - ): FindCursor { + options?: O, + ): FindCursor> { const query = { rid, tunread: { $in: tunread }, }; - return this.find(query, options); + return this.find(query, options); } openByRoomIdAndUserId(roomId: string, userId: string): Promise { @@ -1974,8 +2037,11 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.updateOne(query, update); } - findJoinedByUserId(userId: ISubscription['u']['_id'], options?: FindOptions): FindCursor { - return this.find( + findJoinedByUserId = FindOptionsWithProjection>( + userId: ISubscription['u']['_id'], + options?: O, + ): FindCursor> { + return this.find( { 'u._id': userId, 'status': { $exists: false }, diff --git a/packages/models/src/models/Team.ts b/packages/models/src/models/Team.ts index c459a77e32998..757195ad3453c 100644 --- a/packages/models/src/models/Team.ts +++ b/packages/models/src/models/Team.ts @@ -1,6 +1,6 @@ import type { ITeam, RocketChatRecordDeleted, TeamType } from '@rocket.chat/core-typings'; -import type { FindPaginated, ITeamModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, DeleteResult, Document, Filter, FindOptions, IndexDescription, UpdateResult } from 'mongodb'; +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'; @@ -21,7 +21,7 @@ export class TeamRaw extends BaseRaw implements ITeamModel { findByNames

( names: Array, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor { if (options === undefined) { return this.col.find({ name: { $in: names } }); @@ -29,38 +29,28 @@ export class TeamRaw extends BaseRaw implements ITeamModel { return this.col.find({ name: { $in: names } }, options); } - findByIds(ids: Array, query?: Filter): FindCursor; - - findByIds(ids: Array, options: FindOptions, query?: Filter): FindCursor; - - findByIds

( - ids: Array, - options: FindOptions

, - query?: Filter, - ): FindCursor

; - - findByIds

( + findByIds

= FindOptionsWithProjection

>( ids: Array, - options?: undefined | FindOptions | FindOptions

, + options?: O, query?: Filter, - ): FindCursor

| FindCursor { + ): FindCursor> { if (options === undefined) { - return this.find({ ...query, _id: { $in: ids } }); + return this.find({ ...query, _id: { $in: ids } }); } - return this.find({ ...query, _id: { $in: ids } }, options); + return this.find({ ...query, _id: { $in: ids } }, options); } - findByIdsPaginated( + findByIdsPaginated = FindOptionsWithProjection>( ids: Array, - options?: undefined | FindOptions, + options?: O, query?: Filter, - ): FindPaginated> { + ): FindPaginated>> { if (options === undefined) { - return this.findPaginated({ ...query, _id: { $in: ids } }); + return this.findPaginated({ ...query, _id: { $in: ids } }); } - return this.findPaginated({ ...query, _id: { $in: ids } }, options); + return this.findPaginated({ ...query, _id: { $in: ids } }, options); } findByIdsAndType(ids: Array, type: TeamType): FindCursor; @@ -76,7 +66,7 @@ export class TeamRaw extends BaseRaw implements ITeamModel { findByIdsAndType

( ids: Array, type: TeamType, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor { if (options === undefined) { return this.col.find({ _id: { $in: ids }, type }); @@ -92,7 +82,7 @@ export class TeamRaw extends BaseRaw implements ITeamModel { findByType

( type: number, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor | FindCursor

{ if (options === undefined) { return this.col.find({ type }, options); @@ -113,7 +103,7 @@ export class TeamRaw extends BaseRaw implements ITeamModel { findByNameAndTeamIds

( name: string | RegExp, teamIds: Array, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor { if (options === undefined) { return this.col.find({ @@ -156,7 +146,7 @@ export class TeamRaw extends BaseRaw implements ITeamModel { findOneByName

( name: string | RegExp, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): Promise

| Promise { if (options === undefined) { return this.col.findOne({ name }); @@ -172,7 +162,7 @@ export class TeamRaw extends BaseRaw implements ITeamModel { findOneByMainRoomId

( roomId: string, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): Promise

| Promise { return options ? this.col.findOne({ roomId }, options) : this.col.findOne({ roomId }); } diff --git a/packages/models/src/models/TeamMember.ts b/packages/models/src/models/TeamMember.ts index ed85a0a34ef81..efa73295e4d30 100644 --- a/packages/models/src/models/TeamMember.ts +++ b/packages/models/src/models/TeamMember.ts @@ -40,7 +40,7 @@ export class TeamMemberRaw extends BaseRaw implements ITeamMemberMo findByUserId

( userId: string, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor { return options ? this.col.find({ userId }, options) : this.col.find({ userId }, options); } @@ -54,7 +54,7 @@ export class TeamMemberRaw extends BaseRaw implements ITeamMemberMo findOneByUserIdAndTeamId

( userId: string, teamId: string, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): Promise

{ return options ? this.col.findOne({ userId, teamId }, options) : this.col.findOne({ userId, teamId }, options); } @@ -67,7 +67,7 @@ export class TeamMemberRaw extends BaseRaw implements ITeamMemberMo findByTeamId

( teamId: string, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor { return options ? this.col.find({ teamId }, options) : this.col.find({ teamId }, options); } @@ -84,7 +84,7 @@ export class TeamMemberRaw extends BaseRaw implements ITeamMemberMo findByTeamIds

( teamIds: Array, - options?: undefined | FindOptions | FindOptions

, + options?: FindOptions | FindOptions

, ): FindCursor

| FindCursor { return options ? this.col.find({ teamId: { $in: teamIds } }, options) : this.col.find({ teamId: { $in: teamIds } }, options); } @@ -109,7 +109,7 @@ export class TeamMemberRaw extends BaseRaw implements ITeamMemberMo limit: number, skip: number, query?: Filter, - ): FindPaginated> { + ): FindPaginated>> { return this.findPaginated( { ...query, teamId }, { diff --git a/packages/models/src/models/TwoFactorChallenges.ts b/packages/models/src/models/TwoFactorChallenges.ts index 2d4fb748c1d15..5cfb3fe5834c7 100644 --- a/packages/models/src/models/TwoFactorChallenges.ts +++ b/packages/models/src/models/TwoFactorChallenges.ts @@ -1,8 +1,8 @@ import { randomBytes } from 'crypto'; import type { ITwoFactorChallenge } from '@rocket.chat/core-typings'; -import type { ITwoFactorChallengesModel } from '@rocket.chat/model-typings'; -import type { Db, FindOptions, IndexDescription } from 'mongodb'; +import type { ITwoFactorChallengesModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { Db, IndexDescription, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -15,8 +15,11 @@ export class TwoFactorChallengesRaw extends BaseRaw impleme return [{ key: { expireAt: 1 }, expireAfterSeconds: 0 }]; } - findOneByPendingChallengeId(pendingChallengeId: string, options?: FindOptions) { - return this.findOne({ _id: pendingChallengeId }, options); + findOneByPendingChallengeId< + T extends Document = ITwoFactorChallenge, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(pendingChallengeId: string, options?: O): Promise | null> { + return this.findOne({ _id: pendingChallengeId }, options); } removeByPendingChallengeId(pendingChallengeId: string) { diff --git a/packages/models/src/models/Uploads.ts b/packages/models/src/models/Uploads.ts index 5d7695d6e9cab..4ba182e4a7868 100644 --- a/packages/models/src/models/Uploads.ts +++ b/packages/models/src/models/Uploads.ts @@ -1,6 +1,6 @@ import type { IUpload, RocketChatRecordDeleted, IRoom } from '@rocket.chat/core-typings'; -import type { FindPaginated, IUploadsModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, IndexDescription, WithId, Filter, FindOptions, UpdateResult } from 'mongodb'; +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'; @@ -64,7 +64,10 @@ export class UploadsRaw extends BaseUploadModelRaw implements IUploadsModel { ); } - findAllByOriginalFileId(originalFileId: string, options: FindOptions = {}): FindCursor { - return this.find({ originalFileId }, options); + findAllByOriginalFileId = FindOptionsWithProjection>( + originalFileId: string, + options?: O, + ): FindCursor> { + return this.find({ originalFileId }, options); } } diff --git a/packages/models/src/models/UserDataFiles.ts b/packages/models/src/models/UserDataFiles.ts index bf3c61d792f9c..f1e1f55a6be9a 100644 --- a/packages/models/src/models/UserDataFiles.ts +++ b/packages/models/src/models/UserDataFiles.ts @@ -1,6 +1,6 @@ import type { IUserDataFile, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IUserDataFilesModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, FindOptions, IndexDescription } from 'mongodb'; +import type { IUserDataFilesModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { Collection, Db, IndexDescription, Document } from 'mongodb'; import { BaseUploadModelRaw } from './BaseUploadModel'; @@ -13,12 +13,16 @@ export class UserDataFilesRaw extends BaseUploadModelRaw implements IUserDataFil return [...super.modelIndexes(), { key: { userId: 1 } }]; } - findLastFileByUser(userId: string, options: FindOptions = {}): Promise { + findLastFileByUser = FindOptionsWithProjection>( + userId: string, + options?: O, + ): Promise | null> { const query = { userId, }; - options.sort = { _updatedAt: -1 }; - return this.findOne(query, options); + // 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(query, { ...options, sort: { _updatedAt: -1 } } as unknown as O); } } diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index cccd374af1fae..e412e706aba2e 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -12,7 +12,14 @@ import type { RocketChatRecordDeleted, } from '@rocket.chat/core-typings'; import { ILivechatAgentStatus, UserStatus } from '@rocket.chat/core-typings'; -import type { DefaultFields, InsertionModel, IUsersModel } from '@rocket.chat/model-typings'; +import type { + DefaultFields, + InsertionModel, + IUsersModel, + DocumentWithProjection, + FindOptionsWithProjection, + FindPaginated, +} from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/tools'; import type { Collection, @@ -104,10 +111,10 @@ export class UsersRaw extends BaseRaw> implements IU ]; } - findUsersByIdentifiers( + findUsersByIdentifiers = FindOptionsWithProjection>( { usernames, ids, emails, ldapIds }: { usernames?: string[]; ids?: string[]; emails?: string[]; ldapIds?: string[] }, - options: FindOptions = {}, - ): FindCursor { + options?: O, + ): FindCursor> { const normalizedIds = (ids ?? []).filter(Boolean); const normalizedUsernames = (usernames ?? []).filter(Boolean); const normalizedEmails = (emails ?? []).map((e) => String(e).trim()).filter(Boolean); @@ -133,7 +140,7 @@ export class UsersRaw extends BaseRaw> implements IU $or: or, }; - return this.find(query, options); + return this.find(query, options); } setAbacAttributesById(_id: IUser['_id'], attributes: NonNullable) { @@ -144,8 +151,11 @@ export class UsersRaw extends BaseRaw> implements IU return this.findOneAndUpdate({ _id }, { $unset: { abacAttributes: 1 } }, { returnDocument: 'after' }); } - findActiveByRoomIds(roomIds: IRoom['_id'][], options?: FindOptions) { - return this.find({ active: true, __rooms: { $in: roomIds } }, options); + findActiveByRoomIds = FindOptionsWithProjection>( + roomIds: IRoom['_id'][], + options?: O, + ): FindCursor> { + return this.find({ active: true, __rooms: { $in: roomIds } }, options); } setCasExternalIdByUsername(username: string): Promise { @@ -183,17 +193,18 @@ export class UsersRaw extends BaseRaw> implements IU /** * @param {IRole['_id'][]} roles list of role ids * @param {null} scope the value for the role scope (room id) - not used in the users collection - * @param {any} options */ - findUsersInRoles: IUsersModel['findUsersInRoles'] = (roles: IRole['_id'][] | IRole['_id'], _scope?: null, options?: any) => { - roles = ([] as string[]).concat(roles); - + findUsersInRoles = FindOptionsWithProjection>( + roles: IRole['_id'][] | IRole['_id'], + _scope?: null, + options?: O, + ): FindCursor> { const query = { - roles: { $in: roles }, + roles: { $in: ([] as string[]).concat(roles) }, }; - return this.find(query, options); - }; + return this.find(query, options); + } countUsersInRoles(roles: IRole['_id'][] | IRole['_id']) { roles = ([] as string[]).concat(roles); @@ -205,29 +216,38 @@ export class UsersRaw extends BaseRaw> implements IU return this.countDocuments(query); } - findPaginatedUsersInRoles(roles: IRole['_id'][] | IRole['_id'], options?: FindOptions) { + findPaginatedUsersInRoles = FindOptionsWithProjection>( + roles: IRole['_id'][] | IRole['_id'], + options?: O, + ): FindPaginated>> { roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findOneByUsername(username: string, options?: FindOptions) { + findOneByUsername = FindOptionsWithProjection>( + username: string, + options?: O, + ): Promise | null> { const query = { username }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneAgentById(_id: IUser['_id'], options?: FindOptions) { + findOneAgentById = FindOptionsWithProjection>( + _id: IUser['_id'], + options?: O, + ): Promise | null> { const query = { _id, roles: 'livechat-agent', }; - return this.findOne(query, options); + return this.findOne(query, options); } /** @@ -235,12 +255,16 @@ export class UsersRaw extends BaseRaw> implements IU * @param {any} query * @param {any} options */ - findUsersInRolesWithQuery(roles: IRole['_id'][] | IRole['_id'], query: Filter, options?: FindOptions) { + findUsersInRolesWithQuery = FindOptionsWithProjection>( + roles: IRole['_id'][] | IRole['_id'], + query: Filter, + options?: O, + ): FindCursor> { roles = ([] as string[]).concat(roles); Object.assign(query, { roles: { $in: roles } }); - return this.find(query, options); + return this.find(query, options); } /** @@ -248,16 +272,16 @@ export class UsersRaw extends BaseRaw> implements IU * @param {any} query * @param {any} options */ - findPaginatedUsersInRolesWithQuery( + findPaginatedUsersInRolesWithQuery = FindOptionsWithProjection>( roles: IRole['_id'][] | IRole['_id'], query: Filter, - options?: FindOptions, - ) { + options?: O, + ): FindPaginated>> { roles = ([] as string[]).concat(roles); Object.assign(query, { roles: { $in: roles } }); - return this.findPaginated(query, options); + return this.findPaginated(query, options); } findAgentsWithDepartments( @@ -310,7 +334,11 @@ export class UsersRaw extends BaseRaw> implements IU return this.col.aggregate<{ sortedResults: (T & { departments: string[] })[]; totalCount: { total: number }[] }>(aggregate).toArray(); } - findOneByUsernameAndRoomIgnoringCase(username: string | RegExp, rid: string, options?: FindOptions) { + findOneByUsernameAndRoomIgnoringCase = FindOptionsWithProjection>( + username: string | RegExp, + rid: string, + options?: O, + ): Promise | null> { if (typeof username === 'string') { username = new RegExp(`^${escapeRegExp(username)}$`, 'i'); } @@ -320,31 +348,37 @@ export class UsersRaw extends BaseRaw> implements IU username, }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneByIdAndLoginHashedToken(_id: IUser['_id'], token: string, options: FindOptions = {}) { + findOneByIdAndLoginHashedToken = FindOptionsWithProjection>( + _id: IUser['_id'], + token: string, + options?: O, + ): Promise | null> { const query = { _id, 'services.resume.loginTokens.hashedToken': token, }; - return this.findOne(query, options); + return this.findOne(query, options); } - findByActiveUsersExcept( + findByActiveUsersExcept = FindOptionsWithProjection>( searchTerm: string, exceptions: string[], - options?: FindOptions, + options?: O, searchFields?: string[], extraQuery: Filter[] = [], { startsWith = false, endsWith = false } = {}, - ) { + ): FindCursor> { if (exceptions == null) { exceptions = []; } if (options == null) { - options = {}; + // `{}` is not assignable to `O` (a caller could pin it to a narrower type), but under-projecting only + // ever returns more fields than the declared type claims + options = {} as O; } if (!Array.isArray(exceptions)) { exceptions = [exceptions]; @@ -375,22 +409,24 @@ export class UsersRaw extends BaseRaw> implements IU ], }; - return this.find(query, options); + return this.find(query, options); } - findPaginatedByActiveUsersExcept( + findPaginatedByActiveUsersExcept = FindOptionsWithProjection>( searchTerm: string, exceptions?: string[], - options?: FindOptions, + options?: O, searchFields: string[] = [], extraQuery: Filter[] = [], { startsWith = false, endsWith = false } = {}, - ) { + ): FindPaginated>> { if (exceptions == null) { exceptions = []; } if (options == null) { - options = {}; + // `{}` is not assignable to `O` (a caller could pin it to a narrower type), but under-projecting only + // ever returns more fields than the declared type claims + options = {} as O; } if (!Array.isArray(exceptions)) { exceptions = [exceptions]; @@ -421,85 +457,111 @@ export class UsersRaw extends BaseRaw> implements IU ], }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findPaginatedByActiveLocalUsersExcept( + findPaginatedByActiveLocalUsersExcept = FindOptionsWithProjection>( searchTerm: string, exceptions?: string[], - options?: FindOptions, + options?: O, forcedSearchFields?: string[], localDomain?: string, - ) { + ): FindPaginated>> { const extraQuery = [ { $or: [{ federation: { $exists: false } }, { 'federation.origin': localDomain }], }, ]; - return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); + return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); } - findPaginatedByActiveExternalUsersExcept( + findPaginatedByActiveExternalUsersExcept< + T extends Document = IUser, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( searchTerm: string, exceptions?: string[], - options?: FindOptions, + options?: O, forcedSearchFields?: string[], localDomain?: string, - ) { + ): FindPaginated>> { const extraQuery = [{ federation: { $exists: true } }, { 'federation.origin': { $ne: localDomain } }]; - return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); + return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); } - findActive(query: Filter, options: FindOptions = {}) { + findActive = FindOptionsWithProjection>( + query: Filter, + options?: O, + ): FindCursor> { Object.assign(query, { active: true }); - return this.find(query, options); + return this.find(query, options); } - findActiveByIds(userIds: IUser['_id'][], options: FindOptions = {}) { + findActiveByIds = FindOptionsWithProjection>( + userIds: IUser['_id'][], + options?: O, + ): FindCursor> { const query = { _id: { $in: userIds }, active: true, }; - return this.find(query, options); + return this.find(query, options); } - findActiveByIdsOrUsernames(userIds: IUser['_id'][], options: FindOptions = {}) { + findActiveByIdsOrUsernames = FindOptionsWithProjection>( + userIds: IUser['_id'][], + options?: O, + ): FindCursor> { const query = { $or: [{ _id: { $in: userIds } }, { username: { $in: userIds } }], active: true, }; - return this.find(query, options); + return this.find(query, options); } - findByIds(userIds: IUser['_id'][], options: FindOptions = {}) { + findByIds = FindOptionsWithProjection>( + userIds: IUser['_id'][], + options?: O, + ): FindCursor> { const query = { _id: { $in: userIds }, }; - return this.find(query, options); + return this.find(query, options); } - findOneByImportId(_id: IUser['_id'], options?: FindOptions) { - return this.findOne({ importIds: _id }, options); + findOneByImportId = FindOptionsWithProjection>( + _id: IUser['_id'], + options?: O, + ): Promise | null> { + return this.findOne({ importIds: _id }, options); } - findOneByUsernameIgnoringCase(username: IUser['username'], options?: FindOptions) { + findOneByUsernameIgnoringCase = FindOptionsWithProjection>( + username: IUser['username'], + options?: O, + ): Promise | null> { if (!username) { throw new Error('invalid username'); } const query = { username }; - return this.findOne(query, { - collation: { locale: 'en', strength: 2 }, // Case insensitive + // safe to merge into `O`: only `O['projection']` feeds the return type, and it survives the spread. + // note the model's `collation` wins over a caller-supplied one, so the lookup stays case insensitive. + return this.findOne(query, { ...options, - }); + collation: { locale: 'en', strength: 2 }, // Case insensitive + } as unknown as O); } - findOneWithoutLDAPByUsernameIgnoringCase(username: string, options?: FindOptions) { + findOneWithoutLDAPByUsernameIgnoringCase< + T extends Document = IUser, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(username: string, options?: O): Promise | null> { const expression = new RegExp(`^${escapeRegExp(username)}$`, 'i'); const query = { @@ -509,7 +571,7 @@ export class UsersRaw extends BaseRaw> implements IU }, }; - return this.findOne(query, options); + return this.findOne(query, options); } async findOneByLDAPId(id: string, attribute?: string) { @@ -521,19 +583,27 @@ export class UsersRaw extends BaseRaw> implements IU return this.findOne(query); } - async findOneByAppId(appId: string, options?: FindOptions) { + async findOneByAppId = FindOptionsWithProjection>( + appId: string, + options?: O, + ): Promise | null> { const query = { appId }; - return this.findOne(query, options); + return this.findOne(query, options); } - findLDAPUsers(options?: FindOptions) { + findLDAPUsers = FindOptionsWithProjection>( + options?: O, + ): FindCursor> { const query = { ldap: true }; - return this.find(query, options); + return this.find(query, options); } - findActiveLDAPUsersExceptIds(userIds: IUser['_id'][], options: FindOptions = {}) { + findActiveLDAPUsersExceptIds = FindOptionsWithProjection>( + userIds: IUser['_id'][], + options?: O, + ): FindCursor> { const query = { ldap: true, active: true, @@ -542,10 +612,12 @@ export class UsersRaw extends BaseRaw> implements IU }, }; - return this.find(query, options); + return this.find(query, options); } - findConnectedLDAPUsers(options?: FindOptions) { + findConnectedLDAPUsers = FindOptionsWithProjection>( + options?: O, + ): FindCursor> { const query = { 'ldap': true, 'services.resume.loginTokens': { @@ -554,7 +626,7 @@ export class UsersRaw extends BaseRaw> implements IU }, }; - return this.find(query, options); + return this.find(query, options); } isUserInRole(userId: IUser['_id'], roleId: IRole['_id']) { @@ -880,12 +952,15 @@ export class UsersRaw extends BaseRaw> implements IU .toArray(); } - findActiveByUsernameOrNameRegexWithExceptionsAndConditions( + findActiveByUsernameOrNameRegexWithExceptionsAndConditions< + T extends Document = IUser, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( termRegex: { $regex: string; $options: string } | RegExp, exceptions?: string[], conditions?: Filter, - options?: FindOptions, - ) { + options?: O, + ): FindCursor> { if (exceptions == null) { exceptions = []; } @@ -893,7 +968,9 @@ export class UsersRaw extends BaseRaw> implements IU conditions = {}; } if (options == null) { - options = {}; + // `{}` is not assignable to `O` (a caller could pin it to a narrower type), but under-projecting only + // ever returns more fields than the declared type claims + options = {} as O; } if (!Array.isArray(exceptions)) { exceptions = [exceptions]; @@ -932,7 +1009,7 @@ export class UsersRaw extends BaseRaw> implements IU ], }; - return this.find(query, options); + return this.find(query, options); } countAllAgentsStatus({ @@ -1559,8 +1636,11 @@ export class UsersRaw extends BaseRaw> implements IU ); } - findOneByIdWithEmailAddress(userId: IUser['_id'], options?: FindOptions) { - return this.findOne( + findOneByIdWithEmailAddress = FindOptionsWithProjection>( + userId: IUser['_id'], + options?: O, + ): Promise | null> { + return this.findOne( { _id: userId, emails: { $exists: true, $ne: [] }, @@ -1590,12 +1670,12 @@ export class UsersRaw extends BaseRaw> implements IU return this.find(query); } - findOneOnlineAgentByUserList( + findOneOnlineAgentByUserList = FindOptionsWithProjection>( userList: string | string[], - options?: FindOptions, + options?: O, isLivechatEnabledWhenAgentIdle?: boolean, acceptChatsWithNoAgents?: boolean, - ) { + ): Promise | null> { // TODO:: Create class Agent const username = { $in: ([] as string[]).concat(userList), @@ -1603,7 +1683,7 @@ export class UsersRaw extends BaseRaw> implements IU const query = queryStatusAgentOnline({ username }, isLivechatEnabledWhenAgentIdle, acceptChatsWithNoAgents); - return this.findOne(query, options); + return this.findOne(query, options); } async getUnavailableAgents( @@ -2118,7 +2198,10 @@ export class UsersRaw extends BaseRaw> implements IU ); } - findByIdsWithPublicE2EKey(ids: IUser['_id'][], options?: FindOptions) { + findByIdsWithPublicE2EKey = FindOptionsWithProjection>( + ids: IUser['_id'][], + options?: O, + ): FindCursor> { const query = { '_id': { $in: ids, @@ -2128,7 +2211,7 @@ export class UsersRaw extends BaseRaw> implements IU }, }; - return this.find(query, options); + return this.find(query, options); } resetE2EKey(userId: IUser['_id']) { @@ -2209,7 +2292,10 @@ export class UsersRaw extends BaseRaw> implements IU * @param {IRole['_id'][]} roles the list of role ids * @param {any} options */ - findActiveUsersInRoles(roles: IRole['_id'][], options?: FindOptions) { + findActiveUsersInRoles = FindOptionsWithProjection>( + roles: IRole['_id'][], + options?: O, + ): FindCursor> { roles = ([] as string[]).concat(roles); const query = { @@ -2217,7 +2303,7 @@ export class UsersRaw extends BaseRaw> implements IU active: true, }; - return this.find(query, options); + return this.find(query, options); } countActiveUsersInRoles(roles: IRole['_id'][], options?: FindOptions) { @@ -2231,48 +2317,54 @@ export class UsersRaw extends BaseRaw> implements IU return this.countDocuments(query, options); } - findOneByUsernameAndServiceNameIgnoringCase( - username: string | RegExp, - userId: IUser['_id'], - serviceName: string, - options?: FindOptions, - ) { + findOneByUsernameAndServiceNameIgnoringCase< + T extends Document = IUser, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(username: string | RegExp, userId: IUser['_id'], serviceName: string, options?: O): Promise | null> { if (typeof username === 'string') { username = new RegExp(`^${escapeRegExp(username)}$`, 'i'); } const query = { username, [`services.${serviceName}.id`]: userId }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneByEmailAddressAndServiceNameIgnoringCase( - emailAddress: string, - userId: IUser['_id'], - serviceName: string, - options?: FindOptions, - ) { + findOneByEmailAddressAndServiceNameIgnoringCase< + T extends Document = IUser, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >(emailAddress: string, userId: IUser['_id'], serviceName: string, options?: O): Promise | null> { const query = { 'emails.address': String(emailAddress).trim(), [`services.${serviceName}.id`]: userId, }; - return this.findOne(query, { - collation: { locale: 'en', strength: 2 }, // Case insensitive + // safe to merge into `O`: only `O['projection']` feeds the return type, and it survives the spread. + // note the model's `collation` wins over a caller-supplied one, so the lookup stays case insensitive. + return this.findOne(query, { ...options, - }); + collation: { locale: 'en', strength: 2 }, // Case insensitive + } as unknown as O); } - findOneByEmailAddress(emailAddress: string, options?: FindOptions) { + findOneByEmailAddress = FindOptionsWithProjection>( + emailAddress: string, + options?: O, + ): Promise | null> { const query = { 'emails.address': String(emailAddress).trim() }; - return this.findOne(query, { - collation: { locale: 'en', strength: 2 }, // Case insensitive + // safe to merge into `O`: only `O['projection']` feeds the return type, and it survives the spread. + // note the model's `collation` wins over a caller-supplied one, so the lookup stays case insensitive. + return this.findOne(query, { ...options, - }); + collation: { locale: 'en', strength: 2 }, // Case insensitive + } as unknown as O); } - findOneWithoutLDAPByEmailAddress(emailAddress: string, options?: FindOptions) { + findOneWithoutLDAPByEmailAddress = FindOptionsWithProjection>( + emailAddress: string, + options?: O, + ): Promise | null> { const query = { 'emails.address': emailAddress.trim().toLowerCase(), 'services.ldap': { @@ -2280,40 +2372,47 @@ export class UsersRaw extends BaseRaw> implements IU }, }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneAdmin(userId: IUser['_id'], options?: FindOptions) { + findOneAdmin = FindOptionsWithProjection>( + userId: IUser['_id'], + options?: O, + ): Promise | null> { const query = { roles: { $in: ['admin'] }, _id: userId }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneByIdAndLoginToken(_id: IUser['_id'], token: string, options?: FindOptions) { + findOneByIdAndLoginToken = FindOptionsWithProjection>( + _id: IUser['_id'], + token: string, + options?: O, + ): Promise | null> { const query = { _id, 'services.resume.loginTokens.hashedToken': token, }; - return this.findOne(query, options); - } - - override findOneById(userId: IUser['_id'], options: FindOptions = {}) { - const query = { _id: userId }; - - return this.findOne(query, options); + return this.findOne(query, options); } - findOneActiveById(userId?: IUser['_id'], options?: FindOptions) { + findOneActiveById = FindOptionsWithProjection>( + userId?: IUser['_id'], + options?: O, + ): Promise | null> { const query = { _id: userId, active: true, }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneByIdOrUsername(idOrUsername: IUser['_id'] | IUser['username'], options?: FindOptions) { + findOneByIdOrUsername = FindOptionsWithProjection>( + idOrUsername: IUser['_id'] | IUser['username'], + options?: O, + ): Promise | null> { const query = { $or: [ { @@ -2325,16 +2424,23 @@ export class UsersRaw extends BaseRaw> implements IU ], }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneByRolesAndType(roles: IRole['_id'][], type: string, options?: FindOptions) { + findOneByRolesAndType = FindOptionsWithProjection>( + roles: IRole['_id'][], + type: string, + options?: O, + ): Promise | null> { const query = { roles, type }; - return this.findOne(query, options); + return this.findOne(query, options); } - findPresenceUsersByIds(users: IUser['_id'][], options?: FindOptions) { + findPresenceUsersByIds = FindOptionsWithProjection>( + users: IUser['_id'][], + options?: O, + ): FindCursor> { const query = { _id: { $in: users }, $or: [ @@ -2343,10 +2449,12 @@ export class UsersRaw extends BaseRaw> implements IU { statusExpiresAt: { $exists: true } }, ], }; - return this.find(query, options); + return this.find(query, options); } - findUsersNotOffline(options?: FindOptions) { + findUsersNotOffline = FindOptionsWithProjection>( + options?: O, + ): FindCursor> { const query = { username: { $exists: true, @@ -2356,7 +2464,7 @@ export class UsersRaw extends BaseRaw> implements IU }, }; - return this.find(query, options); + return this.find(query, options); } countUsersNotOffline(options?: FindOptions) { @@ -2372,7 +2480,11 @@ export class UsersRaw extends BaseRaw> implements IU return this.col.countDocuments(query, options); } - findNotIdUpdatedFrom(uid: IUser['_id'], from: Date, options?: FindOptions) { + findNotIdUpdatedFrom = FindOptionsWithProjection>( + uid: IUser['_id'], + from: Date, + options?: O, + ): FindCursor> { const query: Filter = { _id: { $ne: uid }, username: { @@ -2381,16 +2493,23 @@ export class UsersRaw extends BaseRaw> implements IU _updatedAt: { $gte: from }, }; - return this.find(query, options); + return this.find(query, options); } - findOneByIdAndRole(userId: IUser['_id'], role: string, options: FindOptions = {}) { + findOneByIdAndRole = FindOptionsWithProjection>( + userId: IUser['_id'], + role: string, + options?: O, + ): Promise | null> { const query = { _id: userId, roles: role }; - return this.findOne(query, options); + return this.findOne(query, options); } - async findByRoomId(rid: IRoom['_id'], options?: FindOptions) { + async findByRoomId = FindOptionsWithProjection>( + rid: IRoom['_id'], + options?: O, + ): Promise>> { const data = (await Subscriptions.findByRoomId(rid).toArray()).map((item) => item.u._id); const query = { _id: { @@ -2398,27 +2517,36 @@ export class UsersRaw extends BaseRaw> implements IU }, }; - return this.find(query, options); + return this.find(query, options); } - findByUsernames(usernames: string[], options?: FindOptions) { + findByUsernames = FindOptionsWithProjection>( + usernames: string[], + options?: O, + ): FindCursor> { const query = { username: { $in: usernames } }; - return this.find(query, options); + return this.find(query, options); } - findByUsernamesIgnoringCase(usernames: string[], options?: FindOptions) { + findByUsernamesIgnoringCase = FindOptionsWithProjection>( + usernames: string[], + options?: O, + ): FindCursor> { const query = { username: { $in: usernames.filter(Boolean).map((u) => new RegExp(`^${escapeRegExp(u)}$`, 'i')), }, }; - return this.find(query, options); + return this.find(query, options); } - findActiveByUserIds(ids: IUser['_id'][], options: FindOptions = {}) { - return this.find( + findActiveByUserIds = FindOptionsWithProjection>( + ids: IUser['_id'][], + options?: O, + ): FindCursor> { + return this.find( { active: true, type: { $nin: ['app'] }, @@ -2450,10 +2578,12 @@ export class UsersRaw extends BaseRaw> implements IU return this.countDocuments(query); } - findCrowdUsers(options?: FindOptions) { + findCrowdUsers = FindOptionsWithProjection>( + options?: O, + ): FindCursor> { const query = { crowd: true }; - return this.find(query, options); + return this.find(query, options); } async getLastLogin(options: FindOptions = { projection: { _id: 0, lastLogin: 1 } }) { @@ -2462,43 +2592,53 @@ export class UsersRaw extends BaseRaw> implements IU return user?.lastLogin; } - findUsersByUsernames(usernames: string[], options?: FindOptions) { + findUsersByUsernames = FindOptionsWithProjection>( + usernames: string[], + options?: O, + ): FindCursor> { const query = { username: { $in: usernames, }, }; - return this.find(query, options); + return this.find(query, options); } - findUsersByIds(ids: IUser['_id'][], options?: FindOptions) { + findUsersByIds = FindOptionsWithProjection>( + ids: IUser['_id'][], + options?: O, + ): FindCursor> { const query = { _id: { $in: ids, }, }; - return this.find(query, options); + return this.find(query, options); } /** * @param {import('mongodb').Filter} projection */ - getOldest(optionsParams?: FindOptions) { + getOldest = FindOptionsWithProjection>( + optionsParams?: O, + ): Promise | null> { const query = { _id: { $ne: 'rocket.cat', }, }; - const options: FindOptions = { + // safe to merge into `O`: only `O['projection']` feeds the return type, and it survives the spread. + // note the model's `sort` wins over a caller-supplied one. + const options = { ...optionsParams, sort: { createdAt: 1, }, - }; + } as unknown as O; - return this.findOne(query, options); + return this.findOne(query, options); } getSAMLByIdAndSAMLProvider(_id: IUser['_id'], provider: string) { @@ -2513,8 +2653,12 @@ export class UsersRaw extends BaseRaw> implements IU ); } - findBySAMLNameIdOrIdpSession(nameID: string, idpSession: string, options?: FindOptions) { - return this.find( + findBySAMLNameIdOrIdpSession = FindOptionsWithProjection>( + nameID: string, + idpSession: string, + options?: O, + ): FindCursor> { + return this.find( { $or: [{ 'services.saml.nameID': nameID }, { 'services.saml.idpSession': idpSession }], }, @@ -2522,8 +2666,11 @@ export class UsersRaw extends BaseRaw> implements IU ); } - findBySAMLInResponseTo(inResponseTo: string, options?: FindOptions) { - return this.find( + findBySAMLInResponseTo = FindOptionsWithProjection>( + inResponseTo: string, + options?: O, + ): FindCursor> { + return this.find( { 'services.saml.inResponseTo': inResponseTo, }, @@ -2531,8 +2678,11 @@ export class UsersRaw extends BaseRaw> implements IU ); } - findOneByFreeSwitchExtension(freeSwitchExtension: string, options: FindOptions = {}) { - return this.findOne( + findOneByFreeSwitchExtension = FindOptionsWithProjection>( + freeSwitchExtension: string, + options?: O, + ): Promise | null> { + return this.findOne( { freeSwitchExtension, }, @@ -2737,7 +2887,11 @@ export class UsersRaw extends BaseRaw> implements IU return this.updateMany(query, update); } - findActiveNotLoggedInAfterWithRole(latestLastLoginDate: Date, role: IRole['_id'] = 'user', options: FindOptions = {}) { + findActiveNotLoggedInAfterWithRole = FindOptionsWithProjection>( + latestLastLoginDate: Date, + role: IRole['_id'] = 'user', + options?: O, + ): FindCursor> { const neverActive = { lastLogin: { $exists: false }, createdAt: { $lte: latestLastLoginDate } }; const idleTooLong = { lastLogin: { $lte: latestLastLoginDate } }; @@ -2747,7 +2901,7 @@ export class UsersRaw extends BaseRaw> implements IU roles: role, }; - return this.find(query, options); + return this.find(query, options); } unsetRequirePasswordChange(_id: IUser['_id']) { @@ -3137,11 +3291,14 @@ export class UsersRaw extends BaseRaw> implements IU ); } - findOneByEmailVerificationToken(token: string, options?: FindOptions) { + findOneByEmailVerificationToken = FindOptionsWithProjection>( + token: string, + options?: O, + ): Promise | null> { const query = { 'services.email.verificationTokens.token': token, }; - return this.findOne(query, options); + return this.findOne(query, options); } } diff --git a/packages/models/src/models/UsersSessions.ts b/packages/models/src/models/UsersSessions.ts index a210f8ec49824..ed532247c107d 100644 --- a/packages/models/src/models/UsersSessions.ts +++ b/packages/models/src/models/UsersSessions.ts @@ -1,6 +1,6 @@ import type { IUserSession, IUserSessionConnection, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IUsersSessionsModel } from '@rocket.chat/model-typings'; -import type { FindCursor, Collection, Db, FindOptions } from 'mongodb'; +import type { IUsersSessionsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { FindCursor, Collection, Db, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -102,8 +102,11 @@ export class UsersSessionsRaw extends BaseRaw implements IUsersSes return this.updateOne({ _id: userId }, update, { upsert: true }); } - findByOtherInstanceIds(instanceIds: string[], options?: FindOptions): FindCursor { - return this.find( + findByOtherInstanceIds = FindOptionsWithProjection>( + instanceIds: string[], + options?: O, + ): FindCursor> { + return this.find( { 'connections.instanceId': { $exists: true, diff --git a/packages/models/src/models/VideoConference.ts b/packages/models/src/models/VideoConference.ts index e6bba09747c03..fbcad1529f959 100644 --- a/packages/models/src/models/VideoConference.ts +++ b/packages/models/src/models/VideoConference.ts @@ -39,7 +39,9 @@ export class VideoConferenceRaw extends BaseRaw implements IVid rid: IRoom['_id'], { offset, count }: { offset?: number; count?: number } = {}, ): FindPaginated> { - return this.findPaginated( + // 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( { rid }, { sort: { createdAt: -1 }, @@ -229,7 +231,7 @@ export class VideoConferenceRaw extends BaseRaw implements IVid $set: { [`messages.${messageType}`]: messageId, }, - } as UpdateFilter); // TODO: Remove this cast when TypeScript is updated + }); // TODO: Remove this cast when TypeScript is updated // TypeScript is not smart enough to infer that `messages.${'start' | 'end'}` matches two keys of `VideoConference` } diff --git a/packages/models/src/models/WebdavAccounts.ts b/packages/models/src/models/WebdavAccounts.ts index bc1871ef6fa07..8cfd02ba6db6d 100644 --- a/packages/models/src/models/WebdavAccounts.ts +++ b/packages/models/src/models/WebdavAccounts.ts @@ -1,6 +1,6 @@ import type { IWebdavAccount, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IWebdavAccountsModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, DeleteResult, FindOptions, IndexDescription } from 'mongodb'; +import type { IWebdavAccountsModel, DocumentWithProjection, FindOptionsWithProjection } from '@rocket.chat/model-typings'; +import type { Collection, FindCursor, Db, DeleteResult, IndexDescription, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -13,11 +13,18 @@ export class WebdavAccountsRaw extends BaseRaw implements IWebda return [{ key: { userId: 1 } }]; } - findOneByIdAndUserId(_id: string, userId: string, options: FindOptions): Promise { - return this.findOne({ _id, userId }, options); + findOneByIdAndUserId = FindOptionsWithProjection>( + _id: string, + userId: string, + options?: O, + ): Promise | null> { + return this.findOne({ _id, userId }, options); } - findOneByUserIdServerUrlAndUsername( + findOneByUserIdServerUrlAndUsername< + T extends Document = IWebdavAccount, + O extends FindOptionsWithProjection = FindOptionsWithProjection, + >( { userId, serverURL, @@ -27,14 +34,17 @@ export class WebdavAccountsRaw extends BaseRaw implements IWebda serverURL: string; username: string; }, - options: FindOptions, - ): Promise { - return this.findOne({ userId, serverURL, username }, options); + options?: O, + ): Promise | null> { + return this.findOne({ userId, serverURL, username }, options); } - findWithUserId(userId: string, options: FindOptions): FindCursor { + findWithUserId = FindOptionsWithProjection>( + userId: string, + options?: O, + ): FindCursor> { const query = { userId }; - return this.find(query, options); + return this.find(query, options); } removeByUserAndId(_id: string, userId: string): Promise { diff --git a/packages/models/src/updater.ts b/packages/models/src/updater.ts index a95ff2b24721e..8e4b48e80d42f 100644 --- a/packages/models/src/updater.ts +++ b/packages/models/src/updater.ts @@ -18,7 +18,7 @@ export class UpdaterImpl implements Updater { set>(key: K, value: SetProps[K]) { this._set = this._set ?? new Map, any>(); - this._set.set(key as Keys, value); + this._set.set(key, value); return this; } diff --git a/packages/omni-core/src/visitor/create.spec.ts b/packages/omni-core/src/visitor/create.spec.ts index b6e0efb4149b3..bd7bbcbc2c30c 100644 --- a/packages/omni-core/src/visitor/create.spec.ts +++ b/packages/omni-core/src/visitor/create.spec.ts @@ -299,7 +299,7 @@ describe('registerGuest', () => { await registerGuest(guestData, { shouldConsiderIdleAgent: false, shouldConsiderOfflineAgent: false }); - expect(getVisitorByTokenSpy).toHaveBeenCalledWith(token, { projection: { _id: 1 } }); + expect(getVisitorByTokenSpy).toHaveBeenCalledWith(token, { projection: { _id: 1, department: 1, token: 1 } }); // Verify existing visitor data is used and updated expect(updateOneByIdOrTokenSpy).toHaveBeenCalledWith( diff --git a/packages/omni-core/src/visitor/create.ts b/packages/omni-core/src/visitor/create.ts index 5c6ee74b5fdcc..a1c63c87a4dea 100644 --- a/packages/omni-core/src/visitor/create.ts +++ b/packages/omni-core/src/visitor/create.ts @@ -55,7 +55,7 @@ export const registerGuest = makeFunction( } } - const livechatVisitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); + const livechatVisitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1, department: 1, token: 1 } }); if (department && livechatVisitor?.department !== department) { logger.debug({ msg: 'Attempt to find a department with id/name', department });