Skip to content

Commit 0b66bce

Browse files
author
vikasrohit
authored
Merge pull request #300 from maxceem/feature/db-ednpoints
DB endpoints
2 parents 40628db + ed35589 commit 0b66bce

File tree

9 files changed

+1012
-57
lines changed

9 files changed

+1012
-57
lines changed

postman.json

Lines changed: 389 additions & 2 deletions
Large diffs are not rendered by default.

src/models/phaseProduct.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
module.exports = function definePhaseProduct(sequelize, DataTypes) {
42
const PhaseProduct = sequelize.define('PhaseProduct', {
53
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
@@ -38,6 +36,25 @@ module.exports = function definePhaseProduct(sequelize, DataTypes) {
3836
raw: true,
3937
});
4038
},
39+
/**
40+
* Search Phase Products
41+
* @param {Object} parameters the replacements for sequelize
42+
* - projectId id of the project
43+
* - phaseId id of phase
44+
* @param {Object} log the request log
45+
* @return {Object} the result rows and count
46+
*/
47+
async search(parameters = {}, log) {
48+
const whereQuery = 'phase_products."projectId"= :projectId AND phase_products."phaseId" = :phaseId';
49+
const dbQuery = `SELECT * FROM phase_products WHERE ${whereQuery}`;
50+
return sequelize.query(dbQuery,
51+
{ type: sequelize.QueryTypes.SELECT,
52+
replacements: parameters,
53+
logging: (str) => { log.debug(str); },
54+
raw: true,
55+
})
56+
.then(phases => ({ rows: phases, count: phases.length }));
57+
},
4158
},
4259
});
4360

src/models/projectPhase.js

Lines changed: 20 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
/* eslint-disable valid-jsdoc */
2-
31
import _ from 'lodash';
42

53
module.exports = function defineProjectPhase(sequelize, DataTypes) {
@@ -44,62 +42,32 @@ module.exports = function defineProjectPhase(sequelize, DataTypes) {
4442
ProjectPhase.hasMany(models.PhaseProduct, { as: 'products', foreignKey: 'phaseId' });
4543
},
4644
/**
47-
* Search name or status
48-
* @param parameters the parameters
49-
* - filters: the filters contains keyword
50-
* - order: the order
51-
* - limit: the limit
52-
* - offset: the offset
53-
* - attributes: the attributes to get
54-
* @param log the request log
55-
* @return the result rows and count
45+
* Search project phases
46+
* @param {Object} parameters the parameters
47+
* - sortField: the field that will be references when sorting
48+
* - sortType: ASC or DESC
49+
* - fields: the fields to retrieved
50+
* - projectId: the id of project
51+
* @param {Object} log the request log
52+
* @return {Object} the result rows and count
5653
*/
57-
searchText(parameters, log) {
58-
// special handling for keyword filter
59-
let query = '1=1 ';
60-
if (_.has(parameters.filters, 'id')) {
61-
if (_.isObject(parameters.filters.id)) {
62-
if (parameters.filters.id.$in.length === 0) {
63-
parameters.filters.id.$in.push(-1);
64-
}
65-
query += `AND id IN (${parameters.filters.id.$in}) `;
66-
} else if (_.isString(parameters.filters.id) || _.isNumber(parameters.filters.id)) {
67-
query += `AND id = ${parameters.filters.id} `;
68-
}
69-
}
70-
if (_.has(parameters.filters, 'status')) {
71-
const statusFilter = parameters.filters.status;
72-
if (_.isObject(statusFilter)) {
73-
const statuses = statusFilter.$in.join("','");
74-
query += `AND status IN ('${statuses}') `;
75-
} else if (_.isString(statusFilter)) {
76-
query += `AND status ='${statusFilter}'`;
77-
}
78-
}
79-
if (_.has(parameters.filters, 'name')) {
80-
query += `AND name like '%${parameters.filters.name}%' `;
54+
async search(parameters = {}, log) {
55+
let fieldsStr = _.map(parameters.fields, field => `project_phases."${field}"`);
56+
fieldsStr = `${fieldsStr.join(',')}`;
57+
const replacements = {
58+
projectId: parameters.projectId,
59+
};
60+
let dbQuery = `SELECT ${fieldsStr} FROM project_phases WHERE project_phases."projectId" = :projectId`;
61+
if (_.has(parameters, 'sortField') && _.has(parameters, 'sortType')) {
62+
dbQuery = `${dbQuery} ORDER BY project_phases."${parameters.sortField}" ${parameters.sortType}`;
8163
}
82-
83-
const attributesStr = `"${parameters.attributes.join('","')}"`;
84-
const orderStr = `"${parameters.order[0][0]}" ${parameters.order[0][1]}`;
85-
86-
// select count of project_phases
87-
return sequelize.query(`SELECT COUNT(1) FROM project_phases WHERE ${query}`,
64+
return sequelize.query(dbQuery,
8865
{ type: sequelize.QueryTypes.SELECT,
8966
logging: (str) => { log.debug(str); },
67+
replacements,
9068
raw: true,
9169
})
92-
.then((fcount) => {
93-
const count = fcount[0].count;
94-
// select project attributes
95-
return sequelize.query(`SELECT ${attributesStr} FROM project_phases WHERE ${query} ORDER BY ` +
96-
` ${orderStr} LIMIT ${parameters.limit} OFFSET ${parameters.offset}`,
97-
{ type: sequelize.QueryTypes.SELECT,
98-
logging: (str) => { log.debug(str); },
99-
raw: true,
100-
})
101-
.then(phases => ({ rows: phases, count }));
102-
});
70+
.then(phases => ({ rows: phases, count: phases.length }));
10371
},
10472
},
10573
});

src/routes/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ router.route('/v4/projects/:projectId(\\d+)/phases')
130130
.get(require('./phases/list'))
131131
.post(require('./phases/create'));
132132

133+
router.route('/v4/projects/:projectId(\\d+)/phases/db')
134+
.get(require('./phases/list-db'));
135+
133136
router.route('/v4/projects/:projectId(\\d+)/phases/:phaseId(\\d+)')
134137
.get(require('./phases/get'))
135138
.patch(require('./phases/update'))
@@ -139,6 +142,9 @@ router.route('/v4/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/products')
139142
.get(require('./phaseProducts/list'))
140143
.post(require('./phaseProducts/create'));
141144

145+
router.route('/v4/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/products/db')
146+
.get(require('./phaseProducts/list-db'));
147+
142148
router.route('/v4/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/products/:productId(\\d+)')
143149
.get(require('./phaseProducts/get'))
144150
.patch(require('./phaseProducts/update'))

src/routes/phaseProducts/list-db.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import _ from 'lodash';
2+
import { middleware as tcMiddleware } from 'tc-core-library-js';
3+
import util from '../../util';
4+
import models from '../../models';
5+
6+
const permissions = tcMiddleware.permissions;
7+
8+
module.exports = [
9+
permissions('project.view'),
10+
async (req, res, next) => {
11+
const projectId = _.parseInt(req.params.projectId);
12+
const phaseId = _.parseInt(req.params.phaseId);
13+
14+
// check if the project and phase are exist
15+
try {
16+
const countProject = await models.Project.count({ where: { id: projectId } });
17+
if (countProject === 0) {
18+
const apiErr = new Error(`active project not found for project id ${projectId}`);
19+
apiErr.status = 404;
20+
throw apiErr;
21+
}
22+
23+
const countPhase = await models.ProjectPhase.count({ where: { id: phaseId } });
24+
if (countPhase === 0) {
25+
const apiErr = new Error(`active project phase not found for id ${phaseId}`);
26+
apiErr.status = 404;
27+
throw apiErr;
28+
}
29+
} catch (err) {
30+
return next(err);
31+
}
32+
33+
const parameters = {
34+
projectId,
35+
phaseId,
36+
};
37+
38+
try {
39+
const { rows, count } = await models.PhaseProduct.search(parameters, req.log);
40+
return res.json(util.wrapResponse(req.id, rows, count));
41+
} catch (err) {
42+
return next(err);
43+
}
44+
},
45+
];
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/* eslint-disable no-unused-expressions */
2+
import _ from 'lodash';
3+
import request from 'supertest';
4+
import chai from 'chai';
5+
import server from '../../app';
6+
import models from '../../models';
7+
import testUtil from '../../tests/util';
8+
9+
const should = chai.should();
10+
11+
const body = {
12+
name: 'test phase product',
13+
type: 'product1',
14+
estimatedPrice: 20.0,
15+
actualPrice: 1.23456,
16+
details: {
17+
message: 'This can be any json',
18+
},
19+
createdBy: 1,
20+
updatedBy: 1,
21+
};
22+
23+
describe('Phase Products', () => {
24+
let projectId;
25+
let phaseId;
26+
let project;
27+
const memberUser = {
28+
handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
29+
userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
30+
firstName: 'fname',
31+
lastName: 'lName',
32+
email: 'some@abc.com',
33+
};
34+
const copilotUser = {
35+
handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
36+
userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
37+
firstName: 'fname',
38+
lastName: 'lName',
39+
email: 'some@abc.com',
40+
};
41+
before(function beforeHook(done) {
42+
this.timeout(10000);
43+
// mocks
44+
testUtil.clearDb()
45+
.then(() => {
46+
models.Project.create({
47+
type: 'generic',
48+
billingAccountId: 1,
49+
name: 'test1',
50+
description: 'test project1',
51+
status: 'draft',
52+
details: {},
53+
createdBy: 1,
54+
updatedBy: 1,
55+
lastActivityAt: 1,
56+
lastActivityUserId: '1',
57+
}).then((p) => {
58+
projectId = p.id;
59+
project = p.toJSON();
60+
// create members
61+
models.ProjectMember.bulkCreate([{
62+
id: 1,
63+
userId: copilotUser.userId,
64+
projectId,
65+
role: 'copilot',
66+
isPrimary: false,
67+
createdBy: 1,
68+
updatedBy: 1,
69+
}, {
70+
id: 2,
71+
userId: memberUser.userId,
72+
projectId,
73+
role: 'customer',
74+
isPrimary: true,
75+
createdBy: 1,
76+
updatedBy: 1,
77+
}]).then(() => {
78+
models.ProjectPhase.create({
79+
name: 'test project phase',
80+
status: 'active',
81+
startDate: '2018-05-15T00:00:00Z',
82+
endDate: '2018-05-15T12:00:00Z',
83+
budget: 20.0,
84+
progress: 1.23456,
85+
details: {
86+
message: 'This can be any json',
87+
},
88+
createdBy: 1,
89+
updatedBy: 1,
90+
projectId,
91+
}).then((phase) => {
92+
phaseId = phase.id;
93+
_.assign(body, { phaseId, projectId });
94+
project.lastActivityAt = 1;
95+
project.phases = [phase.toJSON()];
96+
97+
models.PhaseProduct.create(body).then((product) => {
98+
project.phases[0].products = [product.toJSON()];
99+
project.lastActivityAt = 1;
100+
done();
101+
});
102+
});
103+
});
104+
});
105+
});
106+
});
107+
108+
after((done) => {
109+
testUtil.clearDb(done);
110+
});
111+
112+
describe('GET /projects/{id}/phases/{phaseId}/products/db', () => {
113+
it('should return 403 when user have no permission (non team member)', (done) => {
114+
request(server)
115+
.get(`/v4/projects/${projectId}/phases/${phaseId}/products/db`)
116+
.set({
117+
Authorization: `Bearer ${testUtil.jwts.member2}`,
118+
})
119+
.send({ param: body })
120+
.expect('Content-Type', /json/)
121+
.expect(403, done);
122+
});
123+
124+
it('should return 404 when no project with specific projectId', (done) => {
125+
request(server)
126+
.get(`/v4/projects/999/phases/${phaseId}/products/db`)
127+
.set({
128+
Authorization: `Bearer ${testUtil.jwts.manager}`,
129+
})
130+
.send({ param: body })
131+
.expect('Content-Type', /json/)
132+
.expect(404, done);
133+
});
134+
135+
it('should return 404 when no phase with specific phaseId', (done) => {
136+
request(server)
137+
.get(`/v4/projects/${projectId}/phases/99999/products/db`)
138+
.set({
139+
Authorization: `Bearer ${testUtil.jwts.manager}`,
140+
})
141+
.send({ param: body })
142+
.expect('Content-Type', /json/)
143+
.expect(404, done);
144+
});
145+
146+
it('should return 1 phase when user have project permission (customer)', (done) => {
147+
request(server)
148+
.get(`/v4/projects/${projectId}/phases/${phaseId}/products/db`)
149+
.set({
150+
Authorization: `Bearer ${testUtil.jwts.member}`,
151+
})
152+
.send({ param: body })
153+
.expect('Content-Type', /json/)
154+
.expect(200)
155+
.end((err, res) => {
156+
if (err) {
157+
done(err);
158+
} else {
159+
const resJson = res.body.result.content;
160+
should.exist(resJson);
161+
resJson.should.have.lengthOf(1);
162+
done();
163+
}
164+
});
165+
});
166+
167+
it('should return 1 phase when user have project permission (copilot)', (done) => {
168+
request(server)
169+
.get(`/v4/projects/${projectId}/phases/${phaseId}/products/db`)
170+
.set({
171+
Authorization: `Bearer ${testUtil.jwts.copilot}`,
172+
})
173+
.send({ param: body })
174+
.expect('Content-Type', /json/)
175+
.expect(200)
176+
.end((err, res) => {
177+
if (err) {
178+
done(err);
179+
} else {
180+
const resJson = res.body.result.content;
181+
should.exist(resJson);
182+
resJson.should.have.lengthOf(1);
183+
done();
184+
}
185+
});
186+
});
187+
});
188+
});

0 commit comments

Comments
 (0)