Skip to content

Commit 75b10df

Browse files
authored
Merge pull request #567 from codex-team/feat/opt-dupl
feat(perf): rm duplicated ids from batched queries
2 parents b86fab1 + c3f59bd commit 75b10df

File tree

8 files changed

+61
-63
lines changed

8 files changed

+61
-63
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.2.10",
3+
"version": "1.2.11",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {

src/dataLoaders.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,17 @@ export default class DataLoaders {
8282
private async batchByField<
8383
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8484
T extends { [key: string]: any },
85-
FieldType extends object | string
85+
FieldType extends ObjectId | string
8686
>(collectionName: string, values: ReadonlyArray<FieldType>, fieldName: string): Promise<(T | null | Error)[]> {
87+
const valuesMap = new Map<string, FieldType>();
88+
89+
for (const value of values) {
90+
valuesMap.set(value.toString(), value);
91+
}
92+
8793
const queryResult = await this.dbConnection.collection(collectionName)
8894
.find({
89-
[fieldName]: { $in: values },
95+
[fieldName]: { $in: Array.from(valuesMap.values()) },
9096
})
9197
.toArray();
9298

@@ -115,7 +121,10 @@ export function createProjectEventsByIdLoader(
115121
projectId: string
116122
): DataLoader<string, EventDbScheme | null> {
117123
return new DataLoader<string, EventDbScheme | null>(async (ids) => {
118-
const objectIds = ids.map((id) => new ObjectId(id));
124+
/**
125+
* Deduplicate only for the DB query; keep original ids array for mapping
126+
*/
127+
const objectIds = [ ...new Set(ids) ].map(id => new ObjectId(id));
119128

120129
const docs = await eventsDb
121130
.collection(`events:${projectId}`)

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,6 @@ class HawkAPI {
237237
id: userId,
238238
accessTokenExpired: isAccessTokenExpired,
239239
},
240-
eventsFactoryCache: new Map(),
241240
// accounting,
242241
};
243242
}

src/resolvers/event.js

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,6 @@
1-
const EventsFactory = require('../models/eventsFactory');
1+
const getEventsFactory = require('./helpers/eventsFactory').default;
22
const sendPersonalNotification = require('../utils/personalNotifications').default;
33

4-
/**
5-
* Returns a per-request, per-project EventsFactory instance
6-
* Uses context.eventsFactoryCache to memoize by projectId
7-
*
8-
* @param {ResolverContextBase} context - resolver context
9-
* @param {string} projectId - project id to get EventsFactory instance for
10-
* @returns {EventsFactory} - EventsFactory instance bound to a specific project object
11-
*/
12-
function getEventsFactoryForProjectId(context, projectId) {
13-
const cache = context.eventsFactoryCache || (context.eventsFactoryCache = new Map());
14-
const cacheKey = projectId.toString();
15-
16-
if (!cache.has(cacheKey)) {
17-
cache.set(cacheKey, new EventsFactory(projectId));
18-
}
19-
20-
return cache.get(cacheKey);
21-
}
22-
234
/**
245
* See all types and fields here {@see ../typeDefs/event.graphql}
256
*/
@@ -48,7 +29,7 @@ module.exports = {
4829
* @return {RepetitionsPortion}
4930
*/
5031
async repetitionsPortion({ projectId, originalEventId }, { limit, cursor }, context) {
51-
const factory = getEventsFactoryForProjectId(context, projectId);
32+
const factory = getEventsFactory(context, projectId);
5233

5334
return factory.getEventRepetitions(originalEventId, limit, cursor);
5435
},
@@ -103,7 +84,7 @@ module.exports = {
10384
* @returns {Promise<ProjectChartItem[]>}
10485
*/
10586
async chartData({ projectId, groupHash }, { days, timezoneOffset }, context) {
106-
const factory = getEventsFactoryForProjectId(context, projectId);
87+
const factory = getEventsFactory(context, projectId);
10788

10889
return factory.findChartData(days, timezoneOffset, groupHash);
10990
},
@@ -116,7 +97,7 @@ module.exports = {
11697
* @returns {Promise<Release>}
11798
*/
11899
async release({ projectId, id: eventId }, _args, context) {
119-
const factory = getEventsFactoryForProjectId(context, projectId);
100+
const factory = getEventsFactory(context, projectId);
120101
const release = await factory.getEventRelease(eventId);
121102

122103
return release;
@@ -133,7 +114,7 @@ module.exports = {
133114
* @return {Promise<boolean>}
134115
*/
135116
async visitEvent(_obj, { projectId, eventId }, { user, ...context }) {
136-
const factory = getEventsFactoryForProjectId(context, projectId);
117+
const factory = getEventsFactory(context, projectId);
137118

138119
const { result } = await factory.visitEvent(eventId, user.id);
139120

@@ -150,7 +131,7 @@ module.exports = {
150131
* @return {Promise<boolean>}
151132
*/
152133
async toggleEventMark(_obj, { project, eventId, mark }, context) {
153-
const factory = getEventsFactoryForProjectId(context, project);
134+
const factory = getEventsFactory(context, project);
154135

155136
const { result } = await factory.toggleEventMark(eventId, mark);
156137

@@ -175,7 +156,7 @@ module.exports = {
175156
*/
176157
async updateAssignee(_obj, { input }, { factories, user, ...context }) {
177158
const { projectId, eventId, assignee } = input;
178-
const factory = getEventsFactoryForProjectId(context, projectId);
159+
const factory = getEventsFactory(context, projectId);
179160

180161
const userExists = await factories.usersFactory.findById(assignee);
181162

@@ -226,7 +207,7 @@ module.exports = {
226207
*/
227208
async removeAssignee(_obj, { input }, context) {
228209
const { projectId, eventId } = input;
229-
const factory = getEventsFactoryForProjectId(context, projectId);
210+
const factory = getEventsFactory(context, projectId);
230211

231212
const { result } = await factory.updateAssignee(eventId, '');
232213

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ResolverContextBase } from '../../types/graphql';
2+
3+
// eslint-disable-next-line @typescript-eslint/no-var-requires
4+
const EventsFactory = require('../../models/eventsFactory');
5+
6+
/**
7+
* Returns a per-request, per-project EventsFactory instance
8+
* Uses context.eventsFactoryCache to memoize by projectId
9+
*
10+
* @param {ResolverContextBase} context - resolver context
11+
* @param {string} projectId - project id to get EventsFactory instance for
12+
* @returns {EventsFactory} - EventsFactory instance bound to a specific project object
13+
*/
14+
export function getEventsFactory(context: ResolverContextBase, projectId: string) {
15+
if (!context.eventsFactoryCache) {
16+
context.eventsFactoryCache = new Map();
17+
}
18+
19+
const cache = context.eventsFactoryCache;
20+
21+
if (cache) {
22+
if (!cache.has(projectId)) {
23+
cache.set(projectId, new EventsFactory(projectId));
24+
}
25+
26+
return cache.get(projectId);
27+
}
28+
29+
return new EventsFactory(projectId);
30+
}
31+
32+
export default getEventsFactory;

src/resolvers/project.js

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { ApolloError, UserInputError } = require('apollo-server-express');
55
const Validator = require('../utils/validator');
66
const UserInProject = require('../models/userInProject');
77
const EventsFactory = require('../models/eventsFactory');
8+
const getEventsFactory = require('./helpers/eventsFactory').default;
89
const ProjectToWorkspace = require('../models/projectToWorkspace');
910
const { dateFromObjectId } = require('../utils/dates');
1011
const ProjectModel = require('../models/project').default;
@@ -14,30 +15,6 @@ const REPETITIONS_GROUP_HASH_INDEX_NAME = 'groupHash_hashed';
1415
const REPETITIONS_USER_ID_INDEX_NAME = 'userId';
1516
const MAX_SEARCH_QUERY_LENGTH = 50;
1617

17-
/**
18-
* Returns a singleton EventsFactory instance bound to a specific project object.
19-
* Uses request-scoped cache to share across nested resolvers.
20-
*
21-
* @param {ProjectDBScheme|Object} project - project instance to make a instance of EventsFactory
22-
* @param {ResolverContextBase} context - resolver context
23-
* @returns {EventsFactory} - EventsFactory instance bound to a specific project object
24-
*/
25-
function getEventsFactoryForProject(project, context) {
26-
const cache = context && context.eventsFactoryCache;
27-
const key = project._id.toString();
28-
29-
if (cache) {
30-
if (!cache.has(key)) {
31-
cache.set(key, new EventsFactory(project._id));
32-
}
33-
34-
return cache.get(key);
35-
}
36-
37-
// Fallback (shouldn't happen in normal resolver flow): return a fresh instance
38-
return new EventsFactory(project._id);
39-
}
40-
4118
/**
4219
* See all types and fields here {@see ../typeDefs/project.graphql}
4320
*/
@@ -386,7 +363,7 @@ module.exports = {
386363
* @returns {EventRepetitionSchema}
387364
*/
388365
async event(project, { eventId: repetitionId, originalEventId }, context) {
389-
const factory = getEventsFactoryForProject(project, context);
366+
const factory = getEventsFactory(context, project._id);
390367
const repetition = await factory.getEventRepetition(repetitionId, originalEventId);
391368

392369
if (!repetition) {
@@ -408,7 +385,7 @@ module.exports = {
408385
* @returns {Event[]}
409386
*/
410387
async events(project, { limit, skip }, context) {
411-
const factory = getEventsFactoryForProject(project, context);
388+
const factory = getEventsFactory(context, project._id);
412389

413390
return factory.find({}, limit, skip);
414391
},
@@ -423,7 +400,7 @@ module.exports = {
423400
* @return {Promise<number>}
424401
*/
425402
async unreadCount(project, data, { user, ...context }) {
426-
const eventsFactory = getEventsFactoryForProject(project, context);
403+
const eventsFactory = getEventsFactory(context, project._id);
427404
const userInProject = new UserInProject(user.id, project._id);
428405
const lastVisit = await userInProject.getLastVisit();
429406

@@ -449,7 +426,7 @@ module.exports = {
449426
}
450427
}
451428

452-
const factory = getEventsFactoryForProject(project, context);
429+
const factory = getEventsFactory(context, project._id);
453430

454431
const dailyEventsPortion = await factory.findDailyEventsPortion(limit, nextCursor, sort, filters, search);
455432

@@ -466,7 +443,7 @@ module.exports = {
466443
* @return {Promise<ProjectChartItem[]>}
467444
*/
468445
async chartData(project, { days, timezoneOffset }, context) {
469-
const factory = getEventsFactoryForProject(project, context);
446+
const factory = getEventsFactory(context, project._id);
470447

471448
return factory.findChartData(days, timezoneOffset);
472449
},

src/types/graphql.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ export interface ResolverContextBase {
2222

2323
/**
2424
* Request-scoped cache for EventsFactory instances keyed by projectId
25+
* Set by getEventsFactory helper in event & project resolvers
2526
*/
2627
// eslint-disable-next-line @typescript-eslint/no-explicit-any
27-
eventsFactoryCache: Map<string, any>;
28+
eventsFactoryCache?: Map<string, any>;
2829

2930
// /**
3031
// * SDK for working with CodeX Accounting API

test/resolvers/billingNew.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ function createComposePaymentTestSetup(options: {
7171
};
7272

7373
const mockContext: ResolverContextWithUser = {
74-
eventsFactoryCache: new Map(),
7574
user: {
7675
id: userId,
7776
accessTokenExpired: false,

0 commit comments

Comments
 (0)