Skip to content

Commit 176cbc3

Browse files
committed
- Update legend model and file structure to be standalone table not belonging to a report
- Legend records include default value that is enforced by unique index and util functions to ensure only 1 record can have True value - Update uploadLegendImage middleware function - Update legend router and routes - Update legend unit tests - Update model associations
1 parent 68a57bd commit 176cbc3

12 files changed

Lines changed: 494 additions & 538 deletions

File tree

app/models/index.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,7 @@ summary.therapeuticTargets = require('./reports/genomic/summary/therapeuticTarge
134134
summary.microbial = require('./reports/genomic/summary/microbial')(sequelize, Sq);
135135

136136
// Pathway Analysis Legends
137-
const pathwayAnalysisLegends = require('./reports/legend')(sequelize, Sq);
138-
139-
pathwayAnalysisLegends.belongsTo(analysisReports, {
140-
as: 'report', foreignKey: 'reportId', targetKey: 'id', onDelete: 'CASCADE', constraints: true,
141-
});
142-
analysisReports.hasMany(pathwayAnalysisLegends, {
143-
as: 'legends', foreignKey: 'reportId', onDelete: 'CASCADE', constraints: true,
144-
});
137+
const pathwayAnalysisLegends = require('./legend/legend')(sequelize, Sq);
145138

146139
summary.pathwayAnalysis.belongsTo(pathwayAnalysisLegends, {
147140
as: 'legend', foreignKey: 'legendId', targetKey: 'id', onDelete: 'SET NULL', constraints: true,

app/models/legend/legend.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const { DEFAULT_COLUMNS } = require('../base');
2+
3+
module.exports = (sequelize, Sq) => {
4+
const legend = sequelize.define(
5+
'legend',
6+
{
7+
...DEFAULT_COLUMNS,
8+
format: {
9+
type: Sq.ENUM('PNG', 'JPG'),
10+
defaultValue: 'PNG',
11+
},
12+
filename: {
13+
type: Sq.TEXT,
14+
allowNull: false,
15+
},
16+
name: {
17+
type: Sq.TEXT,
18+
allowNull: false,
19+
},
20+
data: {
21+
type: Sq.TEXT,
22+
allowNull: false,
23+
},
24+
default: {
25+
type: Sq.BOOLEAN,
26+
defaultValue: false,
27+
},
28+
},
29+
{
30+
tableName: 'pathway_analysis_legends',
31+
indexes: [
32+
{
33+
unique: true,
34+
fields: [],
35+
where: {
36+
default: true,
37+
deleted_at: null,
38+
},
39+
name: 'idx_one_default_legend',
40+
},
41+
],
42+
scopes: {
43+
public: {
44+
attributes: {
45+
exclude: ['id', 'deletedAt', 'updatedBy'],
46+
},
47+
},
48+
},
49+
hooks: {
50+
beforeCreate: async (instance, options) => {
51+
// If setting to true, unset all others
52+
if (instance.default === true) {
53+
await sequelize.models.legend.update(
54+
{default: false},
55+
{
56+
where: {id: {[sequelize.Sequelize.Op.ne]: instance.id}},
57+
transaction: options.transaction,
58+
},
59+
);
60+
}
61+
},
62+
beforeUpdate: async (instance, options) => {
63+
// If setting to true, unset all others
64+
if (instance.changed('default') && instance.default === true) {
65+
await sequelize.models.legend.update(
66+
{default: false},
67+
{
68+
where: {id: {[sequelize.Sequelize.Op.ne]: instance.id}},
69+
transaction: options.transaction,
70+
},
71+
);
72+
}
73+
},
74+
},
75+
},
76+
);
77+
78+
// set instance methods
79+
legend.prototype.view = function (scope) {
80+
if (scope === 'public') {
81+
const {
82+
id, deletedAt, updatedBy, ...publicView
83+
} = this.dataValues;
84+
return publicView;
85+
}
86+
return this;
87+
};
88+
89+
// Ensure at least one default exists
90+
legend.prototype.ensureDefaultExists = async function () {
91+
const hasDefault = await sequelize.models.legend.findOne({
92+
where: {default: true},
93+
});
94+
95+
if (!hasDefault) {
96+
const mostRecent = await sequelize.models.legend.findOne({
97+
order: [['createdAt', 'DESC']],
98+
});
99+
100+
if (mostRecent) {
101+
await mostRecent.update({default: true});
102+
}
103+
}
104+
};
105+
106+
return legend;
107+
};

app/models/reports/legend.js

Lines changed: 0 additions & 80 deletions
This file was deleted.

app/routes/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const notificationRoute = require('./notification');
1919
const variantTextRoute = require('./variantText');
2020
const templateRoute = require('./template');
2121
const appendixRoute = require('./appendix');
22+
const legendRoute = require('./legend');
2223

2324
// Get module route files
2425
const RouterInterface = require('./routingInterface');
@@ -101,6 +102,9 @@ class Routing extends RouterInterface {
101102
// Get appendix routes
102103
this.router.use('/appendix', appendixRoute);
103104

105+
// Global legend routes
106+
this.router.use('/legend', legendRoute);
107+
104108
return true;
105109
}
106110
}

app/routes/legend/index.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
const HTTP_STATUS = require('http-status-codes');
2+
const express = require('express');
3+
4+
const db = require('../../models');
5+
const logger = require('../../log');
6+
const {uploadLegendImage} = require('../report/images');
7+
8+
const router = express.Router({mergeParams: true});
9+
10+
// Middleware for legend lookup
11+
router.param('legend', async (req, res, next, legendIdent) => {
12+
let result;
13+
try {
14+
result = await db.models.legend.findOne({
15+
where: {ident: legendIdent},
16+
});
17+
} catch (error) {
18+
logger.error(`Unable to lookup legend error: ${error}`);
19+
return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({error: {message: 'Unable to lookup legend'}});
20+
}
21+
22+
if (!result) {
23+
logger.error(`Unable to find legend ${legendIdent}`);
24+
return res.status(HTTP_STATUS.NOT_FOUND).json({error: {message: 'Unable to find the requested legend'}});
25+
}
26+
27+
req.legend = result;
28+
return next();
29+
});
30+
31+
router.route('/:legend([A-z0-9-]{36})')
32+
.get((req, res) => {
33+
return res.json(req.legend.view('public'));
34+
})
35+
.put(async (req, res) => {
36+
try {
37+
await req.legend.update(req.body, {userId: req.user.id});
38+
await req.legend.ensureDefaultExists();
39+
return res.json(req.legend.view('public'));
40+
} catch (error) {
41+
logger.error(`Error while updating legend image ${error}`);
42+
return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({error: {message: 'Error while updating legend image'}});
43+
}
44+
})
45+
.delete(async (req, res) => {
46+
// Whether to hard or soft delete legend
47+
const force = (req.query.force === 'true');
48+
49+
// Delete legend image
50+
try {
51+
await req.legend.destroy({force});
52+
await req.legend.ensureDefaultExists();
53+
return res.status(HTTP_STATUS.NO_CONTENT).send();
54+
} catch (error) {
55+
logger.error(`Error while deleting legend image ${error}`);
56+
return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({error: {message: 'Error while deleting legend image'}});
57+
}
58+
});
59+
60+
// Route for adding a legend image
61+
router.route('/')
62+
.get(async (req, res) => {
63+
try {
64+
const legends = await db.models.legend.findAll();
65+
return res.json(legends.map((legend) => legend.view('public')));
66+
} catch (error) {
67+
logger.error(`Error while retrieving legend images ${error}`);
68+
return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({error: {message: 'Error while retrieving legend images'}});
69+
}
70+
})
71+
.post(async (req, res) => {
72+
// Check that image files were uploaded
73+
if (!req.files || Object.keys(req.files).length === 0) {
74+
logger.error('No attached images to upload');
75+
return res.status(HTTP_STATUS.BAD_REQUEST).json({error: {message: 'No attached images to upload'}});
76+
}
77+
78+
try {
79+
const results = await Promise.all(Object.entries(req.files).map(async ([key, image]) => {
80+
try {
81+
// Set options (value or undefined)
82+
const options = {
83+
filename: image.name.trim(),
84+
name: req.body.name || image.name.trim(),
85+
default: req.body.default === undefined ? false : req.body.default,
86+
};
87+
88+
// Load image
89+
const createdLegend = await uploadLegendImage(image.data, options);
90+
await createdLegend.ensureDefaultExists();
91+
// Return that this image was uploaded successfully
92+
return {name: key, upload: 'successful'};
93+
} catch (error) {
94+
return {name: key, upload: 'failed', error};
95+
}
96+
}));
97+
return res.status(HTTP_STATUS.MULTI_STATUS).json(results);
98+
} catch (error) {
99+
logger.error(`Error while uploading images ${error}`);
100+
return res.status(HTTP_STATUS.BAD_REQUEST).json({error: {message: `Error while uploading images ${error}`}});
101+
}
102+
});
103+
104+
module.exports = router;

app/routes/report/images.js

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,33 +56,27 @@ const uploadReportImage = async (reportId, key, image, options = {}) => {
5656
* @param {object} options - An object containing additional image upload options
5757
*
5858
* @property {string} options.filename - An optional filename for the image
59-
* @property {string} options.caption - An optional caption for the image
60-
* @property {string} options.title - An optional title for the image
61-
* @property {string} options.category - An optional category for the image
59+
* @property {string} options.name - An optional name for the legend
60+
* @property {boolean|string} options.default - Whether this legend is the default
6261
* @property {object} options.transaction - An optional transaction to run the create under
6362
*
6463
* @returns {Promise<object>} - Returns the created legend db entry
6564
* @throws {Promise<Error>} - Something goes wrong with image processing and saving entry
6665
*/
67-
const uploadLegendImage = async (reportId, version, image, options = {}) => {
68-
logger.verbose(`Loading legend (${version}) image`);
66+
const uploadLegendImage = async (image, options = {}) => {
67+
logger.verbose('Loading legend image');
6968

7069
const config = {format: DEFAULT_FORMAT, size: IMAGE_SIZE_LIMIT};
7170

7271
try {
7372
const imageData = await processImage(image, config.size, config.format);
7473

7574
return db.models.legend.create({
76-
reportId,
7775
format: config.format,
7876
filename: options.filename,
79-
version,
77+
name: options.name || options.filename,
8078
data: imageData,
81-
caption: options.caption,
82-
title: options.title,
83-
width: config.width,
84-
height: config.height,
85-
category: options.category,
79+
default: options.default === true || options.default === 'true',
8680
}, {transaction: options.transaction});
8781
} catch (error) {
8882
logger.error(`Error processing legend image ${options.filename} ${error}`);

0 commit comments

Comments
 (0)