Skip to content

DB endpoints #300

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
merged 1 commit into from
May 6, 2019
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
391 changes: 389 additions & 2 deletions postman.json

Large diffs are not rendered by default.

21 changes: 19 additions & 2 deletions src/models/phaseProduct.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


module.exports = function definePhaseProduct(sequelize, DataTypes) {
const PhaseProduct = sequelize.define('PhaseProduct', {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
Expand Down Expand Up @@ -38,6 +36,25 @@ module.exports = function definePhaseProduct(sequelize, DataTypes) {
raw: true,
});
},
/**
* Search Phase Products
* @param {Object} parameters the replacements for sequelize
* - projectId id of the project
* - phaseId id of phase
* @param {Object} log the request log
* @return {Object} the result rows and count
*/
async search(parameters = {}, log) {
const whereQuery = 'phase_products."projectId"= :projectId AND phase_products."phaseId" = :phaseId';
const dbQuery = `SELECT * FROM phase_products WHERE ${whereQuery}`;
return sequelize.query(dbQuery,
{ type: sequelize.QueryTypes.SELECT,
replacements: parameters,
logging: (str) => { log.debug(str); },
raw: true,
})
.then(phases => ({ rows: phases, count: phases.length }));
},
},
});

Expand Down
72 changes: 20 additions & 52 deletions src/models/projectPhase.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable valid-jsdoc */

import _ from 'lodash';

module.exports = function defineProjectPhase(sequelize, DataTypes) {
Expand Down Expand Up @@ -44,62 +42,32 @@ module.exports = function defineProjectPhase(sequelize, DataTypes) {
ProjectPhase.hasMany(models.PhaseProduct, { as: 'products', foreignKey: 'phaseId' });
},
/**
* Search name or status
* @param parameters the parameters
* - filters: the filters contains keyword
* - order: the order
* - limit: the limit
* - offset: the offset
* - attributes: the attributes to get
* @param log the request log
* @return the result rows and count
* Search project phases
* @param {Object} parameters the parameters
* - sortField: the field that will be references when sorting
* - sortType: ASC or DESC
* - fields: the fields to retrieved
* - projectId: the id of project
* @param {Object} log the request log
* @return {Object} the result rows and count
*/
searchText(parameters, log) {
// special handling for keyword filter
let query = '1=1 ';
if (_.has(parameters.filters, 'id')) {
if (_.isObject(parameters.filters.id)) {
if (parameters.filters.id.$in.length === 0) {
parameters.filters.id.$in.push(-1);
}
query += `AND id IN (${parameters.filters.id.$in}) `;
} else if (_.isString(parameters.filters.id) || _.isNumber(parameters.filters.id)) {
query += `AND id = ${parameters.filters.id} `;
}
}
if (_.has(parameters.filters, 'status')) {
const statusFilter = parameters.filters.status;
if (_.isObject(statusFilter)) {
const statuses = statusFilter.$in.join("','");
query += `AND status IN ('${statuses}') `;
} else if (_.isString(statusFilter)) {
query += `AND status ='${statusFilter}'`;
}
}
if (_.has(parameters.filters, 'name')) {
query += `AND name like '%${parameters.filters.name}%' `;
async search(parameters = {}, log) {
let fieldsStr = _.map(parameters.fields, field => `project_phases."${field}"`);
fieldsStr = `${fieldsStr.join(',')}`;
const replacements = {
projectId: parameters.projectId,
};
let dbQuery = `SELECT ${fieldsStr} FROM project_phases WHERE project_phases."projectId" = :projectId`;
if (_.has(parameters, 'sortField') && _.has(parameters, 'sortType')) {
dbQuery = `${dbQuery} ORDER BY project_phases."${parameters.sortField}" ${parameters.sortType}`;
}

const attributesStr = `"${parameters.attributes.join('","')}"`;
const orderStr = `"${parameters.order[0][0]}" ${parameters.order[0][1]}`;

// select count of project_phases
return sequelize.query(`SELECT COUNT(1) FROM project_phases WHERE ${query}`,
return sequelize.query(dbQuery,
{ type: sequelize.QueryTypes.SELECT,
logging: (str) => { log.debug(str); },
replacements,
raw: true,
})
.then((fcount) => {
const count = fcount[0].count;
// select project attributes
return sequelize.query(`SELECT ${attributesStr} FROM project_phases WHERE ${query} ORDER BY ` +
` ${orderStr} LIMIT ${parameters.limit} OFFSET ${parameters.offset}`,
{ type: sequelize.QueryTypes.SELECT,
logging: (str) => { log.debug(str); },
raw: true,
})
.then(phases => ({ rows: phases, count }));
});
.then(phases => ({ rows: phases, count: phases.length }));
},
},
});
Expand Down
6 changes: 6 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ router.route('/v4/projects/:projectId(\\d+)/phases')
.get(require('./phases/list'))
.post(require('./phases/create'));

router.route('/v4/projects/:projectId(\\d+)/phases/db')
.get(require('./phases/list-db'));

router.route('/v4/projects/:projectId(\\d+)/phases/:phaseId(\\d+)')
.get(require('./phases/get'))
.patch(require('./phases/update'))
Expand All @@ -139,6 +142,9 @@ router.route('/v4/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/products')
.get(require('./phaseProducts/list'))
.post(require('./phaseProducts/create'));

router.route('/v4/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/products/db')
.get(require('./phaseProducts/list-db'));

router.route('/v4/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/products/:productId(\\d+)')
.get(require('./phaseProducts/get'))
.patch(require('./phaseProducts/update'))
Expand Down
45 changes: 45 additions & 0 deletions src/routes/phaseProducts/list-db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import _ from 'lodash';
import { middleware as tcMiddleware } from 'tc-core-library-js';
import util from '../../util';
import models from '../../models';

const permissions = tcMiddleware.permissions;

module.exports = [
permissions('project.view'),
async (req, res, next) => {
const projectId = _.parseInt(req.params.projectId);
const phaseId = _.parseInt(req.params.phaseId);

// check if the project and phase are exist
try {
const countProject = await models.Project.count({ where: { id: projectId } });
if (countProject === 0) {
const apiErr = new Error(`active project not found for project id ${projectId}`);
apiErr.status = 404;
throw apiErr;
}

const countPhase = await models.ProjectPhase.count({ where: { id: phaseId } });
if (countPhase === 0) {
const apiErr = new Error(`active project phase not found for id ${phaseId}`);
apiErr.status = 404;
throw apiErr;
}
} catch (err) {
return next(err);
}

const parameters = {
projectId,
phaseId,
};

try {
const { rows, count } = await models.PhaseProduct.search(parameters, req.log);
return res.json(util.wrapResponse(req.id, rows, count));
} catch (err) {
return next(err);
}
},
];
188 changes: 188 additions & 0 deletions src/routes/phaseProducts/list-db.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/* eslint-disable no-unused-expressions */
import _ from 'lodash';
import request from 'supertest';
import chai from 'chai';
import server from '../../app';
import models from '../../models';
import testUtil from '../../tests/util';

const should = chai.should();

const body = {
name: 'test phase product',
type: 'product1',
estimatedPrice: 20.0,
actualPrice: 1.23456,
details: {
message: 'This can be any json',
},
createdBy: 1,
updatedBy: 1,
};

describe('Phase Products', () => {
let projectId;
let phaseId;
let project;
const memberUser = {
handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
firstName: 'fname',
lastName: 'lName',
email: 'some@abc.com',
};
const copilotUser = {
handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
firstName: 'fname',
lastName: 'lName',
email: 'some@abc.com',
};
before(function beforeHook(done) {
this.timeout(10000);
// mocks
testUtil.clearDb()
.then(() => {
models.Project.create({
type: 'generic',
billingAccountId: 1,
name: 'test1',
description: 'test project1',
status: 'draft',
details: {},
createdBy: 1,
updatedBy: 1,
lastActivityAt: 1,
lastActivityUserId: '1',
}).then((p) => {
projectId = p.id;
project = p.toJSON();
// create members
models.ProjectMember.bulkCreate([{
id: 1,
userId: copilotUser.userId,
projectId,
role: 'copilot',
isPrimary: false,
createdBy: 1,
updatedBy: 1,
}, {
id: 2,
userId: memberUser.userId,
projectId,
role: 'customer',
isPrimary: true,
createdBy: 1,
updatedBy: 1,
}]).then(() => {
models.ProjectPhase.create({
name: 'test project phase',
status: 'active',
startDate: '2018-05-15T00:00:00Z',
endDate: '2018-05-15T12:00:00Z',
budget: 20.0,
progress: 1.23456,
details: {
message: 'This can be any json',
},
createdBy: 1,
updatedBy: 1,
projectId,
}).then((phase) => {
phaseId = phase.id;
_.assign(body, { phaseId, projectId });
project.lastActivityAt = 1;
project.phases = [phase.toJSON()];

models.PhaseProduct.create(body).then((product) => {
project.phases[0].products = [product.toJSON()];
project.lastActivityAt = 1;
done();
});
});
});
});
});
});

after((done) => {
testUtil.clearDb(done);
});

describe('GET /projects/{id}/phases/{phaseId}/products/db', () => {
it('should return 403 when user have no permission (non team member)', (done) => {
request(server)
.get(`/v4/projects/${projectId}/phases/${phaseId}/products/db`)
.set({
Authorization: `Bearer ${testUtil.jwts.member2}`,
})
.send({ param: body })
.expect('Content-Type', /json/)
.expect(403, done);
});

it('should return 404 when no project with specific projectId', (done) => {
request(server)
.get(`/v4/projects/999/phases/${phaseId}/products/db`)
.set({
Authorization: `Bearer ${testUtil.jwts.manager}`,
})
.send({ param: body })
.expect('Content-Type', /json/)
.expect(404, done);
});

it('should return 404 when no phase with specific phaseId', (done) => {
request(server)
.get(`/v4/projects/${projectId}/phases/99999/products/db`)
.set({
Authorization: `Bearer ${testUtil.jwts.manager}`,
})
.send({ param: body })
.expect('Content-Type', /json/)
.expect(404, done);
});

it('should return 1 phase when user have project permission (customer)', (done) => {
request(server)
.get(`/v4/projects/${projectId}/phases/${phaseId}/products/db`)
.set({
Authorization: `Bearer ${testUtil.jwts.member}`,
})
.send({ param: body })
.expect('Content-Type', /json/)
.expect(200)
.end((err, res) => {
if (err) {
done(err);
} else {
const resJson = res.body.result.content;
should.exist(resJson);
resJson.should.have.lengthOf(1);
done();
}
});
});

it('should return 1 phase when user have project permission (copilot)', (done) => {
request(server)
.get(`/v4/projects/${projectId}/phases/${phaseId}/products/db`)
.set({
Authorization: `Bearer ${testUtil.jwts.copilot}`,
})
.send({ param: body })
.expect('Content-Type', /json/)
.expect(200)
.end((err, res) => {
if (err) {
done(err);
} else {
const resJson = res.body.result.content;
should.exist(resJson);
resJson.should.have.lengthOf(1);
done();
}
});
});
});
});
Loading