Skip to content

Commit 93cad23

Browse files
committed
feat(perf): added dataLoading for dynamic events collections
1 parent 26398b0 commit 93cad23

File tree

6 files changed

+149
-53
lines changed

6 files changed

+149
-53
lines changed

src/dataLoaders.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import DataLoader from 'dataloader';
22
import { Db, ObjectId } from 'mongodb';
3-
import { PlanDBScheme, UserDBScheme, WorkspaceDBScheme, ProjectDBScheme } from '@hawk.so/types';
3+
import { PlanDBScheme, UserDBScheme, WorkspaceDBScheme, ProjectDBScheme, EventData, EventAddons } from '@hawk.so/types';
4+
5+
type EventDbScheme = {
6+
_id: ObjectId;
7+
} & EventData<EventAddons>;
48

59
/**
610
* Class for setting up data loaders
@@ -14,6 +18,11 @@ export default class DataLoaders {
1418
{ cache: false }
1519
);
1620

21+
public eventById = new DataLoader<string, EventDbScheme | null>(
22+
(eventIds) => this.batchByIds<EventDbScheme>('events', eventIds),
23+
{ cache: true }
24+
);
25+
1726
/**
1827
* Loader for fetching workspaces
1928
*/
@@ -65,7 +74,7 @@ export default class DataLoaders {
6574
* @param collectionName - collection name to get entities
6675
* @param ids - ids for resolving
6776
*/
68-
private async batchByIds<T extends {_id: ObjectId}>(collectionName: string, ids: ReadonlyArray<string>): Promise<(T | null | Error)[]> {
77+
private async batchByIds<T extends { _id: ObjectId }>(collectionName: string, ids: ReadonlyArray<string>): Promise<(T | null | Error)[]> {
6978
return this.batchByField<T, ObjectId>(collectionName, ids.map(id => new ObjectId(id)), '_id');
7079
}
7180

@@ -77,9 +86,9 @@ export default class DataLoaders {
7786
*/
7887
private async batchByField<
7988
// eslint-disable-next-line @typescript-eslint/no-explicit-any
80-
T extends {[key: string]: any},
89+
T extends { [key: string]: any },
8190
FieldType extends object | string
82-
>(collectionName: string, values: ReadonlyArray<FieldType>, fieldName: string): Promise<(T | null | Error)[]> {
91+
>(collectionName: string, values: ReadonlyArray<FieldType>, fieldName: string): Promise<(T | null | Error)[]> {
8392
const queryResult = await this.dbConnection.collection(collectionName)
8493
.find({
8594
[fieldName]: { $in: values },
@@ -99,3 +108,30 @@ export default class DataLoaders {
99108
return values.map((field) => entitiesMap[field.toString()] || null);
100109
}
101110
}
111+
112+
/**
113+
* Create DataLoader for events in dynamic collections `events:<projectId>` stored in the events DB
114+
* @param eventsDb - MongoDB connection to the events database
115+
* @param projectId - project id used to pick a dynamic collection
116+
*/
117+
export function createProjectEventsByIdLoader(
118+
eventsDb: Db,
119+
projectId: string
120+
): DataLoader<string, EventDbScheme | null> {
121+
return new DataLoader<string, EventDbScheme | null>(async (ids) => {
122+
const objectIds = ids.map((id) => new ObjectId(id));
123+
124+
const docs = await eventsDb
125+
.collection(`events:${projectId}`)
126+
.find({ _id: { $in: objectIds } })
127+
.toArray();
128+
129+
const map: Record<string, EventDbScheme> = {};
130+
131+
docs.forEach((doc) => {
132+
map[doc._id.toString()] = doc as EventDbScheme;
133+
});
134+
135+
return ids.map((id) => map[id] || null);
136+
}, { cache: true });
137+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ class HawkAPI {
237237
id: userId,
238238
accessTokenExpired: isAccessTokenExpired,
239239
},
240+
eventsFactoryCache: new Map(),
240241
// accounting,
241242
};
242243
}

src/models/eventsFactory.js

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { getMidnightWithTimezoneOffset, getUTCMidnight } from '../utils/dates';
22
import safe from 'safe-regex';
3+
import { createProjectEventsByIdLoader } from '../dataLoaders';
34

45
const Factory = require('./modelFactory');
56
const mongo = require('../mongo');
67
const Event = require('../models/event');
78
const { ObjectID } = require('mongodb');
89
const { composeEventPayloadByRepetition } = require('../utils/merge');
910

11+
1012
const MAX_DB_READ_BATCH_SIZE = Number(process.env.MAX_DB_READ_BATCH_SIZE);
1113

1214
/**
@@ -93,7 +95,10 @@ class EventsFactory extends Factory {
9395
throw new Error('Can not construct Event model, because projectId is not provided');
9496
}
9597

98+
console.log('projectId', projectId);
99+
96100
this.projectId = projectId;
101+
this.eventById = createProjectEventsByIdLoader(mongo.databases.events, this.projectId);
97102
}
98103

99104
/**
@@ -135,7 +140,7 @@ class EventsFactory extends Factory {
135140

136141
const cursor = this.getCollection(this.TYPES.EVENTS)
137142
.find(query)
138-
.sort([ ['_id', -1] ])
143+
.sort([['_id', -1]])
139144
.limit(limit)
140145
.skip(skip);
141146

@@ -156,10 +161,7 @@ class EventsFactory extends Factory {
156161
* @returns {Event|null}
157162
*/
158163
async findById(id) {
159-
const searchResult = await this.getCollection(this.TYPES.EVENTS)
160-
.findOne({
161-
_id: new ObjectID(id),
162-
});
164+
const searchResult = await this.eventById.load(id);
163165

164166
const event = searchResult ? new Event(searchResult) : null;
165167

@@ -311,7 +313,7 @@ class EventsFactory extends Factory {
311313
? Object.fromEntries(
312314
Object
313315
.entries(filters)
314-
.map(([mark, exists]) => [`event.marks.${mark}`, { $exists: exists } ])
316+
.map(([mark, exists]) => [`event.marks.${mark}`, { $exists: exists }])
315317
)
316318
: {};
317319

@@ -603,10 +605,11 @@ class EventsFactory extends Factory {
603605
* If originalEventId equals repetitionId than user wants to get first repetition which is original event
604606
*/
605607
if (repetitionId === originalEventId) {
606-
const originalEvent = await this.getCollection(this.TYPES.EVENTS)
607-
.findOne({
608-
_id: ObjectID(originalEventId),
609-
});
608+
const originalEvent = await this.eventById.load(originalEventId);
609+
610+
if (!originalEvent) {
611+
return null;
612+
}
610613

611614
/**
612615
* All events have same type with originalEvent id
@@ -626,10 +629,11 @@ class EventsFactory extends Factory {
626629
_id: ObjectID(repetitionId),
627630
});
628631

629-
const originalEvent = await this.getCollection(this.TYPES.EVENTS)
630-
.findOne({
631-
_id: ObjectID(originalEventId),
632-
});
632+
const originalEvent = await this.eventById.load(originalEventId);
633+
634+
if (!originalEvent) {
635+
return null;
636+
}
633637

634638
/**
635639
* If one of the ids are invalid (originalEvent or repetition not found) return null
@@ -710,7 +714,7 @@ class EventsFactory extends Factory {
710714
async toggleEventMark(eventId, mark) {
711715
const collection = this.getCollection(this.TYPES.EVENTS);
712716

713-
const event = await this.findById(eventId);
717+
const event = await this.eventsDataLoader.eventById.load(eventId);
714718

715719
if (!event) {
716720
throw new Error(`Event not found for eventId: ${eventId}`);

src/resolvers/event.js

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@ const EventsFactory = require('../models/eventsFactory');
22
const { ObjectID } = require('mongodb');
33
const sendPersonalNotification = require('../utils/personalNotifications').default;
44

5+
/**
6+
* Returns a per-request, per-project EventsFactory instance
7+
* Uses context.eventsFactoryCache to memoize by projectId
8+
* @param {any} context - GraphQL resolver context
9+
* @param {string|ObjectID} ctorProjectId - value passed to EventsFactory constructor
10+
* @param {string|ObjectID} keyProjectId - value used as cache key (string id is preferred)
11+
* @returns {EventsFactory}
12+
*/
13+
function getEventsFactoryForProjectId(context, ctorProjectId, keyProjectId) {
14+
const cache = context.eventsFactoryCache || (context.eventsFactoryCache = new Map());
15+
const cacheKey = (keyProjectId || ctorProjectId).toString();
16+
17+
if (!cache.has(cacheKey)) {
18+
cache.set(cacheKey, new EventsFactory(ctorProjectId));
19+
}
20+
21+
return cache.get(cacheKey);
22+
}
23+
524
/**
625
* See all types and fields here {@see ../typeDefs/event.graphql}
726
*/
@@ -29,8 +48,8 @@ module.exports = {
2948
*
3049
* @return {RepetitionsPortion}
3150
*/
32-
async repetitionsPortion({ projectId, originalEventId }, { limit, cursor }) {
33-
const factory = new EventsFactory(projectId);
51+
async repetitionsPortion({ projectId, originalEventId }, { limit, cursor }, context) {
52+
const factory = getEventsFactoryForProjectId(context, projectId, projectId);
3453

3554
return factory.getEventRepetitions(originalEventId, limit, cursor);
3655
},
@@ -49,7 +68,7 @@ module.exports = {
4968
const project = await factories.projectsFactory.findById(projectId);
5069

5170
if (project.workspaceId.toString() === '6213b6a01e6281087467cc7a') {
52-
return [ await factories.usersFactory.findById(user.id) ];
71+
return [await factories.usersFactory.findById(user.id)];
5372
}
5473

5574
if (!visitedBy || !visitedBy.length) {
@@ -84,8 +103,8 @@ module.exports = {
84103
* @param {number} timezoneOffset - user's local timezone offset in minutes
85104
* @returns {Promise<ProjectChartItem[]>}
86105
*/
87-
async chartData({ projectId, groupHash }, { days, timezoneOffset }) {
88-
const factory = new EventsFactory(new ObjectID(projectId));
106+
async chartData({ projectId, groupHash }, { days, timezoneOffset }, context) {
107+
const factory = getEventsFactoryForProjectId(context, new ObjectID(projectId), projectId);
89108

90109
return factory.findChartData(days, timezoneOffset, groupHash);
91110
},
@@ -97,8 +116,8 @@ module.exports = {
97116
* @param {String} eventId - event id
98117
* @returns {Promise<Release>}
99118
*/
100-
async release({ projectId, id: eventId }) {
101-
const factory = new EventsFactory(new ObjectID(projectId));
119+
async release({ projectId, id: eventId }, _args, context) {
120+
const factory = getEventsFactoryForProjectId(context, new ObjectID(projectId), projectId);
102121
const release = await factory.getEventRelease(eventId);
103122

104123
return release;
@@ -114,8 +133,8 @@ module.exports = {
114133
* @param {UserInContext} user - user context
115134
* @return {Promise<boolean>}
116135
*/
117-
async visitEvent(_obj, { projectId, eventId }, { user }) {
118-
const factory = new EventsFactory(projectId);
136+
async visitEvent(_obj, { projectId, eventId }, { user, ...context }) {
137+
const factory = getEventsFactoryForProjectId(context, projectId, projectId);
119138

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

@@ -131,8 +150,8 @@ module.exports = {
131150
* @param {string} mark - mark to set
132151
* @return {Promise<boolean>}
133152
*/
134-
async toggleEventMark(_obj, { project, eventId, mark }) {
135-
const factory = new EventsFactory(project);
153+
async toggleEventMark(_obj, { project, eventId, mark }, context) {
154+
const factory = getEventsFactoryForProjectId(context, project, project);
136155

137156
const { result } = await factory.toggleEventMark(eventId, mark);
138157

@@ -155,9 +174,9 @@ module.exports = {
155174
* @param factories - factories for working with models
156175
* @return {Promise<boolean>}
157176
*/
158-
async updateAssignee(_obj, { input }, { factories, user }) {
177+
async updateAssignee(_obj, { input }, { factories, user, ...context }) {
159178
const { projectId, eventId, assignee } = input;
160-
const factory = new EventsFactory(projectId);
179+
const factory = getEventsFactoryForProjectId(context, projectId, projectId);
161180

162181
const userExists = await factories.usersFactory.findById(assignee);
163182

@@ -206,9 +225,9 @@ module.exports = {
206225
* @param factories - factories for working with models
207226
* @return {Promise<boolean>}
208227
*/
209-
async removeAssignee(_obj, { input }) {
228+
async removeAssignee(_obj, { input }, context) {
210229
const { projectId, eventId } = input;
211-
const factory = new EventsFactory(projectId);
230+
const factory = getEventsFactoryForProjectId(context, projectId, projectId);
212231

213232
const { result } = await factory.updateAssignee(eventId, '');
214233

0 commit comments

Comments
 (0)