From 55f271d9a771952cda97a53567380cfb2a84a514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 01:58:21 -0400 Subject: [PATCH 01/29] FIX: Half of the file function translated --- global.d.ts | 6 + src/interfaces/post.ts | 26 +++ src/interfaces/search.ts | 18 ++ src/search.js | 2 +- src/search.ts | 361 +++++++++++++++++++++++++++++++++++++++ tsconfig.json | 6 + 6 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 global.d.ts create mode 100644 src/interfaces/post.ts create mode 100644 src/interfaces/search.ts create mode 100644 src/search.ts create mode 100644 tsconfig.json diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000000..32decd37cb --- /dev/null +++ b/global.d.ts @@ -0,0 +1,6 @@ +declare namespace NodeJS { + interface Process { + elapsedTimeSince(start: [number, number]): number; + profile(operation: string, start: [number, number]): void; + } +} diff --git a/src/interfaces/post.ts b/src/interfaces/post.ts new file mode 100644 index 0000000000..2d74592074 --- /dev/null +++ b/src/interfaces/post.ts @@ -0,0 +1,26 @@ +export interface IPost { + pid: number, + uid: number, + tid: number, + timestamp: number, + deleted: boolean, + upvotes: number, + downvotes: number, + category: any, + topic: ITopic, + user: { + username?: string, + }, +} + +export interface ITopic { + postcount?: string, + deleted?: boolean, + category?: any, + tags?: ITag[] | string[], + cid?: number +} + +export interface ITag { + value?: string +} \ No newline at end of file diff --git a/src/interfaces/search.ts b/src/interfaces/search.ts new file mode 100644 index 0000000000..a4a49e5bc0 --- /dev/null +++ b/src/interfaces/search.ts @@ -0,0 +1,18 @@ +export interface ISearchData { + query: string, + searchIn: string, + uid?: number; + hasTags?: string, + categories?: number[], + searchChildren?: boolean, + sortBy?: string, + sortDirection?: string, + matchWords?: string, + returnIds?: number[], + itemsPerPage?: number, + page?: number, + replies?: string, + timeRange?: string, + repliesFilter?: any, + timeFilter?: any +} \ No newline at end of file diff --git a/src/search.js b/src/search.js index df249ec1f6..06a67311c6 100644 --- a/src/search.js +++ b/src/search.js @@ -354,4 +354,4 @@ async function getSearchUids(data) { return await user.getUidsByUsernames(Array.isArray(data.postedBy) ? data.postedBy : [data.postedBy]); } -require('./promisify')(search); +require('./promisify')(search); \ No newline at end of file diff --git a/src/search.ts b/src/search.ts new file mode 100644 index 0000000000..a521488196 --- /dev/null +++ b/src/search.ts @@ -0,0 +1,361 @@ +import _, { Dictionary } from 'lodash'; +import batch from './batch'; +import categories from './categories'; +import db from './database'; +import { IPost, ITag, ITopic } from './interfaces/post'; +import { ISearchData } from './interfaces/search'; +import plugins from './plugins'; +import posts from './posts'; +import privileges from './privileges'; +import topics from './topics'; +import user from './user'; +import utils from './utils'; + +export const search = { + search: async function (data: ISearchData) { + const start = process.hrtime(); + data.sortBy = data.sortBy || 'relevance'; + let result: any; + + if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { + result = await searchInContent(data); + } else if (data.searchIn === 'users') { + result = await user.search(data); + } else if (data.searchIn === 'categories') { + result = await categories.search(data); + } else if (data.searchIn === 'tags') { + result = await topics.searchAndLoadTags(data); + } else if (data.searchIn) { + result = await plugins.hooks.fire('filter:search.searchIn', { + data, + }); + } else { + throw new Error('[[error:unknown-search-filter]]'); + } + + result.time = (process.elapsedTimeSince(start) / 1000).toFixed(2); + return result; + } +} + +async function searchInContent(data: ISearchData) { + data.uid = data.uid || 0; + + const [searchCids, searchUids] = await Promise.all([ + getSearchCids(data), + getSearchUids(data), + ]); + + async function doSearch(type: string, searchIn: string[]): Promise { + if (searchIn.includes(data.searchIn)) { + const result = await plugins.hooks.fire('filter:search.query', { + index: type, + content: data.query, + matchWords: data.matchWords || 'all', + cid: searchCids, + uid: searchUids, + searchData: data, + ids: [], + }); + return Array.isArray(result) ? result : result.ids; + } + return []; + } + + let pids = []; + let tids = []; + + const inTopic = `${data.query || ''}`.match(/^in:topic-([\d]+) /); + + if (inTopic) { + const tid = inTopic[1]; + const cleanedTerm = data.query.replace(inTopic[0], ''); + pids = await topics.search(tid, cleanedTerm); + } else if (data.searchIn === 'bookmarks') { + pids = await searchInBookmarks(data, searchCids, searchUids); + } else { + [pids, tids] = await Promise.all([ + doSearch('post', ['posts', 'titlesposts']), + doSearch('topic', ['titles', 'titlesposts']), + ]); + } + + const mainPids = await topics.getMainPids(tids); + + let allPids = mainPids.concat(pids).filter(Boolean); + + allPids = await privileges.posts.filter('topics:read', allPids, data.uid); + allPids = await filterAndSort(allPids, data); + + const metadata = await plugins.hooks.fire('filter:search.inContent', { + pids: allPids, + data: data, + }); + + if (data.returnIds) { + const mainPidsSet = new Set(mainPids); + const mainPidToTid = _.zipObject(mainPids, tids); + const pidsSet = new Set(pids); + const returnPids = allPids.filter(pid => pidsSet.has(pid)); + const returnTids = allPids.filter(pid => mainPidsSet.has(pid)).map(pid => mainPidToTid[pid]); + return { pids: returnPids, tids: returnTids }; + } + + const itemsPerPage = Math.min(data.itemsPerPage || 10, 100); + + const returnData = { + posts: [], + matchCount: metadata.pids.length, + pageCount: Math.max(1, Math.ceil(parseInt(metadata.pids.length, 10) / itemsPerPage)), + }; + + if (data.page) { + const start = Math.max(0, (data.page - 1)) * itemsPerPage; + metadata.pids = metadata.pids.slice(start, start + itemsPerPage); + } + + returnData.posts = await posts.getPostSummaryByPids(metadata.pids, data.uid, {}); + await plugins.hooks.fire('filter:search.contentGetResult', { result: returnData, data: data }); + delete metadata.pids; + delete metadata.data; + return Object.assign(returnData, metadata); +} + +async function searchInBookmarks(data: ISearchData, searchCids: number[], searchUids: number[]): Promise { + const { uid, query, matchWords } = data; + const allPids: number[] = []; + await batch.processSortedSet(`uid:${uid}:bookmarks`, async (pids: number[]) => { + if (Array.isArray(searchCids) && searchCids.length) { + pids = await posts.filterPidsByCid(pids, searchCids); + } + + if (Array.isArray(searchUids) && searchUids.length) { + pids = await posts.filterPidsByUid(pids, searchUids); + } + + if (query) { + const tokens = query.toString().split(' '); + const postData = await db.getObjectsFields(pids.map(pid => `post:${pid}`), ['content', 'tid']); + const tids: number[] = _.uniq(postData.map((p: { tid: number; }) => p.tid)); + const topicData = await db.getObjectsFields(tids.map(tid => `topic:${tid}`), ['title']); + const tidToTopic: Dictionary<{ title: string }> = _.zipObject(tids, topicData); + pids = pids.filter((_, i) => { + const content = postData[i].content.toString(); + const title = tidToTopic[postData[i].tid].title.toString(); + const method = (matchWords === 'any' ? 'some' : 'every'); + return tokens[method]( + token => content.includes(token) || title.includes(token) + ); + }); + } + allPids.push(...pids); + }, { + batch: 500, + }); + + return allPids; +} + +async function filterAndSort(pids: number[], data: ISearchData): Promise { + if (data.sortBy === 'relevance' && + !data.replies && + !data.timeRange && + !data.hasTags && + data.searchIn !== 'bookmarks' && + !plugins.hooks.hasListeners('filter:search.filterAndSort')) { + return pids; + } + let postsData = await getMatchedPosts(pids, data); + if (!postsData.length) { + return pids; + } + postsData = postsData.filter(Boolean); + + postsData = filterByPostcount(postsData, data.replies, data.repliesFilter); + postsData = filterByTimerange(postsData, data.timeRange, data.timeFilter); + postsData = filterByTags(postsData, data.hasTags); + + sortPosts(postsData, data); + + const result = await plugins.hooks.fire('filter:search.filterAndSort', { pids: pids, posts: postsData, data: data }); + return result.posts.map(post => post && post.pid); +} + +async function getMatchedPosts(pids: number[], data: ISearchData): Promise { + const postFields = ['pid', 'uid', 'tid', 'timestamp', 'deleted', 'upvotes', 'downvotes']; + + let postsData = await posts.getPostsFields(pids, postFields); + postsData = postsData.filter((post: IPost) => post && !post.deleted); + const uids: number[] = _.uniq(postsData.map((post: IPost) => post.uid)); + const tids: number[] = _.uniq(postsData.map((post: IPost) => post.tid)); + + const [users, topics] = await Promise.all([ + getUsers(uids, data), + getTopics(tids, data), + ]); + + const tidToTopic = _.zipObject(tids, topics); + const uidToUser = _.zipObject(uids, users); + + postsData.forEach((post: IPost) => { + if (topics && tidToTopic[post.tid]) { + post.topic = tidToTopic[post.tid]; + if (post.topic && post.topic.category) { + post.category = post.topic.category; + } + } + + if (uidToUser[post.uid]) { + post.user = uidToUser[post.uid]; + } + }); + + return postsData.filter((post: IPost) => post && post.topic && !post.topic.deleted); +} + +async function getUsers(uids: number[], data: ISearchData): Promise { + if (data.sortBy.startsWith('user')) { + return user.getUsersFields(uids, ['username']); + } + return []; +} + +async function getTopics(tids: number[], data: ISearchData): Promise { + const topicsData = await topics.getTopicsData(tids); + const cids: number[] = _.uniq(topicsData.map((topic: ITopic) => topic && topic.cid)); + const categories = await getCategories(cids, data); + + const cidToCategory = _.zipObject(cids, categories); + topicsData.forEach((topic: ITopic) => { + if (topic && categories && cidToCategory[topic.cid]) { + topic.category = cidToCategory[topic.cid]; + } + if (topic?.tags) { + topic.tags = topic.tags.map((tag: ITag) => tag.value); + } + }); + + return topicsData; +} + +async function getCategories(cids: number[], data: ISearchData): Promise[]> { + const categoryFields = []; + + if (data.sortBy.startsWith('category.')) { + categoryFields.push(data.sortBy.split('.')[1]); + } + if (!categoryFields.length) { + return null; + } + + return await db.getObjectsFields(cids.map(cid => `category:${cid}`), categoryFields); +} + +function filterByPostcount(posts: IPost[], postCount: string, repliesFilter: string): IPost[] { + const parsedPostCount = parseInt(postCount, 10); + if (postCount) { + const filterCondition = repliesFilter === 'atleast' + ? (post: IPost) => Number(post?.topic.postcount) >= parsedPostCount + : (post: IPost) => Number(post?.topic.postcount) <= parsedPostCount; + + posts = posts.filter(filterCondition); + } + return posts; +} + +function filterByTimerange(posts: IPost[], timeRange: string, timeFilter: string): IPost[] { + const parsedTimeRange = parseInt(timeRange, 10) * 1000; + if (timeRange) { + const time = Date.now() - parsedTimeRange; + if (timeFilter === 'newer') { + posts = posts.filter(post => post.timestamp >= time); + } else { + posts = posts.filter(post => post.timestamp <= time); + } + } + return posts; +} + +function filterByTags(posts: IPost[], hasTags: string): IPost[] { + if (Array.isArray(hasTags) && hasTags.length) { + posts = posts.filter((post) => { + let hasAllTags = false; + if (post && post.topic && Array.isArray(post.topic.tags) && post.topic.tags.length) { + hasAllTags = hasTags.every(tag => post.topic.tags.includes(tag)); + } + return hasAllTags; + }); + } + return posts; +} + +function sortPosts(posts: IPost[], data: ISearchData): IPost[] { + if (!posts.length || data.sortBy === 'relevance') { + return; + } + + data.sortDirection = data.sortDirection || 'desc'; + const direction = data.sortDirection === 'desc' ? 1 : -1; + const fields = data.sortBy.split('.'); + if (fields.length === 1) { + return posts.sort((post_1, post_2) => direction * (post_2[fields[0]] - post_1[fields[0]])); + } + + const firstPost = posts[0]; + if (!fields || fields.length !== 2 || !firstPost[fields[0]] || !firstPost[fields[0]][fields[1]]) { + return; + } + + const isNumeric = utils.isNumber(firstPost[fields[0]][fields[1]]); + + if (isNumeric) { + posts.sort((post_1, post_2) => direction * (post_2[fields[0]][fields[1]] - post_1[fields[0]][fields[1]])); + } else { + posts.sort((post_1, post_2) => { + if (post_1[fields[0]][fields[1]] > post_2[fields[0]][fields[1]]) { + return direction; + } else if (post_1[fields[0]][fields[1]] < post_2[fields[0]][fields[1]]) { + return -direction; + } + return 0; + }); + } +} + +async function getSearchCids(data) { + if (!Array.isArray(data.categories) || !data.categories.length) { + return []; + } + + if (data.categories.includes('all')) { + return await categories.getCidsByPrivilege('categories:cid', data.uid, 'read'); + } + + const [watchedCids, childrenCids] = await Promise.all([ + getWatchedCids(data), + getChildrenCids(data), + ]); + return _.uniq(watchedCids.concat(childrenCids).concat(data.categories).filter(Boolean)); +} + +async function getWatchedCids(data) { + if (!data.categories.includes('watched')) { + return []; + } + return await user.getWatchedCategories(data.uid); +} + +async function getChildrenCids(data) { + if (!data.searchChildren) { + return []; + } + const childrenCids = await Promise.all(data.categories.map(cid => categories.getChildrenCids(cid))); + return await privileges.categories.filterCids('find', _.uniq(_.flatten(childrenCids)), data.uid); +} + +async function getSearchUids(data) { + if (!data.postedBy) { + return []; + } + return await user.getUidsByUsernames(Array.isArray(data.postedBy) ? data.postedBy : [data.postedBy]); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..b12db9f68c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "esModuleInterop": true + } +} \ No newline at end of file From aed1b33b44ff758d7fce47ce57dfb6dcfd043278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 02:03:20 -0400 Subject: [PATCH 02/29] FIX: Correct some types in ISearchData Interface - GetSearchCids typed - sortPosts typed --- src/interfaces/search.ts | 2 +- src/search.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/interfaces/search.ts b/src/interfaces/search.ts index a4a49e5bc0..ee65bac5d4 100644 --- a/src/interfaces/search.ts +++ b/src/interfaces/search.ts @@ -3,7 +3,7 @@ export interface ISearchData { searchIn: string, uid?: number; hasTags?: string, - categories?: number[], + categories?: string[], searchChildren?: boolean, sortBy?: string, sortDirection?: string, diff --git a/src/search.ts b/src/search.ts index a521488196..dce74ba08e 100644 --- a/src/search.ts +++ b/src/search.ts @@ -297,12 +297,14 @@ function sortPosts(posts: IPost[], data: ISearchData): IPost[] { data.sortDirection = data.sortDirection || 'desc'; const direction = data.sortDirection === 'desc' ? 1 : -1; const fields = data.sortBy.split('.'); + if (fields.length === 1) { return posts.sort((post_1, post_2) => direction * (post_2[fields[0]] - post_1[fields[0]])); } const firstPost = posts[0]; - if (!fields || fields.length !== 2 || !firstPost[fields[0]] || !firstPost[fields[0]][fields[1]]) { + const isValid = fields && fields.length === 2 && firstPost?.[fields[0]]?.[fields[1]]; + if (!isValid) { return; } @@ -322,7 +324,7 @@ function sortPosts(posts: IPost[], data: ISearchData): IPost[] { } } -async function getSearchCids(data) { +async function getSearchCids(data: ISearchData): Promise { if (!Array.isArray(data.categories) || !data.categories.length) { return []; } From c657c7e7529747df5d3eb312783072761d02589a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 02:10:06 -0400 Subject: [PATCH 03/29] FIX: Search.ts typing completed - Create new interceptor exppression in getSearchCids to convert categories into number before calling uniq method --- src/interfaces/search.ts | 3 ++- src/search.ts | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/interfaces/search.ts b/src/interfaces/search.ts index ee65bac5d4..0b9a6a381b 100644 --- a/src/interfaces/search.ts +++ b/src/interfaces/search.ts @@ -14,5 +14,6 @@ export interface ISearchData { replies?: string, timeRange?: string, repliesFilter?: any, - timeFilter?: any + timeFilter?: any, + postedBy?: string } \ No newline at end of file diff --git a/src/search.ts b/src/search.ts index dce74ba08e..6ab7342c6c 100644 --- a/src/search.ts +++ b/src/search.ts @@ -337,17 +337,22 @@ async function getSearchCids(data: ISearchData): Promise { getWatchedCids(data), getChildrenCids(data), ]); - return _.uniq(watchedCids.concat(childrenCids).concat(data.categories).filter(Boolean)); + + const categoryIds = data.categories + .map(category => Number(category)) + .filter(cid => !isNaN(cid)); + + return _.uniq(watchedCids.concat(childrenCids).concat(categoryIds).filter(Boolean)); } -async function getWatchedCids(data) { +async function getWatchedCids(data: ISearchData): Promise { if (!data.categories.includes('watched')) { return []; } return await user.getWatchedCategories(data.uid); } -async function getChildrenCids(data) { +async function getChildrenCids(data: ISearchData): Promise { if (!data.searchChildren) { return []; } @@ -355,7 +360,7 @@ async function getChildrenCids(data) { return await privileges.categories.filterCids('find', _.uniq(_.flatten(childrenCids)), data.uid); } -async function getSearchUids(data) { +async function getSearchUids(data: ISearchData): Promise { if (!data.postedBy) { return []; } From 53f7f8429ce2e4d3d5e94be4aba0032373826471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 02:25:01 -0400 Subject: [PATCH 04/29] FIX: Improving data typing using an spread operator to handle concat. --- src/interfaces/search.ts | 2 +- src/search.ts | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/interfaces/search.ts b/src/interfaces/search.ts index 0b9a6a381b..ab968e3bed 100644 --- a/src/interfaces/search.ts +++ b/src/interfaces/search.ts @@ -3,7 +3,7 @@ export interface ISearchData { searchIn: string, uid?: number; hasTags?: string, - categories?: string[], + categories?: any[], searchChildren?: boolean, sortBy?: string, sortDirection?: string, diff --git a/src/search.ts b/src/search.ts index 6ab7342c6c..9458b8c844 100644 --- a/src/search.ts +++ b/src/search.ts @@ -338,11 +338,9 @@ async function getSearchCids(data: ISearchData): Promise { getChildrenCids(data), ]); - const categoryIds = data.categories - .map(category => Number(category)) - .filter(cid => !isNaN(cid)); + const concatenatedData = [...watchedCids, ...childrenCids, ...data.categories]; - return _.uniq(watchedCids.concat(childrenCids).concat(categoryIds).filter(Boolean)); + return _.uniq(concatenatedData.filter(Boolean)); } async function getWatchedCids(data: ISearchData): Promise { From cfe87f0c6526ccd63df9e877184620862ec07a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 02:31:46 -0400 Subject: [PATCH 05/29] FIX: Update Cid typing with an extra string type --- src/interfaces/search.ts | 2 +- src/search.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interfaces/search.ts b/src/interfaces/search.ts index ab968e3bed..0b9a6a381b 100644 --- a/src/interfaces/search.ts +++ b/src/interfaces/search.ts @@ -3,7 +3,7 @@ export interface ISearchData { searchIn: string, uid?: number; hasTags?: string, - categories?: any[], + categories?: string[], searchChildren?: boolean, sortBy?: string, sortDirection?: string, diff --git a/src/search.ts b/src/search.ts index 9458b8c844..f5f952afd2 100644 --- a/src/search.ts +++ b/src/search.ts @@ -121,7 +121,7 @@ async function searchInContent(data: ISearchData) { return Object.assign(returnData, metadata); } -async function searchInBookmarks(data: ISearchData, searchCids: number[], searchUids: number[]): Promise { +async function searchInBookmarks(data: ISearchData, searchCids: (string | number)[], searchUids: number[]): Promise { const { uid, query, matchWords } = data; const allPids: number[] = []; await batch.processSortedSet(`uid:${uid}:bookmarks`, async (pids: number[]) => { @@ -324,7 +324,7 @@ function sortPosts(posts: IPost[], data: ISearchData): IPost[] { } } -async function getSearchCids(data: ISearchData): Promise { +async function getSearchCids(data: ISearchData): Promise<(string | number)[]> { if (!Array.isArray(data.categories) || !data.categories.length) { return []; } From 0d7d70819f6aba6847fb1e9f7e1c702f03db4c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 03:06:48 -0400 Subject: [PATCH 06/29] ADD: New type setting on .eslintrc to avoid some tslint issues - Updated utils.js adding elapsedTimeSince as method to use without process --- .eslintrc | 17 ++++++++++++++++- src/search.ts | 51 ++++++++++++++++++++++++++------------------------- src/utils.js | 18 ++++++++++++++---- tsconfig.json | 5 ++++- 4 files changed, 60 insertions(+), 31 deletions(-) diff --git a/.eslintrc b/.eslintrc index abd292af1b..6e3cfed9bf 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,18 @@ { - "extends": "nodebb" + "parser": "@typescript-eslint/parser", + "extends": [ + "nodebb", + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "plugins": [ + "@typescript-eslint" + ], + "settings": { + "import/resolver": { + "typescript": { + "alwaysTryTypes": true + } + } + } } diff --git a/src/search.ts b/src/search.ts index f5f952afd2..ff8cf1cb89 100644 --- a/src/search.ts +++ b/src/search.ts @@ -1,4 +1,4 @@ -import _, { Dictionary } from 'lodash'; +import { Dictionary, flatten, uniq, zipObject } from 'lodash'; import batch from './batch'; import categories from './categories'; import db from './database'; @@ -11,11 +11,11 @@ import topics from './topics'; import user from './user'; import utils from './utils'; -export const search = { +export default { search: async function (data: ISearchData) { const start = process.hrtime(); data.sortBy = data.sortBy || 'relevance'; - let result: any; + let result: { time: string, [key: string]: unknown }; if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { result = await searchInContent(data); @@ -33,10 +33,10 @@ export const search = { throw new Error('[[error:unknown-search-filter]]'); } - result.time = (process.elapsedTimeSince(start) / 1000).toFixed(2); + result.time = (utils.elapsedTimeSince(start) / 1000).toFixed(2); return result; - } -} + }, +}; async function searchInContent(data: ISearchData) { data.uid = data.uid || 0; @@ -94,10 +94,10 @@ async function searchInContent(data: ISearchData) { if (data.returnIds) { const mainPidsSet = new Set(mainPids); - const mainPidToTid = _.zipObject(mainPids, tids); + const mainPidToTid = zipObject(mainPids, tids); const pidsSet = new Set(pids); - const returnPids = allPids.filter(pid => pidsSet.has(pid)); - const returnTids = allPids.filter(pid => mainPidsSet.has(pid)).map(pid => mainPidToTid[pid]); + const returnPids = allPids.filter((pid: number) => pidsSet.has(pid)); + const returnTids = allPids.filter((pid: number) => mainPidsSet.has(pid)).map(pid => mainPidToTid[pid]); return { pids: returnPids, tids: returnTids }; } @@ -121,7 +121,8 @@ async function searchInContent(data: ISearchData) { return Object.assign(returnData, metadata); } -async function searchInBookmarks(data: ISearchData, searchCids: (string | number)[], searchUids: number[]): Promise { +async function searchInBookmarks(data: ISearchData, + searchCids: (string | number)[], searchUids: number[]): Promise { const { uid, query, matchWords } = data; const allPids: number[] = []; await batch.processSortedSet(`uid:${uid}:bookmarks`, async (pids: number[]) => { @@ -136,9 +137,9 @@ async function searchInBookmarks(data: ISearchData, searchCids: (string | number if (query) { const tokens = query.toString().split(' '); const postData = await db.getObjectsFields(pids.map(pid => `post:${pid}`), ['content', 'tid']); - const tids: number[] = _.uniq(postData.map((p: { tid: number; }) => p.tid)); + const tids: number[] = uniq(postData.map((p: { tid: number; }) => p.tid)); const topicData = await db.getObjectsFields(tids.map(tid => `topic:${tid}`), ['title']); - const tidToTopic: Dictionary<{ title: string }> = _.zipObject(tids, topicData); + const tidToTopic: Dictionary<{ title: string }> = zipObject(tids, topicData); pids = pids.filter((_, i) => { const content = postData[i].content.toString(); const title = tidToTopic[postData[i].tid].title.toString(); @@ -178,7 +179,7 @@ async function filterAndSort(pids: number[], data: ISearchData): Promise post && post.pid); + return result.posts.map((post: IPost) => post && post.pid); } async function getMatchedPosts(pids: number[], data: ISearchData): Promise { @@ -186,16 +187,16 @@ async function getMatchedPosts(pids: number[], data: ISearchData): Promise post && !post.deleted); - const uids: number[] = _.uniq(postsData.map((post: IPost) => post.uid)); - const tids: number[] = _.uniq(postsData.map((post: IPost) => post.tid)); + const uids: number[] = uniq(postsData.map((post: IPost) => post.uid)); + const tids: number[] = uniq(postsData.map((post: IPost) => post.tid)); const [users, topics] = await Promise.all([ getUsers(uids, data), getTopics(tids, data), ]); - const tidToTopic = _.zipObject(tids, topics); - const uidToUser = _.zipObject(uids, users); + const tidToTopic = zipObject(tids, topics); + const uidToUser = zipObject(uids, users); postsData.forEach((post: IPost) => { if (topics && tidToTopic[post.tid]) { @@ -222,10 +223,10 @@ async function getUsers(uids: number[], data: ISearchData): Promise { async function getTopics(tids: number[], data: ISearchData): Promise { const topicsData = await topics.getTopicsData(tids); - const cids: number[] = _.uniq(topicsData.map((topic: ITopic) => topic && topic.cid)); + const cids: number[] = uniq(topicsData.map((topic: ITopic) => topic && topic.cid)); const categories = await getCategories(cids, data); - const cidToCategory = _.zipObject(cids, categories); + const cidToCategory = zipObject(cids, categories); topicsData.forEach((topic: ITopic) => { if (topic && categories && cidToCategory[topic.cid]) { topic.category = cidToCategory[topic.cid]; @@ -254,9 +255,9 @@ async function getCategories(cids: number[], data: ISearchData): Promise Number(post?.topic.postcount) >= parsedPostCount - : (post: IPost) => Number(post?.topic.postcount) <= parsedPostCount; + const filterCondition = repliesFilter === 'atleast' ? + (post: IPost) => Number(post?.topic.postcount) >= parsedPostCount : + (post: IPost) => Number(post?.topic.postcount) <= parsedPostCount; posts = posts.filter(filterCondition); } @@ -340,7 +341,7 @@ async function getSearchCids(data: ISearchData): Promise<(string | number)[]> { const concatenatedData = [...watchedCids, ...childrenCids, ...data.categories]; - return _.uniq(concatenatedData.filter(Boolean)); + return uniq(concatenatedData.filter(Boolean)); } async function getWatchedCids(data: ISearchData): Promise { @@ -355,7 +356,7 @@ async function getChildrenCids(data: ISearchData): Promise { return []; } const childrenCids = await Promise.all(data.categories.map(cid => categories.getChildrenCids(cid))); - return await privileges.categories.filterCids('find', _.uniq(_.flatten(childrenCids)), data.uid); + return await privileges.categories.filterCids('find', uniq(flatten(childrenCids)), data.uid); } async function getSearchUids(data: ISearchData): Promise { @@ -363,4 +364,4 @@ async function getSearchUids(data: ISearchData): Promise { return []; } return await user.getUidsByUsernames(Array.isArray(data.postedBy) ? data.postedBy : [data.postedBy]); -} \ No newline at end of file +} diff --git a/src/utils.js b/src/utils.js index fb59865f69..f95bf25cef 100644 --- a/src/utils.js +++ b/src/utils.js @@ -5,18 +5,28 @@ const nconf = require('nconf'); const path = require('node:path'); process.profile = function (operation, start) { - console.log('%s took %d milliseconds', operation, process.elapsedTimeSince(start)); + console.log( + '%s took %d milliseconds', + operation, + process.elapsedTimeSince(start) + ); }; -process.elapsedTimeSince = function (start) { +var elapsedTimeSince = function (start) { const diff = process.hrtime(start); - return (diff[0] * 1e3) + (diff[1] / 1e6); + return diff[0] * 1e3 + diff[1] / 1e6; }; + +process.elapsedTimeSince = elapsedTimeSince; const utils = { ...require('../public/src/utils.common') }; +utils.elapsedTimeSince = elapsedTimeSince; + utils.getLanguage = function () { const meta = require('./meta'); - return meta.config && meta.config.defaultLang ? meta.config.defaultLang : 'en-GB'; + return meta.config && meta.config.defaultLang + ? meta.config.defaultLang + : 'en-GB'; }; utils.generateUUID = function () { diff --git a/tsconfig.json b/tsconfig.json index b12db9f68c..d2035b08f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,9 @@ { "compilerOptions": { "moduleResolution": "node", - "esModuleInterop": true + "esModuleInterop": true, + "declaration": true, + "emitDeclarationOnly": true, + "allowImportingTsExtensions": true } } \ No newline at end of file From cf93e3a775454559fbe740db6383ae185f3bd83c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 04:11:02 -0400 Subject: [PATCH 07/29] FIX: Enhanced types in Data interface, add some eslinter deactivation - @typescript-eslint/no-unsafe-argument - @typescript-eslint/no-unsafe-assignment - @typescript-eslint/no-unsafe-return - No way to get over these errors --- src/interfaces/search.ts | 4 +- src/search.ts | 645 ++++++++++++++++++++------------------- tsconfig.json | 3 + 3 files changed, 330 insertions(+), 322 deletions(-) diff --git a/src/interfaces/search.ts b/src/interfaces/search.ts index 0b9a6a381b..2403fb4283 100644 --- a/src/interfaces/search.ts +++ b/src/interfaces/search.ts @@ -13,7 +13,7 @@ export interface ISearchData { page?: number, replies?: string, timeRange?: string, - repliesFilter?: any, - timeFilter?: any, + repliesFilter?: string, + timeFilter?: string, postedBy?: string } \ No newline at end of file diff --git a/src/search.ts b/src/search.ts index ff8cf1cb89..8953240be6 100644 --- a/src/search.ts +++ b/src/search.ts @@ -1,9 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ +/* These comments are because this file imported some modules form .js files, so the linter isn't working */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { Dictionary, flatten, uniq, zipObject } from 'lodash'; import batch from './batch'; import categories from './categories'; import db from './database'; -import { IPost, ITag, ITopic } from './interfaces/post'; -import { ISearchData } from './interfaces/search'; +import { IPost, ITag, ITopic } from './interfaces/post.ts'; +import { ISearchData } from './interfaces/search.ts'; import plugins from './plugins'; import posts from './posts'; import privileges from './privileges'; @@ -11,357 +16,357 @@ import topics from './topics'; import user from './user'; import utils from './utils'; -export default { - search: async function (data: ISearchData) { - const start = process.hrtime(); - data.sortBy = data.sortBy || 'relevance'; - let result: { time: string, [key: string]: unknown }; - - if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { - result = await searchInContent(data); - } else if (data.searchIn === 'users') { - result = await user.search(data); - } else if (data.searchIn === 'categories') { - result = await categories.search(data); - } else if (data.searchIn === 'tags') { - result = await topics.searchAndLoadTags(data); - } else if (data.searchIn) { - result = await plugins.hooks.fire('filter:search.searchIn', { - data, - }); - } else { - throw new Error('[[error:unknown-search-filter]]'); - } - - result.time = (utils.elapsedTimeSince(start) / 1000).toFixed(2); - return result; - }, -}; - -async function searchInContent(data: ISearchData) { - data.uid = data.uid || 0; - - const [searchCids, searchUids] = await Promise.all([ - getSearchCids(data), - getSearchUids(data), - ]); - - async function doSearch(type: string, searchIn: string[]): Promise { - if (searchIn.includes(data.searchIn)) { - const result = await plugins.hooks.fire('filter:search.query', { - index: type, - content: data.query, - matchWords: data.matchWords || 'all', - cid: searchCids, - uid: searchUids, - searchData: data, - ids: [], - }); - return Array.isArray(result) ? result : result.ids; - } - return []; - } - - let pids = []; - let tids = []; - - const inTopic = `${data.query || ''}`.match(/^in:topic-([\d]+) /); - - if (inTopic) { - const tid = inTopic[1]; - const cleanedTerm = data.query.replace(inTopic[0], ''); - pids = await topics.search(tid, cleanedTerm); - } else if (data.searchIn === 'bookmarks') { - pids = await searchInBookmarks(data, searchCids, searchUids); - } else { - [pids, tids] = await Promise.all([ - doSearch('post', ['posts', 'titlesposts']), - doSearch('topic', ['titles', 'titlesposts']), - ]); - } - - const mainPids = await topics.getMainPids(tids); - - let allPids = mainPids.concat(pids).filter(Boolean); - - allPids = await privileges.posts.filter('topics:read', allPids, data.uid); - allPids = await filterAndSort(allPids, data); - - const metadata = await plugins.hooks.fire('filter:search.inContent', { - pids: allPids, - data: data, - }); - - if (data.returnIds) { - const mainPidsSet = new Set(mainPids); - const mainPidToTid = zipObject(mainPids, tids); - const pidsSet = new Set(pids); - const returnPids = allPids.filter((pid: number) => pidsSet.has(pid)); - const returnTids = allPids.filter((pid: number) => mainPidsSet.has(pid)).map(pid => mainPidToTid[pid]); - return { pids: returnPids, tids: returnTids }; - } - - const itemsPerPage = Math.min(data.itemsPerPage || 10, 100); - - const returnData = { - posts: [], - matchCount: metadata.pids.length, - pageCount: Math.max(1, Math.ceil(parseInt(metadata.pids.length, 10) / itemsPerPage)), - }; - - if (data.page) { - const start = Math.max(0, (data.page - 1)) * itemsPerPage; - metadata.pids = metadata.pids.slice(start, start + itemsPerPage); - } - - returnData.posts = await posts.getPostSummaryByPids(metadata.pids, data.uid, {}); - await plugins.hooks.fire('filter:search.contentGetResult', { result: returnData, data: data }); - delete metadata.pids; - delete metadata.data; - return Object.assign(returnData, metadata); +async function getWatchedCids(data: ISearchData): Promise { + if (!data.categories.includes('watched')) { + return []; + } + return await user.getWatchedCategories(data.uid); } -async function searchInBookmarks(data: ISearchData, - searchCids: (string | number)[], searchUids: number[]): Promise { - const { uid, query, matchWords } = data; - const allPids: number[] = []; - await batch.processSortedSet(`uid:${uid}:bookmarks`, async (pids: number[]) => { - if (Array.isArray(searchCids) && searchCids.length) { - pids = await posts.filterPidsByCid(pids, searchCids); - } - - if (Array.isArray(searchUids) && searchUids.length) { - pids = await posts.filterPidsByUid(pids, searchUids); - } - - if (query) { - const tokens = query.toString().split(' '); - const postData = await db.getObjectsFields(pids.map(pid => `post:${pid}`), ['content', 'tid']); - const tids: number[] = uniq(postData.map((p: { tid: number; }) => p.tid)); - const topicData = await db.getObjectsFields(tids.map(tid => `topic:${tid}`), ['title']); - const tidToTopic: Dictionary<{ title: string }> = zipObject(tids, topicData); - pids = pids.filter((_, i) => { - const content = postData[i].content.toString(); - const title = tidToTopic[postData[i].tid].title.toString(); - const method = (matchWords === 'any' ? 'some' : 'every'); - return tokens[method]( - token => content.includes(token) || title.includes(token) - ); - }); - } - allPids.push(...pids); - }, { - batch: 500, - }); - - return allPids; +async function getChildrenCids(data: ISearchData): Promise { + if (!data.searchChildren) { + return []; + } + const childrenCids: string[] = await Promise.all(data.categories.map(cid => categories.getChildrenCids(cid))); + return await privileges.categories.filterCids('find', uniq(flatten(childrenCids)), data.uid); } -async function filterAndSort(pids: number[], data: ISearchData): Promise { - if (data.sortBy === 'relevance' && - !data.replies && - !data.timeRange && - !data.hasTags && - data.searchIn !== 'bookmarks' && - !plugins.hooks.hasListeners('filter:search.filterAndSort')) { - return pids; - } - let postsData = await getMatchedPosts(pids, data); - if (!postsData.length) { - return pids; - } - postsData = postsData.filter(Boolean); - - postsData = filterByPostcount(postsData, data.replies, data.repliesFilter); - postsData = filterByTimerange(postsData, data.timeRange, data.timeFilter); - postsData = filterByTags(postsData, data.hasTags); - - sortPosts(postsData, data); - - const result = await plugins.hooks.fire('filter:search.filterAndSort', { pids: pids, posts: postsData, data: data }); - return result.posts.map((post: IPost) => post && post.pid); +async function getSearchUids(data: ISearchData): Promise { + if (!data.postedBy) { + return []; + } + return await user.getUidsByUsernames(Array.isArray(data.postedBy) ? data.postedBy : [data.postedBy]); } -async function getMatchedPosts(pids: number[], data: ISearchData): Promise { - const postFields = ['pid', 'uid', 'tid', 'timestamp', 'deleted', 'upvotes', 'downvotes']; - - let postsData = await posts.getPostsFields(pids, postFields); - postsData = postsData.filter((post: IPost) => post && !post.deleted); - const uids: number[] = uniq(postsData.map((post: IPost) => post.uid)); - const tids: number[] = uniq(postsData.map((post: IPost) => post.tid)); - - const [users, topics] = await Promise.all([ - getUsers(uids, data), - getTopics(tids, data), - ]); - - const tidToTopic = zipObject(tids, topics); - const uidToUser = zipObject(uids, users); - - postsData.forEach((post: IPost) => { - if (topics && tidToTopic[post.tid]) { - post.topic = tidToTopic[post.tid]; - if (post.topic && post.topic.category) { - post.category = post.topic.category; - } - } - - if (uidToUser[post.uid]) { - post.user = uidToUser[post.uid]; - } - }); - - return postsData.filter((post: IPost) => post && post.topic && !post.topic.deleted); -} +async function getSearchCids(data: ISearchData): Promise<(string | number)[]> { + if (!Array.isArray(data.categories) || !data.categories.length) { + return []; + } -async function getUsers(uids: number[], data: ISearchData): Promise { - if (data.sortBy.startsWith('user')) { - return user.getUsersFields(uids, ['username']); - } - return []; -} + if (data.categories.includes('all')) { + return await categories.getCidsByPrivilege('categories:cid', data.uid, 'read'); + } -async function getTopics(tids: number[], data: ISearchData): Promise { - const topicsData = await topics.getTopicsData(tids); - const cids: number[] = uniq(topicsData.map((topic: ITopic) => topic && topic.cid)); - const categories = await getCategories(cids, data); - - const cidToCategory = zipObject(cids, categories); - topicsData.forEach((topic: ITopic) => { - if (topic && categories && cidToCategory[topic.cid]) { - topic.category = cidToCategory[topic.cid]; - } - if (topic?.tags) { - topic.tags = topic.tags.map((tag: ITag) => tag.value); - } - }); - - return topicsData; -} + const [watchedCids, childrenCids] = await Promise.all([ + getWatchedCids(data), + getChildrenCids(data), + ]); -async function getCategories(cids: number[], data: ISearchData): Promise[]> { - const categoryFields = []; + const concatenatedData = [...watchedCids, ...childrenCids, ...data.categories]; - if (data.sortBy.startsWith('category.')) { - categoryFields.push(data.sortBy.split('.')[1]); - } - if (!categoryFields.length) { - return null; - } + return uniq(concatenatedData.filter(Boolean)); +} - return await db.getObjectsFields(cids.map(cid => `category:${cid}`), categoryFields); +async function searchInBookmarks(data: ISearchData, + searchCids: (string | number)[], searchUids: number[]): Promise { + const { uid, query, matchWords } = data; + const allPids: number[] = []; + await batch.processSortedSet(`uid:${uid}:bookmarks`, async (pids: number[]) => { + if (Array.isArray(searchCids) && searchCids.length) { + pids = await posts.filterPidsByCid(pids, searchCids); + } + + if (Array.isArray(searchUids) && searchUids.length) { + pids = await posts.filterPidsByUid(pids, searchUids); + } + + if (query) { + const tokens = query.toString().split(' '); + const postData: { tid: number; content: unknown }[] = await db.getObjectsFields(pids.map(pid => `post:${pid}`), ['content', 'tid']); + const tids: number[] = uniq(postData.map((p: { tid: number; }) => p.tid)); + const topicData: { title: string }[] = await db.getObjectsFields(tids.map(tid => `topic:${tid}`), ['title']); + const tidToTopic: Dictionary<{ title: string }> = zipObject(tids, topicData); + pids = pids.filter((_, i) => { + const content = JSON.stringify(postData[i].content); + const title = `${tidToTopic[postData[i].tid].title}`; + const method = (matchWords === 'any' ? 'some' : 'every'); + return tokens[method]( + token => content.includes(token) || title.includes(token) + ); + }); + } + allPids.push(...pids); + }, { + batch: 500, + }); + + return allPids; } function filterByPostcount(posts: IPost[], postCount: string, repliesFilter: string): IPost[] { - const parsedPostCount = parseInt(postCount, 10); - if (postCount) { - const filterCondition = repliesFilter === 'atleast' ? - (post: IPost) => Number(post?.topic.postcount) >= parsedPostCount : - (post: IPost) => Number(post?.topic.postcount) <= parsedPostCount; - - posts = posts.filter(filterCondition); - } - return posts; + const parsedPostCount = parseInt(postCount, 10); + if (postCount) { + const filterCondition = repliesFilter === 'atleast' ? + (post: IPost) => Number(post?.topic.postcount) >= parsedPostCount : + (post: IPost) => Number(post?.topic.postcount) <= parsedPostCount; + + posts = posts.filter(filterCondition); + } + return posts; } function filterByTimerange(posts: IPost[], timeRange: string, timeFilter: string): IPost[] { - const parsedTimeRange = parseInt(timeRange, 10) * 1000; - if (timeRange) { - const time = Date.now() - parsedTimeRange; - if (timeFilter === 'newer') { - posts = posts.filter(post => post.timestamp >= time); - } else { - posts = posts.filter(post => post.timestamp <= time); - } - } - return posts; + const parsedTimeRange = parseInt(timeRange, 10) * 1000; + if (timeRange) { + const time = Date.now() - parsedTimeRange; + if (timeFilter === 'newer') { + posts = posts.filter(post => post.timestamp >= time); + } else { + posts = posts.filter(post => post.timestamp <= time); + } + } + return posts; } function filterByTags(posts: IPost[], hasTags: string): IPost[] { - if (Array.isArray(hasTags) && hasTags.length) { - posts = posts.filter((post) => { - let hasAllTags = false; - if (post && post.topic && Array.isArray(post.topic.tags) && post.topic.tags.length) { - hasAllTags = hasTags.every(tag => post.topic.tags.includes(tag)); - } - return hasAllTags; - }); - } - return posts; + if (Array.isArray(hasTags) && hasTags.length) { + posts = posts.filter((post) => { + let hasAllTags = false; + if (post && post.topic && Array.isArray(post.topic.tags) && post.topic.tags.length) { + hasAllTags = hasTags.every((tag: string) => post.topic.tags.includes(tag)); + } + return hasAllTags; + }); + } + return posts; } function sortPosts(posts: IPost[], data: ISearchData): IPost[] { - if (!posts.length || data.sortBy === 'relevance') { - return; - } - - data.sortDirection = data.sortDirection || 'desc'; - const direction = data.sortDirection === 'desc' ? 1 : -1; - const fields = data.sortBy.split('.'); - - if (fields.length === 1) { - return posts.sort((post_1, post_2) => direction * (post_2[fields[0]] - post_1[fields[0]])); - } - - const firstPost = posts[0]; - const isValid = fields && fields.length === 2 && firstPost?.[fields[0]]?.[fields[1]]; - if (!isValid) { - return; - } - - const isNumeric = utils.isNumber(firstPost[fields[0]][fields[1]]); - - if (isNumeric) { - posts.sort((post_1, post_2) => direction * (post_2[fields[0]][fields[1]] - post_1[fields[0]][fields[1]])); - } else { - posts.sort((post_1, post_2) => { - if (post_1[fields[0]][fields[1]] > post_2[fields[0]][fields[1]]) { - return direction; - } else if (post_1[fields[0]][fields[1]] < post_2[fields[0]][fields[1]]) { - return -direction; - } - return 0; - }); - } + if (!posts.length || data.sortBy === 'relevance') { + return; + } + + data.sortDirection = data.sortDirection || 'desc'; + const direction = data.sortDirection === 'desc' ? 1 : -1; + const fields = data.sortBy.split('.'); + + if (fields.length === 1) { + return posts.sort((post_1, post_2) => direction * (post_2[fields[0]] - post_1[fields[0]])); + } + + const firstPost = posts[0]; + const isValid = fields && fields.length === 2 && firstPost?.[fields[0]]?.[fields[1]]; + if (!isValid) { + return; + } + + const isNumeric = utils.isNumber(firstPost[fields[0]][fields[1]]); + + if (isNumeric) { + posts.sort((post_1, post_2) => direction * (post_2[fields[0]][fields[1]] - post_1[fields[0]][fields[1]])); + } else { + posts.sort((post_1, post_2) => { + if (post_1[fields[0]][fields[1]] > post_2[fields[0]][fields[1]]) { + return direction; + } else if (post_1[fields[0]][fields[1]] < post_2[fields[0]][fields[1]]) { + return -direction; + } + return 0; + }); + } } -async function getSearchCids(data: ISearchData): Promise<(string | number)[]> { - if (!Array.isArray(data.categories) || !data.categories.length) { - return []; - } +async function getUsers(uids: number[], data: ISearchData): Promise { + if (data.sortBy.startsWith('user')) { + return await user.getUsersFields(uids, ['username']); + } + return []; +} - if (data.categories.includes('all')) { - return await categories.getCidsByPrivilege('categories:cid', data.uid, 'read'); - } +async function getCategories(cids: number[], data: ISearchData): Promise[]> { + const categoryFields = []; - const [watchedCids, childrenCids] = await Promise.all([ - getWatchedCids(data), - getChildrenCids(data), - ]); + if (data.sortBy.startsWith('category.')) { + categoryFields.push(data.sortBy.split('.')[1]); + } + if (!categoryFields.length) { + return null; + } - const concatenatedData = [...watchedCids, ...childrenCids, ...data.categories]; + return await db.getObjectsFields(cids.map(cid => `category:${cid}`), categoryFields); +} - return uniq(concatenatedData.filter(Boolean)); +async function getTopics(tids: number[], data: ISearchData): Promise { + const topicsData: ITopic[] = await topics.getTopicsData(tids); + const cids: number[] = uniq(topicsData.map((topic: ITopic) => topic && topic.cid)); + const categories = await getCategories(cids, data); + + const cidToCategory = zipObject(cids, categories); + topicsData.forEach((topic: ITopic) => { + if (topic && categories && cidToCategory[topic.cid]) { + topic.category = cidToCategory[topic.cid]; + } + if (topic?.tags) { + topic.tags = topic.tags.map((tag: ITag) => tag.value); + } + }); + + return topicsData; } -async function getWatchedCids(data: ISearchData): Promise { - if (!data.categories.includes('watched')) { - return []; - } - return await user.getWatchedCategories(data.uid); +async function getMatchedPosts(pids: number[], data: ISearchData): Promise { + const postFields = ['pid', 'uid', 'tid', 'timestamp', 'deleted', 'upvotes', 'downvotes']; + + let postsData: IPost[] = await posts.getPostsFields(pids, postFields); + postsData = postsData.filter((post: IPost) => post && !post.deleted); + const uids: number[] = uniq(postsData.map((post: IPost) => post.uid)); + const tids: number[] = uniq(postsData.map((post: IPost) => post.tid)); + + const [users, topics] = await Promise.all([ + getUsers(uids, data), + getTopics(tids, data), + ]); + + const tidToTopic = zipObject(tids, topics); + const uidToUser = zipObject(uids, users); + + postsData.forEach((post: IPost) => { + if (topics && tidToTopic[post.tid]) { + post.topic = tidToTopic[post.tid]; + if (post.topic && post.topic.category) { + post.category = post.topic.category; + } + } + + if (uidToUser[post.uid]) { + post.user = uidToUser[post.uid]; + } + }); + + return postsData.filter((post: IPost) => post && post.topic && !post.topic.deleted); } -async function getChildrenCids(data: ISearchData): Promise { - if (!data.searchChildren) { - return []; - } - const childrenCids = await Promise.all(data.categories.map(cid => categories.getChildrenCids(cid))); - return await privileges.categories.filterCids('find', uniq(flatten(childrenCids)), data.uid); +async function filterAndSort(pids: number[], data: ISearchData): Promise { + if (data.sortBy === 'relevance' && + !data.replies && + !data.timeRange && + !data.hasTags && + data.searchIn !== 'bookmarks' && + !plugins.hooks.hasListeners('filter:search.filterAndSort')) { + return pids; + } + let postsData = await getMatchedPosts(pids, data); + if (!postsData.length) { + return pids; + } + postsData = postsData.filter(Boolean); + + postsData = filterByPostcount(postsData, data.replies, data.repliesFilter); + postsData = filterByTimerange(postsData, data.timeRange, data.timeFilter); + postsData = filterByTags(postsData, data.hasTags); + + sortPosts(postsData, data); + + const result = await plugins.hooks.fire('filter:search.filterAndSort', { pids: pids, posts: postsData, data: data }); + return result.posts.map((post: IPost) => post && post.pid); } -async function getSearchUids(data: ISearchData): Promise { - if (!data.postedBy) { - return []; - } - return await user.getUidsByUsernames(Array.isArray(data.postedBy) ? data.postedBy : [data.postedBy]); +async function searchInContent(data: ISearchData) { + data.uid = data.uid || 0; + + const [searchCids, searchUids] = await Promise.all([ + getSearchCids(data), + getSearchUids(data), + ]); + + async function doSearch(type: string, searchIn: string[]): Promise { + if (searchIn.includes(data.searchIn)) { + const result = await plugins.hooks.fire('filter:search.query', { + index: type, + content: data.query, + matchWords: data.matchWords || 'all', + cid: searchCids, + uid: searchUids, + searchData: data, + ids: [], + }); + return Array.isArray(result) ? result : result.ids; + } + return []; + } + + let pids = []; + let tids = []; + + const inTopic = `${data.query || ''}`.match(/^in:topic-([\d]+) /); + + if (inTopic) { + const tid = inTopic[1]; + const cleanedTerm = data.query.replace(inTopic[0], ''); + pids = await topics.search(tid, cleanedTerm); + } else if (data.searchIn === 'bookmarks') { + pids = await searchInBookmarks(data, searchCids, searchUids); + } else { + [pids, tids] = await Promise.all([ + doSearch('post', ['posts', 'titlesposts']), + doSearch('topic', ['titles', 'titlesposts']), + ]); + } + + const mainPids = await topics.getMainPids(tids); + + let allPids = mainPids.concat(pids).filter(Boolean); + + allPids = await privileges.posts.filter('topics:read', allPids, data.uid); + allPids = await filterAndSort(allPids, data); + + const metadata = await plugins.hooks.fire('filter:search.inContent', { + pids: allPids, + data: data, + }); + + if (data.returnIds) { + const mainPidsSet = new Set(mainPids); + const mainPidToTid = zipObject(mainPids, tids); + const pidsSet = new Set(pids); + const returnPids = allPids.filter((pid: number) => pidsSet.has(pid)); + const returnTids = allPids.filter((pid: number) => mainPidsSet.has(pid)).map(pid => mainPidToTid[pid]); + return { pids: returnPids, tids: returnTids }; + } + + const itemsPerPage = Math.min(data.itemsPerPage || 10, 100); + + const returnData = { + posts: [], + matchCount: metadata.pids.length, + pageCount: Math.max(1, Math.ceil(parseInt(metadata.pids.length, 10) / itemsPerPage)), + }; + + if (data.page) { + const start = Math.max(0, (data.page - 1)) * itemsPerPage; + metadata.pids = metadata.pids.slice(start, start + itemsPerPage); + } + + returnData.posts = await posts.getPostSummaryByPids(metadata.pids, data.uid, {}); + await plugins.hooks.fire('filter:search.contentGetResult', { result: returnData, data: data }); + delete metadata.pids; + delete metadata.data; + return Object.assign(returnData, metadata); } + +export default { + search: async function (data: ISearchData) { + const start = process.hrtime(); + data.sortBy = data.sortBy || 'relevance'; + let result: { time: string, [key: string]: unknown }; + + if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { + result = await searchInContent(data); + } else if (data.searchIn === 'users') { + result = await user.search(data); + } else if (data.searchIn === 'categories') { + result = await categories.search(data); + } else if (data.searchIn === 'tags') { + result = await topics.searchAndLoadTags(data); + } else if (data.searchIn) { + result = await plugins.hooks.fire('filter:search.searchIn', { + data, + }); + } else { + throw new Error('[[error:unknown-search-filter]]'); + } + + result.time = (utils.elapsedTimeSince(start) / 1000).toFixed(2); + return result; + }, +}; diff --git a/tsconfig.json b/tsconfig.json index bea955fb22..41405d37e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,9 @@ "module": "commonjs", "moduleResolution": "node", "esModuleInterop": true, + "allowImportingTsExtensions": true, + "emitDeclarationOnly": true, + "declaration": true }, "include": [ "public/src/**/*", From 6a1b88af527f32f2d628795d30f842582ab72aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 04:13:27 -0400 Subject: [PATCH 08/29] Remove unnecesary explanation comment --- src/search.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/search.ts b/src/search.ts index 8953240be6..bc8c19d31a 100644 --- a/src/search.ts +++ b/src/search.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ -/* These comments are because this file imported some modules form .js files, so the linter isn't working */ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ From a586fa7ea73f49134661d417158e335105c74c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 04:33:02 -0400 Subject: [PATCH 09/29] FIX eslint tests errors --- src/search.ts | 4 ++-- tsconfig.json | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/search.ts b/src/search.ts index bc8c19d31a..743374771d 100644 --- a/src/search.ts +++ b/src/search.ts @@ -6,8 +6,8 @@ import { Dictionary, flatten, uniq, zipObject } from 'lodash'; import batch from './batch'; import categories from './categories'; import db from './database'; -import { IPost, ITag, ITopic } from './interfaces/post.ts'; -import { ISearchData } from './interfaces/search.ts'; +import { IPost, ITag, ITopic } from './interfaces/post'; +import { ISearchData } from './interfaces/search'; import plugins from './plugins'; import posts from './posts'; import privileges from './privileges'; diff --git a/tsconfig.json b/tsconfig.json index 41405d37e1..2eded7eadd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,10 +4,7 @@ "target": "es6", "module": "commonjs", "moduleResolution": "node", - "esModuleInterop": true, - "allowImportingTsExtensions": true, - "emitDeclarationOnly": true, - "declaration": true + "esModuleInterop": true }, "include": [ "public/src/**/*", From 98cb1c366b6e34ef2b235d2596bac6412fd63d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 04:43:11 -0400 Subject: [PATCH 10/29] ADD: Optional chain to tag.value --- src/search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search.ts b/src/search.ts index 743374771d..ce45a202cd 100644 --- a/src/search.ts +++ b/src/search.ts @@ -196,7 +196,7 @@ async function getTopics(tids: number[], data: ISearchData): Promise { topic.category = cidToCategory[topic.cid]; } if (topic?.tags) { - topic.tags = topic.tags.map((tag: ITag) => tag.value); + topic.tags = topic.tags.map((tag: ITag) => tag?.value); } }); From 72e1a5fc9a4d66d05a8d29c8a99a98001c61e595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 04:52:15 -0400 Subject: [PATCH 11/29] FIX Tag ambiguous typing --- src/search.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/search.ts b/src/search.ts index ce45a202cd..98cd8be0bb 100644 --- a/src/search.ts +++ b/src/search.ts @@ -195,8 +195,8 @@ async function getTopics(tids: number[], data: ISearchData): Promise { if (topic && categories && cidToCategory[topic.cid]) { topic.category = cidToCategory[topic.cid]; } - if (topic?.tags) { - topic.tags = topic.tags.map((tag: ITag) => tag?.value); + if (Array.isArray(topic.tags) && topic.tags.length > 0 && typeof topic.tags[0] !== 'string') { + topic.tags = (topic.tags as ITag[]).map((tag: ITag) => tag.value); } }); @@ -237,11 +237,11 @@ async function getMatchedPosts(pids: number[], data: ISearchData): Promise { if (data.sortBy === 'relevance' && - !data.replies && - !data.timeRange && - !data.hasTags && - data.searchIn !== 'bookmarks' && - !plugins.hooks.hasListeners('filter:search.filterAndSort')) { + !data.replies && + !data.timeRange && + !data.hasTags && + data.searchIn !== 'bookmarks' && + !plugins.hooks.hasListeners('filter:search.filterAndSort')) { return pids; } let postsData = await getMatchedPosts(pids, data); From 4892bf294056fdc0ae14875931354b77cae645c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 05:16:54 -0400 Subject: [PATCH 12/29] FIX linter errors --- src/interfaces/search.ts | 2 +- src/utils.js | 52 ++++++++++++++++++++-------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/interfaces/search.ts b/src/interfaces/search.ts index 2403fb4283..3eb89cd27b 100644 --- a/src/interfaces/search.ts +++ b/src/interfaces/search.ts @@ -16,4 +16,4 @@ export interface ISearchData { repliesFilter?: string, timeFilter?: string, postedBy?: string -} \ No newline at end of file +} diff --git a/src/utils.js b/src/utils.js index f95bf25cef..553747147d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,32 +1,32 @@ -'use strict'; +"use strict"; -const crypto = require('crypto'); -const nconf = require('nconf'); -const path = require('node:path'); +const crypto = require("crypto"); +const nconf = require("nconf"); +const path = require("node:path"); + +const elapsedTimeSince = function (start) { + const diff = process.hrtime(start); + return diff[0] * 1e3 + diff[1] / 1e6; +}; process.profile = function (operation, start) { console.log( - '%s took %d milliseconds', + "%s took %d milliseconds", operation, process.elapsedTimeSince(start) ); }; -var elapsedTimeSince = function (start) { - const diff = process.hrtime(start); - return diff[0] * 1e3 + diff[1] / 1e6; -}; - process.elapsedTimeSince = elapsedTimeSince; -const utils = { ...require('../public/src/utils.common') }; +const utils = { ...require("../public/src/utils.common") }; utils.elapsedTimeSince = elapsedTimeSince; utils.getLanguage = function () { - const meta = require('./meta'); + const meta = require("./meta"); return meta.config && meta.config.defaultLang ? meta.config.defaultLang - : 'en-GB'; + : "en-GB"; }; utils.generateUUID = function () { @@ -36,24 +36,24 @@ utils.generateUUID = function () { rnd[6] = (rnd[6] & 0x0f) | 0x40; rnd[8] = (rnd[8] & 0x3f) | 0x80; /* eslint-enable no-bitwise */ - rnd = rnd.toString('hex').match(/(.{8})(.{4})(.{4})(.{4})(.{12})/); + rnd = rnd.toString("hex").match(/(.{8})(.{4})(.{4})(.{4})(.{12})/); rnd.shift(); - return rnd.join('-'); + return rnd.join("-"); }; utils.getSass = function () { try { - const sass = require('sass-embedded'); + const sass = require("sass-embedded"); return sass; } catch (_err) { - return require('sass'); + return require("sass"); } }; utils.getFontawesomePath = function () { - let packageName = '@fortawesome/fontawesome-free'; - if (nconf.get('fontawesome:pro') === true) { - packageName = '@fortawesome/fontawesome-pro'; + let packageName = "@fortawesome/fontawesome-free"; + if (nconf.get("fontawesome:pro") === true) { + packageName = "@fortawesome/fontawesome-pro"; } const pathToMainFile = require.resolve(packageName); // main file will be in `js/fontawesome.js` - we need to go up two directories to get to the root of the package @@ -62,12 +62,12 @@ utils.getFontawesomePath = function () { }; utils.getFontawesomeStyles = function () { - let styles = nconf.get('fontawesome:styles') || '*'; + let styles = nconf.get("fontawesome:styles") || "*"; // "*" is a special case, it means all styles, spread is used to support both string and array (["*"]) - if ([...styles][0] === '*') { - styles = ['solid', 'brands', 'regular']; - if (nconf.get('fontawesome:pro')) { - styles.push('light', 'thin', 'sharp', 'duotone'); + if ([...styles][0] === "*") { + styles = ["solid", "brands", "regular"]; + if (nconf.get("fontawesome:pro")) { + styles.push("light", "thin", "sharp", "duotone"); } } if (!Array.isArray(styles)) { @@ -78,7 +78,7 @@ utils.getFontawesomeStyles = function () { utils.getFontawesomeVersion = function () { const fontawesomePath = utils.getFontawesomePath(); - const packageJson = require(path.join(fontawesomePath, 'package.json')); + const packageJson = require(path.join(fontawesomePath, "package.json")); return packageJson.version; }; From 336f74f7ae185edf196f026719f1b42f0f025c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 05:17:55 -0400 Subject: [PATCH 13/29] Fix Single quote issue --- src/utils.js | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/utils.js b/src/utils.js index 553747147d..59af129337 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,8 +1,8 @@ -"use strict"; +'use strict'; -const crypto = require("crypto"); -const nconf = require("nconf"); -const path = require("node:path"); +const crypto = require('crypto'); +const nconf = require('nconf'); +const path = require('node:path'); const elapsedTimeSince = function (start) { const diff = process.hrtime(start); @@ -11,22 +11,22 @@ const elapsedTimeSince = function (start) { process.profile = function (operation, start) { console.log( - "%s took %d milliseconds", + '%s took %d milliseconds', operation, process.elapsedTimeSince(start) ); }; process.elapsedTimeSince = elapsedTimeSince; -const utils = { ...require("../public/src/utils.common") }; +const utils = { ...require('../public/src/utils.common') }; utils.elapsedTimeSince = elapsedTimeSince; utils.getLanguage = function () { - const meta = require("./meta"); + const meta = require('./meta'); return meta.config && meta.config.defaultLang ? meta.config.defaultLang - : "en-GB"; + : 'en-GB'; }; utils.generateUUID = function () { @@ -36,24 +36,24 @@ utils.generateUUID = function () { rnd[6] = (rnd[6] & 0x0f) | 0x40; rnd[8] = (rnd[8] & 0x3f) | 0x80; /* eslint-enable no-bitwise */ - rnd = rnd.toString("hex").match(/(.{8})(.{4})(.{4})(.{4})(.{12})/); + rnd = rnd.toString('hex').match(/(.{8})(.{4})(.{4})(.{4})(.{12})/); rnd.shift(); - return rnd.join("-"); + return rnd.join('-'); }; utils.getSass = function () { try { - const sass = require("sass-embedded"); + const sass = require('sass-embedded'); return sass; } catch (_err) { - return require("sass"); + return require('sass'); } }; utils.getFontawesomePath = function () { - let packageName = "@fortawesome/fontawesome-free"; - if (nconf.get("fontawesome:pro") === true) { - packageName = "@fortawesome/fontawesome-pro"; + let packageName = '@fortawesome/fontawesome-free'; + if (nconf.get('fontawesome:pro') === true) { + packageName = '@fortawesome/fontawesome-pro'; } const pathToMainFile = require.resolve(packageName); // main file will be in `js/fontawesome.js` - we need to go up two directories to get to the root of the package @@ -62,12 +62,12 @@ utils.getFontawesomePath = function () { }; utils.getFontawesomeStyles = function () { - let styles = nconf.get("fontawesome:styles") || "*"; - // "*" is a special case, it means all styles, spread is used to support both string and array (["*"]) - if ([...styles][0] === "*") { - styles = ["solid", "brands", "regular"]; - if (nconf.get("fontawesome:pro")) { - styles.push("light", "thin", "sharp", "duotone"); + let styles = nconf.get('fontawesome:styles') || '*'; + // '*' is a special case, it means all styles, spread is used to support both string and array (['*']) + if ([...styles][0] === '*') { + styles = ['solid', 'brands', 'regular']; + if (nconf.get('fontawesome:pro')) { + styles.push('light', 'thin', 'sharp', 'duotone'); } } if (!Array.isArray(styles)) { @@ -78,7 +78,7 @@ utils.getFontawesomeStyles = function () { utils.getFontawesomeVersion = function () { const fontawesomePath = utils.getFontawesomePath(); - const packageJson = require(path.join(fontawesomePath, "package.json")); + const packageJson = require(path.join(fontawesomePath, 'package.json')); return packageJson.version; }; From e1bf71fc5753cdabef3d83a26803ccd3a365e686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 05:20:36 -0400 Subject: [PATCH 14/29] Fix util parenthesis issue --- src/interfaces/search.ts | 2 +- src/utils.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/interfaces/search.ts b/src/interfaces/search.ts index 3eb89cd27b..2f137b8d33 100644 --- a/src/interfaces/search.ts +++ b/src/interfaces/search.ts @@ -16,4 +16,4 @@ export interface ISearchData { repliesFilter?: string, timeFilter?: string, postedBy?: string -} +}; \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index 59af129337..e9571253e9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -6,7 +6,7 @@ const path = require('node:path'); const elapsedTimeSince = function (start) { const diff = process.hrtime(start); - return diff[0] * 1e3 + diff[1] / 1e6; + return (diff[0] * 1e3) + (diff[1] / 1e6); }; process.profile = function (operation, start) { @@ -24,9 +24,9 @@ utils.elapsedTimeSince = elapsedTimeSince; utils.getLanguage = function () { const meta = require('./meta'); - return meta.config && meta.config.defaultLang - ? meta.config.defaultLang - : 'en-GB'; + return meta.config && meta.config.defaultLang ? + meta.config.defaultLang : + 'en-GB'; }; utils.generateUUID = function () { From a5a9daa7bb322d704eac23bad9f88137d1ca7e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 05:23:46 -0400 Subject: [PATCH 15/29] Delete unused global d ts --- global.d.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 global.d.ts diff --git a/global.d.ts b/global.d.ts deleted file mode 100644 index 32decd37cb..0000000000 --- a/global.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare namespace NodeJS { - interface Process { - elapsedTimeSince(start: [number, number]): number; - profile(operation: string, start: [number, number]): void; - } -} From 2901a0f4130d473c850155b7f2e9798dd669802c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 05:28:40 -0400 Subject: [PATCH 16/29] Add typing to category property on post interfaces --- src/interfaces/post.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interfaces/post.ts b/src/interfaces/post.ts index 2d74592074..bf0a9cad62 100644 --- a/src/interfaces/post.ts +++ b/src/interfaces/post.ts @@ -6,7 +6,7 @@ export interface IPost { deleted: boolean, upvotes: number, downvotes: number, - category: any, + category: Record, topic: ITopic, user: { username?: string, @@ -16,7 +16,7 @@ export interface IPost { export interface ITopic { postcount?: string, deleted?: boolean, - category?: any, + category?: Record, tags?: ITag[] | string[], cid?: number } From 484af2d19e80184cf5a18d9dbc509fc6c552bacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 05:31:42 -0400 Subject: [PATCH 17/29] Fix new line lint issue problem on post interface --- src/interfaces/post.js | 2 + src/interfaces/post.ts | 4 +- src/interfaces/search.js | 2 + src/interfaces/search.ts | 3 +- src/search.js | 679 ++++++++++++++++++++------------------- 5 files changed, 349 insertions(+), 341 deletions(-) create mode 100644 src/interfaces/post.js create mode 100644 src/interfaces/search.js diff --git a/src/interfaces/post.js b/src/interfaces/post.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/interfaces/post.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/interfaces/post.ts b/src/interfaces/post.ts index bf0a9cad62..45958028d4 100644 --- a/src/interfaces/post.ts +++ b/src/interfaces/post.ts @@ -23,4 +23,6 @@ export interface ITopic { export interface ITag { value?: string -} \ No newline at end of file +} + + diff --git a/src/interfaces/search.js b/src/interfaces/search.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/interfaces/search.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/interfaces/search.ts b/src/interfaces/search.ts index 2f137b8d33..8c55911ce7 100644 --- a/src/interfaces/search.ts +++ b/src/interfaces/search.ts @@ -16,4 +16,5 @@ export interface ISearchData { repliesFilter?: string, timeFilter?: string, postedBy?: string -}; \ No newline at end of file +} + diff --git a/src/search.js b/src/search.js index 06a67311c6..ef516f70e8 100644 --- a/src/search.js +++ b/src/search.js @@ -1,357 +1,358 @@ -'use strict'; - -const _ = require('lodash'); - -const db = require('./database'); -const batch = require('./batch'); -const posts = require('./posts'); -const topics = require('./topics'); -const categories = require('./categories'); -const user = require('./user'); -const plugins = require('./plugins'); -const privileges = require('./privileges'); -const utils = require('./utils'); - -const search = module.exports; - -search.search = async function (data) { - const start = process.hrtime(); - data.sortBy = data.sortBy || 'relevance'; - - let result; - if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { - result = await searchInContent(data); - } else if (data.searchIn === 'users') { - result = await user.search(data); - } else if (data.searchIn === 'categories') { - result = await categories.search(data); - } else if (data.searchIn === 'tags') { - result = await topics.searchAndLoadTags(data); - } else if (data.searchIn) { - result = await plugins.hooks.fire('filter:search.searchIn', { - data, - }); - } else { - throw new Error('[[error:unknown-search-filter]]'); - } - - result.time = (process.elapsedTimeSince(start) / 1000).toFixed(2); - return result; +"use strict"; +/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); }; - -async function searchInContent(data) { - data.uid = data.uid || 0; - - const [searchCids, searchUids] = await Promise.all([ - getSearchCids(data), - getSearchUids(data), - ]); - - async function doSearch(type, searchIn) { - if (searchIn.includes(data.searchIn)) { - const result = await plugins.hooks.fire('filter:search.query', { - index: type, - content: data.query, - matchWords: data.matchWords || 'all', - cid: searchCids, - uid: searchUids, - searchData: data, - ids: [], - }); - return Array.isArray(result) ? result : result.ids; - } - return []; - } - let pids = []; - let tids = []; - const inTopic = String(data.query || '').match(/^in:topic-([\d]+) /); - if (inTopic) { - const tid = inTopic[1]; - const cleanedTerm = data.query.replace(inTopic[0], ''); - pids = await topics.search(tid, cleanedTerm); - } else if (data.searchIn === 'bookmarks') { - pids = await searchInBookmarks(data, searchCids, searchUids); - } else { - [pids, tids] = await Promise.all([ - doSearch('post', ['posts', 'titlesposts']), - doSearch('topic', ['titles', 'titlesposts']), - ]); - } - - const mainPids = await topics.getMainPids(tids); - - let allPids = mainPids.concat(pids).filter(Boolean); - - allPids = await privileges.posts.filter('topics:read', allPids, data.uid); - allPids = await filterAndSort(allPids, data); - - const metadata = await plugins.hooks.fire('filter:search.inContent', { - pids: allPids, - data: data, - }); - - if (data.returnIds) { - const mainPidsSet = new Set(mainPids); - const mainPidToTid = _.zipObject(mainPids, tids); - const pidsSet = new Set(pids); - const returnPids = allPids.filter(pid => pidsSet.has(pid)); - const returnTids = allPids.filter(pid => mainPidsSet.has(pid)).map(pid => mainPidToTid[pid]); - return { pids: returnPids, tids: returnTids }; - } - - const itemsPerPage = Math.min(data.itemsPerPage || 10, 100); - const returnData = { - posts: [], - matchCount: metadata.pids.length, - pageCount: Math.max(1, Math.ceil(parseInt(metadata.pids.length, 10) / itemsPerPage)), - }; - - if (data.page) { - const start = Math.max(0, (data.page - 1)) * itemsPerPage; - metadata.pids = metadata.pids.slice(start, start + itemsPerPage); - } - - returnData.posts = await posts.getPostSummaryByPids(metadata.pids, data.uid, {}); - await plugins.hooks.fire('filter:search.contentGetResult', { result: returnData, data: data }); - delete metadata.pids; - delete metadata.data; - return Object.assign(returnData, metadata); -} - -async function searchInBookmarks(data, searchCids, searchUids) { - const { uid, query, matchWords } = data; - const allPids = []; - await batch.processSortedSet(`uid:${uid}:bookmarks`, async (pids) => { - if (Array.isArray(searchCids) && searchCids.length) { - pids = await posts.filterPidsByCid(pids, searchCids); - } - if (Array.isArray(searchUids) && searchUids.length) { - pids = await posts.filterPidsByUid(pids, searchUids); - } - if (query) { - const tokens = String(query).split(' '); - const postData = await db.getObjectsFields(pids.map(pid => `post:${pid}`), ['content', 'tid']); - const tids = _.uniq(postData.map(p => p.tid)); - const topicData = await db.getObjectsFields(tids.map(tid => `topic:${tid}`), ['title']); - const tidToTopic = _.zipObject(tids, topicData); - pids = pids.filter((pid, i) => { - const content = String(postData[i].content); - const title = String(tidToTopic[postData[i].tid].title); - const method = (matchWords === 'any' ? 'some' : 'every'); - return tokens[method]( - token => content.includes(token) || title.includes(token) - ); - }); - } - allPids.push(...pids); - }, { - batch: 500, - }); - - return allPids; -} - -async function filterAndSort(pids, data) { - if (data.sortBy === 'relevance' && - !data.replies && - !data.timeRange && - !data.hasTags && - data.searchIn !== 'bookmarks' && - !plugins.hooks.hasListeners('filter:search.filterAndSort')) { - return pids; - } - let postsData = await getMatchedPosts(pids, data); - if (!postsData.length) { - return pids; - } - postsData = postsData.filter(Boolean); - - postsData = filterByPostcount(postsData, data.replies, data.repliesFilter); - postsData = filterByTimerange(postsData, data.timeRange, data.timeFilter); - postsData = filterByTags(postsData, data.hasTags); - - sortPosts(postsData, data); - - const result = await plugins.hooks.fire('filter:search.filterAndSort', { pids: pids, posts: postsData, data: data }); - return result.posts.map(post => post && post.pid); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const lodash_1 = require("lodash"); +const batch_1 = __importDefault(require("./batch")); +const categories_1 = __importDefault(require("./categories")); +const database_1 = __importDefault(require("./database")); +const plugins_1 = __importDefault(require("./plugins")); +const posts_1 = __importDefault(require("./posts")); +const privileges_1 = __importDefault(require("./privileges")); +const topics_1 = __importDefault(require("./topics")); +const user_1 = __importDefault(require("./user")); +const utils_1 = __importDefault(require("./utils")); +function getWatchedCids(data) { + return __awaiter(this, void 0, void 0, function* () { + if (!data.categories.includes('watched')) { + return []; + } + return yield user_1.default.getWatchedCategories(data.uid); + }); } - -async function getMatchedPosts(pids, data) { - const postFields = ['pid', 'uid', 'tid', 'timestamp', 'deleted', 'upvotes', 'downvotes']; - - let postsData = await posts.getPostsFields(pids, postFields); - postsData = postsData.filter(post => post && !post.deleted); - const uids = _.uniq(postsData.map(post => post.uid)); - const tids = _.uniq(postsData.map(post => post.tid)); - - const [users, topics] = await Promise.all([ - getUsers(uids, data), - getTopics(tids, data), - ]); - - const tidToTopic = _.zipObject(tids, topics); - const uidToUser = _.zipObject(uids, users); - postsData.forEach((post) => { - if (topics && tidToTopic[post.tid]) { - post.topic = tidToTopic[post.tid]; - if (post.topic && post.topic.category) { - post.category = post.topic.category; - } - } - - if (uidToUser[post.uid]) { - post.user = uidToUser[post.uid]; - } - }); - - return postsData.filter(post => post && post.topic && !post.topic.deleted); +function getChildrenCids(data) { + return __awaiter(this, void 0, void 0, function* () { + if (!data.searchChildren) { + return []; + } + const childrenCids = yield Promise.all(data.categories.map(cid => categories_1.default.getChildrenCids(cid))); + return yield privileges_1.default.categories.filterCids('find', (0, lodash_1.uniq)((0, lodash_1.flatten)(childrenCids)), data.uid); + }); } - -async function getUsers(uids, data) { - if (data.sortBy.startsWith('user')) { - return user.getUsersFields(uids, ['username']); - } - return []; +function getSearchUids(data) { + return __awaiter(this, void 0, void 0, function* () { + if (!data.postedBy) { + return []; + } + return yield user_1.default.getUidsByUsernames(Array.isArray(data.postedBy) ? data.postedBy : [data.postedBy]); + }); } - -async function getTopics(tids, data) { - const topicsData = await topics.getTopicsData(tids); - const cids = _.uniq(topicsData.map(topic => topic && topic.cid)); - const categories = await getCategories(cids, data); - - const cidToCategory = _.zipObject(cids, categories); - topicsData.forEach((topic) => { - if (topic && categories && cidToCategory[topic.cid]) { - topic.category = cidToCategory[topic.cid]; - } - if (topic && topic.tags) { - topic.tags = topic.tags.map(tag => tag.value); - } - }); - - return topicsData; +function getSearchCids(data) { + return __awaiter(this, void 0, void 0, function* () { + if (!Array.isArray(data.categories) || !data.categories.length) { + return []; + } + if (data.categories.includes('all')) { + return yield categories_1.default.getCidsByPrivilege('categories:cid', data.uid, 'read'); + } + const [watchedCids, childrenCids] = yield Promise.all([ + getWatchedCids(data), + getChildrenCids(data), + ]); + const concatenatedData = [...watchedCids, ...childrenCids, ...data.categories]; + return (0, lodash_1.uniq)(concatenatedData.filter(Boolean)); + }); } - -async function getCategories(cids, data) { - const categoryFields = []; - - if (data.sortBy.startsWith('category.')) { - categoryFields.push(data.sortBy.split('.')[1]); - } - if (!categoryFields.length) { - return null; - } - - return await db.getObjectsFields(cids.map(cid => `category:${cid}`), categoryFields); +function searchInBookmarks(data, searchCids, searchUids) { + return __awaiter(this, void 0, void 0, function* () { + const { uid, query, matchWords } = data; + const allPids = []; + yield batch_1.default.processSortedSet(`uid:${uid}:bookmarks`, (pids) => __awaiter(this, void 0, void 0, function* () { + if (Array.isArray(searchCids) && searchCids.length) { + pids = yield posts_1.default.filterPidsByCid(pids, searchCids); + } + if (Array.isArray(searchUids) && searchUids.length) { + pids = yield posts_1.default.filterPidsByUid(pids, searchUids); + } + if (query) { + const tokens = query.toString().split(' '); + const postData = yield database_1.default.getObjectsFields(pids.map(pid => `post:${pid}`), ['content', 'tid']); + const tids = (0, lodash_1.uniq)(postData.map((p) => p.tid)); + const topicData = yield database_1.default.getObjectsFields(tids.map(tid => `topic:${tid}`), ['title']); + const tidToTopic = (0, lodash_1.zipObject)(tids, topicData); + pids = pids.filter((_, i) => { + const content = JSON.stringify(postData[i].content); + const title = `${tidToTopic[postData[i].tid].title}`; + const method = (matchWords === 'any' ? 'some' : 'every'); + return tokens[method](token => content.includes(token) || title.includes(token)); + }); + } + allPids.push(...pids); + }), { + batch: 500, + }); + return allPids; + }); } - function filterByPostcount(posts, postCount, repliesFilter) { - postCount = parseInt(postCount, 10); - if (postCount) { - if (repliesFilter === 'atleast') { - posts = posts.filter(post => post.topic && post.topic.postcount >= postCount); - } else { - posts = posts.filter(post => post.topic && post.topic.postcount <= postCount); - } - } - return posts; + const parsedPostCount = parseInt(postCount, 10); + if (postCount) { + const filterCondition = repliesFilter === 'atleast' ? + (post) => Number(post === null || post === void 0 ? void 0 : post.topic.postcount) >= parsedPostCount : + (post) => Number(post === null || post === void 0 ? void 0 : post.topic.postcount) <= parsedPostCount; + posts = posts.filter(filterCondition); + } + return posts; } - function filterByTimerange(posts, timeRange, timeFilter) { - timeRange = parseInt(timeRange, 10) * 1000; - if (timeRange) { - const time = Date.now() - timeRange; - if (timeFilter === 'newer') { - posts = posts.filter(post => post.timestamp >= time); - } else { - posts = posts.filter(post => post.timestamp <= time); - } - } - return posts; + const parsedTimeRange = parseInt(timeRange, 10) * 1000; + if (timeRange) { + const time = Date.now() - parsedTimeRange; + if (timeFilter === 'newer') { + posts = posts.filter(post => post.timestamp >= time); + } + else { + posts = posts.filter(post => post.timestamp <= time); + } + } + return posts; } - function filterByTags(posts, hasTags) { - if (Array.isArray(hasTags) && hasTags.length) { - posts = posts.filter((post) => { - let hasAllTags = false; - if (post && post.topic && Array.isArray(post.topic.tags) && post.topic.tags.length) { - hasAllTags = hasTags.every(tag => post.topic.tags.includes(tag)); - } - return hasAllTags; - }); - } - return posts; + if (Array.isArray(hasTags) && hasTags.length) { + posts = posts.filter((post) => { + let hasAllTags = false; + if (post && post.topic && Array.isArray(post.topic.tags) && post.topic.tags.length) { + hasAllTags = hasTags.every((tag) => post.topic.tags.includes(tag)); + } + return hasAllTags; + }); + } + return posts; } - function sortPosts(posts, data) { - if (!posts.length || data.sortBy === 'relevance') { - return; - } - - data.sortDirection = data.sortDirection || 'desc'; - const direction = data.sortDirection === 'desc' ? 1 : -1; - const fields = data.sortBy.split('.'); - if (fields.length === 1) { - return posts.sort((p1, p2) => direction * (p2[fields[0]] - p1[fields[0]])); - } - - const firstPost = posts[0]; - if (!fields || fields.length !== 2 || !firstPost[fields[0]] || !firstPost[fields[0]][fields[1]]) { - return; - } - - const isNumeric = utils.isNumber(firstPost[fields[0]][fields[1]]); - - if (isNumeric) { - posts.sort((p1, p2) => direction * (p2[fields[0]][fields[1]] - p1[fields[0]][fields[1]])); - } else { - posts.sort((p1, p2) => { - if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) { - return direction; - } else if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) { - return -direction; - } - return 0; - }); - } + var _a; + if (!posts.length || data.sortBy === 'relevance') { + return; + } + data.sortDirection = data.sortDirection || 'desc'; + const direction = data.sortDirection === 'desc' ? 1 : -1; + const fields = data.sortBy.split('.'); + if (fields.length === 1) { + return posts.sort((post_1, post_2) => direction * (post_2[fields[0]] - post_1[fields[0]])); + } + const firstPost = posts[0]; + const isValid = fields && fields.length === 2 && ((_a = firstPost === null || firstPost === void 0 ? void 0 : firstPost[fields[0]]) === null || _a === void 0 ? void 0 : _a[fields[1]]); + if (!isValid) { + return; + } + const isNumeric = utils_1.default.isNumber(firstPost[fields[0]][fields[1]]); + if (isNumeric) { + posts.sort((post_1, post_2) => direction * (post_2[fields[0]][fields[1]] - post_1[fields[0]][fields[1]])); + } + else { + posts.sort((post_1, post_2) => { + if (post_1[fields[0]][fields[1]] > post_2[fields[0]][fields[1]]) { + return direction; + } + else if (post_1[fields[0]][fields[1]] < post_2[fields[0]][fields[1]]) { + return -direction; + } + return 0; + }); + } } - -async function getSearchCids(data) { - if (!Array.isArray(data.categories) || !data.categories.length) { - return []; - } - - if (data.categories.includes('all')) { - return await categories.getCidsByPrivilege('categories:cid', data.uid, 'read'); - } - - const [watchedCids, childrenCids] = await Promise.all([ - getWatchedCids(data), - getChildrenCids(data), - ]); - return _.uniq(watchedCids.concat(childrenCids).concat(data.categories).filter(Boolean)); +function getUsers(uids, data) { + return __awaiter(this, void 0, void 0, function* () { + if (data.sortBy.startsWith('user')) { + return yield user_1.default.getUsersFields(uids, ['username']); + } + return []; + }); } - -async function getWatchedCids(data) { - if (!data.categories.includes('watched')) { - return []; - } - return await user.getWatchedCategories(data.uid); +function getCategories(cids, data) { + return __awaiter(this, void 0, void 0, function* () { + const categoryFields = []; + if (data.sortBy.startsWith('category.')) { + categoryFields.push(data.sortBy.split('.')[1]); + } + if (!categoryFields.length) { + return null; + } + return yield database_1.default.getObjectsFields(cids.map(cid => `category:${cid}`), categoryFields); + }); } - -async function getChildrenCids(data) { - if (!data.searchChildren) { - return []; - } - const childrenCids = await Promise.all(data.categories.map(cid => categories.getChildrenCids(cid))); - return await privileges.categories.filterCids('find', _.uniq(_.flatten(childrenCids)), data.uid); +function getTopics(tids, data) { + return __awaiter(this, void 0, void 0, function* () { + const topicsData = yield topics_1.default.getTopicsData(tids); + const cids = (0, lodash_1.uniq)(topicsData.map((topic) => topic && topic.cid)); + const categories = yield getCategories(cids, data); + const cidToCategory = (0, lodash_1.zipObject)(cids, categories); + topicsData.forEach((topic) => { + if (topic && categories && cidToCategory[topic.cid]) { + topic.category = cidToCategory[topic.cid]; + } + if (Array.isArray(topic.tags) && topic.tags.length > 0 && typeof topic.tags[0] !== 'string') { + topic.tags = topic.tags.map((tag) => tag.value); + } + }); + return topicsData; + }); } - -async function getSearchUids(data) { - if (!data.postedBy) { - return []; - } - return await user.getUidsByUsernames(Array.isArray(data.postedBy) ? data.postedBy : [data.postedBy]); +function getMatchedPosts(pids, data) { + return __awaiter(this, void 0, void 0, function* () { + const postFields = ['pid', 'uid', 'tid', 'timestamp', 'deleted', 'upvotes', 'downvotes']; + let postsData = yield posts_1.default.getPostsFields(pids, postFields); + postsData = postsData.filter((post) => post && !post.deleted); + const uids = (0, lodash_1.uniq)(postsData.map((post) => post.uid)); + const tids = (0, lodash_1.uniq)(postsData.map((post) => post.tid)); + const [users, topics] = yield Promise.all([ + getUsers(uids, data), + getTopics(tids, data), + ]); + const tidToTopic = (0, lodash_1.zipObject)(tids, topics); + const uidToUser = (0, lodash_1.zipObject)(uids, users); + postsData.forEach((post) => { + if (topics && tidToTopic[post.tid]) { + post.topic = tidToTopic[post.tid]; + if (post.topic && post.topic.category) { + post.category = post.topic.category; + } + } + if (uidToUser[post.uid]) { + post.user = uidToUser[post.uid]; + } + }); + return postsData.filter((post) => post && post.topic && !post.topic.deleted); + }); } - -require('./promisify')(search); \ No newline at end of file +function filterAndSort(pids, data) { + return __awaiter(this, void 0, void 0, function* () { + if (data.sortBy === 'relevance' && + !data.replies && + !data.timeRange && + !data.hasTags && + data.searchIn !== 'bookmarks' && + !plugins_1.default.hooks.hasListeners('filter:search.filterAndSort')) { + return pids; + } + let postsData = yield getMatchedPosts(pids, data); + if (!postsData.length) { + return pids; + } + postsData = postsData.filter(Boolean); + postsData = filterByPostcount(postsData, data.replies, data.repliesFilter); + postsData = filterByTimerange(postsData, data.timeRange, data.timeFilter); + postsData = filterByTags(postsData, data.hasTags); + sortPosts(postsData, data); + const result = yield plugins_1.default.hooks.fire('filter:search.filterAndSort', { pids: pids, posts: postsData, data: data }); + return result.posts.map((post) => post && post.pid); + }); +} +function searchInContent(data) { + return __awaiter(this, void 0, void 0, function* () { + data.uid = data.uid || 0; + const [searchCids, searchUids] = yield Promise.all([ + getSearchCids(data), + getSearchUids(data), + ]); + function doSearch(type, searchIn) { + return __awaiter(this, void 0, void 0, function* () { + if (searchIn.includes(data.searchIn)) { + const result = yield plugins_1.default.hooks.fire('filter:search.query', { + index: type, + content: data.query, + matchWords: data.matchWords || 'all', + cid: searchCids, + uid: searchUids, + searchData: data, + ids: [], + }); + return Array.isArray(result) ? result : result.ids; + } + return []; + }); + } + let pids = []; + let tids = []; + const inTopic = `${data.query || ''}`.match(/^in:topic-([\d]+) /); + if (inTopic) { + const tid = inTopic[1]; + const cleanedTerm = data.query.replace(inTopic[0], ''); + pids = yield topics_1.default.search(tid, cleanedTerm); + } + else if (data.searchIn === 'bookmarks') { + pids = yield searchInBookmarks(data, searchCids, searchUids); + } + else { + [pids, tids] = yield Promise.all([ + doSearch('post', ['posts', 'titlesposts']), + doSearch('topic', ['titles', 'titlesposts']), + ]); + } + const mainPids = yield topics_1.default.getMainPids(tids); + let allPids = mainPids.concat(pids).filter(Boolean); + allPids = yield privileges_1.default.posts.filter('topics:read', allPids, data.uid); + allPids = yield filterAndSort(allPids, data); + const metadata = yield plugins_1.default.hooks.fire('filter:search.inContent', { + pids: allPids, + data: data, + }); + if (data.returnIds) { + const mainPidsSet = new Set(mainPids); + const mainPidToTid = (0, lodash_1.zipObject)(mainPids, tids); + const pidsSet = new Set(pids); + const returnPids = allPids.filter((pid) => pidsSet.has(pid)); + const returnTids = allPids.filter((pid) => mainPidsSet.has(pid)).map(pid => mainPidToTid[pid]); + return { pids: returnPids, tids: returnTids }; + } + const itemsPerPage = Math.min(data.itemsPerPage || 10, 100); + const returnData = { + posts: [], + matchCount: metadata.pids.length, + pageCount: Math.max(1, Math.ceil(parseInt(metadata.pids.length, 10) / itemsPerPage)), + }; + if (data.page) { + const start = Math.max(0, (data.page - 1)) * itemsPerPage; + metadata.pids = metadata.pids.slice(start, start + itemsPerPage); + } + returnData.posts = yield posts_1.default.getPostSummaryByPids(metadata.pids, data.uid, {}); + yield plugins_1.default.hooks.fire('filter:search.contentGetResult', { result: returnData, data: data }); + delete metadata.pids; + delete metadata.data; + return Object.assign(returnData, metadata); + }); +} +exports.default = { + search: function (data) { + return __awaiter(this, void 0, void 0, function* () { + const start = process.hrtime(); + data.sortBy = data.sortBy || 'relevance'; + let result; + if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { + result = yield searchInContent(data); + } + else if (data.searchIn === 'users') { + result = yield user_1.default.search(data); + } + else if (data.searchIn === 'categories') { + result = yield categories_1.default.search(data); + } + else if (data.searchIn === 'tags') { + result = yield topics_1.default.searchAndLoadTags(data); + } + else if (data.searchIn) { + result = yield plugins_1.default.hooks.fire('filter:search.searchIn', { + data, + }); + } + else { + throw new Error('[[error:unknown-search-filter]]'); + } + result.time = (utils_1.default.elapsedTimeSince(start) / 1000).toFixed(2); + return result; + }); + }, +}; From 396f59b22859542ad2af1d016394982591926eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 05:42:44 -0400 Subject: [PATCH 18/29] Fix exporting default issues --- src/interfaces/search.ts | 4 +++ src/search.ts | 56 +++++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/interfaces/search.ts b/src/interfaces/search.ts index 8c55911ce7..f9f66590b2 100644 --- a/src/interfaces/search.ts +++ b/src/interfaces/search.ts @@ -18,3 +18,7 @@ export interface ISearchData { postedBy?: string } +export interface ISearch { + search(data: ISearchData): Promise<{ time: string, [key: string]: unknown }>; +} + diff --git a/src/search.ts b/src/search.ts index 98cd8be0bb..1238febbeb 100644 --- a/src/search.ts +++ b/src/search.ts @@ -7,13 +7,16 @@ import batch from './batch'; import categories from './categories'; import db from './database'; import { IPost, ITag, ITopic } from './interfaces/post'; -import { ISearchData } from './interfaces/search'; +import { ISearch, ISearchData } from './interfaces/search'; import plugins from './plugins'; import posts from './posts'; import privileges from './privileges'; import topics from './topics'; import user from './user'; import utils from './utils'; +import { promisify } from './promisify'; + +const search: ISearch = {} as ISearch; async function getWatchedCids(data: ISearchData): Promise { if (!data.categories.includes('watched')) { @@ -343,29 +346,30 @@ async function searchInContent(data: ISearchData) { return Object.assign(returnData, metadata); } -export default { - search: async function (data: ISearchData) { - const start = process.hrtime(); - data.sortBy = data.sortBy || 'relevance'; - let result: { time: string, [key: string]: unknown }; - - if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { - result = await searchInContent(data); - } else if (data.searchIn === 'users') { - result = await user.search(data); - } else if (data.searchIn === 'categories') { - result = await categories.search(data); - } else if (data.searchIn === 'tags') { - result = await topics.searchAndLoadTags(data); - } else if (data.searchIn) { - result = await plugins.hooks.fire('filter:search.searchIn', { - data, - }); - } else { - throw new Error('[[error:unknown-search-filter]]'); - } +search.search = async function (data: ISearchData) { + const start = process.hrtime(); + data.sortBy = data.sortBy || 'relevance'; + let result: { time: string, [key: string]: unknown }; + + if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { + result = await searchInContent(data); + } else if (data.searchIn === 'users') { + result = await user.search(data); + } else if (data.searchIn === 'categories') { + result = await categories.search(data); + } else if (data.searchIn === 'tags') { + result = await topics.searchAndLoadTags(data); + } else if (data.searchIn) { + result = await plugins.hooks.fire('filter:search.searchIn', { + data, + }); + } else { + throw new Error('[[error:unknown-search-filter]]'); + } + + result.time = (utils.elapsedTimeSince(start) / 1000).toFixed(2); + return result; +} - result.time = (utils.elapsedTimeSince(start) / 1000).toFixed(2); - return result; - }, -}; +promisify(search); +export default search; \ No newline at end of file From 0c6475d5d1f97e125de875ab37ecf3857627f722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 05:44:29 -0400 Subject: [PATCH 19/29] Fix semicolon and newline on search.ts --- src/search.js | 62 ++++++++++++++++++++++++++------------------------- src/search.ts | 6 +++-- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/search.js b/src/search.js index ef516f70e8..3fcea391a7 100644 --- a/src/search.js +++ b/src/search.js @@ -25,6 +25,8 @@ const privileges_1 = __importDefault(require("./privileges")); const topics_1 = __importDefault(require("./topics")); const user_1 = __importDefault(require("./user")); const utils_1 = __importDefault(require("./utils")); +const promisify_1 = require("./promisify"); +const search = {}; function getWatchedCids(data) { return __awaiter(this, void 0, void 0, function* () { if (!data.categories.includes('watched')) { @@ -325,34 +327,34 @@ function searchInContent(data) { return Object.assign(returnData, metadata); }); } -exports.default = { - search: function (data) { - return __awaiter(this, void 0, void 0, function* () { - const start = process.hrtime(); - data.sortBy = data.sortBy || 'relevance'; - let result; - if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { - result = yield searchInContent(data); - } - else if (data.searchIn === 'users') { - result = yield user_1.default.search(data); - } - else if (data.searchIn === 'categories') { - result = yield categories_1.default.search(data); - } - else if (data.searchIn === 'tags') { - result = yield topics_1.default.searchAndLoadTags(data); - } - else if (data.searchIn) { - result = yield plugins_1.default.hooks.fire('filter:search.searchIn', { - data, - }); - } - else { - throw new Error('[[error:unknown-search-filter]]'); - } - result.time = (utils_1.default.elapsedTimeSince(start) / 1000).toFixed(2); - return result; - }); - }, +search.search = function (data) { + return __awaiter(this, void 0, void 0, function* () { + const start = process.hrtime(); + data.sortBy = data.sortBy || 'relevance'; + let result; + if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { + result = yield searchInContent(data); + } + else if (data.searchIn === 'users') { + result = yield user_1.default.search(data); + } + else if (data.searchIn === 'categories') { + result = yield categories_1.default.search(data); + } + else if (data.searchIn === 'tags') { + result = yield topics_1.default.searchAndLoadTags(data); + } + else if (data.searchIn) { + result = yield plugins_1.default.hooks.fire('filter:search.searchIn', { + data, + }); + } + else { + throw new Error('[[error:unknown-search-filter]]'); + } + result.time = (utils_1.default.elapsedTimeSince(start) / 1000).toFixed(2); + return result; + }); }; +(0, promisify_1.promisify)(search); +exports.default = search; diff --git a/src/search.ts b/src/search.ts index 1238febbeb..be1c147b50 100644 --- a/src/search.ts +++ b/src/search.ts @@ -369,7 +369,9 @@ search.search = async function (data: ISearchData) { result.time = (utils.elapsedTimeSince(start) / 1000).toFixed(2); return result; -} +}; promisify(search); -export default search; \ No newline at end of file +export default search; + + From 5d3d3b804dd1f0c8b69303e0dffa0d6d81c6e7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 05:54:02 -0400 Subject: [PATCH 20/29] Fix promisify import on ts file --- src/search.js | 4 ++-- src/search.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/search.js b/src/search.js index 3fcea391a7..7b4ca3b52e 100644 --- a/src/search.js +++ b/src/search.js @@ -25,7 +25,7 @@ const privileges_1 = __importDefault(require("./privileges")); const topics_1 = __importDefault(require("./topics")); const user_1 = __importDefault(require("./user")); const utils_1 = __importDefault(require("./utils")); -const promisify_1 = require("./promisify"); +const promisify_1 = __importDefault(require("./promisify")); const search = {}; function getWatchedCids(data) { return __awaiter(this, void 0, void 0, function* () { @@ -356,5 +356,5 @@ search.search = function (data) { return result; }); }; -(0, promisify_1.promisify)(search); +(0, promisify_1.default)(search); exports.default = search; diff --git a/src/search.ts b/src/search.ts index be1c147b50..bc7ee5d266 100644 --- a/src/search.ts +++ b/src/search.ts @@ -14,7 +14,7 @@ import privileges from './privileges'; import topics from './topics'; import user from './user'; import utils from './utils'; -import { promisify } from './promisify'; +import promisify from './promisify'; const search: ISearch = {} as ISearch; From c8b74e81ef0afd0c9ad921fa8dbdda26abc5a517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 06:31:25 -0400 Subject: [PATCH 21/29] Fix search.search is not a function trying to define it from the beginning --- src/search.ts | 52 +++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/search.ts b/src/search.ts index bc7ee5d266..237b0d9cb5 100644 --- a/src/search.ts +++ b/src/search.ts @@ -11,12 +11,10 @@ import { ISearch, ISearchData } from './interfaces/search'; import plugins from './plugins'; import posts from './posts'; import privileges from './privileges'; +import promisify from './promisify'; import topics from './topics'; import user from './user'; import utils from './utils'; -import promisify from './promisify'; - -const search: ISearch = {} as ISearch; async function getWatchedCids(data: ISearchData): Promise { if (!data.categories.includes('watched')) { @@ -346,30 +344,32 @@ async function searchInContent(data: ISearchData) { return Object.assign(returnData, metadata); } -search.search = async function (data: ISearchData) { - const start = process.hrtime(); - data.sortBy = data.sortBy || 'relevance'; - let result: { time: string, [key: string]: unknown }; - - if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { - result = await searchInContent(data); - } else if (data.searchIn === 'users') { - result = await user.search(data); - } else if (data.searchIn === 'categories') { - result = await categories.search(data); - } else if (data.searchIn === 'tags') { - result = await topics.searchAndLoadTags(data); - } else if (data.searchIn) { - result = await plugins.hooks.fire('filter:search.searchIn', { - data, - }); - } else { - throw new Error('[[error:unknown-search-filter]]'); - } +const search: ISearch = { + search: async function (data: ISearchData) { + const start = process.hrtime(); + data.sortBy = data.sortBy || 'relevance'; + let result: { time: string, [key: string]: unknown }; + + if (['posts', 'titles', 'titlesposts', 'bookmarks'].includes(data.searchIn)) { + result = await searchInContent(data); + } else if (data.searchIn === 'users') { + result = await user.search(data); + } else if (data.searchIn === 'categories') { + result = await categories.search(data); + } else if (data.searchIn === 'tags') { + result = await topics.searchAndLoadTags(data); + } else if (data.searchIn) { + result = await plugins.hooks.fire('filter:search.searchIn', { + data, + }); + } else { + throw new Error('[[error:unknown-search-filter]]'); + } - result.time = (utils.elapsedTimeSince(start) / 1000).toFixed(2); - return result; -}; + result.time = (utils.elapsedTimeSince(start) / 1000).toFixed(2); + return result; + } +} promisify(search); export default search; From 6cc2b18c214484e13c246317b4c7633c13ecb84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 06:37:21 -0400 Subject: [PATCH 22/29] Fix trailing comma and semicolon --- src/search.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/search.ts b/src/search.ts index 237b0d9cb5..f1092b9202 100644 --- a/src/search.ts +++ b/src/search.ts @@ -368,8 +368,8 @@ const search: ISearch = { result.time = (utils.elapsedTimeSince(start) / 1000).toFixed(2); return result; - } -} + }, +}; promisify(search); export default search; From 109da6331f6ee342fdc6cf2f7a1ef51db5fc8404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 06:50:00 -0400 Subject: [PATCH 23/29] Change default export with module export --- dump.rdb | Bin 0 -> 31466 bytes src/dummy.js | 0 src/search.js | 61 +++++++++++++++++++++++++------------------------- src/search.ts | 3 +-- 4 files changed, 32 insertions(+), 32 deletions(-) create mode 100644 dump.rdb create mode 100644 src/dummy.js diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000000000000000000000000000000000000..bf4ac1b3e0e80536c6948c76f4c702a4746e845e GIT binary patch literal 31466 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 Date: Mon, 14 Oct 2024 07:05:31 -0400 Subject: [PATCH 24/29] Revert export module --- dump.rdb | Bin 31466 -> 50834 bytes src/search.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/dump.rdb b/dump.rdb index bf4ac1b3e0e80536c6948c76f4c702a4746e845e..f429bbf29dfa86c5669ee14bde2985170e1ce5a9 100644 GIT binary patch delta 4943 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 bIjk Date: Mon, 14 Oct 2024 16:17:22 -0400 Subject: [PATCH 25/29] Testing green 2 pendings --- src/dummy.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/dummy.js diff --git a/src/dummy.js b/src/dummy.js new file mode 100644 index 0000000000..e69de29bb2 From 951d34772e674cc522249d1e6fa58acc99e246e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 16:44:20 -0400 Subject: [PATCH 26/29] Fix export type in search.ts --- src/search.ts | 2 +- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/search.ts b/src/search.ts index d84e2938a6..776f394731 100644 --- a/src/search.ts +++ b/src/search.ts @@ -372,5 +372,5 @@ const search: ISearch = { }; promisify(search); -export default search; +export = search; diff --git a/tsconfig.json b/tsconfig.json index 2eded7eadd..9e8d61b6f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "allowJs": false, "target": "es6", - "module": "commonjs", + "module": "CommonJS", "moduleResolution": "node", "esModuleInterop": true }, From 98dc8b13f3c79abe7e77fcc4050454d8b5ccb7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 17:02:59 -0400 Subject: [PATCH 27/29] Fix test asynchronous for search js --- src/search.js | 3 +- test/search.js | 301 +++++++++++++++++++++++++++---------------------- 2 files changed, 165 insertions(+), 139 deletions(-) diff --git a/src/search.js b/src/search.js index 74e6404f4f..b0de4fed57 100644 --- a/src/search.js +++ b/src/search.js @@ -14,7 +14,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; -Object.defineProperty(exports, "__esModule", { value: true }); const lodash_1 = require("lodash"); const batch_1 = __importDefault(require("./batch")); const categories_1 = __importDefault(require("./categories")); @@ -358,4 +357,4 @@ const search = { }, }; (0, promisify_1.default)(search); -exports.default = search; +module.exports = search; diff --git a/test/search.js b/test/search.js index f0e285cb9d..8734f56b80 100644 --- a/test/search.js +++ b/test/search.js @@ -1,18 +1,17 @@ -'use strict'; +"use strict"; +const assert = require("assert"); +const nconf = require("nconf"); -const assert = require('assert'); -const nconf = require('nconf'); +const db = require("./mocks/databasemock"); +const topics = require("../src/topics"); +const categories = require("../src/categories"); +const user = require("../src/user"); +const search = require("../src/search"); +const privileges = require("../src/privileges"); +const request = require("../src/request"); -const db = require('./mocks/databasemock'); -const topics = require('../src/topics'); -const categories = require('../src/categories'); -const user = require('../src/user'); -const search = require('../src/search'); -const privileges = require('../src/privileges'); -const request = require('../src/request'); - -describe('Search', () => { +describe("Search", () => { let phoebeUid; let gingerUid; @@ -26,202 +25,230 @@ describe('Search', () => { let cid3; before(async () => { - phoebeUid = await user.create({ username: 'phoebe' }); - gingerUid = await user.create({ username: 'ginger' }); - cid1 = (await categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - })).cid; - - cid2 = (await categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - })).cid; - - cid3 = (await categories.create({ - name: 'Child Test Category', - description: 'Test category created by testing script', - parentCid: cid2, - })).cid; + phoebeUid = await user.create({ username: "phoebe" }); + gingerUid = await user.create({ username: "ginger" }); + cid1 = ( + await categories.create({ + name: "Test Category", + description: "Test category created by testing script", + }) + ).cid; + + cid2 = ( + await categories.create({ + name: "Test Category", + description: "Test category created by testing script", + }) + ).cid; + + cid3 = ( + await categories.create({ + name: "Child Test Category", + description: "Test category created by testing script", + parentCid: cid2, + }) + ).cid; ({ topicData: topic1Data, postData: post1Data } = await topics.post({ uid: phoebeUid, cid: cid1, - title: 'nodebb mongodb bugs', - content: 'avocado cucumber apple orange fox', - tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'jquery'], + title: "nodebb mongodb bugs", + content: "avocado cucumber apple orange fox", + tags: ["nodebb", "bug", "plugin", "nodebb-plugin", "jquery"], })); ({ topicData: topic2Data, postData: post2Data } = await topics.post({ uid: gingerUid, cid: cid2, - title: 'java mongodb redis', - content: 'avocado cucumber carrot armadillo', - tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'javascript'], + title: "java mongodb redis", + content: "avocado cucumber carrot armadillo", + tags: ["nodebb", "bug", "plugin", "nodebb-plugin", "javascript"], })); post3Data = await topics.reply({ uid: phoebeUid, - content: 'reply post apple', + content: "reply post apple", tid: topic2Data.tid, }); }); - it('should search term in titles and posts', async () => { - const meta = require('../src/meta'); + it("should search term in titles and posts", async () => { + const meta = require("../src/meta"); const qs = `/api/search?term=cucumber&in=titlesposts&categories[]=${cid1}&by=phoebe&replies=1&repliesFilter=atleast&sortBy=timestamp&sortDirection=desc&showAs=posts`; - await privileges.global.give(['groups:search:content'], 'guests'); + await privileges.global.give(["groups:search:content"], "guests"); - const { body } = await request.get(nconf.get('url') + qs); + const { body } = await request.get(nconf.get("url") + qs); assert(body); assert.equal(body.matchCount, 1); assert.equal(body.posts.length, 1); assert.equal(body.posts[0].pid, post1Data.pid); assert.equal(body.posts[0].uid, phoebeUid); - await privileges.global.rescind(['groups:search:content'], 'guests'); + await privileges.global.rescind(["groups:search:content"], "guests"); }); - it('should search for a user', (done) => { - search.search({ - query: 'gin', - searchIn: 'users', - }, (err, data) => { - assert.ifError(err); + it("should search for a user", async () => { + try { + const data = await search.search({ + query: "gin", + searchIn: "users", + }); assert(data); assert.equal(data.matchCount, 1); assert.equal(data.users.length, 1); assert.equal(data.users[0].uid, gingerUid); - assert.equal(data.users[0].username, 'ginger'); - done(); - }); + assert.equal(data.users[0].username, "ginger"); + } catch (err) { + assert.ifError(err); + } }); - it('should search for a tag', (done) => { - search.search({ - query: 'plug', - searchIn: 'tags', - }, (err, data) => { - assert.ifError(err); - assert(data); - assert.equal(data.matchCount, 1); - assert.equal(data.tags.length, 1); - assert.equal(data.tags[0].value, 'plugin'); - assert.equal(data.tags[0].score, 2); - done(); - }); + it("should search for a tag", (done) => { + search.search( + { + query: "plug", + searchIn: "tags", + }, + (err, data) => { + assert.ifError(err); + assert(data); + assert.equal(data.matchCount, 1); + assert.equal(data.tags.length, 1); + assert.equal(data.tags[0].value, "plugin"); + assert.equal(data.tags[0].score, 2); + done(); + } + ); }); - it('should search for a category', async () => { + it("should search for a category", async () => { await categories.create({ - name: 'foo category', - description: 'Test category created by testing script', + name: "foo category", + description: "Test category created by testing script", }); await categories.create({ - name: 'baz category', - description: 'Test category created by testing script', + name: "baz category", + description: "Test category created by testing script", }); const result = await search.search({ - query: 'baz', - searchIn: 'categories', + query: "baz", + searchIn: "categories", }); assert.strictEqual(result.matchCount, 1); - assert.strictEqual(result.categories[0].name, 'baz category'); + assert.strictEqual(result.categories[0].name, "baz category"); }); - it('should search for categories', async () => { - const socketCategories = require('../src/socket.io/categories'); - let data = await socketCategories.categorySearch({ uid: phoebeUid }, { query: 'baz', parentCid: 0 }); - assert.strictEqual(data[0].name, 'baz category'); - data = await socketCategories.categorySearch({ uid: phoebeUid }, { query: '', parentCid: 0 }); + it("should search for categories", async () => { + const socketCategories = require("../src/socket.io/categories"); + let data = await socketCategories.categorySearch( + { uid: phoebeUid }, + { query: "baz", parentCid: 0 } + ); + assert.strictEqual(data[0].name, "baz category"); + data = await socketCategories.categorySearch( + { uid: phoebeUid }, + { query: "", parentCid: 0 } + ); assert.strictEqual(data.length, 5); }); - it('should fail if searchIn is wrong', (done) => { - search.search({ - query: 'plug', - searchIn: '', - }, (err) => { - assert.equal(err.message, '[[error:unknown-search-filter]]'); - done(); - }); + it("should fail if searchIn is wrong", (done) => { + search.search( + { + query: "plug", + searchIn: "", + }, + (err) => { + assert.equal(err.message, "[[error:unknown-search-filter]]"); + done(); + } + ); }); - it('should search with tags filter', (done) => { - search.search({ - query: 'mongodb', - searchIn: 'titles', - hasTags: ['nodebb', 'javascript'], - }, (err, data) => { - assert.ifError(err); - assert.equal(data.posts[0].tid, topic2Data.tid); - done(); - }); + it("should search with tags filter", (done) => { + search.search( + { + query: "mongodb", + searchIn: "titles", + hasTags: ["nodebb", "javascript"], + }, + (err, data) => { + assert.ifError(err); + assert.equal(data.posts[0].tid, topic2Data.tid); + done(); + } + ); }); - it('should not crash if tags is not an array', (done) => { - search.search({ - query: 'mongodb', - searchIn: 'titles', - hasTags: 'nodebb,javascript', - }, (err, data) => { - assert.ifError(err); - done(); - }); + it("should not crash if tags is not an array", (done) => { + search.search( + { + query: "mongodb", + searchIn: "titles", + hasTags: "nodebb,javascript", + }, + (err, data) => { + assert.ifError(err); + done(); + } + ); }); - it('should not find anything', (done) => { - search.search({ - query: 'xxxxxxxxxxxxxx', - searchIn: 'titles', - }, (err, data) => { - assert.ifError(err); - assert(Array.isArray(data.posts)); - assert(!data.matchCount); - done(); - }); + it("should not find anything", (done) => { + search.search( + { + query: "xxxxxxxxxxxxxx", + searchIn: "titles", + }, + (err, data) => { + assert.ifError(err); + assert(Array.isArray(data.posts)); + assert(!data.matchCount); + done(); + } + ); }); - it('should search child categories', async () => { + it("should search child categories", async () => { await topics.post({ uid: gingerUid, cid: cid3, - title: 'child category topic', - content: 'avocado cucumber carrot armadillo', + title: "child category topic", + content: "avocado cucumber carrot armadillo", }); const result = await search.search({ - query: 'avocado', - searchIn: 'titlesposts', + query: "avocado", + searchIn: "titlesposts", categories: [cid2], searchChildren: true, - sortBy: 'topic.timestamp', - sortDirection: 'desc', + sortBy: "topic.timestamp", + sortDirection: "desc", }); assert(result.posts.length, 2); - assert(result.posts[0].topic.title === 'child category topic'); - assert(result.posts[1].topic.title === 'java mongodb redis'); + assert(result.posts[0].topic.title === "child category topic"); + assert(result.posts[1].topic.title === "java mongodb redis"); }); - it('should return json search data with no categories', async () => { - const qs = '/api/search?term=cucumber&in=titlesposts&searchOnly=1'; - await privileges.global.give(['groups:search:content'], 'guests'); + it("should return json search data with no categories", async () => { + const qs = "/api/search?term=cucumber&in=titlesposts&searchOnly=1"; + await privileges.global.give(["groups:search:content"], "guests"); - const { body } = await request.get(nconf.get('url') + qs); + const { body } = await request.get(nconf.get("url") + qs); assert(body); - assert(body.hasOwnProperty('matchCount')); - assert(body.hasOwnProperty('pagination')); - assert(body.hasOwnProperty('pageCount')); - assert(body.hasOwnProperty('posts')); - assert(!body.hasOwnProperty('categories')); + assert(body.hasOwnProperty("matchCount")); + assert(body.hasOwnProperty("pagination")); + assert(body.hasOwnProperty("pageCount")); + assert(body.hasOwnProperty("posts")); + assert(!body.hasOwnProperty("categories")); - await privileges.global.rescind(['groups:search:content'], 'guests'); + await privileges.global.rescind(["groups:search:content"], "guests"); }); - it('should not crash without a search term', async () => { - const qs = '/api/search'; - await privileges.global.give(['groups:search:content'], 'guests'); - const { response, body } = await request.get(nconf.get('url') + qs); + it("should not crash without a search term", async () => { + const qs = "/api/search"; + await privileges.global.give(["groups:search:content"], "guests"); + const { response, body } = await request.get(nconf.get("url") + qs); assert(body); assert.strictEqual(response.statusCode, 200); - await privileges.global.rescind(['groups:search:content'], 'guests'); + await privileges.global.rescind(["groups:search:content"], "guests"); }); }); From 161ba40afa4c137fd37c6457820ea1733e67df4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 17:18:10 -0400 Subject: [PATCH 28/29] Fix single quotes on tests --- test/search.js | 182 ++++++++++++++++++++++++------------------------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/test/search.js b/test/search.js index 8734f56b80..9fa82bc20d 100644 --- a/test/search.js +++ b/test/search.js @@ -1,17 +1,17 @@ -"use strict"; +'use strict'; -const assert = require("assert"); -const nconf = require("nconf"); +const assert = require('assert'); +const nconf = require('nconf'); -const db = require("./mocks/databasemock"); -const topics = require("../src/topics"); -const categories = require("../src/categories"); -const user = require("../src/user"); -const search = require("../src/search"); -const privileges = require("../src/privileges"); -const request = require("../src/request"); +const db = require('./mocks/databasemock'); +const topics = require('../src/topics'); +const categories = require('../src/categories'); +const user = require('../src/user'); +const search = require('../src/search'); +const privileges = require('../src/privileges'); +const request = require('../src/request'); -describe("Search", () => { +describe('Search', () => { let phoebeUid; let gingerUid; @@ -25,26 +25,26 @@ describe("Search", () => { let cid3; before(async () => { - phoebeUid = await user.create({ username: "phoebe" }); - gingerUid = await user.create({ username: "ginger" }); + phoebeUid = await user.create({ username: 'phoebe' }); + gingerUid = await user.create({ username: 'ginger' }); cid1 = ( await categories.create({ - name: "Test Category", - description: "Test category created by testing script", + name: 'Test Category', + description: 'Test category created by testing script', }) ).cid; cid2 = ( await categories.create({ - name: "Test Category", - description: "Test category created by testing script", + name: 'Test Category', + description: 'Test category created by testing script', }) ).cid; cid3 = ( await categories.create({ - name: "Child Test Category", - description: "Test category created by testing script", + name: 'Child Test Category', + description: 'Test category created by testing script', parentCid: cid2, }) ).cid; @@ -52,124 +52,124 @@ describe("Search", () => { ({ topicData: topic1Data, postData: post1Data } = await topics.post({ uid: phoebeUid, cid: cid1, - title: "nodebb mongodb bugs", - content: "avocado cucumber apple orange fox", - tags: ["nodebb", "bug", "plugin", "nodebb-plugin", "jquery"], + title: 'nodebb mongodb bugs', + content: 'avocado cucumber apple orange fox', + tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'jquery'], })); ({ topicData: topic2Data, postData: post2Data } = await topics.post({ uid: gingerUid, cid: cid2, - title: "java mongodb redis", - content: "avocado cucumber carrot armadillo", - tags: ["nodebb", "bug", "plugin", "nodebb-plugin", "javascript"], + title: 'java mongodb redis', + content: 'avocado cucumber carrot armadillo', + tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'javascript'], })); post3Data = await topics.reply({ uid: phoebeUid, - content: "reply post apple", + content: 'reply post apple', tid: topic2Data.tid, }); }); - it("should search term in titles and posts", async () => { - const meta = require("../src/meta"); + it('should search term in titles and posts', async () => { + const meta = require('../src/meta'); const qs = `/api/search?term=cucumber&in=titlesposts&categories[]=${cid1}&by=phoebe&replies=1&repliesFilter=atleast&sortBy=timestamp&sortDirection=desc&showAs=posts`; - await privileges.global.give(["groups:search:content"], "guests"); + await privileges.global.give(['groups:search:content'], 'guests'); - const { body } = await request.get(nconf.get("url") + qs); + const { body } = await request.get(nconf.get('url') + qs); assert(body); assert.equal(body.matchCount, 1); assert.equal(body.posts.length, 1); assert.equal(body.posts[0].pid, post1Data.pid); assert.equal(body.posts[0].uid, phoebeUid); - await privileges.global.rescind(["groups:search:content"], "guests"); + await privileges.global.rescind(['groups:search:content'], 'guests'); }); - it("should search for a user", async () => { + it('should search for a user', async () => { try { const data = await search.search({ - query: "gin", - searchIn: "users", + query: 'gin', + searchIn: 'users', }); assert(data); assert.equal(data.matchCount, 1); assert.equal(data.users.length, 1); assert.equal(data.users[0].uid, gingerUid); - assert.equal(data.users[0].username, "ginger"); + assert.equal(data.users[0].username, 'ginger'); } catch (err) { assert.ifError(err); } }); - it("should search for a tag", (done) => { + it('should search for a tag', (done) => { search.search( { - query: "plug", - searchIn: "tags", + query: 'plug', + searchIn: 'tags', }, (err, data) => { assert.ifError(err); assert(data); assert.equal(data.matchCount, 1); assert.equal(data.tags.length, 1); - assert.equal(data.tags[0].value, "plugin"); + assert.equal(data.tags[0].value, 'plugin'); assert.equal(data.tags[0].score, 2); done(); } ); }); - it("should search for a category", async () => { + it('should search for a category', async () => { await categories.create({ - name: "foo category", - description: "Test category created by testing script", + name: 'foo category', + description: 'Test category created by testing script', }); await categories.create({ - name: "baz category", - description: "Test category created by testing script", + name: 'baz category', + description: 'Test category created by testing script', }); const result = await search.search({ - query: "baz", - searchIn: "categories", + query: 'baz', + searchIn: 'categories', }); assert.strictEqual(result.matchCount, 1); - assert.strictEqual(result.categories[0].name, "baz category"); + assert.strictEqual(result.categories[0].name, 'baz category'); }); - it("should search for categories", async () => { - const socketCategories = require("../src/socket.io/categories"); + it('should search for categories', async () => { + const socketCategories = require('../src/socket.io/categories'); let data = await socketCategories.categorySearch( { uid: phoebeUid }, - { query: "baz", parentCid: 0 } + { query: 'baz', parentCid: 0 } ); - assert.strictEqual(data[0].name, "baz category"); + assert.strictEqual(data[0].name, 'baz category'); data = await socketCategories.categorySearch( { uid: phoebeUid }, - { query: "", parentCid: 0 } + { query: '', parentCid: 0 } ); assert.strictEqual(data.length, 5); }); - it("should fail if searchIn is wrong", (done) => { + it('should fail if searchIn is wrong', (done) => { search.search( { - query: "plug", - searchIn: "", + query: 'plug', + searchIn: '', }, (err) => { - assert.equal(err.message, "[[error:unknown-search-filter]]"); + assert.equal(err.message, '[[error:unknown-search-filter]]'); done(); } ); }); - it("should search with tags filter", (done) => { + it('should search with tags filter', (done) => { search.search( { - query: "mongodb", - searchIn: "titles", - hasTags: ["nodebb", "javascript"], + query: 'mongodb', + searchIn: 'titles', + hasTags: ['nodebb', 'javascript'], }, (err, data) => { assert.ifError(err); @@ -179,12 +179,12 @@ describe("Search", () => { ); }); - it("should not crash if tags is not an array", (done) => { + it('should not crash if tags is not an array', (done) => { search.search( { - query: "mongodb", - searchIn: "titles", - hasTags: "nodebb,javascript", + query: 'mongodb', + searchIn: 'titles', + hasTags: 'nodebb,javascript', }, (err, data) => { assert.ifError(err); @@ -193,11 +193,11 @@ describe("Search", () => { ); }); - it("should not find anything", (done) => { + it('should not find anything', (done) => { search.search( { - query: "xxxxxxxxxxxxxx", - searchIn: "titles", + query: 'xxxxxxxxxxxxxx', + searchIn: 'titles', }, (err, data) => { assert.ifError(err); @@ -208,47 +208,47 @@ describe("Search", () => { ); }); - it("should search child categories", async () => { + it('should search child categories', async () => { await topics.post({ uid: gingerUid, cid: cid3, - title: "child category topic", - content: "avocado cucumber carrot armadillo", + title: 'child category topic', + content: 'avocado cucumber carrot armadillo', }); const result = await search.search({ - query: "avocado", - searchIn: "titlesposts", + query: 'avocado', + searchIn: 'titlesposts', categories: [cid2], searchChildren: true, - sortBy: "topic.timestamp", - sortDirection: "desc", + sortBy: 'topic.timestamp', + sortDirection: 'desc', }); assert(result.posts.length, 2); - assert(result.posts[0].topic.title === "child category topic"); - assert(result.posts[1].topic.title === "java mongodb redis"); + assert(result.posts[0].topic.title === 'child category topic'); + assert(result.posts[1].topic.title === 'java mongodb redis'); }); - it("should return json search data with no categories", async () => { - const qs = "/api/search?term=cucumber&in=titlesposts&searchOnly=1"; - await privileges.global.give(["groups:search:content"], "guests"); + it('should return json search data with no categories', async () => { + const qs = '/api/search?term=cucumber&in=titlesposts&searchOnly=1'; + await privileges.global.give(['groups:search:content'], 'guests'); - const { body } = await request.get(nconf.get("url") + qs); + const { body } = await request.get(nconf.get('url') + qs); assert(body); - assert(body.hasOwnProperty("matchCount")); - assert(body.hasOwnProperty("pagination")); - assert(body.hasOwnProperty("pageCount")); - assert(body.hasOwnProperty("posts")); - assert(!body.hasOwnProperty("categories")); + assert(body.hasOwnProperty('matchCount')); + assert(body.hasOwnProperty('pagination')); + assert(body.hasOwnProperty('pageCount')); + assert(body.hasOwnProperty('posts')); + assert(!body.hasOwnProperty('categories')); - await privileges.global.rescind(["groups:search:content"], "guests"); + await privileges.global.rescind(['groups:search:content'], 'guests'); }); - it("should not crash without a search term", async () => { - const qs = "/api/search"; - await privileges.global.give(["groups:search:content"], "guests"); - const { response, body } = await request.get(nconf.get("url") + qs); + it('should not crash without a search term', async () => { + const qs = '/api/search'; + await privileges.global.give(['groups:search:content'], 'guests'); + const { response, body } = await request.get(nconf.get('url') + qs); assert(body); assert.strictEqual(response.statusCode, 200); - await privileges.global.rescind(["groups:search:content"], "guests"); + await privileges.global.rescind(['groups:search:content'], 'guests'); }); }); From f1c329c5349f95f74c0e49733a7e6c7d6be53f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Gonz=C3=A1lez?= Date: Mon, 14 Oct 2024 17:28:28 -0400 Subject: [PATCH 29/29] Fix tests asynchrony --- test/search.js | 101 +++++++++++++++++++------------------------------ 1 file changed, 38 insertions(+), 63 deletions(-) diff --git a/test/search.js b/test/search.js index 9fa82bc20d..b5cc0d3d98 100644 --- a/test/search.js +++ b/test/search.js @@ -102,22 +102,16 @@ describe('Search', () => { } }); - it('should search for a tag', (done) => { - search.search( - { - query: 'plug', - searchIn: 'tags', - }, - (err, data) => { - assert.ifError(err); - assert(data); - assert.equal(data.matchCount, 1); - assert.equal(data.tags.length, 1); - assert.equal(data.tags[0].value, 'plugin'); - assert.equal(data.tags[0].score, 2); - done(); - } - ); + it('should search for a tag', async () => { + const data = await search.search({ + query: 'plug', + searchIn: 'tags', + }); + assert(data); + assert.equal(data.matchCount, 1); + assert.equal(data.tags.length, 1); + assert.equal(data.tags[0].value, 'plugin'); + assert.equal(data.tags[0].score, 2); }); it('should search for a category', async () => { @@ -151,61 +145,42 @@ describe('Search', () => { assert.strictEqual(data.length, 5); }); - it('should fail if searchIn is wrong', (done) => { - search.search( - { + it('should fail if searchIn is wrong', async () => { + try { + await search.search({ query: 'plug', searchIn: '', - }, - (err) => { - assert.equal(err.message, '[[error:unknown-search-filter]]'); - done(); - } - ); + }); + } catch (err) { + assert.equal(err.message, '[[error:unknown-search-filter]]'); + } }); - it('should search with tags filter', (done) => { - search.search( - { - query: 'mongodb', - searchIn: 'titles', - hasTags: ['nodebb', 'javascript'], - }, - (err, data) => { - assert.ifError(err); - assert.equal(data.posts[0].tid, topic2Data.tid); - done(); - } - ); + it('should search with tags filter', async () => { + const data = await search.search({ + query: 'mongodb', + searchIn: 'titles', + hasTags: ['nodebb', 'javascript'], + }); + assert.equal(data.posts[0].tid, topic2Data.tid); }); - it('should not crash if tags is not an array', (done) => { - search.search( - { - query: 'mongodb', - searchIn: 'titles', - hasTags: 'nodebb,javascript', - }, - (err, data) => { - assert.ifError(err); - done(); - } - ); + it('should not crash if tags is not an array', async () => { + const data = await search.search({ + query: 'mongodb', + searchIn: 'titles', + hasTags: 'nodebb,javascript', + }); + assert(data); }); - it('should not find anything', (done) => { - search.search( - { - query: 'xxxxxxxxxxxxxx', - searchIn: 'titles', - }, - (err, data) => { - assert.ifError(err); - assert(Array.isArray(data.posts)); - assert(!data.matchCount); - done(); - } - ); + it('should not find anything', async () => { + const data = await search.search({ + query: 'xxxxxxxxxxxxxx', + searchIn: 'titles', + }); + assert(Array.isArray(data.posts)); + assert(!data.matchCount); }); it('should search child categories', async () => {