Skip to content

Commit 60f3ce0

Browse files
Ritesh Sangwanparthshah
Ritesh Sangwan
authored andcommitted
TOPCODER CONNECT PROJECTS API ENHANCEMENTS WINNING SUBMISSION (#45)
* update readme, update node js version * fix #34 * fix #37, keep project history * move project history update to core API's from event handlers * fix #43 * add unit tests * update direct mock service * update swagger.yaml * add sync script * update postman * update readme * update tests * filter members by copilot role, when promoting copilot
1 parent cf8eeb7 commit 60f3ce0

File tree

19 files changed

+2003
-578
lines changed

19 files changed

+2003
-578
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Microservice to manage CRUD operations for all things Projects.
44

55
### Local Development
66
* We use docker-compose for running dependencies locally. Instructions for Docker compose setup - https://docs.docker.com/compose/install/
7-
* Nodejs 5.10.1 - consider using [nvm](https://github.com/creationix/nvm) or equivalent to manage your node version
7+
* Nodejs 6.9.4 - consider using [nvm](https://github.com/creationix/nvm) or equivalent to manage your node version
88
* Install [libpg](https://www.npmjs.com/package/pg-native)
99
* Install node dependencies
1010
`npm install | ./node_modules/.bin/bunyan`
@@ -25,6 +25,8 @@ npm run -s build
2525
> 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));"
2626
```
2727

28+
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.
29+
2830
#### Redis
2931
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`
3032

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

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

56+
#### Deploying without docker
57+
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`.
58+
5459
### Deployment
5560
Using awsebcli - http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/eb-cli3-install.html

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"prestart": "npm run -s build",
1616
"seed": "babel-node src/tests/seed.js --presets es2015",
1717
"direct": "babel-node src/mocks/direct.js --presets es2015",
18+
"sync": "node sync.js",
1819
"lint": "./node_modules/.bin/eslint src"
1920
},
2021
"repository": {

postman.json

Lines changed: 1101 additions & 503 deletions
Large diffs are not rendered by default.

src/events/projectMembers/index.js

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,54 @@ const projectMemberAddedHandler = (logger, msg, channel) => {
4444
})
4545
} else {
4646
logger.info('project not associated with a direct project, skipping')
47-
ack(msg)
47+
channel.ack(msg)
48+
}
49+
})
50+
.catch(err => {
51+
// if the message has been redelivered dont attempt to reprocess it
52+
logger.error('Error retrieving project', err, msg)
53+
channel.nack(msg, false, !msg.fields.redelivered)
54+
})
55+
} else if (newMember.role === PROJECT_MEMBER_ROLE.MANAGER) {
56+
// if manager is added we have to add the manager to direct project
57+
return models.Project.getDirectProjectId(newMember.projectId)
58+
.then(directProjectId => {
59+
if (directProjectId) {
60+
// retrieve system user token
61+
return util.getSystemUserToken(logger)
62+
.then(token => {
63+
const req = {
64+
id: origRequestId,
65+
log: logger,
66+
headers: {
67+
authorization: `Bearer ${token}`
68+
}
69+
}
70+
return directProject.editProjectPermissions(req, directProjectId, {
71+
permissions: [
72+
{
73+
userId: newMember.userId,
74+
permissionType: {
75+
permissionTypeId: 3,
76+
name: 'project_full'
77+
},
78+
studio: false
79+
}
80+
]
81+
})
82+
.then(resp => {
83+
logger.debug('added manager to direct')
84+
// acknowledge
85+
channel.ack(msg)
86+
})
87+
})
88+
.catch(err => {
89+
logger.error('Error caught while adding manager from direct', err)
90+
channel.nack(msg, false, false)
91+
})
92+
} else {
93+
logger.info('project not associated with a direct project, skipping')
94+
channel.ack(msg)
4895
}
4996
})
5097
.catch(err => {
@@ -63,7 +110,7 @@ const projectMemberRemovedHandler = (logger, msg, channel) => {
63110
const member = JSON.parse(msg.content.toString())
64111

65112
if (member.role === PROJECT_MEMBER_ROLE.COPILOT) {
66-
// Add co-pilot when a co-pilot is added to a project
113+
// Delete co-pilot when a co-pilot is deleted from a project
67114
return models.Project.getDirectProjectId(member.projectId)
68115
.then(directProjectId => {
69116
if (directProjectId) {
@@ -100,6 +147,54 @@ const projectMemberRemovedHandler = (logger, msg, channel) => {
100147
logger.error('Error retrieving project', err, msg)
101148
channel.nack(msg, false, !msg.fields.redelivered)
102149
})
150+
} else if (member.role === PROJECT_MEMBER_ROLE.MANAGER) {
151+
// when a manager is removed from direct project we have to remove manager from direct
152+
return models.Project.getDirectProjectId(member.projectId)
153+
.then(directProjectId => {
154+
if (directProjectId) {
155+
// retrieve system user token
156+
return util.getSystemUserToken(logger)
157+
.then(token => {
158+
const req = {
159+
id: origRequestId,
160+
log: logger,
161+
headers: {
162+
authorization: `Bearer ${token}`
163+
}
164+
}
165+
return directProject.editProjectPermissions(req, directProjectId, {
166+
permissions: [
167+
{
168+
userId: member.userId,
169+
resourceId: directProjectId,
170+
permissionType: {
171+
permissionTypeId: '',
172+
name: 'project_full'
173+
},
174+
studio: false
175+
}
176+
]
177+
})
178+
.then(resp => {
179+
logger.debug('removed manager from direct')
180+
// acknowledge
181+
channel.ack(msg)
182+
})
183+
})
184+
.catch(err => {
185+
logger.error('Error caught while adding manager from direct', err)
186+
channel.nack(msg, false, false)
187+
})
188+
} else {
189+
logger.info('project not associated with a direct project, skipping')
190+
channel.ack(msg)
191+
}
192+
})
193+
.catch(err => {
194+
// if the message has been redelivered dont attempt to reprocess it
195+
logger.error('Error retrieving project', err, msg)
196+
channel.nack(msg, false, !msg.fields.redelivered)
197+
})
103198
} else {
104199
// nothing to do
105200
channel.ack(msg)

src/mocks/direct.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ router.route('/v3/direct/projects/:projectId(\\d+)/copilot')
9696
}
9797
})
9898

99+
router.route('/v3/direct/projects/:projectId(\\d+)/permissions')
100+
.post((req, res)=>{
101+
const projectId = req.params.projectId
102+
app.logger.info({body: req.body, projectId: projectId }, 'add permissions to Project')
103+
if(projects[projectId]) {
104+
res.json()
105+
} else {
106+
res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`))
107+
}
108+
})
109+
99110
app.use(router)
100111

101112
// =======================

src/models/project.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module.exports = function(sequelize, DataTypes) {
3535
},
3636
details: { type: DataTypes.JSON },
3737
challengeEligibility: DataTypes.JSON,
38+
cancelReason: DataTypes.STRING,
3839
deletedAt: { type: DataTypes.DATE, allowNull: true },
3940
createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
4041
updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },

src/models/projectHistory.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict'
2+
3+
module.exports = function(sequelize, DataTypes) {
4+
var ProjectHistory = sequelize.define('ProjectHistory', {
5+
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
6+
projectId: { type: DataTypes.BIGINT, allowNull: false },
7+
status: { type: DataTypes.STRING, allowNull: false },
8+
cancelReason: { type: DataTypes.STRING, allowNull: true },
9+
createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
10+
updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
11+
updatedBy: { type: DataTypes.INTEGER, allowNull: false }
12+
}, {
13+
tableName: 'project_history',
14+
paranoid: false,
15+
timestamps: true,
16+
updatedAt: 'updatedAt',
17+
createdAt: 'createdAt',
18+
indexes: []
19+
})
20+
21+
return ProjectHistory
22+
}

src/routes/attachments/create.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const addAttachmentValidations = {
2525
filePath: Joi.string().required(),
2626
s3Bucket: Joi.string().required(),
2727
contentType: Joi.string().required()
28-
})
28+
}).required()
2929
}
3030
}
3131

src/routes/projectMembers/create.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const addMemberValidations = {
2121
userId: Joi.number().required(),
2222
isPrimary: Joi.boolean(),
2323
role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.COPILOT).required()
24-
})
24+
}).required()
2525
}
2626
}
2727

src/routes/projectMembers/delete.js

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import _ from 'lodash'
44

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

99
/**
1010
* API to delete a project member.
@@ -23,29 +23,65 @@ module.exports = [
2323
return models.ProjectMember.findOne({
2424
where: {id: memberRecordId, projectId: projectId}
2525
})
26-
.then((member) => {
27-
if (!member) {
28-
let err = new Error('Record not found')
29-
err.status = 404
30-
return Promise.reject(err)
31-
}
32-
return member.destroy({logging: console.log})
33-
})
34-
.then(member => {
35-
return member.save()
36-
})
37-
.then(member => {
38-
// fire event
39-
member = member.get({plain:true})
40-
req.app.services.pubsub.publish(
41-
EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED,
42-
member,
43-
{ correlationId: req.id }
44-
)
45-
req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, { req, member })
46-
res.status(204).json({})
47-
})
48-
.catch(err => next(err))
49-
})
26+
.then((member) => {
27+
if (!member) {
28+
let err = new Error('Record not found')
29+
err.status = 404
30+
return Promise.reject(err)
31+
}
32+
return member.destroy({logging: console.log})
33+
})
34+
.then(member => {
35+
return member.save()
36+
})
37+
// if primary co-pilot is removed promote the next co-pilot to primary #43
38+
.then((member) => {
39+
return new Promise((accept, reject) => {
40+
if (member.role === PROJECT_MEMBER_ROLE.COPILOT && member.isPrimary) {
41+
// find the next copilot
42+
models.ProjectMember.findAll({
43+
limit: 1,
44+
// return only non-deleted records
45+
paranoid: true,
46+
where: {
47+
projectId: projectId,
48+
role: PROJECT_MEMBER_ROLE.COPILOT
49+
},
50+
order: [['createdAt', 'ASC']]
51+
}).then((members) => {
52+
if (members && members.length > 0) {
53+
// mark the copilot as primary
54+
const nextMember = members[0]
55+
nextMember.set({ isPrimary: true })
56+
nextMember.save().then(() => {
57+
accept(member)
58+
}).catch((err) => {
59+
reject(err)
60+
})
61+
} else {
62+
// no copilot found nothing to do
63+
accept(member)
64+
}
65+
}).catch((err) => {
66+
reject(err)
67+
})
68+
} else {
69+
// nothing to do
70+
accept(member)
71+
}
72+
})
73+
})
74+
}).then((member) => {
75+
// only return the response after transaction is committed
76+
// fire event
77+
member = member.get({plain:true})
78+
req.app.services.pubsub.publish(
79+
EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED,
80+
member,
81+
{ correlationId: req.id }
82+
)
83+
req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, { req, member })
84+
res.status(204).json({})
85+
}).catch((err) => next(err))
5086
}
5187
]

0 commit comments

Comments
 (0)