diff --git a/.eslintrc.js b/.eslintrc.js index 00f90d1c..12245e12 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,5 +11,13 @@ module.exports = { 'require-jsdoc': 'warn', 'no-shadow': 'warn', 'no-unused-expressions': 'warn' - } + }, + overrides: [ + { + files: ['*.js'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off' + } + } + ] }; diff --git a/package.json b/package.json index 8b051888..4f3b0fde 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.14", + "version": "1.1.15", "main": "index.ts", "license": "UNLICENSED", "scripts": { diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 3ffe309c..a11b4f8f 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -149,6 +149,7 @@ class EventsFactory extends Factory { * @param {Number} skip - certain number of documents to skip * @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order * @param {EventsFilters} filters - marks by which events should be filtered + * @param {String} search - Search query * * @return {RecentEventSchema[]} */ @@ -156,8 +157,15 @@ class EventsFactory extends Factory { limit = 10, skip = 0, sort = 'BY_DATE', - filters = {} + filters = {}, + search = '' ) { + if (typeof search !== 'string') { + throw new Error('Search parameter must be a string'); + } + + const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + limit = this.validateLimit(limit); switch (sort) { @@ -184,71 +192,64 @@ class EventsFactory extends Factory { }, ]; - /** - * If some events should be omitted, use alternative pipeline - */ - if (Object.values(filters).length > 0) { - pipeline.push( - /** - * Lookup events object for each daily event - */ - { - $lookup: { - from: 'events:' + this.projectId, - localField: 'groupHash', - foreignField: 'groupHash', - as: 'event', + const searchFilter = search.trim().length > 0 + ? { + $or: [ + { + 'event.payload.title': { + $regex: escapedSearch, + $options: 'i', + }, }, - }, - { - $unwind: '$event', - }, - /** - * Match filters - */ - { - $match: { - ...Object.fromEntries( - Object - .entries(filters) - .map(([mark, exists]) => [`event.marks.${mark}`, { $exists: exists } ]) - ), + { + 'event.payload.backtrace.file': { + $regex: escapedSearch, + $options: 'i', + }, }, + ], + } + : {}; + + const matchFilter = filters + ? Object.fromEntries( + Object + .entries(filters) + .map(([mark, exists]) => [`event.marks.${mark}`, { $exists: exists } ]) + ) + : {}; + + pipeline.push( + { + $lookup: { + from: 'events:' + this.projectId, + localField: 'groupHash', + foreignField: 'groupHash', + as: 'event', }, - { $skip: skip }, - { $limit: limit }, - { - $group: { - _id: null, - dailyInfo: { $push: '$$ROOT' }, - events: { $push: '$event' }, - }, + }, + { + $unwind: '$event', + }, + { + $match: { + ...matchFilter, + ...searchFilter, }, - { - $unset: 'dailyInfo.event', - } - ); - } else { - pipeline.push( - { $skip: skip }, - { $limit: limit }, - { - $group: { - _id: null, - groupHash: { $addToSet: '$groupHash' }, - dailyInfo: { $push: '$$ROOT' }, - }, + }, + { $skip: skip }, + { $limit: limit }, + { + $group: { + _id: null, + dailyInfo: { $push: '$$ROOT' }, + events: { $push: '$event' }, }, - { - $lookup: { - from: 'events:' + this.projectId, - localField: 'groupHash', - foreignField: 'groupHash', - as: 'events', - }, - } - ); - } + }, + { + $unset: 'dailyInfo.event', + } + ); const cursor = this.getCollection(this.TYPES.DAILY_EVENTS).aggregate(pipeline); @@ -316,7 +317,7 @@ class EventsFactory extends Factory { }); /** - * Group events using 'groupByTimestamp:NNNNNNNN' key + * Group events using 'groupingTimestamp:NNNNNNNN' key * @type {ProjectChartItem[]} */ const groupedData = groupBy('groupingTimestamp')(dailyEvents); diff --git a/src/resolvers/project.js b/src/resolvers/project.js index 27811b3e..9c0ada37 100644 --- a/src/resolvers/project.js +++ b/src/resolvers/project.js @@ -11,6 +11,7 @@ const ProjectModel = require('../models/project').default; const EVENTS_GROUP_HASH_INDEX_NAME = 'groupHashUnique'; const REPETITIONS_GROUP_HASH_INDEX_NAME = 'groupHash_hashed'; const REPETITIONS_USER_ID_INDEX_NAME = 'userId'; +const MAX_SEARCH_QUERY_LENGTH = 50; /** * See all types and fields here {@see ../typeDefs/project.graphql} @@ -304,13 +305,20 @@ module.exports = { * @param {Number} skip - certain number of documents to skip * @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order * @param {EventsFilters} filters - marks by which events should be filtered + * @param {String} search - search query * * @return {Promise} */ - async recentEvents(project, { limit, skip, sort, filters }) { + async recentEvents(project, { limit, skip, sort, filters, search }) { + if (search) { + if (search.length > MAX_SEARCH_QUERY_LENGTH) { + search = search.slice(0, MAX_SEARCH_QUERY_LENGTH); + } + } + const factory = new EventsFactory(project._id); - return factory.findRecent(limit, skip, sort, filters); + return factory.findRecent(limit, skip, sort, filters, search); }, /** diff --git a/src/typeDefs/project.ts b/src/typeDefs/project.ts index 9101ae59..2bac1daa 100644 --- a/src/typeDefs/project.ts +++ b/src/typeDefs/project.ts @@ -124,6 +124,9 @@ type Project { "Event marks by which events should be sorted" filters: EventsFiltersInput + + "Search query" + search: String ): RecentEvents """ Return events that occurred after a certain timestamp