diff --git a/scripts/migrate-repo-url.js b/scripts/migrate-repo-url.js index 834ff12..2afbcf5 100644 --- a/scripts/migrate-repo-url.js +++ b/scripts/migrate-repo-url.js @@ -2,6 +2,7 @@ const AWS = require('aws-sdk'); const helper = require('../src/common/helper'); const dbHelper = require('../src/common/db-helper'); const Project = require('../src/models').Project; +const Repository = require('../src/models').Repository; if (process.env.IS_LOCAL=="true") { AWS.config.update({ @@ -21,8 +22,23 @@ var documentClient = new AWS.DynamoDB.DocumentClient(); items = await documentClient.scan(params).promise(); items.Items.forEach(async (item) => { console.log(item); - item.repoUrls = [item.repoUrl]; - await dbHelper.update(Project, item.id, item); + let repoUrls; + if (item.repoUrls) { + repoUrls = item.repoUrls.values; + } + else { + repoUrls = [item.repoUrl]; + } + for (const url of repoUrls) { // eslint-disable-line + console.log(`Creating ${url}`); + await dbHelper.create(Repository, { + id: helper.generateIdentifier(), + projectId: item.id, + url, + archived: item.archived, + registeredWebhookId: item.registeredWebhookId + }); + } }); params.ExclusiveStartKey = items.LastEvaluatedKey; } while(typeof items.LastEvaluatedKey !== 'undefined'); diff --git a/src/common/db-helper.js b/src/common/db-helper.js index 034d369..c82ddae 100644 --- a/src/common/db-helper.js +++ b/src/common/db-helper.js @@ -1,4 +1,7 @@ +// eslint-disable-line max-lines +const _ = require('lodash'); const logger = require('./logger'); +const models = require('../models'); /* * Copyright (c) 2018 TopCoder, Inc. All rights reserved. @@ -67,6 +70,50 @@ async function scan(model, scanParams) { }); } +/** + * Get data collection by scan parameters with paging + * @param {Object} model The dynamoose model to scan + * @param {String} size The size of result + * @param {String} lastKey The lastKey param + * @returns {Promise} + */ +async function scanAll(model, size, lastKey) { + return await new Promise((resolve, reject) => { + const scanMethod = model.scan({}).limit(size); + if (lastKey) scanMethod.startAt(lastKey); + scanMethod.exec((err, result) => { + if (err) { + logger.error(`DynamoDB scan error ${err}`); + return reject(err); + } + return resolve(result.count === 0 ? [] : result); + }); + }); +} + +/** + * Get data collection by scan with search + * @param {Object} model The dynamoose model to scan + * @param {String} size The size of result + * @param {String} lastKey The lastKey param + * @param {String} containsKey The contains key param + * @param {String} contains The contains value + * @returns {Promise} + */ +async function scanAllWithSearch(model, size, lastKey, containsKey, contains) { + return await new Promise((resolve, reject) => { + const scanMethod = model.scan(containsKey).contains(contains).limit(size); + if (lastKey) scanMethod.startAt(lastKey); + scanMethod.exec((err, result) => { + if (err) { + logger.error(`DynamoDB scan error ${err}`); + return reject(err); + } + return resolve(result.count === 0 ? [] : result); + }); + }); +} + /** * Get single data by query parameters * @param {Object} model The dynamoose model to query @@ -171,16 +218,16 @@ async function queryOneUserMappingByTCUsername(model, tcusername) { */ async function queryOneActiveProject(model, repoUrl) { return await new Promise((resolve, reject) => { - model.scan('repoUrls').contains(repoUrl) - .filter('archived') - .eq('false') - .all() - .exec((err, result) => { - if (err || !result) { - logger.debug(`queryOneActiveProject. Error. ${err}`); - return reject(err); - } - return resolve(result.count === 0 ? null : result[0]); + queryOneActiveRepository(models.Repository, repoUrl).then((repo) => { + if (!repo) resolve(null); + else model.queryOne('id').eq(repo.projectId).consistent() + .exec((err, result) => { + if (err) { + logger.debug(`queryOneActiveProject. Error. ${err}`); + return reject(err); + } + return resolve(result); + }); }); }); } @@ -268,18 +315,16 @@ async function queryOneUserTeamMapping(model, teamId, githubUserName, githubOrgI */ async function queryOneActiveProjectWithFilter(model, repoUrl, projectIdToFilter) { return await new Promise((resolve, reject) => { - model.scan('repoUrls').contains(repoUrl) - .filter('archived') - .eq('false') - .filter('id') - .not().eq(projectIdToFilter) - .all() - .exec((err, result) => { - if (err || !result) { - logger.debug(`queryOneActiveProjectWithFilter. Error. ${err}`); - return reject(err); - } - return resolve(result.count === 0 ? null : result[0]); + queryActiveRepositoriesExcludeByProjectId(models.Repository, repoUrl, projectIdToFilter).then((repos) => { + if (!repos || repos.length === 0) resolve(null); + else model.queryOne('id').eq(repos[0].projectId).consistent() + .exec((err, result) => { + if (err) { + logger.debug(`queryOneActiveProjectWithFilter. Error. ${err}`); + return reject(err); + } + return resolve(result); + }); }); }); } @@ -387,22 +432,138 @@ async function queryOneOrganisation(model, organisation) { }); } +/** + * Query one active repository + * @param {Object} model the dynamoose model + * @param {String} url the repository url + * @returns {Promise} + */ +async function queryOneActiveRepository(model, url) { + return await new Promise((resolve, reject) => { + model.queryOne({ + url, + archived: 'false' + }) + .all() + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(result); + }); + }); +} + +/** + * Query active repository with project id exclude filter. + * @param {String} url the repository url + * @param {String} projectId the project id + * @returns {Promise} + */ +async function queryActiveRepositoriesExcludeByProjectId(url, projectId) { + return await new Promise((resolve, reject) => { + models.Repository.query({ + url, + archived: 'false' + }) + .filter('projectId') + .not().eq(projectId) + .all() + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(result); + }); + }); +} + +/** + * Query repository by project id. + * @param {String} projectId the project id + * @returns {Promise} + */ +async function queryRepositoriesByProjectId(projectId) { + return await new Promise((resolve, reject) => { + models.Repository.query({ + projectId + }) + .all() + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(result); + }); + }); +} + +/** + * Query repository by project id with url filter. + * @param {String} projectId the project id + * @param {String} url the repo url + * @returns {Promise} + */ +async function queryRepositoryByProjectIdFilterUrl(projectId, url) { + return await new Promise((resolve, reject) => { + models.Repository.query({ + projectId + }) + .filter('url') + .eq(url) + .all() + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(result.count === 0 ? null : result[0]); + }); + }); +} + +/** + * Find array of repo url of given project id + * @param {String} projectId the project id + * @returns {Promise} + */ +async function populateRepoUrls(projectId) { + return await new Promise((resolve, reject) => { + models.Repository.query({ + projectId + }) + .all() + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(_.map(result, 'url')); + }); + }); +} + module.exports = { getById, getByKey, scan, + scanAll, + scanAllWithSearch, create, update, removeById, removeUser, + populateRepoUrls, + queryActiveRepositoriesExcludeByProjectId, queryOneActiveCopilotPayment, queryOneActiveProject, queryOneActiveProjectWithFilter, + queryOneActiveRepository, queryOneOrganisation, queryOneIssue, queryOneUserByType, queryOneUserByTypeAndRole, queryOneUserGroupMapping, queryOneUserTeamMapping, - queryOneUserMappingByTCUsername + queryOneUserMappingByTCUsername, + queryRepositoriesByProjectId, + queryRepositoryByProjectIdFilterUrl }; diff --git a/src/controllers/IssueController.js b/src/controllers/IssueController.js index 162873b..09d57d8 100644 --- a/src/controllers/IssueController.js +++ b/src/controllers/IssueController.js @@ -21,16 +21,6 @@ async function search(req) { return await IssueService.search(req.query, req.currentUser.handle); } -/** - * create an issue - * @param {Object} req the request - * @param {Object} res the response - * @returns {Object} the result - */ -async function create(req) { - return await IssueService.create(req.body, req.currentUser); -} - /** * recreate an issue * Remove the related db record. @@ -45,7 +35,6 @@ async function recreate(req) { module.exports = { search, - create, recreate }; diff --git a/src/controllers/ProjectController.js b/src/controllers/ProjectController.js index 1526796..e37416e 100644 --- a/src/controllers/ProjectController.js +++ b/src/controllers/ProjectController.js @@ -9,6 +9,7 @@ * @version 1.0 */ const helper = require('../common/helper'); +const dbHelper = require('../common/db-helper'); const ProjectService = require('../services/ProjectService'); const models = require('../models'); @@ -49,7 +50,8 @@ async function getAll(req) { */ async function createLabel(req) { const dbProject = await helper.ensureExists(models.Project, req.body.projectId, 'Project'); - for (const repoUrl of dbProject.repoUrls) { // eslint-disable-line no-restricted-syntax + const repoUrls = await dbHelper.populateRepoUrls(dbProject.id); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax try { await ProjectService.createLabel(req.body, req.currentUser, repoUrl); } @@ -70,7 +72,8 @@ async function createLabel(req) { */ async function createHook(req) { const dbProject = await helper.ensureExists(models.Project, req.body.projectId, 'Project'); - for (const repoUrl of dbProject.repoUrls) { // eslint-disable-line no-restricted-syntax + const repoUrls = await dbHelper.populateRepoUrls(dbProject.id); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax try { await ProjectService.createHook(req.body, req.currentUser, repoUrl); } @@ -91,7 +94,8 @@ async function createHook(req) { */ async function addWikiRules(req) { const dbProject = await helper.ensureExists(models.Project, req.body.projectId, 'Project'); - for (const repoUrl of dbProject.repoUrls) { // eslint-disable-line no-restricted-syntax + const repoUrls = await dbHelper.populateRepoUrls(dbProject.id); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax try { await ProjectService.addWikiRules(req.body, req.currentUser, repoUrl); } diff --git a/src/front/src/app/app.js b/src/front/src/app/app.js index e2f1172..fa0b206 100644 --- a/src/front/src/app/app.js +++ b/src/front/src/app/app.js @@ -95,14 +95,6 @@ angular.module('topcoderX', [ data: { pageTitle: 'Project Management' }, resolve: { auth: authenticate } }) - .state('app.issue', { - url: '/upsertissue', - controller: 'IssueController', - controllerAs: 'vm', - templateUrl: 'app/upsertissue/upsertissue.html', - data: { pageTitle: 'Project Management' }, - resolve: { auth: authenticate } - }) // following code is commented to hide the menu // un comment this when pages are developed // .state('app.challenges', { diff --git a/src/front/src/app/projects/projects.controller.js b/src/front/src/app/projects/projects.controller.js index 7d111f0..b833f0c 100644 --- a/src/front/src/app/projects/projects.controller.js +++ b/src/front/src/app/projects/projects.controller.js @@ -26,11 +26,6 @@ angular.module('topcoderX') $state.go('app.project'); }; - //go to a add issue page - $scope.goIssue = function () { - $state.go('app.issue'); - }; - //the actived project list $scope.projects = []; //the archived project list diff --git a/src/front/src/app/upsertissue/upsertissue.controller.js b/src/front/src/app/upsertissue/upsertissue.controller.js deleted file mode 100644 index 1e37924..0000000 --- a/src/front/src/app/upsertissue/upsertissue.controller.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2018 TopCoder, Inc. All rights reserved. - * - * This is the upsertproject controller. - */ -'use strict'; - -angular.module('topcoderX').controller('IssueController', ['currentUser', '$scope', '$timeout', 'ProjectService', 'IssueService', - '$rootScope', '$state', 'Alert', '$log', - function (currentUser, $scope, $timeout, ProjectService, IssueService, $rootScope, $state, - Alert, $log) { - // Maintain the navigation state. - $timeout(function () { - angular.element('#projectsManagement').addClass('active'); - }, 0); - - // get topcoderx projects - $scope.getProjects = function () { - $log.log('getProjects dipanggil cui... dari upsert'); - - ProjectService.getProjects('active', false).then(function (response) { - $scope.projects = response.data; - if ($scope.projects.length === 0) { - _handleError({}, 'There are no projects in Topcoder-X. Please Create a project first.'); - } - }).catch(function (error) { - _handleError(error, 'There are no projects in Topcoder-X. Please Create a project first.'); - }); - }; - $scope.getProjects(); - - // handle error output - function _handleError(error, defaultMsg) { - const errMsg = error.data ? error.data.message : defaultMsg; - Alert.error(errMsg, $scope); - } - - $scope.selectedProject = null; - $scope.issue = { - projectId: '', - prize: null, - title: '', - comment: '', - repoUrl: '' - }; - $scope.title = 'Add an Issue'; - - // save the issue info. - $scope.save = function () { - const selectedProject = angular.fromJson($scope.selectedProject); - $scope.issue.projectId = selectedProject.id; - $scope.issue.repoUrl = selectedProject.repoUrl; - - IssueService.create($scope.issue).then(function (response) { - $scope.selectedProject = null; - $scope.issue = { - projectId: '', - prize: null, - title: '', - comment: '', - repoUrl: '' - }; - Alert.info('Issue #' + response.data.number + ' has been created', $scope); - }).catch(function (error) { - Alert.error(error.data.message, $scope); - }); - }; - }]); diff --git a/src/front/src/app/upsertissue/upsertissue.html b/src/front/src/app/upsertissue/upsertissue.html deleted file mode 100644 index df05fa0..0000000 --- a/src/front/src/app/upsertissue/upsertissue.html +++ /dev/null @@ -1,53 +0,0 @@ -
-
-
-
-

{{title}}

-
-
- -
-
-
-
-
- - - The - project is required. -
-
- - - The price amount of the issue - The - price is required. -
-
- - - The title of the issue - The - title is required. -
-
- - - The content text of the issue -
-
- - -
-
-
-
-
-
diff --git a/src/front/src/app/upsertproject/upsertproject.controller.js b/src/front/src/app/upsertproject/upsertproject.controller.js index afa7865..5ec6b95 100644 --- a/src/front/src/app/upsertproject/upsertproject.controller.js +++ b/src/front/src/app/upsertproject/upsertproject.controller.js @@ -32,6 +32,7 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc $scope.project.id = $rootScope.project.id; $scope.project.copilot = $rootScope.project.copilot; $scope.project.owner = $rootScope.project.owner; + $scope.project.repoUrl = $rootScope.project.repoUrls.join(','); $scope.editing = true; } else { $scope.title = 'Add a Project'; diff --git a/src/front/src/app/user-mappings/user-mappings-controller.js b/src/front/src/app/user-mappings/user-mappings-controller.js index ffd332d..0524e86 100644 --- a/src/front/src/app/user-mappings/user-mappings-controller.js +++ b/src/front/src/app/user-mappings/user-mappings-controller.js @@ -14,7 +14,8 @@ angular.module('topcoderX') sortDir: 'asc', totalPages: 1, initialized: false, - query: '' + query: '', + lastKey: [] }; $scope.addUserMapping = function () { @@ -37,10 +38,15 @@ angular.module('topcoderX') $scope.getUserMappings = function () { var config = $scope.tableConfig; config.isLoading = true; - UserMappingsService.search(config.query, config.sortBy, config.sortDir, config.pageNumber, config.pageSize) + UserMappingsService.search(config.query, config.sortBy, config.sortDir, config.pageNumber, config.pageSize, config.lastKey[config.pageNumber]) // eslint-disable-line max-len .then(function (res) { config.items = res.data.docs; - config.pages = res.data.pages; + if (res.data.lastKey && (res.data.lastKey.githubLastKey || res.data.lastKey.gitlabLastKey)) { + config.lastKey[config.pageNumber + 1] = res.data.lastKey; + if (!config.pages || config.pages <= config.pageNumber) { + config.pages = config.pageNumber + 1; + } + } config.initialized = true; config.isLoading = false; }).catch(function (err) { diff --git a/src/front/src/app/user-mappings/user-mappings.html b/src/front/src/app/user-mappings/user-mappings.html index a8c1408..167c239 100644 --- a/src/front/src/app/user-mappings/user-mappings.html +++ b/src/front/src/app/user-mappings/user-mappings.html @@ -21,7 +21,7 @@

User Mappings

-
+
@@ -105,9 +105,6 @@

You don't have any User Mappings, Please
  • -
  • - » -
  • diff --git a/src/front/src/app/user-mappings/user-mappings.service.js b/src/front/src/app/user-mappings/user-mappings.service.js index 5314695..51fe68b 100644 --- a/src/front/src/app/user-mappings/user-mappings.service.js +++ b/src/front/src/app/user-mappings/user-mappings.service.js @@ -18,9 +18,11 @@ angular.module('topcoderX') * @param {Number} pageNo the page number * @param {Number} pageSize the page size */ - service.search = function (query, sortBy, sortDir, pageNo, pageSize) { - if (query) return service.searchWithQuery(query, sortBy, sortDir, pageNo, pageSize); - else return $http.get(baseUrl + '/api/v1/users/mappings?sortBy=' + sortBy + '&sortDir=' + sortDir + '&page=' + pageNo + '&perPage=' + pageSize) + service.search = function (query, sortBy, sortDir, pageNo, pageSize, lastKey) { + if (query) return service.searchWithQuery(query, sortBy, sortDir, pageNo, pageSize, lastKey); + else return $http.get(baseUrl + '/api/v1/users/mappings?sortBy=' + sortBy + '&sortDir=' + sortDir + '&page=' + pageNo + '&perPage=' + pageSize + + (lastKey && lastKey.githubLastKey ? '&githubLastKey=' + lastKey.githubLastKey : '' ) + + (lastKey && lastKey.gitlabLastKey ? '&gitlabLastKey=' + lastKey.gitlabLastKey : '' )) .then(function (response) { return response; }); @@ -34,8 +36,10 @@ angular.module('topcoderX') * @param {Number} pageNo the page number * @param {Number} pageSize the page size */ - service.searchWithQuery = function (query, sortBy, sortDir, pageNo, pageSize) { - return $http.get(baseUrl + '/api/v1/users/mappings?query=' + query + '&sortBy=' + sortBy + '&sortDir=' + sortDir + '&page=' + pageNo + '&perPage=' + pageSize) + service.searchWithQuery = function (query, sortBy, sortDir, pageNo, pageSize, lastKey) { + return $http.get(baseUrl + '/api/v1/users/mappings?query=' + query + '&sortBy=' + sortBy + '&sortDir=' + sortDir + '&page=' + pageNo + '&perPage=' + pageSize + + (lastKey && lastKey.githubLastKey ? '&githubLastKey=' + lastKey.githubLastKey : '' ) + + (lastKey && lastKey.gitlabLastKey ? '&gitlabLastKey=' + lastKey.gitlabLastKey : '' )) .then(function (response) { return response; }); diff --git a/src/models/Project.js b/src/models/Project.js index b8f70fb..ea10c7a 100644 --- a/src/models/Project.js +++ b/src/models/Project.js @@ -24,22 +24,12 @@ const schema = new Schema({ type: Number, required: true }, - repoUrl: { - type: String, - required: true - }, - repoUrls: { - type: [String], - required: true - }, - repoId: {type: String, required: false}, rocketChatWebhook: {type: String, required: false}, rocketChatChannelName: {type: String, required: false}, archived: {type: String, required: true}, owner: {type: String, required: true}, secretWebhookKey: {type: String, required: true}, copilot: {type: String, required: false}, - registeredWebhookId: {type: String, required: false}, updatedAt: { type: Date, default: Date.now, diff --git a/src/models/Repository.js b/src/models/Repository.js new file mode 100644 index 0000000..60a958b --- /dev/null +++ b/src/models/Repository.js @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018 TopCoder, Inc. All rights reserved. + */ +'use strict'; + +/** + * Schema for project and repository mapping. + * @author TCSCODER + * @version 1.0 + */ +const dynamoose = require('dynamoose'); + +const Schema = dynamoose.Schema; + +const schema = new Schema({ + id: { + type: String, + hashKey: true, + required: true + }, + projectId: { + type: String, + required: true, + index: { + global: true, + project: true, + name: 'ProjectIdIndex' + }, + }, + url: { + type: String, + required: true, + index: { + global: true, + project: true, + rangKey: 'archived', + name: 'URLIndex' + } + }, + archived: {type: String, required: true}, + repoId: {type: String, required: false}, + registeredWebhookId: {type: String, required: false} +}); + +module.exports = schema; diff --git a/src/routes.js b/src/routes.js index 2f05b50..53e6884 100644 --- a/src/routes.js +++ b/src/routes.js @@ -226,10 +226,6 @@ module.exports = { controller: 'IssueController', method: 'search', }, - post: { - controller: 'IssueController', - method: 'create', - }, }, '/issues/recreate': { post: { diff --git a/src/services/IssueService.js b/src/services/IssueService.js index d032b13..e2882b6 100644 --- a/src/services/IssueService.js +++ b/src/services/IssueService.js @@ -64,7 +64,10 @@ async function search(criteria, currentUserTopcoderHandle) { for (const issue of docs) { // eslint-disable-line guard-for-in,no-restricted-syntax issue.projectId = await dbHelper.getById(models.Project, issue.projectId); issue.assignedAt = moment(issue.assignedAt).format('YYYY-MM-DD HH:mm:ss'); - if (!issue.repoUrl && issue.projectId.repoUrls) issue.repoUrl = issue.projectId.repoUrls[0]; + if (!issue.repoUrl) { + const repoUrls = await dbHelper.populateRepoUrls(issue.projectId.id); + issue.repoUrl = repoUrls && repoUrls.length > 0 ? repoUrls[0] : undefined; + } } const offset = (criteria.page - 1) * criteria.perPage; @@ -117,91 +120,6 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { return dbProject; } -/** - * create issue - * @param {Object} issue the issue detail - * @param {String} currentUser the topcoder current user - * @returns {Object} created issue - */ -async function create(issue, currentUser) { - const dbProject = await _ensureEditPermissionAndGetInfo(issue.projectId, currentUser); - const provider = await helper.getProviderType(dbProject.repoUrls[0]); - const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); - const results = dbProject.repoUrls[0].split('/'); - const index = 1; - const repoName = results[results.length - index]; - const excludePart = 3; - const repoOwner = _(results).slice(excludePart, results.length - 1).join('/'); - const title = `[$${issue.prize}] ${issue.title}`; - if (provider === 'github') { - try { - const github = new GitHub({token: userRole.accessToken}); - const githubIssueWrapper = github.getIssues(repoOwner, repoName); - const newIssue = { - title, - body: issue.comment, - labels: [config.OPEN_FOR_PICKUP_ISSUE_LABEL], - }; - const createdIssueResp = await githubIssueWrapper.createIssue(newIssue); - const createdIssueData = createdIssueResp.data; - return { - success: true, - url: createdIssueData.html_url, - number: createdIssueData.number, - }; - } catch (err) { - // if error is already exists discard - if (_.chain(err).get('body.errors').countBy({ - code: 'already_exists', - }).get('true') - .isUndefined() - .value()) { - throw helper.convertGitHubError(err, 'Failed to create issue.'); - } - } - } else { - try { - const client = new Gitlab({ - url: config.GITLAB_API_BASE_URL, - oauthToken: userRole.accessToken, - }); - const response = await client.Issues.create(`${repoOwner}/${repoName}`, { - title, - description: issue.comment, - labels: config.OPEN_FOR_PICKUP_ISSUE_LABEL, - }); - return { - success: true, - url: response.web_url, - number: response.iid - }; - } catch (err) { - if (_.get(err, 'error.message') !== 'Label already exists') { - throw helper.convertGitLabError(err, 'Failed to create labels.'); - } - } - } - return { - success: false, - }; -} - -const currentUserSchema = Joi.object().keys({ - handle: Joi.string().required(), - roles: Joi.array().required(), -}); - -create.schema = { - issue: { - projectId: Joi.string().required(), - prize: Joi.number().required(), - title: Joi.string().required(), - comment: Joi.string().required(), - repoUrl: Joi.string().required(), - }, - currentUser: currentUserSchema, -}; - /** * recreate issue * @param {Object} issue the issue detail @@ -210,9 +128,9 @@ create.schema = { */ async function recreate(issue, currentUser) { const dbProject = await _ensureEditPermissionAndGetInfo(issue.projectId, currentUser); - const provider = await helper.getProviderType(dbProject.repoUrls[0]); + const provider = await helper.getProviderType(issue.url); const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); - const results = dbProject.repoUrls[0].split('/'); + const results = issue.url.split('/'); const index = 1; const repoName = results[results.length - index]; const excludePart = 3; @@ -305,6 +223,11 @@ async function recreate(issue, currentUser) { }; } +const currentUserSchema = Joi.object().keys({ + handle: Joi.string().required(), + roles: Joi.array().required(), +}); + recreate.schema = { issue: { projectId: Joi.string().required(), @@ -317,7 +240,6 @@ recreate.schema = { module.exports = { search, - create, recreate, }; diff --git a/src/services/ProjectService.js b/src/services/ProjectService.js index 97918ee..a23ac94 100644 --- a/src/services/ProjectService.js +++ b/src/services/ProjectService.js @@ -70,20 +70,21 @@ const createProjectSchema = { /** * ensures the requested project detail is valid * @param {Object} project the project detail + * @param {String} repoUrl the repo url * @private */ -async function _validateProjectData(project) { +async function _validateProjectData(project, repoUrl) { let existsInDatabase; if (project.id) { - existsInDatabase = await dbHelper.queryOneActiveProjectWithFilter(models.Project, project.repoUrls[0], project.id) + existsInDatabase = await dbHelper.queryOneActiveProjectWithFilter(models.Project, repoUrl, project.id) } else { - existsInDatabase = await dbHelper.queryOneActiveProject(models.Project, project.repoUrls[0]) + existsInDatabase = await dbHelper.queryOneActiveProject(models.Project, repoUrl) } if (existsInDatabase) { throw new errors.ValidationError(`This repo already has a Topcoder-X project associated with it. Copilot: ${existsInDatabase.copilot}, Owner: ${existsInDatabase.owner}`) } - const provider = await helper.getProviderType(project.repoUrl); + const provider = await helper.getProviderType(repoUrl); const userRole = project.copilot ? project.copilot : project.owner; const setting = await userService.getUserSetting(userRole); if (!setting[provider]) { @@ -123,8 +124,10 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { async function create(project, currentUser) { const currentUserTopcoderHandle = currentUser.handle; project.owner = currentUserTopcoderHandle; - project.repoUrls = _.map(project.repoUrl.split(','), repoUrl => repoUrl.trim()); - await _validateProjectData(project); + const repoUrls = _.map(project.repoUrl.split(','), repoUrl => repoUrl.trim()); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax + await _validateProjectData(project, repoUrl); + } /** * Uncomment below code to enable the function of raising event when 'project was created' * @@ -141,7 +144,13 @@ async function create(project, currentUser) { const createdProject = await dbHelper.create(models.Project, project); - for (const repoUrl of project.repoUrls) { // eslint-disable-line no-restricted-syntax + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax + await dbHelper.create(models.Repository, { + id: helper.generateIdentifier(), + projectId: project.id, + url: repoUrl, + archived: project.archived + }) try { await createLabel({projectId: project.id}, currentUser, repoUrl); await createHook({projectId: project.id}, currentUser, repoUrl); @@ -165,7 +174,10 @@ create.schema = createProjectSchema; */ async function update(project, currentUser) { const dbProject = await _ensureEditPermissionAndGetInfo(project.id, currentUser); - await _validateProjectData(project); + const repoUrls = _.map(project.repoUrl.split(','), repoUrl => repoUrl.trim()); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax + await _validateProjectData(project, repoUrl); + } if (dbProject.archived === 'false' && project.archived === true) { // project archived detected. const result = { @@ -189,9 +201,19 @@ async function update(project, currentUser) { dbProject[item[0]] = item[1]; return item; }); - dbProject.repoUrls = _.map(dbProject.repoUrl.split(','), repoUrl => repoUrl.trim()); + const oldRepositories = await dbHelper.queryRepositoriesByProjectId(dbProject.id); + for (const repo of oldRepositories) { // eslint-disable-line + await dbHelper.removeById(models.Repository, repo.id); + } + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax + await dbHelper.create(models.Repository, { + id: helper.generateIdentifier(), + projectId: dbProject.id, + url: repoUrl, + archived: project.archived + }) + } dbProject.updatedAt = new Date(); - return await dbHelper.update(models.Project, dbProject.id, dbProject); } @@ -220,6 +242,9 @@ async function getAll(query, currentUser) { } return project; }); + for (const project of projects) { // eslint-disable-line + project.repoUrls = await dbHelper.populateRepoUrls(project.id); + } return _.orderBy(projects, ['updatedAt', 'title'], ['desc', 'asc']); } @@ -242,6 +267,9 @@ async function getAll(query, currentUser) { } return project; }); + for (const project of projects) { // eslint-disable-line + project.repoUrls = await dbHelper.populateRepoUrls(project.id); + } return _.orderBy(projects, ['updatedAt', 'title'], ['desc', 'asc']); } @@ -336,6 +364,7 @@ createLabel.schema = Joi.object().keys({ */ async function createHook(body, currentUser, repoUrl) { const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); + const dbRepo = await dbHelper.queryRepositoryByProjectIdFilterUrl(dbProject.id, repoUrl); const provider = await helper.getProviderType(repoUrl); const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); const results = repoUrl.split('/'); @@ -343,16 +372,16 @@ async function createHook(body, currentUser, repoUrl) { const repoName = results[results.length - index]; const excludePart = 3; const repoOwner = _(results).slice(excludePart, results.length - 1).join('/'); - const updateExisting = dbProject.registeredWebhookId !== undefined; + const updateExisting = dbRepo.registeredWebhookId !== undefined; if (provider === 'github') { try { const github = new GitHub({token: userRole.accessToken}); const repoWrapper = github.getRepo(repoOwner, repoName); await new Promise((resolve, reject) => { repoWrapper.listHooks(async (err, hooks) => { - if (!err && dbProject.registeredWebhookId && - _.find(hooks, {id: parseInt(dbProject.registeredWebhookId, 10)})) { - await repoWrapper.deleteHook(dbProject.registeredWebhookId); + if (!err && dbRepo.registeredWebhookId && + _.find(hooks, {id: parseInt(dbRepo.registeredWebhookId, 10)})) { + await repoWrapper.deleteHook(dbRepo.registeredWebhookId); } repoWrapper.createHook({ name: 'web', @@ -371,13 +400,13 @@ async function createHook(body, currentUser, repoUrl) { content_type: 'json', secret: dbProject.secretWebhookKey, }, - }, (error, hook) => { + }, async (error, hook) => { if (error) { return reject(error); } if (hook && hook.id) { - dbProject.registeredWebhookId = hook.id.toString(); - update(dbProject, currentUser); + dbRepo.registeredWebhookId = hook.id.toString(); + await dbHelper.update(models.Repository, dbRepo.id, dbRepo); } return resolve(); }); @@ -404,9 +433,9 @@ async function createHook(body, currentUser, repoUrl) { oauthToken: userRole.accessToken, }); const hooks = await client.ProjectHooks.all(`${repoOwner}/${repoName}`); - if (hooks && dbProject.registeredWebhookId && - _.find(hooks, {id: parseInt(dbProject.registeredWebhookId, 10)})) { - await client.ProjectHooks.remove(`${repoOwner}/${repoName}`, dbProject.registeredWebhookId); + if (hooks && dbRepo.registeredWebhookId && + _.find(hooks, {id: parseInt(dbRepo.registeredWebhookId, 10)})) { + await client.ProjectHooks.remove(`${repoOwner}/${repoName}`, dbRepo.registeredWebhookId); } const hook = await client.ProjectHooks.add(`${repoOwner}/${repoName}`, `${config.HOOK_BASE_URL}/webhooks/gitlab`, { @@ -423,8 +452,8 @@ async function createHook(body, currentUser, repoUrl) { } ); if (hook && hook.id) { - dbProject.registeredWebhookId = hook.id.toString(); - await update(dbProject, currentUser); + dbRepo.registeredWebhookId = hook.id.toString(); + await dbHelper.update(models.Repository, dbRepo.id, dbRepo); } } catch (err) { const errMsg = 'Failed to create webhook'; @@ -520,14 +549,18 @@ async function transferOwnerShip(body, currentUser) { throw new errors.ForbiddenError('You can\'t transfer the ownership of this project'); } const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); - const provider = await helper.getProviderType(dbProject.repoUrls[0]); + + const repoUrls = await dbHelper.populateRepoUrls(dbProject.id); const setting = await userService.getUserSetting(body.owner); - if (!setting.gitlab && !setting.github) { - throw new errors.ValidationError(`User ${body.owner} doesn't currently have Topcoder-X access. - Please have them sign in and set up their Gitlab and Github accounts with Topcoder-X before transferring ownership.`); - } else if (!setting[provider]) { - throw new errors.ValidationError(`User ${body.owner} doesn't currently have Topcoder-X access setup for ${provider}. - Please have them sign in and set up their ${provider} accounts with Topcoder-X before transferring ownership.`); + for (const repoUrl of repoUrls) { // eslint-disable-line + const provider = await helper.getProviderType(repoUrl); + if (!setting.gitlab && !setting.github) { + throw new errors.ValidationError(`User ${body.owner} doesn't currently have Topcoder-X access. + Please have them sign in and set up their Gitlab and Github accounts with Topcoder-X before transferring ownership.`); + } else if (!setting[provider]) { + throw new errors.ValidationError(`User ${body.owner} doesn't currently have Topcoder-X access setup for ${provider}. + Please have them sign in and set up their ${provider} accounts with Topcoder-X before transferring ownership.`); + } } return await dbHelper.update(models.Project, dbProject.id, { diff --git a/src/services/UserService.js b/src/services/UserService.js index de88a24..f706844 100644 --- a/src/services/UserService.js +++ b/src/services/UserService.js @@ -156,6 +156,8 @@ const searchSchema = { page: Joi.number().integer().min(1).required(), perPage: Joi.number().integer().min(1).required(), query: Joi.string(), + gitlabLastKey: Joi.string(), + githubLastKey: Joi.string() }).required(), }; @@ -179,30 +181,35 @@ const removeUserMappingSchema = { * @returns {Array} user mappings */ async function search(criteria) { - const githubUserMappings = await dbHelper.scan(GithubUserMapping, {}); - const gitlabUserMappings = await dbHelper.scan(GitlabUserMapping, {}); - const offset = (criteria.page - 1) * criteria.perPage; - - const filteredGithubUserMappings = _(githubUserMappings).slice(offset).take(criteria.perPage) - .filter(userMapping => { - if (!criteria.query) return true; - else { - return _.includes(userMapping.topcoderUsername.toLowerCase(), criteria.query.toLowerCase()) || - _.includes(userMapping.githubUsername.toLowerCase(), criteria.query.toLowerCase()) - } - }) - .value(); - const filteredGitlabUserMappings = _(gitlabUserMappings).slice(offset).take(criteria.perPage) - .filter(userMapping => { - if (!criteria.query) return true; - else { - return _.includes(userMapping.topcoderUsername.toLowerCase(), criteria.query.toLowerCase()) || - _.includes(userMapping.gitlabUsername.toLowerCase(), criteria.query.toLowerCase()) - } - }) - .value(); - - const userMappings = _.concat(filteredGithubUserMappings, filteredGitlabUserMappings); + let githubUserMappings; + let gitlabUserMappings; + + if (criteria.query) { + githubUserMappings = await dbHelper.scanAllWithSearch( + GithubUserMapping, + criteria.perPage / 2, // eslint-disable-line + criteria.githubLastKey ? JSON.parse(criteria.githubLastKey) : undefined, // eslint-disable-line + 'topcoderUsername', + criteria.query.toLowerCase()); + gitlabUserMappings = await dbHelper.scanAllWithSearch( + GitlabUserMapping, + criteria.perPage / 2, // eslint-disable-line + criteria.gitlabLastKey ? JSON.parse(criteria.gitlabLastKey) : undefined, // eslint-disable-line + 'topcoderUsername', + criteria.query.toLowerCase()); + } + else { + githubUserMappings = await dbHelper.scanAll( + GithubUserMapping, + criteria.perPage / 2, // eslint-disable-line + criteria.githubLastKey ? JSON.parse(criteria.githubLastKey) : undefined); // eslint-disable-line + gitlabUserMappings = await dbHelper.scanAll( + GitlabUserMapping, + criteria.perPage / 2, // eslint-disable-line + criteria.gitlabLastKey ? JSON.parse(criteria.gitlabLastKey) : undefined); // eslint-disable-line + } + + const userMappings = _.concat(githubUserMappings, gitlabUserMappings); const orderedUserMappings = _.orderBy(userMappings, criteria.sortBy, criteria.sortDir); const tcUsernames = _.map(orderedUserMappings, 'topcoderUsername'); const uniqueTcUsernames = _.uniq(tcUsernames); @@ -238,7 +245,10 @@ async function search(criteria) { })); const result = { - pages: Math.ceil(githubUserMappings.length / criteria.perPage) || 1, + lastKey : { + githubLastKey: githubUserMappings.lastKey ? JSON.stringify(githubUserMappings.lastKey) : undefined, // eslint-disable-line + gitlabLastKey: gitlabUserMappings.lastKey ? JSON.stringify(gitlabUserMappings.lastKey) : undefined // eslint-disable-line + }, docs, }; return result;