Skip to content

Commit a3fefd8

Browse files
author
liuliquan
authored
Merge pull request #681 from topcoder-platform/PLAT-3491-transformer
Plat 3491 transformer
2 parents b595cca + 8aa77b2 commit a3fefd8

File tree

9 files changed

+130
-85
lines changed

9 files changed

+130
-85
lines changed

app-routes.js

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,13 @@ const helper = require("./src/common/helper");
1010
const errors = require("./src/common/errors");
1111
const logger = require("./src/common/logger");
1212
const routes = require("./src/routes");
13-
const transformations = require("./src/common/transformations");
1413
const authenticator = require("tc-core-library-js").middleware.jwtAuthenticator;
1514

1615
/**
1716
* Configure all routes for express app
1817
* @param app the express app
1918
*/
2019
module.exports = (app) => {
21-
app.use((req, res, next) => {
22-
req.appVersion = req.headers["app-version"] || "1.0.0";
23-
if (!transformations[req.appVersion]) {
24-
req.appVersion = "1.0.0"; // default to 1.0.0 if provided version doesn't match any transformation
25-
}
26-
next();
27-
});
28-
2920
// Load all routes
3021
_.each(routes, (verbs, path) => {
3122
_.each(verbs, (def, verb) => {
@@ -45,42 +36,6 @@ module.exports = (app) => {
4536
next();
4637
});
4738

48-
if (def.versioned) {
49-
actions.push((req, res, next) => {
50-
// TODO: Overriding res.send is a temporary solution to inject version-based transformations.
51-
// TODO: A more conventional approach in Express would be to use res.locals to pass data through middleware,
52-
// TODO: and then send the response in a centralized middleware after all transformations are applied.
53-
// TODO: This would require a refactor of the current controllers' response handling.
54-
// TODO: Consider revisiting this implementation in the future for a more maintainable architecture.
55-
56-
const originalSend = res.send;
57-
const originalStatus = res.status;
58-
let currentStatusCode = 200; // Default status code for Express
59-
60-
// Override res.status to capture the status code
61-
res.status = function (code) {
62-
currentStatusCode = code;
63-
return originalStatus.apply(this, arguments);
64-
};
65-
66-
res.send = (data) => {
67-
// If the status code indicates a successful response, apply the transformation
68-
if (currentStatusCode >= 200 && currentStatusCode < 300) {
69-
const transformer = transformations[req.appVersion] || transformations["1.0.0"];
70-
data = transformer(data);
71-
}
72-
73-
// Reset the send function to its original behavior
74-
res.send = originalSend;
75-
76-
// Call the original send function with the transformed (or original) data
77-
originalSend.call(res, data);
78-
};
79-
80-
next();
81-
});
82-
}
83-
8439
actions.push((req, res, next) => {
8540
if (_.get(req, "query.token")) {
8641
_.set(req, "headers.authorization", `Bearer ${_.trim(req.query.token)}`);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"axios-retry": "^3.4.0",
5050
"bluebird": "^3.5.1",
5151
"body-parser": "^1.15.1",
52+
"compare-versions": "^6.1.0",
5253
"config": "^3.0.1",
5354
"cors": "^2.8.5",
5455
"decimal.js": "^10.4.3",

src/common/challenge-helper.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,25 @@ class ChallengeHelper {
8080
}
8181
}
8282

83+
/**
84+
* Validate Challenge groups.
85+
* @param {Object} groups the group of a challenge
86+
*/
87+
async validateGroups(groups) {
88+
const promises = [];
89+
_.each(groups, (g) => {
90+
promises.push(
91+
(async () => {
92+
const group = await helper.getGroupById(g);
93+
if (!group || group.status !== "active") {
94+
throw new errors.BadRequestError("The groups provided are invalid " + g);
95+
}
96+
})()
97+
);
98+
});
99+
await Promise.all(promises);
100+
}
101+
83102
async validateCreateChallengeRequest(currentUser, challenge) {
84103
// projectId is required for non self-service challenges
85104
if (challenge.legacy.selfService == null && challenge.projectId == null) {
@@ -98,7 +117,13 @@ class ChallengeHelper {
98117
// helper.ensureNoDuplicateOrNullElements(challenge.events, 'events')
99118

100119
// check groups authorization
101-
await helper.ensureAccessibleByGroupsAccess(currentUser, challenge);
120+
if (challenge.groups && challenge.groups.length > 0) {
121+
if (currentUser.isMachine || hasAdminRole(currentUser)) {
122+
await this.validateGroups(challenge.groups);
123+
} else {
124+
await helper.ensureAccessibleByGroupsAccess(currentUser, challenge);
125+
}
126+
}
102127

103128
if (challenge.constraints) {
104129
await ChallengeHelper.validateChallengeConstraints(challenge.constraints);
@@ -118,8 +143,12 @@ class ChallengeHelper {
118143
}
119144

120145
// check groups access to be updated group values
121-
if (data.groups) {
122-
await ensureAcessibilityToModifiedGroups(currentUser, data, challenge);
146+
if (data.groups && data.groups.length > 0) {
147+
if (currentUser.isMachine || hasAdminRole(currentUser)) {
148+
await this.validateGroups(data.groups);
149+
} else {
150+
await ensureAcessibilityToModifiedGroups(currentUser, data, challenge);
151+
}
123152
}
124153

125154
// Ensure descriptionFormat is either 'markdown' or 'html'

src/common/transformations.js

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/common/transformer.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const _ = require("lodash");
2+
const { compareVersions } = require("compare-versions");
3+
const challengeService = require("../services/ChallengeService");
4+
5+
function transformData(data, fieldsToDelete) {
6+
if (!fieldsToDelete || !fieldsToDelete.length) {
7+
return data;
8+
}
9+
10+
if (_.isArray(data)) {
11+
return data.map((item) => transformData(item, fieldsToDelete));
12+
} else if (_.isObject(data)) {
13+
const clonedData = { ...data };
14+
for (const field of fieldsToDelete) {
15+
delete clonedData[field];
16+
}
17+
if (clonedData.result) {
18+
clonedData.result = transformData(clonedData.result, fieldsToDelete);
19+
}
20+
return clonedData;
21+
}
22+
23+
return data;
24+
}
25+
26+
function transformServices() {
27+
_.each(services, (service, serviceName) => {
28+
const serviceConfig = servicesConfig[serviceName];
29+
if (!serviceConfig) {
30+
return;
31+
}
32+
33+
_.each(service, (method, methodName) => {
34+
service[methodName] = async function () {
35+
const args = Array.prototype.slice.call(arguments);
36+
const data = await method.apply(this, args.slice(1));
37+
38+
// No transform need for this method
39+
if (!serviceConfig.methods.includes(methodName)) {
40+
return data;
41+
}
42+
43+
// args[0] is request, get version header
44+
const apiVersion = args[0].headers["challenge-api-version"] || "1.0.0";
45+
46+
const fieldsToDelete = [];
47+
_.each(serviceConfig.fieldsVersion, (version, field) => {
48+
// If input version less than required version, delete fields from response
49+
if (compareVersions(apiVersion, version) < 0) {
50+
fieldsToDelete.push(field);
51+
}
52+
});
53+
54+
// Transform response data by deleting fields
55+
return transformData(data, fieldsToDelete);
56+
};
57+
service[methodName].params = ["req", ...method.params];
58+
});
59+
});
60+
}
61+
62+
// Define the version config for services
63+
const servicesConfig = {
64+
challengeService: {
65+
methods: ["searchChallenges", "getChallenge", "createChallenge", "updateChallenge"],
66+
fieldsVersion: {
67+
skills: "1.1.0",
68+
payments: "2.0.0",
69+
},
70+
},
71+
};
72+
73+
// Define the services to export
74+
const services = {
75+
challengeService,
76+
};
77+
78+
// Transform services before export
79+
transformServices();
80+
81+
module.exports = services;

src/controllers/ChallengeController.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Controller for challenge endpoints
33
*/
44
const HttpStatus = require("http-status-codes");
5-
const service = require("../services/ChallengeService");
5+
const { challengeService: service } = require("../common/transformer");
66
const helper = require("../common/helper");
77
const logger = require("../common/logger");
88

@@ -12,7 +12,7 @@ const logger = require("../common/logger");
1212
* @param {Object} res the response
1313
*/
1414
async function searchChallenges(req, res) {
15-
let result = await service.searchChallenges(req.authUser, {
15+
let result = await service.searchChallenges(req, req.authUser, {
1616
...req.query,
1717
...req.body,
1818
});
@@ -23,7 +23,7 @@ async function searchChallenges(req, res) {
2323
logger.debug(`Staring to get mm challengeId`);
2424
const legacyId = await helper.getProjectIdByRoundId(req.query.legacyId);
2525
logger.debug(`Get mm challengeId successfully ${legacyId}`);
26-
result = await service.searchChallenges(req.authUser, {
26+
result = await service.searchChallenges(req, req.authUser, {
2727
...req.query,
2828
...req.body,
2929
legacyId,
@@ -50,7 +50,7 @@ async function createChallenge(req, res) {
5050
logger.debug(
5151
`createChallenge User: ${JSON.stringify(req.authUser)} - Body: ${JSON.stringify(req.body)}`
5252
);
53-
const result = await service.createChallenge(req.authUser, req.body, req.userToken);
53+
const result = await service.createChallenge(req, req.authUser, req.body, req.userToken);
5454
res.status(HttpStatus.CREATED).send(result);
5555
}
5656

@@ -60,7 +60,7 @@ async function createChallenge(req, res) {
6060
* @param {Object} res the response
6161
*/
6262
async function sendNotifications(req, res) {
63-
const result = await service.sendNotifications(req.authUser, req.params.challengeId);
63+
const result = await service.sendNotifications(req, req.authUser, req.params.challengeId);
6464
res.status(HttpStatus.CREATED).send(result);
6565
}
6666

@@ -71,6 +71,7 @@ async function sendNotifications(req, res) {
7171
*/
7272
async function getChallenge(req, res) {
7373
const result = await service.getChallenge(
74+
req,
7475
req.authUser,
7576
req.params.challengeId,
7677
req.query.checkIfExists
@@ -84,7 +85,7 @@ async function getChallenge(req, res) {
8485
* @param {Object} res the response
8586
*/
8687
async function getChallengeStatistics(req, res) {
87-
const result = await service.getChallengeStatistics(req.authUser, req.params.challengeId);
88+
const result = await service.getChallengeStatistics(req, req.authUser, req.params.challengeId);
8889
res.send(result);
8990
}
9091

@@ -99,7 +100,7 @@ async function updateChallenge(req, res) {
99100
req.params.challengeId
100101
} - Body: ${JSON.stringify(req.body)}`
101102
);
102-
const result = await service.updateChallenge(req.authUser, req.params.challengeId, req.body);
103+
const result = await service.updateChallenge(req, req.authUser, req.params.challengeId, req.body);
103104
res.send(result);
104105
}
105106

@@ -112,7 +113,7 @@ async function deleteChallenge(req, res) {
112113
logger.debug(
113114
`deleteChallenge User: ${JSON.stringify(req.authUser)} - ChallengeID: ${req.params.challengeId}`
114115
);
115-
const result = await service.deleteChallenge(req.authUser, req.params.challengeId);
116+
const result = await service.deleteChallenge(req, req.authUser, req.params.challengeId);
116117
res.send(result);
117118
}
118119

@@ -122,7 +123,7 @@ async function deleteChallenge(req, res) {
122123
* @param {Object} res the response
123124
*/
124125
async function advancePhase(req, res) {
125-
res.send(await service.advancePhase(req.authUser, req.params.challengeId, req.body));
126+
res.send(await service.advancePhase(req, req.authUser, req.params.challengeId, req.body));
126127
}
127128

128129
module.exports = {

src/routes.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ module.exports = {
2020
constants.UserRoles.User,
2121
],
2222
scopes: [READ, ALL],
23-
versioned: true,
2423
},
2524
post: {
2625
controller: "ChallengeController",
@@ -52,7 +51,6 @@ module.exports = {
5251
controller: "ChallengeController",
5352
method: "getChallenge",
5453
scopes: [READ, ALL],
55-
versioned: true,
5654
},
5755
// @deprecated
5856
put: {

src/services/ChallengeService.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ const { ChallengeDomain } = require("@topcoder-framework/domain-challenge");
3636

3737
const { hasAdminRole } = require("../common/role-helper");
3838
const {
39-
validateChallengeUpdateRequest,
4039
enrichChallengeForResponse,
4140
sanitizeRepeatedFieldsInUpdateRequest,
4241
convertPrizeSetValuesToCents,
@@ -1495,7 +1494,7 @@ async function updateChallenge(currentUser, challengeId, data) {
14951494

14961495
const challengeResources = await helper.getChallengeResources(challengeId);
14971496

1498-
await validateChallengeUpdateRequest(currentUser, challenge, data, challengeResources);
1497+
await challengeHelper.validateChallengeUpdateRequest(currentUser, challenge, data, challengeResources);
14991498
validateTask(currentUser, challenge, data, challengeResources);
15001499

15011500
let sendActivationEmail = false;

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,11 @@ commondir@^1.0.1:
989989
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
990990
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
991991

992+
compare-versions@^6.1.0:
993+
version "6.1.0"
994+
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a"
995+
integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==
996+
992997
component-emitter@^1.2.0, component-emitter@^1.3.0:
993998
version "1.3.0"
994999
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"

0 commit comments

Comments
 (0)