Skip to content

TOPCODER CONNECT PROJECTS API ENHANCEMENTS WINNING SUBMISSION #45

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
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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Microservice to manage CRUD operations for all things Projects.

### Local Development
* We use docker-compose for running dependencies locally. Instructions for Docker compose setup - https://docs.docker.com/compose/install/
* Nodejs 5.10.1 - consider using [nvm](https://github.com/creationix/nvm) or equivalent to manage your node version
* Nodejs 6.9.4 - consider using [nvm](https://github.com/creationix/nvm) or equivalent to manage your node version
* Install [libpg](https://www.npmjs.com/package/pg-native)
* Install node dependencies
`npm install | ./node_modules/.bin/bunyan`
Expand All @@ -25,6 +25,8 @@ npm run -s build
> NODE_ENV=development node -e "require('./dist/models').default.sequelize.sync({force: true}).then((res)=> console.log('Success: ', res)).catch((err)=> console.log('Failed: ', err));"
```

Other simple approach to create tables is to run `npm run sync` from root of project. This additional script is added to make the above task simpler.

#### Redis
Docker compose command will start a local redis instance as well. You should be able to connect to this instance using url `$(docker-machine ip):6379`

Expand All @@ -51,5 +53,8 @@ You may replace 172.17.0.1 with your docker0 IP.

You can paste **swagger.yaml** to [swagger editor](http://editor.swagger.io/) or import **postman.json** to verify endpoints.

#### Deploying without docker
If you don't want to use docker to deploy to localhost. You can simply run `npm run start` from root of project. This should start the server on default port `3000`.

### Deployment
Using awsebcli - http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/eb-cli3-install.html
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"prestart": "npm run -s build",
"seed": "babel-node src/tests/seed.js --presets es2015",
"direct": "babel-node src/mocks/direct.js --presets es2015",
"sync": "node sync.js",
"lint": "./node_modules/.bin/eslint src"
},
"repository": {
Expand Down
1,604 changes: 1,101 additions & 503 deletions postman.json

Large diffs are not rendered by default.

99 changes: 97 additions & 2 deletions src/events/projectMembers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,54 @@ const projectMemberAddedHandler = (logger, msg, channel) => {
})
} else {
logger.info('project not associated with a direct project, skipping')
ack(msg)
channel.ack(msg)
}
})
.catch(err => {
// if the message has been redelivered dont attempt to reprocess it
logger.error('Error retrieving project', err, msg)
channel.nack(msg, false, !msg.fields.redelivered)
})
} else if (newMember.role === PROJECT_MEMBER_ROLE.MANAGER) {
// if manager is added we have to add the manager to direct project
return models.Project.getDirectProjectId(newMember.projectId)
.then(directProjectId => {
if (directProjectId) {
// retrieve system user token
return util.getSystemUserToken(logger)
.then(token => {
const req = {
id: origRequestId,
log: logger,
headers: {
authorization: `Bearer ${token}`
}
}
return directProject.editProjectPermissions(req, directProjectId, {
permissions: [
{
userId: newMember.userId,
permissionType: {
permissionTypeId: 3,
name: 'project_full'
},
studio: false
}
]
})
.then(resp => {
logger.debug('added manager to direct')
// acknowledge
channel.ack(msg)
})
})
.catch(err => {
logger.error('Error caught while adding manager from direct', err)
channel.nack(msg, false, false)
})
} else {
logger.info('project not associated with a direct project, skipping')
channel.ack(msg)
}
})
.catch(err => {
Expand All @@ -63,7 +110,7 @@ const projectMemberRemovedHandler = (logger, msg, channel) => {
const member = JSON.parse(msg.content.toString())

if (member.role === PROJECT_MEMBER_ROLE.COPILOT) {
// Add co-pilot when a co-pilot is added to a project
// Delete co-pilot when a co-pilot is deleted from a project
return models.Project.getDirectProjectId(member.projectId)
.then(directProjectId => {
if (directProjectId) {
Expand Down Expand Up @@ -100,6 +147,54 @@ const projectMemberRemovedHandler = (logger, msg, channel) => {
logger.error('Error retrieving project', err, msg)
channel.nack(msg, false, !msg.fields.redelivered)
})
} else if (member.role === PROJECT_MEMBER_ROLE.MANAGER) {
// when a manager is removed from direct project we have to remove manager from direct
return models.Project.getDirectProjectId(member.projectId)
.then(directProjectId => {
if (directProjectId) {
// retrieve system user token
return util.getSystemUserToken(logger)
.then(token => {
const req = {
id: origRequestId,
log: logger,
headers: {
authorization: `Bearer ${token}`
}
}
return directProject.editProjectPermissions(req, directProjectId, {
permissions: [
{
userId: member.userId,
resourceId: directProjectId,
permissionType: {
permissionTypeId: '',
name: 'project_full'
},
studio: false
}
]
})
.then(resp => {
logger.debug('removed manager from direct')
// acknowledge
channel.ack(msg)
})
})
.catch(err => {
logger.error('Error caught while adding manager from direct', err)
channel.nack(msg, false, false)
})
} else {
logger.info('project not associated with a direct project, skipping')
channel.ack(msg)
}
})
.catch(err => {
// if the message has been redelivered dont attempt to reprocess it
logger.error('Error retrieving project', err, msg)
channel.nack(msg, false, !msg.fields.redelivered)
})
} else {
// nothing to do
channel.ack(msg)
Expand Down
11 changes: 11 additions & 0 deletions src/mocks/direct.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ router.route('/v3/direct/projects/:projectId(\\d+)/copilot')
}
})

router.route('/v3/direct/projects/:projectId(\\d+)/permissions')
.post((req, res)=>{
const projectId = req.params.projectId
app.logger.info({body: req.body, projectId: projectId }, 'add permissions to Project')
if(projects[projectId]) {
res.json()
} else {
res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`))
}
})

app.use(router)

// =======================
Expand Down
1 change: 1 addition & 0 deletions src/models/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ module.exports = function(sequelize, DataTypes) {
},
details: { type: DataTypes.JSON },
challengeEligibility: DataTypes.JSON,
cancelReason: DataTypes.STRING,
deletedAt: { type: DataTypes.DATE, allowNull: true },
createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
Expand Down
22 changes: 22 additions & 0 deletions src/models/projectHistory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict'

module.exports = function(sequelize, DataTypes) {
var ProjectHistory = sequelize.define('ProjectHistory', {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
projectId: { type: DataTypes.BIGINT, allowNull: false },
status: { type: DataTypes.STRING, allowNull: false },
cancelReason: { type: DataTypes.STRING, allowNull: true },
createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
updatedBy: { type: DataTypes.INTEGER, allowNull: false }
}, {
tableName: 'project_history',
paranoid: false,
timestamps: true,
updatedAt: 'updatedAt',
createdAt: 'createdAt',
indexes: []
})

return ProjectHistory
}
2 changes: 1 addition & 1 deletion src/routes/attachments/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const addAttachmentValidations = {
filePath: Joi.string().required(),
s3Bucket: Joi.string().required(),
contentType: Joi.string().required()
})
}).required()
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/routes/projectMembers/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const addMemberValidations = {
userId: Joi.number().required(),
isPrimary: Joi.boolean(),
role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.COPILOT).required()
})
}).required()
}
}

Expand Down
86 changes: 61 additions & 25 deletions src/routes/projectMembers/delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import _ from 'lodash'

import models from '../../models'
import { middleware as tcMiddleware } from 'tc-core-library-js'
import { EVENT } from '../../constants'
import { EVENT, PROJECT_MEMBER_ROLE } from '../../constants'

/**
* API to delete a project member.
Expand All @@ -23,29 +23,65 @@ module.exports = [
return models.ProjectMember.findOne({
where: {id: memberRecordId, projectId: projectId}
})
.then((member) => {
if (!member) {
let err = new Error('Record not found')
err.status = 404
return Promise.reject(err)
}
return member.destroy({logging: console.log})
})
.then(member => {
return member.save()
})
.then(member => {
// fire event
member = member.get({plain:true})
req.app.services.pubsub.publish(
EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED,
member,
{ correlationId: req.id }
)
req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, { req, member })
res.status(204).json({})
})
.catch(err => next(err))
})
.then((member) => {
if (!member) {
let err = new Error('Record not found')
err.status = 404
return Promise.reject(err)
}
return member.destroy({logging: console.log})
})
.then(member => {
return member.save()
})
// if primary co-pilot is removed promote the next co-pilot to primary #43
.then((member) => {
return new Promise((accept, reject) => {
if (member.role === PROJECT_MEMBER_ROLE.COPILOT && member.isPrimary) {
// find the next copilot
models.ProjectMember.findAll({
limit: 1,
// return only non-deleted records
paranoid: true,
where: {
projectId: projectId,
role: PROJECT_MEMBER_ROLE.COPILOT
},
order: [['createdAt', 'ASC']]
}).then((members) => {
if (members && members.length > 0) {
// mark the copilot as primary
const nextMember = members[0]
nextMember.set({ isPrimary: true })
nextMember.save().then(() => {
accept(member)
}).catch((err) => {
reject(err)
})
} else {
// no copilot found nothing to do
accept(member)
}
}).catch((err) => {
reject(err)
})
} else {
// nothing to do
accept(member)
}
})
})
}).then((member) => {
// only return the response after transaction is committed
// fire event
member = member.get({plain:true})
req.app.services.pubsub.publish(
EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED,
member,
{ correlationId: req.id }
)
req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, { req, member })
res.status(204).json({})
}).catch((err) => next(err))
}
]
Loading