Skip to content

Commit 20d5433

Browse files
resolve conflict
2 parents 81d65ea + afaf103 commit 20d5433

8 files changed

Lines changed: 358 additions & 19 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.4.8",
3+
"version": "1.4.12",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {
@@ -41,7 +41,7 @@
4141
"@graphql-tools/merge": "^8.3.1",
4242
"@graphql-tools/schema": "^8.5.1",
4343
"@graphql-tools/utils": "^8.9.0",
44-
"@hawk.so/nodejs": "^3.3.1",
44+
"@hawk.so/nodejs": "^3.3.2",
4545
"@hawk.so/types": "^0.5.9",
4646
"@n1ru4l/json-patch-plus": "^0.2.0",
4747
"@node-saml/node-saml": "^5.0.1",

src/models/eventsFactory.js

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,11 @@ class EventsFactory extends Factory {
203203
*
204204
* @param {Number} limit - events count limitations
205205
* @param {DailyEventsCursor} paginationCursor - object that contains boundary values of the last event in the previous portion
206-
* @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order
207-
* @param {EventsFilters} filters - marks by which events should be filtered
206+
* @param {'BY_DATE' | 'BY_COUNT' | 'BY_AFFECTED_USERS'} sort - events sort order
207+
* @param {EventsFilters} filters - marks by which events should be filtered (resolved, starred, ignored only; assignee is separate)
208208
* @param {String} search - Search query
209-
* @param {String} release - release name
209+
* @param {String|undefined} release - release name
210+
* @param {String|undefined} assignee - user id or __filter_unassigned__ / __filter_any_assignee__
210211
*
211212
* @return {DaylyEventsPortionSchema}
212213
*/
@@ -216,7 +217,8 @@ class EventsFactory extends Factory {
216217
sort = 'BY_DATE',
217218
filters = {},
218219
search = '',
219-
release
220+
release,
221+
assignee
220222
) {
221223
if (typeof search !== 'string') {
222224
throw new Error('Search parameter must be a string');
@@ -334,10 +336,12 @@ class EventsFactory extends Factory {
334336
}
335337
: {};
336338

339+
const markFilters = ['resolved', 'starred', 'ignored'];
337340
const matchFilter = filters
338341
? Object.fromEntries(
339342
Object
340343
.entries(filters)
344+
.filter(([ mark ]) => markFilters.includes(mark))
341345
.map(([mark, exists]) => [`event.marks.${mark}`, { $exists: exists } ])
342346
)
343347
: {};
@@ -361,6 +365,44 @@ class EventsFactory extends Factory {
361365
}
362366
: {};
363367

368+
/**
369+
* Sentinel values from garage assignee filter (not user ids)
370+
*/
371+
const FILTER_UNASSIGNED = '__filter_unassigned__';
372+
const FILTER_ANY_ASSIGNEE = '__filter_any_assignee__';
373+
374+
const assigneeFilter = (() => {
375+
if (!assignee) {
376+
return {};
377+
}
378+
if (assignee === FILTER_UNASSIGNED) {
379+
/**
380+
* Use $and so this does not collide with searchFilter’s top-level $or in $match spread
381+
*/
382+
return {
383+
$and: [
384+
{
385+
$or: [
386+
{ 'event.assignee': { $exists: false } },
387+
{ 'event.assignee': null },
388+
{ 'event.assignee': '' },
389+
],
390+
},
391+
],
392+
};
393+
}
394+
if (assignee === FILTER_ANY_ASSIGNEE) {
395+
return {
396+
'event.assignee': {
397+
$exists: true,
398+
$nin: [null, ''],
399+
},
400+
};
401+
}
402+
403+
return { 'event.assignee': String(assignee) };
404+
})();
405+
364406
pipeline.push(
365407
/**
366408
* Left outer join original event on groupHash field
@@ -398,6 +440,7 @@ class EventsFactory extends Factory {
398440
...matchFilter,
399441
...searchFilter,
400442
...releaseFilter,
443+
...assigneeFilter,
401444
},
402445
},
403446
{ $limit: limit + 1 },

src/resolvers/billingNew.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export default {
8686
currency: string;
8787
checksum: string;
8888
nextPaymentDate: Date;
89+
cloudPaymentsPublicId: string;
8990
}> {
9091
const { workspaceId, tariffPlanId, shouldSaveCard } = input;
9192

@@ -178,6 +179,7 @@ debug: ${Boolean(workspace.isDebug)}`
178179
currency: 'RUB',
179180
checksum,
180181
nextPaymentDate,
182+
cloudPaymentsPublicId: process.env.CLOUDPAYMENTS_PUBLIC_ID || '',
181183
};
182184
},
183185
},

src/resolvers/project.js

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,54 @@ const GROUPING_TIMESTAMP_INDEX_NAME = 'groupingTimestamp';
2020
const GROUPING_TIMESTAMP_AND_LAST_REPETITION_TIME_AND_ID_INDEX_NAME = 'groupingTimestampAndLastRepetitionTimeAndId';
2121
const GROUPING_TIMESTAMP_AND_GROUP_HASH_INDEX_NAME = 'groupingTimestampAndGroupHash';
2222
const MAX_SEARCH_QUERY_LENGTH = 50;
23+
const FALLBACK_EVENT_TITLE = 'Unknown';
24+
25+
/**
26+
* Ensures each daily event has non-empty payload title
27+
* and writes warning log with identifiers when fallback is used.
28+
*
29+
* @param {object} dailyEventsPortion - portion returned by events factory
30+
* @param {string|ObjectId} projectId - project id for logs
31+
* @returns {object}
32+
*/
33+
function normalizeDailyEventsPayloadTitle(dailyEventsPortion, projectId) {
34+
if (!dailyEventsPortion || !Array.isArray(dailyEventsPortion.dailyEvents)) {
35+
return dailyEventsPortion;
36+
}
37+
38+
dailyEventsPortion.dailyEvents = dailyEventsPortion.dailyEvents.map((dailyEvent) => {
39+
const event = dailyEvent && dailyEvent.event ? dailyEvent.event : null;
40+
const payload = event && event.payload ? event.payload : null;
41+
const hasValidTitle = payload &&
42+
typeof payload.title === 'string' &&
43+
payload.title.trim().length > 0;
44+
45+
if (hasValidTitle) {
46+
return dailyEvent;
47+
}
48+
49+
console.warn('🔴🔴🔴 [ProjectResolver.dailyEventsPortion] Missing event payload title. Fallback title applied.', {
50+
projectId: projectId ? projectId.toString() : null,
51+
dailyEventId: dailyEvent && dailyEvent.id ? dailyEvent.id.toString() : null,
52+
dailyEventGroupHash: dailyEvent && dailyEvent.groupHash ? dailyEvent.groupHash.toString() : null,
53+
eventOriginalId: event && event.originalEventId ? event.originalEventId.toString() : null,
54+
eventId: event && event._id ? event._id.toString() : null,
55+
});
56+
57+
return {
58+
...dailyEvent,
59+
event: {
60+
...(event || {}),
61+
payload: {
62+
...(payload || {}),
63+
title: FALLBACK_EVENT_TITLE,
64+
},
65+
},
66+
};
67+
});
68+
69+
return dailyEventsPortion;
70+
}
2371

2472
/**
2573
* See all types and fields here {@see ../typeDefs/project.graphql}
@@ -571,19 +619,21 @@ module.exports = {
571619
},
572620

573621
/**
574-
* Returns recent Events grouped by day
575-
*
576-
* @param {ProjectDBScheme} project - result of parent resolver
577-
* @param {Number} limit - limit for events count
578-
* @param {DailyEventsCursor} cursor - object with boundary values of the first event in the next portion
579-
* @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order
580-
* @param {EventsFilters} filters - marks by which events should be filtered
581-
* @param {String} release - release name
582-
* @param {String} search - search query
622+
* Returns a paginated portion of daily-grouped events
583623
*
584-
* @return {Promise<RecentEventSchema[]>}
624+
* @param {ProjectDBScheme} project - parent resolver result
625+
* @param {object} args - GraphQL arguments
626+
* @param {number} args.limit - max rows in portion
627+
* @param {object|null} args.nextCursor - pagination cursor
628+
* @param {string} args.sort - BY_DATE | BY_COUNT | BY_AFFECTED_USERS (mapped in factory)
629+
* @param {object} args.filters - mark filters only: resolved, starred, ignored (assignee uses args.assignee)
630+
* @param {string} args.search - search query
631+
* @param {string|undefined} args.release - optional release label filter
632+
* @param {string|undefined} args.assignee - user id or __filter_unassigned__ / __filter_any_assignee__
633+
* @param {object} context - GraphQL context
634+
* @returns {Promise<object>} dailyEventsPortion payload from factory
585635
*/
586-
async dailyEventsPortion(project, { limit, nextCursor, sort, filters, search, release }, context) {
636+
async dailyEventsPortion(project, { limit, nextCursor, sort, filters, search, release, assignee }, context) {
587637
if (search) {
588638
if (search.length > MAX_SEARCH_QUERY_LENGTH) {
589639
search = search.slice(0, MAX_SEARCH_QUERY_LENGTH);
@@ -592,7 +642,17 @@ module.exports = {
592642

593643
const factory = getEventsFactory(context, project._id);
594644

595-
const dailyEventsPortion = await factory.findDailyEventsPortion(limit, nextCursor, sort, filters, search, release);
645+
const dailyEventsPortion = await factory.findDailyEventsPortion(
646+
limit,
647+
nextCursor,
648+
sort,
649+
filters,
650+
search,
651+
release,
652+
assignee
653+
);
654+
655+
normalizeDailyEventsPayloadTitle(dailyEventsPortion, project._id);
596656

597657
return dailyEventsPortion;
598658
},

src/resolvers/workspace.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,8 @@ module.exports = {
436436

437437
const defaultPlan = await factories.plansFactory.getDefaultPlan();
438438

439-
if (workspaceModel.tariffPlanId === defaultPlan.id) {
439+
// Prevent re-applying the free plan if workspace is already on it.
440+
if (workspaceModel.tariffPlanId.toString() === defaultPlan._id.toString()) {
440441
throw new UserInputError('You already use default plan');
441442
}
442443

src/typeDefs/billing.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ type ComposePaymentResponse {
270270
Next payment date (recurrent start)
271271
"""
272272
nextPaymentDate: DateTime!
273+
274+
"""
275+
CloudPayments public id (merchant identifier for payment widget)
276+
"""
277+
cloudPaymentsPublicId: String!
273278
}
274279
275280

src/typeDefs/project.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ type Project {
347347
Release label to filter events by payload.release
348348
"""
349349
release: String
350+
351+
"""
352+
Filter by assignee: workspace user id, or sentinels __filter_unassigned__ (no assignee) / __filter_any_assignee__ (has assignee)
353+
"""
354+
assignee: ID
350355
): DailyEventsPortion
351356
352357
"""

0 commit comments

Comments
 (0)