From 8a7d63a71ea945197ae0a77b134202fbcb7147e8 Mon Sep 17 00:00:00 2001 From: Pavel Zotikov Date: Thu, 31 Jul 2025 13:58:51 +0300 Subject: [PATCH 1/4] Add support for date range and marks filtering in recentEvents --- .gitignore | 1 + src/models/eventsFactory.js | 66 +++++++++++++++++++++++-------------- src/typeDefs/project.ts | 33 +++++++++++++++---- 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index b0e31773..8d7674dc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ uploads globalConfig.json coverage tls +.history \ No newline at end of file diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 865dc9c7..1c9b443c 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -149,8 +149,11 @@ class EventsFactory extends Factory { * @param {Number} limit - events count limitations * @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 + * @param {Object} filters - filter object + * @param {Object.} [filters.marks] - mark filters: e.g. { starred: true, ignored: false } + * @param {string|number} [filters.dateFrom] - filter start date (ISO or timestamp) + * @param {string|number} [filters.dateTo] - filter end date (ISO or timestamp) + * @param {String} search - search query * * @return {RecentEventSchema[]} */ @@ -165,9 +168,6 @@ class EventsFactory extends Factory { throw new Error('Search parameter must be a string'); } - /** - * Check if pattern is safe RegExp - */ if (!safe(search)) { throw new Error('Invalid regular expression pattern'); } @@ -231,13 +231,41 @@ class EventsFactory extends Factory { } : {}; - const matchFilter = filters - ? Object.fromEntries( - Object - .entries(filters) - .map(([mark, exists]) => [`event.marks.${mark}`, { $exists: exists } ]) - ) - : {}; + const matchFilter = { + ...searchFilter, + }; + + // Filter by marks (event.marks.{key}) + if (filters.marks && typeof filters.marks === 'object') { + for (const [mark, exists] of Object.entries(filters.marks)) { + matchFilter[`event.marks.${mark}`] = { $exists: exists }; + } + } + + // Filter by date (groupingTimestamp) + if (filters.dateFrom || filters.dateTo) { + matchFilter.groupingTimestamp = {}; + + if (filters.dateFrom) { + const from = typeof filters.dateFrom === 'string' + ? Math.floor(new Date(filters.dateFrom).getTime() / 1000) + : filters.dateFrom; + + matchFilter.groupingTimestamp.$gte = from; + } + + if (filters.dateTo) { + const to = typeof filters.dateTo === 'string' + ? Math.floor(new Date(filters.dateTo).getTime() / 1000) + : filters.dateTo; + + matchFilter.groupingTimestamp.$lte = to; + } + + if (Object.keys(matchFilter.groupingTimestamp).length === 0) { + delete matchFilter.groupingTimestamp; + } + } pipeline.push( { @@ -252,10 +280,7 @@ class EventsFactory extends Factory { $unwind: '$event', }, { - $match: { - ...matchFilter, - ...searchFilter, - }, + $match: matchFilter, }, { $skip: skip }, { $limit: limit }, @@ -272,17 +297,8 @@ class EventsFactory extends Factory { ); const cursor = this.getCollection(this.TYPES.DAILY_EVENTS).aggregate(pipeline); - const result = (await cursor.toArray()).shift(); - /** - * aggregation can return empty array so that - * result can be undefined - * - * for that we check result existence - * - * extra field `projectId` needs to satisfy GraphQL query - */ if (result && result.events) { result.events.forEach(event => { event.projectId = this.projectId; diff --git a/src/typeDefs/project.ts b/src/typeDefs/project.ts index f0fab0e2..69f088ac 100644 --- a/src/typeDefs/project.ts +++ b/src/typeDefs/project.ts @@ -16,16 +16,32 @@ Events filters input type """ input EventsFiltersInput { """ - If True, includes events with resolved mark to the output + Filter events by marks (e.g. starred, resolved, ignored). + Set to true to include only events that have the mark, or false to exclude those. + Example: { starred: true, ignored: false } """ - resolved: Boolean + marks: MarksFilterInput + """ - If True, includes events with starred mark to the output + Include only events that occurred after this date (inclusive). + Accepts ISO date string or Unix timestamp (in seconds). """ - starred: Boolean + dateFrom: Timestamp + """ - If True, includes events with ignored mark to the output + Include only events that occurred before this date (inclusive). + Accepts ISO date string or Unix timestamp (in seconds). """ + dateTo: Timestamp +} + +""" +Allows filtering by specific event marks. +Each field corresponds to event.marks.{name}. +""" +input MarksFilterInput { + starred: Boolean + resolved: Boolean ignored: Boolean } @@ -122,12 +138,17 @@ type Project { "Events sort order" sort: EventsSortOrder = lastRepetitionTime - "Event marks by which events should be sorted" + """ + Filters for narrowing down the event results: + - By marks (e.g., starred, resolved) + - By date range (dateFrom / dateTo) + """ filters: EventsFiltersInput "Search query" search: String ): RecentEvents + """ Return events that occurred after a certain timestamp """ From 1bd29cd46f86b0367b92718126b84d39617d2e93 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:00:48 +0000 Subject: [PATCH 2/4] Bump version up to 1.1.29 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f47295e..f9b32ca1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.28", + "version": "1.1.29", "main": "index.ts", "license": "UNLICENSED", "scripts": { From cfc7e12aa2cf9522f3abf566fb94219b2340403e Mon Sep 17 00:00:00 2001 From: Pavel Zotikov Date: Thu, 31 Jul 2025 14:16:28 +0300 Subject: [PATCH 3/4] Refactor recentEvents filtering: move marks to top-level and add date range support --- src/models/eventsFactory.js | 21 ++++++++++----------- src/typeDefs/project.ts | 32 ++++++++++++++------------------ 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 1c9b443c..f76c7c11 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -23,9 +23,11 @@ const { ObjectID } = require('mongodb'); /** * @typedef {Object} EventsFilters - * @property {boolean} [starred] - if true, events with 'starred' mark should be included to the output - * @property {boolean} [resolved] - if true, events with 'resolved' should be included to the output - * @property {boolean} [ignored] - if true, events with 'ignored' mark should be included to the output + * @property {boolean} [starred] + * @property {boolean} [resolved] + * @property {boolean} [ignored] + * @property {string|number} [dateFrom] + * @property {string|number} [dateTo] */ /** @@ -149,10 +151,7 @@ class EventsFactory extends Factory { * @param {Number} limit - events count limitations * @param {Number} skip - certain number of documents to skip * @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order - * @param {Object} filters - filter object - * @param {Object.} [filters.marks] - mark filters: e.g. { starred: true, ignored: false } - * @param {string|number} [filters.dateFrom] - filter start date (ISO or timestamp) - * @param {string|number} [filters.dateTo] - filter end date (ISO or timestamp) + * @param {EventsFilters} filters - filter object * @param {String} search - search query * * @return {RecentEventSchema[]} @@ -236,11 +235,11 @@ class EventsFactory extends Factory { }; // Filter by marks (event.marks.{key}) - if (filters.marks && typeof filters.marks === 'object') { - for (const [mark, exists] of Object.entries(filters.marks)) { - matchFilter[`event.marks.${mark}`] = { $exists: exists }; + ['starred', 'resolved', 'ignored'].forEach((mark) => { + if (typeof filters[mark] === 'boolean') { + matchFilter[`event.marks.${mark}`] = { $exists: filters[mark] }; } - } + }); // Filter by date (groupingTimestamp) if (filters.dateFrom || filters.dateTo) { diff --git a/src/typeDefs/project.ts b/src/typeDefs/project.ts index 69f088ac..cd824e40 100644 --- a/src/typeDefs/project.ts +++ b/src/typeDefs/project.ts @@ -16,35 +16,31 @@ Events filters input type """ input EventsFiltersInput { """ - Filter events by marks (e.g. starred, resolved, ignored). - Set to true to include only events that have the mark, or false to exclude those. - Example: { starred: true, ignored: false } + If true, includes events with resolved mark """ - marks: MarksFilterInput + resolved: Boolean + + """ + If true, includes events with starred mark + """ + starred: Boolean + + """ + If true, includes events with ignored mark + """ + ignored: Boolean """ - Include only events that occurred after this date (inclusive). - Accepts ISO date string or Unix timestamp (in seconds). + Include events with groupingTimestamp >= dateFrom (ISO or timestamp in seconds) """ dateFrom: Timestamp """ - Include only events that occurred before this date (inclusive). - Accepts ISO date string or Unix timestamp (in seconds). + Include events with groupingTimestamp <= dateTo (ISO or timestamp in seconds) """ dateTo: Timestamp } -""" -Allows filtering by specific event marks. -Each field corresponds to event.marks.{name}. -""" -input MarksFilterInput { - starred: Boolean - resolved: Boolean - ignored: Boolean -} - """ Respose object with updated project and his id """ From 2672e6b36b144d17cdc70a16b6135e237568742e Mon Sep 17 00:00:00 2001 From: Pavel Zotikov Date: Thu, 31 Jul 2025 19:06:38 +0300 Subject: [PATCH 4/4] Update eventsFactory.js --- src/models/eventsFactory.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index f76c7c11..8c636505 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -23,9 +23,9 @@ const { ObjectID } = require('mongodb'); /** * @typedef {Object} EventsFilters - * @property {boolean} [starred] - * @property {boolean} [resolved] - * @property {boolean} [ignored] + * @property {boolean} [starred] - if true, events with 'starred' mark should be included to the output + * @property {boolean} [resolved] - if true, events with 'resolved' should be included to the output + * @property {boolean} [ignored] - if true, events with 'ignored' mark should be included to the output * @property {string|number} [dateFrom] * @property {string|number} [dateTo] */ @@ -167,6 +167,9 @@ class EventsFactory extends Factory { throw new Error('Search parameter must be a string'); } + /** + * Check if pattern is safe RegExp + */ if (!safe(search)) { throw new Error('Invalid regular expression pattern'); } @@ -234,7 +237,6 @@ class EventsFactory extends Factory { ...searchFilter, }; - // Filter by marks (event.marks.{key}) ['starred', 'resolved', 'ignored'].forEach((mark) => { if (typeof filters[mark] === 'boolean') { matchFilter[`event.marks.${mark}`] = { $exists: filters[mark] }; @@ -298,6 +300,14 @@ class EventsFactory extends Factory { const cursor = this.getCollection(this.TYPES.DAILY_EVENTS).aggregate(pipeline); const result = (await cursor.toArray()).shift(); + /** + * aggregation can return empty array so that + * result can be undefined + * + * for that we check result existence + * + * extra field `projectId` needs to satisfy GraphQL query + */ if (result && result.events) { result.events.forEach(event => { event.projectId = this.projectId;