Skip to content

Plat 3491 transformer #681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 0 additions & 45 deletions app-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,13 @@ const helper = require("./src/common/helper");
const errors = require("./src/common/errors");
const logger = require("./src/common/logger");
const routes = require("./src/routes");
const transformations = require("./src/common/transformations");
const authenticator = require("tc-core-library-js").middleware.jwtAuthenticator;

/**
* Configure all routes for express app
* @param app the express app
*/
module.exports = (app) => {
app.use((req, res, next) => {
req.appVersion = req.headers["app-version"] || "1.0.0";
if (!transformations[req.appVersion]) {
req.appVersion = "1.0.0"; // default to 1.0.0 if provided version doesn't match any transformation
}
next();
});

// Load all routes
_.each(routes, (verbs, path) => {
_.each(verbs, (def, verb) => {
Expand All @@ -45,42 +36,6 @@ module.exports = (app) => {
next();
});

if (def.versioned) {
actions.push((req, res, next) => {
// TODO: Overriding res.send is a temporary solution to inject version-based transformations.
// TODO: A more conventional approach in Express would be to use res.locals to pass data through middleware,
// TODO: and then send the response in a centralized middleware after all transformations are applied.
// TODO: This would require a refactor of the current controllers' response handling.
// TODO: Consider revisiting this implementation in the future for a more maintainable architecture.

const originalSend = res.send;
const originalStatus = res.status;
let currentStatusCode = 200; // Default status code for Express

// Override res.status to capture the status code
res.status = function (code) {
currentStatusCode = code;
return originalStatus.apply(this, arguments);
};

res.send = (data) => {
// If the status code indicates a successful response, apply the transformation
if (currentStatusCode >= 200 && currentStatusCode < 300) {
const transformer = transformations[req.appVersion] || transformations["1.0.0"];
data = transformer(data);
}

// Reset the send function to its original behavior
res.send = originalSend;

// Call the original send function with the transformed (or original) data
originalSend.call(res, data);
};

next();
});
}

actions.push((req, res, next) => {
if (_.get(req, "query.token")) {
_.set(req, "headers.authorization", `Bearer ${_.trim(req.query.token)}`);
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"axios-retry": "^3.4.0",
"bluebird": "^3.5.1",
"body-parser": "^1.15.1",
"compare-versions": "^6.1.0",
"config": "^3.0.1",
"cors": "^2.8.5",
"decimal.js": "^10.4.3",
Expand Down
35 changes: 32 additions & 3 deletions src/common/challenge-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,25 @@ class ChallengeHelper {
}
}

/**
* Validate Challenge groups.
* @param {Object} groups the group of a challenge
*/
async validateGroups(groups) {
const promises = [];
_.each(groups, (g) => {
promises.push(
(async () => {
const group = await helper.getGroupById(g);
if (!group || group.status !== "active") {
throw new errors.BadRequestError("The groups provided are invalid " + g);
}
})()
);
});
await Promise.all(promises);
}

async validateCreateChallengeRequest(currentUser, challenge) {
// projectId is required for non self-service challenges
if (challenge.legacy.selfService == null && challenge.projectId == null) {
Expand All @@ -98,7 +117,13 @@ class ChallengeHelper {
// helper.ensureNoDuplicateOrNullElements(challenge.events, 'events')

// check groups authorization
await helper.ensureAccessibleByGroupsAccess(currentUser, challenge);
if (challenge.groups && challenge.groups.length > 0) {
if (currentUser.isMachine || hasAdminRole(currentUser)) {
await this.validateGroups(challenge.groups);
} else {
await helper.ensureAccessibleByGroupsAccess(currentUser, challenge);
}
}

if (challenge.constraints) {
await ChallengeHelper.validateChallengeConstraints(challenge.constraints);
Expand All @@ -118,8 +143,12 @@ class ChallengeHelper {
}

// check groups access to be updated group values
if (data.groups) {
await ensureAcessibilityToModifiedGroups(currentUser, data, challenge);
if (data.groups && data.groups.length > 0) {
if (currentUser.isMachine || hasAdminRole(currentUser)) {
await this.validateGroups(data.groups);
} else {
await ensureAcessibilityToModifiedGroups(currentUser, data, challenge);
}
}

// Ensure descriptionFormat is either 'markdown' or 'html'
Expand Down
24 changes: 0 additions & 24 deletions src/common/transformations.js

This file was deleted.

81 changes: 81 additions & 0 deletions src/common/transformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const _ = require("lodash");
const { compareVersions } = require("compare-versions");
const challengeService = require("../services/ChallengeService");

function transformData(data, fieldsToDelete) {
if (!fieldsToDelete || !fieldsToDelete.length) {
return data;
}

if (_.isArray(data)) {
return data.map((item) => transformData(item, fieldsToDelete));
} else if (_.isObject(data)) {
const clonedData = { ...data };
for (const field of fieldsToDelete) {
delete clonedData[field];
}
if (clonedData.result) {
clonedData.result = transformData(clonedData.result, fieldsToDelete);
}
return clonedData;
}

return data;
}

function transformServices() {
_.each(services, (service, serviceName) => {
const serviceConfig = servicesConfig[serviceName];
if (!serviceConfig) {
return;
}

_.each(service, (method, methodName) => {
service[methodName] = async function () {
const args = Array.prototype.slice.call(arguments);
const data = await method.apply(this, args.slice(1));

// No transform need for this method
if (!serviceConfig.methods.includes(methodName)) {
return data;
}

// args[0] is request, get version header
const apiVersion = args[0].headers["challenge-api-version"] || "1.0.0";

const fieldsToDelete = [];
_.each(serviceConfig.fieldsVersion, (version, field) => {
// If input version less than required version, delete fields from response
if (compareVersions(apiVersion, version) < 0) {
fieldsToDelete.push(field);
}
});

// Transform response data by deleting fields
return transformData(data, fieldsToDelete);
};
service[methodName].params = ["req", ...method.params];
});
});
}

// Define the version config for services
const servicesConfig = {
challengeService: {
methods: ["searchChallenges", "getChallenge", "createChallenge", "updateChallenge"],
fieldsVersion: {
skills: "1.1.0",
payments: "2.0.0",
},
},
};

// Define the services to export
const services = {
challengeService,
};

// Transform services before export
transformServices();

module.exports = services;
19 changes: 10 additions & 9 deletions src/controllers/ChallengeController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Controller for challenge endpoints
*/
const HttpStatus = require("http-status-codes");
const service = require("../services/ChallengeService");
const { challengeService: service } = require("../common/transformer");
const helper = require("../common/helper");
const logger = require("../common/logger");

Expand All @@ -12,7 +12,7 @@ const logger = require("../common/logger");
* @param {Object} res the response
*/
async function searchChallenges(req, res) {
let result = await service.searchChallenges(req.authUser, {
let result = await service.searchChallenges(req, req.authUser, {
...req.query,
...req.body,
});
Expand All @@ -23,7 +23,7 @@ async function searchChallenges(req, res) {
logger.debug(`Staring to get mm challengeId`);
const legacyId = await helper.getProjectIdByRoundId(req.query.legacyId);
logger.debug(`Get mm challengeId successfully ${legacyId}`);
result = await service.searchChallenges(req.authUser, {
result = await service.searchChallenges(req, req.authUser, {
...req.query,
...req.body,
legacyId,
Expand All @@ -50,7 +50,7 @@ async function createChallenge(req, res) {
logger.debug(
`createChallenge User: ${JSON.stringify(req.authUser)} - Body: ${JSON.stringify(req.body)}`
);
const result = await service.createChallenge(req.authUser, req.body, req.userToken);
const result = await service.createChallenge(req, req.authUser, req.body, req.userToken);
res.status(HttpStatus.CREATED).send(result);
}

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

Expand All @@ -71,6 +71,7 @@ async function sendNotifications(req, res) {
*/
async function getChallenge(req, res) {
const result = await service.getChallenge(
req,
req.authUser,
req.params.challengeId,
req.query.checkIfExists
Expand All @@ -84,7 +85,7 @@ async function getChallenge(req, res) {
* @param {Object} res the response
*/
async function getChallengeStatistics(req, res) {
const result = await service.getChallengeStatistics(req.authUser, req.params.challengeId);
const result = await service.getChallengeStatistics(req, req.authUser, req.params.challengeId);
res.send(result);
}

Expand All @@ -99,7 +100,7 @@ async function updateChallenge(req, res) {
req.params.challengeId
} - Body: ${JSON.stringify(req.body)}`
);
const result = await service.updateChallenge(req.authUser, req.params.challengeId, req.body);
const result = await service.updateChallenge(req, req.authUser, req.params.challengeId, req.body);
res.send(result);
}

Expand All @@ -112,7 +113,7 @@ async function deleteChallenge(req, res) {
logger.debug(
`deleteChallenge User: ${JSON.stringify(req.authUser)} - ChallengeID: ${req.params.challengeId}`
);
const result = await service.deleteChallenge(req.authUser, req.params.challengeId);
const result = await service.deleteChallenge(req, req.authUser, req.params.challengeId);
res.send(result);
}

Expand All @@ -122,7 +123,7 @@ async function deleteChallenge(req, res) {
* @param {Object} res the response
*/
async function advancePhase(req, res) {
res.send(await service.advancePhase(req.authUser, req.params.challengeId, req.body));
res.send(await service.advancePhase(req, req.authUser, req.params.challengeId, req.body));
}

module.exports = {
Expand Down
2 changes: 0 additions & 2 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ module.exports = {
constants.UserRoles.User,
],
scopes: [READ, ALL],
versioned: true,
},
post: {
controller: "ChallengeController",
Expand Down Expand Up @@ -52,7 +51,6 @@ module.exports = {
controller: "ChallengeController",
method: "getChallenge",
scopes: [READ, ALL],
versioned: true,
},
// @deprecated
put: {
Expand Down
3 changes: 1 addition & 2 deletions src/services/ChallengeService.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const { ChallengeDomain } = require("@topcoder-framework/domain-challenge");

const { hasAdminRole } = require("../common/role-helper");
const {
validateChallengeUpdateRequest,
enrichChallengeForResponse,
sanitizeRepeatedFieldsInUpdateRequest,
convertPrizeSetValuesToCents,
Expand Down Expand Up @@ -1495,7 +1494,7 @@ async function updateChallenge(currentUser, challengeId, data) {

const challengeResources = await helper.getChallengeResources(challengeId);

await validateChallengeUpdateRequest(currentUser, challenge, data, challengeResources);
await challengeHelper.validateChallengeUpdateRequest(currentUser, challenge, data, challengeResources);
validateTask(currentUser, challenge, data, challengeResources);

let sendActivationEmail = false;
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,11 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==

compare-versions@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a"
integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==

component-emitter@^1.2.0, component-emitter@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
Expand Down