Files
pp-tcms/backend/routes/cases/import.js

201 lines
6.7 KiB
JavaScript

import path from 'path';
import express from 'express';
const router = express.Router();
import multer from 'multer';
import XLSX from 'xlsx';
import { DataTypes } from 'sequelize';
import defineCase from '../../models/cases.js';
import defineStep from '../../models/steps.js';
import defineCaseStep from '../../models/caseSteps.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
import { priorities, testTypes, automationStatus, templates } from '../../config/enums.js';
const fileFilter = (req, file, cb) => {
const allowedFileTypes = ['.xlsx', '.xls'];
const allowedMimeTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel',
];
const extname = allowedFileTypes.includes(path.extname(file.originalname).toLowerCase());
const mimetype = allowedMimeTypes.includes(file.mimetype);
if (extname && mimetype) {
return cb(null, true);
} else {
cb(new Error('Only Excel files (.xlsx, .xls) are allowed!'));
}
};
const storage = multer.memoryStorage();
const upload = multer({
storage: storage,
fileFilter,
limits: { fileSize: 50 * 1024 * 1024 }, // 50 MB limit
});
export default function (sequelize) {
const Case = defineCase(sequelize, DataTypes);
const Step = defineStep(sequelize, DataTypes);
const CaseStep = defineCaseStep(sequelize, DataTypes);
Case.belongsToMany(Step, { through: CaseStep });
Step.belongsToMany(Case, { through: CaseStep });
const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectDeveloperFromFolderId } = editableMiddleware(sequelize);
router.post(
'/import',
(req, res, next) => {
upload.single('file')(req, res, function (err) {
if (err) {
return res.status(400).json({ error: err.message });
}
next();
});
},
verifySignedIn,
verifyProjectDeveloperFromFolderId,
async (req, res) => {
const { folderId } = req.query;
if (!req.file || !req.file.buffer) {
return res.status(400).json({ error: 'No file uploaded' });
}
if (!folderId) {
return res.status(400).json({ error: 'folderId is required' });
}
const t = await sequelize.transaction();
try {
const workbook = XLSX.read(req.file.buffer, { type: 'buffer' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet);
let currentTitle = null;
let previousTitle = null;
let stepNo = 1;
const casesToCreate = [];
const stepsToCreate = [];
for (const [index, row] of jsonData.entries()) {
const errorMessage = _getRowValidationError(row, index);
if (errorMessage) {
return res.status(400).json({ error: errorMessage });
}
// Add step to the same case if the current row title is equal to previous row title
// This handle cases with multiple steps :)
currentTitle = row['title'].trim();
previousTitle = casesToCreate[casesToCreate.length - 1]?.title.trim();
if (casesToCreate.length > 0 && previousTitle === currentTitle) {
stepNo += 1;
stepsToCreate.push({
caseIndex: casesToCreate.length - 1,
stepNo: stepNo,
step: row['step'] || '',
result: row['expectedStepResult'] || '',
});
} else {
stepNo = 1;
casesToCreate.push({
folderId: folderId,
title: currentTitle,
description: row['description'] || '',
state: 0, // default state
priority: row['priority'] ? priorities.indexOf(row['priority']) : priorities.indexOf('medium'),
type: row['type'] ? testTypes.indexOf(row['type']) : testTypes.indexOf('other'),
preConditions: row['preConditions'],
expectedResults: row['expectedResults'],
automationStatus: row['automationStatus']
? automationStatus.indexOf(row['automationStatus'])
: automationStatus.indexOf('automation-not-required'),
template: row['template'] ? templates.indexOf(row['template']) : templates.indexOf('text'),
});
stepsToCreate.push({
caseIndex: casesToCreate.length - 1,
stepNo: stepNo,
step: row['step'] || '',
result: row['expectedStepResult'] || '',
});
}
}
// 'Manually' create cases, steps and caseStep association.
const createdCases = await Case.bulkCreate(casesToCreate, { transaction: t });
for (const stepData of stepsToCreate) {
const createdCase = createdCases[stepData.caseIndex];
const createdStep = await Step.create(
{
step: stepData.step,
result: stepData.result,
},
{ transaction: t }
);
await CaseStep.create(
{
caseId: createdCase.id,
stepId: createdStep.id,
stepNo: stepData.stepNo,
},
{ transaction: t }
);
}
await t.commit();
res.json(createdCases);
} catch (error) {
await t.rollback();
console.error(error);
}
}
);
return router;
}
function _getRowValidationError(row, index) {
const requiredFields = ['title', 'priority', 'type', 'template'];
const rowNumber = index + 2;
for (const field of requiredFields) {
if (!row[field]) {
return `Row ${rowNumber} is missing required field: ${field}`;
}
}
// Validate priority if provided
if (row['priority']) {
const priorityIndex = priorities.indexOf(row['priority']?.toLowerCase());
if (priorityIndex === -1) {
return `Row ${rowNumber} has invalid priority: ${row['priority']}`;
}
}
// Validate type if provided
if (row['type']) {
const typeIndex = testTypes.indexOf(row['type']?.toLowerCase());
if (typeIndex === -1) {
return `Row ${rowNumber} has invalid type: ${row['type']}`;
}
}
// Validate automationStatus if provided
if (row['automationStatus']) {
const automationStatusIndex = automationStatus.indexOf(row['automationStatus']?.toLowerCase());
if (automationStatusIndex === -1) {
return `Row ${rowNumber} has invalid automationStatus: ${row['automationStatus']}`;
}
}
// Validate template if provided
if (row['template']) {
const templateIndex = templates.indexOf(row['template']?.toLowerCase());
if (templateIndex === -1) {
return `Row ${rowNumber} has invalid template: ${row['template']}`;
}
}
return null;
}