diff --git a/models/CopilotPayment.js b/models/CopilotPayment.js index 46c8674..92b127f 100644 --- a/models/CopilotPayment.js +++ b/models/CopilotPayment.js @@ -14,6 +14,19 @@ const dynamoose = require('dynamoose'); const Schema = dynamoose.Schema; +/** + * @typedef {Object} CopilotPayment + * @property {String} id The unique identifier for the CopilotPayment entity. + * @property {String} project The project associated with the payment. + * @property {Number} amount The payment amount. + * @property {String} description The description of the payment. + * @property {Number} challengeId The ID of the associated challenge (if applicable). + * @property {String} challengeUUID The UUID of the associated challenge (if applicable). + * @property {String} closed Indicates whether the payment is closed or not (default is 'false'). + * @property {String} username The username of the Copilot receiving the payment. + * @property {String} status The status of the payment. + */ + const schema = new Schema({ id: { type: String, diff --git a/models/GithubUserMapping.js b/models/GithubUserMapping.js index 6cd5d23..56b0cb5 100644 --- a/models/GithubUserMapping.js +++ b/models/GithubUserMapping.js @@ -7,6 +7,14 @@ const dynamoose = require('dynamoose'); const Schema = dynamoose.Schema; +/** + * @typedef {Object} GithubUserMapping + * @property {String} id The unique identifier for the GithubUserMapping entity. + * @property {String} topcoderUsername The Topcoder username associated with the GitHub user. + * @property {String} githubUsername The GitHub username. + * @property {Number} githubUserId The GitHub user's numeric identifier. + */ + const schema = new Schema({ id: { type: String, diff --git a/models/GitlabUserMapping.js b/models/GitlabUserMapping.js index f87f1d9..6377122 100644 --- a/models/GitlabUserMapping.js +++ b/models/GitlabUserMapping.js @@ -7,6 +7,14 @@ const dynamoose = require('dynamoose'); const Schema = dynamoose.Schema; +/** + * @typedef {Object} GitlabUserMapping + * @property {String} id The unique identifier for the GitlabUserMapping entity. + * @property {String} topcoderUsername The Topcoder username associated with the GitLab user. + * @property {String} gitlabUsername The GitLab username. + * @property {Number} gitlabUserId The GitLab user's numeric identifier. + */ + const schema = new Schema({ id: { type: String, diff --git a/models/Issue.js b/models/Issue.js index 79fb016..61ba943 100644 --- a/models/Issue.js +++ b/models/Issue.js @@ -12,6 +12,27 @@ const dynamoose = require('dynamoose'); const Schema = dynamoose.Schema; +/** + * @typedef {Object} Issue + * @property {String} id The id. + * @property {Number} number From the receiver service. + * @property {String} title The title. + * @property {String} body The body. + * @property {Number[]} prizes Prizes extracted from title. + * @property {String} provider Provider (github or gitlab). + * @property {Number} repositoryId Repository ID. + * @property {String} repoUrl Repository URL. + * @property {String} repositoryIdStr Repository ID as a String. + * @property {Array} labels Labels associated with the issue. + * @property {String} assignee Assignee for the issue. + * @property {Date} updatedAt Date when the issue was last updated. + * @property {Number} challengeId Challenge ID from topcoder API. + * @property {String} challengeUUID Challenge UUID. + * @property {String} projectId Project ID. + * @property {String} status Status of the issue. + * @property {Date} assignedAt Date when the issue was assigned (if applicable). + */ + const schema = new Schema({ id: {type: String, hashKey: true, required: true}, // From the receiver service diff --git a/models/Project.js b/models/Project.js index df53b8d..d2d1499 100755 --- a/models/Project.js +++ b/models/Project.js @@ -12,6 +12,23 @@ const dynamoose = require('dynamoose'); const Schema = dynamoose.Schema; +/** + * @typedef {Object} ProjectChallengeMapping + * @property {String} id The id. + * @property {String} title The title. + * @property {Number} tcDirectId The tc direct id. + * @property {String} tags The tags. + * @property {String} rocketChatWebhook The rocket chat webhook. + * @property {String} rocketChatChannelName The rocket chat channel name. + * @property {String} archived The archived. + * @property {String} owner The owner. + * @property {String} secretWebhookKey The secret webhook key. + * @property {String} copilot The copilot. + * @property {Date} updatedAt The updated at. + * @property {String} createCopilotPayments The create copilot payments. + * @property {Boolean} isConnect Is Topcoder connect. + */ + const schema = new Schema({ id: { type: String, diff --git a/models/ProjectChallengeMapping.js b/models/ProjectChallengeMapping.js index 5b4c585..53a25be 100644 --- a/models/ProjectChallengeMapping.js +++ b/models/ProjectChallengeMapping.js @@ -10,6 +10,13 @@ const dynamoose = require('dynamoose'); const Schema = dynamoose.Schema; +/** + * @typedef {Object} ProjectChallengeMapping + * @property {String} id the id + * @property {String} projectId the project id + * @property {String} challengeId the challenge id + */ + const schema = new Schema({ id: { type: String, diff --git a/models/Repository.js b/models/Repository.js index e9a8f1a..ae63fb2 100644 --- a/models/Repository.js +++ b/models/Repository.js @@ -12,6 +12,16 @@ const dynamoose = require('dynamoose'); const Schema = dynamoose.Schema; +/** + * @typedef {Object} Repository + * @property {String} id The unique identifier for the Repository entity. + * @property {String} projectId The project ID associated with the repository. + * @property {String} url The URL of the repository. + * @property {String} archived Indicates whether the repository is archived or not. + * @property {String} repoId The repository ID (if applicable). + * @property {String} registeredWebhookId The ID of the registered webhook (if applicable). + */ + const schema = new Schema({ id: { type: String, diff --git a/models/User.js b/models/User.js index 7237dfe..96cef0a 100755 --- a/models/User.js +++ b/models/User.js @@ -12,6 +12,21 @@ const constants = require('../constants'); const Schema = dynamoose.Schema; +/** + * @typedef {Object} User + * @property {String} id The user's unique identifier. + * @property {Number} userProviderId The user provider's numeric identifier. + * @property {String} userProviderIdStr The user provider's identifier as a string. + * @property {String} username The user's username. + * @property {String} role The user's role, one of the allowed constants.USER_ROLES. + * @property {String} type The user's type, one of the allowed constants.USER_TYPES. + * @property {String} accessToken GitLab token data (if applicable). + * @property {Date} accessTokenExpiration Expiration date of the access token (if applicable). + * @property {String} refreshToken GitLab token refresh token (if applicable). + * @property {String} lockId Lock identifier (if applicable). + * @property {Date} lockExpiration Expiration date of the lock (if applicable). + */ + const schema = new Schema({ id: { type: String, diff --git a/models/index.js b/models/index.js index 4d8dbc1..07f58e2 100644 --- a/models/index.js +++ b/models/index.js @@ -35,16 +35,23 @@ if (process.env.CREATE_DB) { /* eslint-disable global-require */ const models = { + /** @type {import('dynamoose').ModelConstructor<import('./Issue').Issue>} */ Issue: dynamoose.model('Topcoder_X.Issue', require('./Issue')), + /** @type {import('dynamoose').ModelConstructor<import('./Project').Project>} */ Project: dynamoose.model('Topcoder_X.Project', require('./Project')), + /** @type {import('dynamoose').ModelConstructor<import('./ProjectChallengeMapping').ProjectChallengeMapping>} */ ProjectChallengeMapping: dynamoose.model('Topcoder_X.ProjectChallengeMapping', require('./ProjectChallengeMapping')), + /** @type {import('dynamoose').ModelConstructor<import('./User').User>} */ User: dynamoose.model('Topcoder_X.User', require('./User')), + /** @type {import('dynamoose').ModelConstructor<import('./CopilotPayment').CopilotPayment>} */ CopilotPayment: dynamoose.model('Topcoder_X.CopilotPayment', require('./CopilotPayment')), + /** @type {import('dynamoose').ModelConstructor<import('./GithubUserMapping').GithubUserMapping>} */ GithubUserMapping: dynamoose.model('Topcoder_X.GithubUserMapping', require('./GithubUserMapping')), + /** @type {import('dynamoose').ModelConstructor<import('./GitlabUserMapping').GitlabUserMapping>} */ GitlabUserMapping: dynamoose.model('Topcoder_X.GitlabUserMapping', require('./GitlabUserMapping')), + /** @type {import('dynamoose').ModelConstructor<import('./Repository').Repository>} */ Repository: dynamoose.model('Topcoder_X.Repository', require('./Repository')) }; /* eslint-enable global-require */ - module.exports = models; diff --git a/package-lock.json b/package-lock.json index 929ac63..e5420b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "dependencies": { "@gitbeaker/rest": "^39.13.0", "@octokit/rest": "^18.9.0", + "archiver": "^6.0.1", "axios": "^0.19.0", "circular-json": "^0.5.7", "config": "^1.30.0", "dynamoose": "^1.11.1", + "form-data": "^4.0.0", "fs-extra": "^7.0.0", "get-parameter-names": "^0.3.0", "global-request-logger": "^0.1.1", @@ -831,6 +833,102 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/archiver": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-6.0.1.tgz", + "integrity": "sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ==", + "dependencies": { + "archiver-utils": "^4.0.1", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^5.0.1" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/archiver-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-4.0.1.tgz", + "integrity": "sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==", + "dependencies": { + "glob": "^8.0.0", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/are-docs-informative": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", @@ -893,6 +991,11 @@ "node": "*" } }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -932,17 +1035,6 @@ } ] }, - "node_modules/auth0-js/node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/auth0-js/node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -969,19 +1061,6 @@ } } }, - "node_modules/auth0-js/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/auth0-js/node_modules/formidable": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", @@ -1157,6 +1236,11 @@ "is-buffer": "^2.0.2" } }, + "node_modules/b4a": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", + "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" + }, "node_modules/babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -1245,8 +1329,7 @@ "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "devOptional": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "node_modules/base64-js": { "version": "1.3.1", @@ -1591,9 +1674,9 @@ } }, "node_modules/combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1621,6 +1704,33 @@ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" }, + "node_modules/compress-commons": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", + "integrity": "sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^5.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1721,6 +1831,42 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-5.0.0.tgz", + "integrity": "sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -3310,6 +3456,11 @@ "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -3460,16 +3611,16 @@ } }, "node_modules/form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.12" + "node": ">= 6" } }, "node_modules/formidable": { @@ -3513,8 +3664,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "node_modules/function-bind": { "version": "1.1.1", @@ -3722,12 +3872,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "engines": { - "node": ">=0.4.0" - } + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -4086,7 +4233,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -4683,6 +4829,17 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, "node_modules/le_node": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/le_node/-/le_node-1.8.0.tgz", @@ -5222,6 +5379,14 @@ "validate-npm-package-license": "^3.0.1" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.2.0.tgz", @@ -5592,8 +5757,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/progress": { "version": "1.1.8", @@ -5700,6 +5864,11 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5786,7 +5955,6 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5797,6 +5965,33 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readline2": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", @@ -5904,6 +6099,20 @@ "request": "^2.34" } }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/require-uncached": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", @@ -6308,11 +6517,19 @@ "node": ">=0.10.0" } }, + "node_modules/streamx": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", + "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==", + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -6417,6 +6634,19 @@ "resolved": "https://registry.npmjs.org/superagent-promise/-/superagent-promise-1.1.0.tgz", "integrity": "sha1-uvIti73UOamwfdEPjAj1T+JQNTM=" }, + "node_modules/superagent/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/superagent/node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6587,6 +6817,16 @@ "node": ">=0.8.0" } }, + "node_modules/tar-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", + "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tc-core-library-js": { "version": "2.4.1", "resolved": "git+ssh://git@github.com/appirio-tech/tc-core-library-js.git#f45352974dafe5a10c86fc50bdd59ef399b50c65", @@ -6627,17 +6867,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, - "node_modules/tc-core-library-js/node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/tc-core-library-js/node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7229,6 +7458,32 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zip-stream": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-5.0.1.tgz", + "integrity": "sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==", + "dependencies": { + "archiver-utils": "^4.0.1", + "compress-commons": "^5.0.1", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } } } } diff --git a/package.json b/package.json index 5d74f99..eda15c3 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,12 @@ "dependencies": { "@gitbeaker/rest": "^39.13.0", "@octokit/rest": "^18.9.0", + "archiver": "^6.0.1", "axios": "^0.19.0", "circular-json": "^0.5.7", "config": "^1.30.0", "dynamoose": "^1.11.1", + "form-data": "^4.0.0", "fs-extra": "^7.0.0", "get-parameter-names": "^0.3.0", "global-request-logger": "^0.1.1", diff --git a/services/GitlabService.js b/services/GitlabService.js index 8dd94db..9932da7 100644 --- a/services/GitlabService.js +++ b/services/GitlabService.js @@ -73,7 +73,7 @@ class GitlabService { /** @type {User} */ #user = null; - /** @type {Gitlab} */ + /** @type {import('@gitbeaker/rest').Gitlab} */ #gitlab = null; constructor(user) { @@ -84,6 +84,15 @@ class GitlabService { this.#user = user; } + /** + * Get the full URL for a Gitlab repository from its full name. + * @param {String} repoFullName Repo full name + * @returns {String} + */ + static getRepoUrl(repoFullName) { + return `${config.GITLAB_API_BASE_URL}/${repoFullName}`; + } + /** * Helper method for initializing a GitlabService instance with an active * access token. @@ -478,8 +487,8 @@ class GitlabService { acc += `deleted file mode ${file.a_mode}\n`; } if (file.diff && file.diff !== '') { - acc += `--- a/${file.new_file ? '/dev/null' : file.old_path}\n`; - acc += `+++ b/${file.deleted_file ? '/dev/null' : file.new_path}\n`; + acc += `--- ${file.new_file ? '/dev/null' : `a/${file.old_path}`}\n`; + acc += `+++ ${file.deleted_file ? '/dev/null' : `b/${file.new_path}`}\n`; acc += file.diff; } else if (file.renamed_file) { acc += 'similarity index 100%\n'; @@ -488,7 +497,9 @@ class GitlabService { } return acc; }, ''); + console.log(`PATCH FILE STARTS::${mergeRequest.web_url}`); console.log(patchFile); + console.log(`PATCH FILE ENDS::${mergeRequest.web_url}`); return patchFile; } diff --git a/services/PrivateForkService.js b/services/PrivateForkService.js index 4d24411..8c25ff1 100644 --- a/services/PrivateForkService.js +++ b/services/PrivateForkService.js @@ -10,7 +10,6 @@ const errors = require('../utils/errors'); const GitlabService = require('../services/GitlabService'); const {GITLAB_ACCESS_LEVELS, USER_ROLES, USER_TYPES} = require('../constants'); -const ProjectChallengeMapping = models.ProjectChallengeMapping; const Project = models.Project; const User = models.User; const GitlabUserMapping = models.GitlabUserMapping; @@ -30,24 +29,13 @@ async function process(payload) { logger.debug(`${logPrefix}: Member ID: ${memberId}`); logger.debug(`${logPrefix}: Member Handle: ${memberHandle}`); // Check if there are projects mapped to the challenge - const filterValues = {}; - const filter = { - FilterExpression: '#challengeId = :challengeId', - ExpressionAttributeNames: { - '#challengeId': 'challengeId' - }, - ExpressionAttributeValues: { - ':challengeId': challengeId - } - }; - const projectChallengeMapping = await dbHelper.scan(ProjectChallengeMapping, filter, filterValues); - if (projectChallengeMapping.length === 0) { + const projectId = await dbHelper.queryProjectIdByChallengeId(challengeId); + if (!projectId) { logger.info(`${logPrefix} ProjectChallengeMapping not found for challengeId: ${challengeId}`); return; } - logger.debug(`${logPrefix} ProjectChallengeMapping: ${JSON.stringify(projectChallengeMapping)}`); + logger.debug(`${logPrefix} TCX ProjectId: ${projectId}`); // Get Project - const projectId = projectChallengeMapping[0].projectId; const project = await dbHelper.getById(Project, projectId); if (!project) { logger.info(`${logPrefix} Project not found for projectId: ${projectId}`); diff --git a/services/PullRequestService.js b/services/PullRequestService.js index 5cc37c9..28c1816 100644 --- a/services/PullRequestService.js +++ b/services/PullRequestService.js @@ -3,13 +3,24 @@ const _ = require('lodash'); const Joi = require('joi'); const uuid = require('uuid').v4; +const archiver = require('archiver'); const models = require('../models'); const dbHelper = require('../utils/db-helper'); const logger = require('../utils/logger'); const GitlabService = require('../services/GitlabService'); +const TopcoderApiHelper = require('../utils/topcoder-api-helper'); const GitlabUserMapping = models.GitlabUserMapping; +/** + * Normalizes a string to be used as a file name. + * @param {String} fileName File name to normalize + * @returns {String} Normalized file name + */ +function normalizeFileName(fileName) { + return fileName.replace(/[^a-zA-Z0-9_\-.]/g, '_'); +} + /** * Handles a pull request creation event. * @param {Object} payload The event payload. @@ -59,44 +70,53 @@ async function process(payload) { return; } logger.debug(`${logPrefix} Project: ${JSON.stringify(project)}`); - // 4. Find all repositories corresponding to the TCX project + // 4. Find the challenge ID for the TCX project + const challengeId = await dbHelper.queryChallengeIdByProjectId(project.id); + if (!challengeId) { + logger.info(`${logPrefix} ProjectChallengeMapping not found for projectId: ${project.id}`); + return; + } + logger.debug(`${logPrefix} Challenge ID: ${challengeId}`); + // 5. Find all repositories corresponding to the TCX project const repositories = await dbHelper.queryAllRepositoriesByProjectId(project.id); if (!repositories || repositories.length === 0) { logger.info(`${logPrefix} Repositories not found for projectId: ${project.id}`); return; } logger.debug(`${logPrefix} Repositories: ${JSON.stringify(repositories)}`); - // 5. Get co-pilot's GitlabUserMapping + // 6. Get co-pilot's GitlabUserMapping const copilot = await dbHelper.queryOneUserMappingByTCUsername(GitlabUserMapping, project.copilot); if (!copilot) { logger.info(`${logPrefix} GitlabUserMapping not found for copilot: ${project.copilot}`); return; } logger.debug(`${logPrefix} GitlabUserMapping[Copilot]: ${JSON.stringify(copilot)}`); - // 6. Get co-pilot's Gitlab user + // 7. Get co-pilot's Gitlab user const copilotGitlabUser = await dbHelper.queryOneUserByType(models.User, copilot.gitlabUsername, 'gitlab'); if (!copilotGitlabUser) { logger.info(`${logPrefix} GitlabUser not found for copilot: ${project.copilot}`); return; } logger.debug(`${logPrefix} GitlabUser[Copilot]: ${JSON.stringify(copilotGitlabUser)}`); - // 7. For each project, get the repositories - const gitRepositories = await Promise.all(repositories.map((repo) => GitlabService.getRepository(copilotGitlabUser, repo.url))); + // 8. Init the Gitlab service for co-pilot + const copilotGitlabService = await GitlabService.create(copilotGitlabUser); + // 9. For each project, get the repositories + const gitRepositories = await Promise.all(repositories.map((repo) => copilotGitlabService.getRepository(repo.url))); if (!gitRepositories || gitRepositories.length === 0) { logger.info(`${logPrefix} Git repositories not found for repositories: ${JSON.stringify(repositories)}`); return; } logger.debug(`${logPrefix} Git repositories: ${JSON.stringify(gitRepositories)}`); - // 8. For each repository, get the merge requests + // 10. For each repository, get the merge requests const mergeRequests = await Promise.all( - gitRepositories.map((repo) => GitlabService.getOpenMergeRequestsByUser(copilotGitlabUser, repo, submitter.gitlabUserId)) + gitRepositories.map((repo) => copilotGitlabService.getOpenMergeRequestsByUser(repo, submitter.gitlabUserId)) ); if (!mergeRequests || mergeRequests.length === 0) { logger.info(`${logPrefix} Merge requests not found for repositories: ${JSON.stringify(gitRepositories)}`); return; } logger.debug(`${logPrefix} Merge requests: ${JSON.stringify(mergeRequests)}`); - // 9. Ensure that there exists a merge request by the same member as the pull request for each project (if not, return) + // 11. Ensure that there exists a merge request by the same member as the pull request for each project (if not, return) // eslint-disable-next-line no-restricted-syntax for (let i = 0; i < mergeRequests.length; i += 1) { const mr = mergeRequests[i]; @@ -105,21 +125,54 @@ async function process(payload) { return; } } - // 10. Get the latest merge request for each project + // 12. Get the latest merge request for each project const latestMergeRequests = mergeRequests.map((mrs) => _.maxBy(mrs, (mr) => new Date(mr.created_at).getTime())); logger.debug(`${logPrefix} Latest merge requests: ${JSON.stringify(latestMergeRequests)}`); - // 11. Create patch files for each merge request + // 13. Create patch files for each merge request const patches = await Promise.all( - latestMergeRequests.map((mr) => GitlabService.getMergeRequestDiffPatches(copilotGitlabUser, mr)) + latestMergeRequests.map((mr) => copilotGitlabService.getMergeRequestDiffPatches(mr)) ); if (!patches || patches.length !== latestMergeRequests.length) { logger.info(`${logPrefix} Patches not found for merge requests.`); return; } logger.debug(`${logPrefix} Patches: ${JSON.stringify(patches)}`); - // 11. Get the topcoder M2M token - // 10. Create a zip file containing all patch files - // 11. Use the submission API to submit the zip file + // 14. Create a zip file containing all patch files + logger.debug(`${logPrefix} Creating zip file...`); + const zipStream = archiver('zip'); + const zipBufferPromise = new Promise((resolve, reject) => { + const buffers = []; + zipStream.on('data', (data) => { + buffers.push(data); + }); + zipStream.on('end', () => { + resolve(Buffer.concat(buffers)); + }); + zipStream.on('error', reject); + }); + patches.forEach((patch) => { + const buffer = Buffer.from(patch); + zipStream.append(buffer, {name: `${normalizeFileName(repoFullName)}.patch`}); + }); + await zipStream.finalize(); + const zipBuffer = await zipBufferPromise; + logger.debug(`${logPrefix} Zip file size: ${zipBuffer.length}`); + // 15. Get the Topcoder user's member ID + const memberId = await TopcoderApiHelper.getTopcoderMemberId(submitter.topcoderUsername); + if (!memberId) { + logger.info(`${logPrefix} Member ID not found for topcoderUsername: ${submitter.topcoderUsername}`); + return; + } + logger.debug(`${logPrefix} Member ID: ${memberId}`); + // 16. Use the submission API to submit the zip file + logger.debug(`${logPrefix} Submitting the zip file...`); + const submission = await TopcoderApiHelper.createSubmission( + challengeId, + memberId, + zipBuffer, + `${correlationId}.zip`, + ); + logger.debug(`${logPrefix} Submission: ${JSON.stringify(submission.data)}`); } process.schema = Joi.object().keys({ diff --git a/utils/db-helper.js b/utils/db-helper.js index 873fca6..2c707b0 100644 --- a/utils/db-helper.js +++ b/utils/db-helper.js @@ -11,6 +11,24 @@ const logger = require('./logger'); * @version 1.0 */ +/** + * @typedef {Object} GitlabUserMapping + * @property {String} id the id + * @property {Number} gitlabUserId the gitlab user id + * @property {String} topcoderUsername the topcoder username + * @property {String} gitlabUsername the gitlab username + */ + +/** + * @typedef {Object} Repository + * @property {String} id the id + * @property {String} projectId the project id + * @property {String} url the repository url + * @property {String} archived the archived flag + * @property {String} repoId the repository id + * @property {String} registeredWebhookId the registered webhook id + */ + /** * Get Data by model id * @param {Object} model The dynamoose model to query @@ -150,7 +168,7 @@ async function queryOneUserByTypeAndRole(model, username, type, role) { /** * Query project by repository url * @param {String} repoUrl the repo url - * @returns {Promise<Object>} + * @returns {Promise<Repository>} */ async function queryOneProjectByRepositoryLink(repoUrl) { const projectId = await new Promise((resolve, reject) => { @@ -187,7 +205,7 @@ async function queryOneProjectByRepositoryLink(repoUrl) { * Get single data by query parameters * @param {Object} model The dynamoose model to query * @param {String} tcusername The tc username - * @returns {Promise<void>} + * @returns {Promise<GitlabUserMapping>} */ async function queryOneUserMappingByTCUsername(model, tcusername) { return await new Promise((resolve, reject) => { @@ -267,7 +285,7 @@ async function queryOneUserMappingByGithubUserId(model, userId) { * Get single data by query parameters * @param {Object} model The dynamoose model to query * @param {Number} userId The The user id - * @returns {Promise<void>} + * @returns {Promise<GitlabUserMapping>} */ async function queryOneUserMappingByGitlabUserId(model, userId) { return await new Promise((resolve, reject) => { @@ -490,6 +508,56 @@ async function releaseLockOnUser(id, lockId) { return user; } +/** + * Find the TC Challenge ID for a given TCX project ID + * @param {String} projectId Project ID + * @returns {Promise<String | null>} Challenge ID + */ +async function queryChallengeIdByProjectId(projectId) { + const filter = { + FilterExpression: '#projectId = :projectId', + ExpressionAttributeNames: { + '#projectId': 'projectId' + }, + ExpressionAttributeValues: { + ':projectId': projectId + } + }; + return new Promise((resolve, reject) => { + models.ProjectChallengeMapping.scan(filter, (err, result) => { + if (err) { + return reject(err); + } + return resolve(result.count === 0 ? null : result[0].challengeId); + }); + }); +} + +/** + * Find the TCX Project ID for a given TC Challenge ID + * @param {String} challengeId Challenge ID + * @returns {Promise<String | null>} Project ID + */ +async function queryProjectIdByChallengeId(challengeId) { + const filter = { + FilterExpression: '#challengeId = :challengeId', + ExpressionAttributeNames: { + '#challengeId': 'challengeId' + }, + ExpressionAttributeValues: { + ':challengeId': challengeId + } + }; + return new Promise((resolve, reject) => { + models.ProjectChallengeMapping.scan(filter, (err, result) => { + if (err) { + return reject(err); + } + return resolve(result.count === 0 ? null : result[0].projectId); + }); + }); +} + module.exports = { getById, scan, @@ -512,5 +580,7 @@ module.exports = { removeCopilotPayment, removeIssue, acquireLockOnUser, - releaseLockOnUser + releaseLockOnUser, + queryChallengeIdByProjectId, + queryProjectIdByChallengeId }; diff --git a/utils/topcoder-api-helper.js b/utils/topcoder-api-helper.js index de63381..1ba3092 100644 --- a/utils/topcoder-api-helper.js +++ b/utils/topcoder-api-helper.js @@ -13,9 +13,10 @@ 'use strict'; const config = require('config'); -const axios = require('axios'); +const axios = require('axios').default; const _ = require('lodash'); const circularJSON = require('circular-json'); +const FormData = require('form-data'); const m2mAuth = require('tc-core-library-js').auth.m2m; @@ -465,8 +466,43 @@ async function getProjectByDirectId(id, directId) { }); } -async function createSubmission(challengeId, submissionFileStream, submissionFileName, submissionType) { - // TODO: Implement submission creation +/** + * Create a new submission. + * @param {String} challengeId Challenge ID + * @param {Number} memberId Member ID + * @param {Buffer} submissionFile Submission file + * @param {String} submissionFileName Submission file name + */ +async function createSubmission(challengeId, memberId, submissionFile, submissionFileName) { + try { + const formData = new FormData(); + formData.append('submission', submissionFile, { + filename: submissionFileName, + contentType: 'application/zip', + knownLength: submissionFile.length + }); + formData.append('type', 'Contest Submission'); + formData.append('memberId', memberId); + formData.append('challengeId', challengeId); + const apiKey = await getM2Mtoken(); + const res = await axios.post(`${config.TC_API_URL}/submissions`, formData, { + headers: { + authorization: `Bearer ${apiKey}`, + accept: 'application/json', + ...formData.getHeaders() + } + }); + return res; + } catch (error) { + logger.error('createSubmission ERROR.'); + if (error.isAxiosError) { + logger.error(`Request: ${JSON.stringify(error.config)}`); + logger.error(`Response Data: ${JSON.stringify(error.response.data)}`); + } else { + logger.error(`${error.message}`, error); + } + throw errors.convertTopcoderApiError(error, 'Failed to create submission.'); + } } module.exports = { @@ -485,5 +521,6 @@ module.exports = { cancelPrivateContent, assignUserAsRegistrant, removeResourceToChallenge, - getProjectByDirectId + getProjectByDirectId, + createSubmission };