Skip to content

Commit 4065644

Browse files
Merge pull request #1199 from ReliefApplications/next
Next
2 parents 59783da + 927066b commit 4065644

File tree

14 files changed

+281
-20
lines changed

14 files changed

+281
-20
lines changed

CHANGELOG/CHANGELOG_beta.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# [2.13.0-beta.1](https://github.com/ReliefApplications/ems-backend/compare/v2.12.2...v2.13.0-beta.1) (2025-02-27)
2+
3+
4+
### Features
5+
6+
* improve back-end caching for common services requests ([173877b](https://github.com/ReliefApplications/ems-backend/commit/173877b6f880194f1da554e4711bbbcb647181bd))
7+
18
# [2.12.0-beta.14](https://github.com/ReliefApplications/ems-backend/compare/v2.12.0-beta.13...v2.12.0-beta.14) (2025-02-14)
29

310

CHANGELOG/CHANGELOG_next.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
# [2.13.0-rc.3](https://github.com/ReliefApplications/ems-backend/compare/v2.13.0-rc.2...v2.13.0-rc.3) (2025-03-04)
2+
3+
4+
### Features
5+
6+
* Distribution list CRUD ([#1193](https://github.com/ReliefApplications/ems-backend/issues/1193)) ([1b06057](https://github.com/ReliefApplications/ems-backend/commit/1b060575b6574cc46c8b865464057c3cc63a5551))
7+
8+
# [2.13.0-rc.2](https://github.com/ReliefApplications/ems-backend/compare/v2.13.0-rc.1...v2.13.0-rc.2) (2025-02-27)
9+
10+
11+
### Bug Fixes
12+
13+
* nested fields from ref data can now be used as display field in contextual dashboards ([#1197](https://github.com/ReliefApplications/ems-backend/issues/1197)) ([943c108](https://github.com/ReliefApplications/ems-backend/commit/943c1083fcec4fd2147c070e24a6b858f9a4d19d)), closes [AB#110861](https://github.com/AB/issues/110861)
14+
15+
# [2.13.0-rc.1](https://github.com/ReliefApplications/ems-backend/compare/v2.12.2...v2.13.0-rc.1) (2025-02-27)
16+
17+
18+
### Features
19+
20+
* improve back-end caching for common services requests ([173877b](https://github.com/ReliefApplications/ems-backend/commit/173877b6f880194f1da554e4711bbbcb647181bd))
21+
122
## [2.12.2-rc.1](https://github.com/ReliefApplications/ems-backend/compare/v2.12.1...v2.12.2-rc.1) (2025-02-19)
223

324

__tests__/unit-tests/routes/activity/activity.service.spec.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,13 +216,25 @@ describe('Activity Service', () => {
216216
const query = {};
217217
const aggregationResult = [];
218218
const listAggregationMock = jest.spyOn(service as any, 'listAggregation');
219-
listAggregationMock.mockResolvedValueOnce(aggregationResult);
219+
// Count
220+
listAggregationMock.mockReturnValueOnce({
221+
count: () => [{ count: 20000 }],
222+
});
223+
// 2 pages
224+
listAggregationMock.mockReturnValueOnce({
225+
skip: jest.fn().mockReturnThis(),
226+
limit: jest.fn().mockResolvedValueOnce(aggregationResult),
227+
});
228+
listAggregationMock.mockReturnValueOnce({
229+
skip: jest.fn().mockReturnThis(),
230+
limit: jest.fn().mockResolvedValueOnce(aggregationResult),
231+
});
220232

221233
const mockFile = Buffer.from('Test content');
222234
jest.spyOn(XLSBuilder, 'default').mockResolvedValueOnce(mockFile as any);
223235

224236
const result = await service.downloadList(query);
225-
expect(listAggregationMock).toHaveBeenCalled();
237+
expect(listAggregationMock).toHaveBeenCalledTimes(3); // 1 for count, 2 for pagination
226238
expect(result).toEqual({
227239
fileName: (service as any).listExportFileName,
228240
file: mockFile,
@@ -235,7 +247,11 @@ describe('Activity Service', () => {
235247

236248
const listAggregationMock = jest
237249
.spyOn(service as any, 'listAggregation')
238-
.mockRejectedValueOnce(error);
250+
.mockReturnValueOnce({
251+
count: () => {
252+
throw error;
253+
},
254+
});
239255

240256
await expect(service.downloadList(query)).rejects.toThrow(error);
241257
expect(listAggregationMock).toHaveBeenCalled();

package-lock.json

Lines changed: 41 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ems-backend",
3-
"version": "2.12.2",
3+
"version": "2.13.0-rc.3",
44
"description": "",
55
"main": "index.js",
66
"scripts": {
@@ -45,6 +45,7 @@
4545
"@ucast/mongo2js": "^1.3.3",
4646
"apollo-server-cache-redis": "^3.3.1",
4747
"axios": "^1.4.0",
48+
"axios-cache-interceptor": "^1.6.2",
4849
"body-parser": "^1.20.1",
4950
"bson": "^4.2.3",
5051
"config": "^3.3.9",

src/routes/activity/activity.service.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -352,18 +352,33 @@ export class ActivityService {
352352
filters.push(queryFilters);
353353
}
354354

355-
const aggregation = await this.listAggregation(
355+
const totalCountAggregation = await this.listAggregation(
356356
sortField,
357357
sortOrder,
358358
filters
359-
);
360-
const formattedData = aggregation.map((activity) => ({
361-
timestamp: this.formatDate(activity.createdAt, timeZone),
362-
userId: activity.userId?.toString(),
363-
page: activity.metadata?.title || '',
364-
username: activity.username,
365-
attributes: activity.attributes,
366-
}));
359+
).count('count');
360+
361+
const formattedData = [];
362+
363+
for (let skip = 0; skip < totalCountAggregation[0].count; skip += 10000) {
364+
const aggregation = await this.listAggregation(
365+
sortField,
366+
sortOrder,
367+
filters
368+
)
369+
.skip(skip)
370+
.limit(10000);
371+
formattedData.push(
372+
...aggregation.map((activity) => ({
373+
timestamp: this.formatDate(activity.createdAt, timeZone),
374+
userId: activity.userId?.toString(),
375+
page: activity.metadata?.title || '',
376+
username: activity.username,
377+
attributes: activity.attributes,
378+
}))
379+
);
380+
}
381+
367382
const file = await xlsBuilder(
368383
this.listExportFileName,
369384
this.listExportColumns,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { GraphQLError, GraphQLNonNull, GraphQLID } from 'graphql';
2+
import { logger } from '@services/logger.service';
3+
import { graphQLAuthCheck } from '@schema/shared';
4+
import { Context } from '@server/apollo/context';
5+
import { EmailDistributionList, EmailNotification } from '@models';
6+
import { EmailDistributionListType } from '@schema/types/emailDistribution.type';
7+
import extendAbilityForApplications from '@security/extendAbilityForApplication';
8+
import { AppAbility } from '@security/defineUserAbility';
9+
10+
/**
11+
* Mutation to delete an existing distribution list.
12+
*/
13+
export default {
14+
type: EmailDistributionListType,
15+
args: {
16+
id: { type: new GraphQLNonNull(GraphQLID) },
17+
},
18+
async resolve(_, { id }, context: Context) {
19+
graphQLAuthCheck(context);
20+
21+
try {
22+
const distributionList = await EmailDistributionList.findOne({
23+
_id: id,
24+
isDeleted: 0,
25+
});
26+
27+
if (!distributionList) {
28+
throw new GraphQLError(context.i18next.t('common.errors.dataNotFound'));
29+
}
30+
31+
const user = context.user;
32+
const ability: AppAbility = extendAbilityForApplications(
33+
user,
34+
distributionList.applicationId.toString()
35+
);
36+
37+
if (ability.cannot('update', 'EmailNotification')) {
38+
throw new GraphQLError(
39+
context.i18next.t('common.errors.permissionNotGranted')
40+
);
41+
}
42+
43+
// Unset the reference in any EmailNotifications using this distribution list
44+
await EmailNotification.updateMany(
45+
{ emailDistributionList: distributionList._id },
46+
{ $unset: { emailDistributionList: 1 } }
47+
);
48+
49+
// Delete DL
50+
await distributionList.deleteOne();
51+
52+
return distributionList;
53+
} catch (err) {
54+
logger.error(err.message, { stack: err.stack });
55+
if (err instanceof GraphQLError) {
56+
throw new GraphQLError(err.message);
57+
}
58+
throw new GraphQLError(
59+
context.i18next.t('common.errors.internalServerError')
60+
);
61+
}
62+
},
63+
};

src/schema/mutation/deleteEmailNotification.mutation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Types } from 'mongoose';
1515
import { Context } from '@server/apollo/context';
1616
import extendAbilityForApplications from '@security/extendAbilityForApplication';
1717
import { CustomTemplate } from '@models/customTemplate.model';
18+
import { deleteFile } from '@utils/notification/util';
1819

1920
/** Arguments for the deleteEmailNotification mutation */
2021
type DeleteEmailNotificationArgs = {
@@ -60,6 +61,7 @@ export default {
6061
_id: args.id,
6162
applicationId: args.applicationId,
6263
});
64+
await deleteFile(emailNotification.attachments, context);
6365
} else {
6466
// Handle cases where the user has limited permissions
6567
const accessibleEmailNotification = await EmailNotification.findOne({
@@ -73,6 +75,7 @@ export default {
7375
_id: args.id,
7476
applicationId: args.applicationId,
7577
});
78+
await deleteFile(emailNotification.attachments, context);
7679
}
7780
}
7881

src/schema/mutation/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import editEmailDistributionList from './editEmailDistributionList.mutation';
9696
import addCustomTemplate from './addCustomTemplate.mutation';
9797
import editCustomTemplate from './editCustomTemplate.mutation';
9898
import deleteEmailNotification from './deleteEmailNotification.mutation';
99+
import deleteEmailDistributionList from './deleteEmailDistributionList.mutation';
99100

100101
/** GraphQL mutation definition */
101102
const Mutation = new GraphQLObjectType({
@@ -198,6 +199,7 @@ const Mutation = new GraphQLObjectType({
198199
deleteDashboardTemplates,
199200
editCustomTemplate,
200201
deleteEmailNotification,
202+
deleteEmailDistributionList,
201203
},
202204
});
203205

src/server/common-services.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Axios from 'axios';
2+
import {
3+
AxiosCacheInstance,
4+
setupCache,
5+
buildKeyGenerator,
6+
} from 'axios-cache-interceptor';
7+
8+
let axios: AxiosCacheInstance;
9+
10+
/**
11+
* Create a dedicated axios instance for Common Services
12+
* Add cache mechanism
13+
* Cached requests expire after 5 minutes.
14+
*
15+
* @returns Common Services axios instance
16+
*/
17+
export default () => {
18+
if (!axios) {
19+
const instance = Axios.create();
20+
axios = setupCache(instance, {
21+
methods: ['get', 'post'],
22+
// Avoid using cache control set to false
23+
interpretHeader: false,
24+
// Generate an unique key, based on all parameters listed
25+
generateKey: buildKeyGenerator((request) => ({
26+
method: request.method,
27+
baseURL: request.baseURL,
28+
params: request.params,
29+
url: request.url,
30+
data: request.data,
31+
custom: request.headers.Authorization,
32+
})),
33+
});
34+
}
35+
return axios;
36+
};

0 commit comments

Comments
 (0)