diff --git a/backend/package-lock.json b/backend/package-lock.json index 9c8eee9..5523736 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,7 @@ "sequelize": "^6.37.7", "sequelize-cli": "^6.6.3", "sqlite3": "^5.1.7", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "xmlbuilder2": "^3.1.1" } }, @@ -585,9 +586,10 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1114,9 +1116,10 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -1137,7 +1140,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -1152,6 +1155,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { @@ -2427,9 +2434,10 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/pg-connection-string": { "version": "2.9.1", @@ -3182,9 +3190,10 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -3356,9 +3365,9 @@ } }, "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -3457,6 +3466,18 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xlsx": { + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", + "license": "Apache-2.0", + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xmlbuilder2": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz", @@ -3953,9 +3974,9 @@ } }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4344,9 +4365,9 @@ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" }, "express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -4367,7 +4388,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -5292,9 +5313,9 @@ } }, "path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "pg-connection-string": { "version": "2.9.1", @@ -5807,9 +5828,9 @@ } }, "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "requires": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -5942,9 +5963,9 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==" + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==" }, "vary": { "version": "1.1.2", @@ -6014,6 +6035,10 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "xlsx": { + "version": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==" + }, "xmlbuilder2": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index 0a22155..07f13f7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,7 @@ "sequelize": "^6.37.7", "sequelize-cli": "^6.6.3", "sqlite3": "^5.1.7", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "xmlbuilder2": "^3.1.1" } } diff --git a/backend/routes/cases/import.js b/backend/routes/cases/import.js new file mode 100644 index 0000000..cfd6088 --- /dev/null +++ b/backend/routes/cases/import.js @@ -0,0 +1,145 @@ +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 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 { 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' }); + } + + 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 errorMessage = null; + const casesToCreate = []; + const requiredFields = ['title', 'preConditions', 'expectedResults']; + for (const [index, row] of jsonData.entries()) { + const rowNumber = index + 2; + for (const field of requiredFields) { + if (!row[field]) { + errorMessage = `Row ${rowNumber} is missing required field: ${field}`; + console.log(`Error found for field: ${errorMessage}`); + } + } + + // Validate priority if provided + let priorityIndex = priorities.indexOf('medium'); // default to 'medium' + if (row['priority']) { + priorityIndex = priorities.indexOf(row['priority'].toLowerCase()); + if (priorityIndex === -1) { + errorMessage = `Row ${rowNumber} has invalid priority: ${row['priority']}`; + } + } + + // Validate type if provided + let typeIndex = testTypes.indexOf('other'); // default to 'other' + if (row['type']) { + typeIndex = testTypes.indexOf(row['type'].toLowerCase()); + if (typeIndex === -1) { + errorMessage = `Row ${rowNumber} has invalid type: ${row['type']}`; + } + } + + // Validate automationStatus if provided + let automationStatusIndex = automationStatus.indexOf('automation-not-required'); // default to 'automation-not-required' + if (row['automationStatus']) { + automationStatusIndex = automationStatus.indexOf(row['automationStatus'].toLowerCase()); + if (automationStatusIndex === -1) { + errorMessage = `Row ${rowNumber} has invalid automationStatus: ${row['automationStatus']}`; + } + } + + // Validate template if provided + let templateIndex = templates.indexOf('text'); // default to 'text' + if (row['template']) { + templateIndex = templates.indexOf(row['template'].toLowerCase()); + if (templateIndex === -1) { + errorMessage = `Row ${rowNumber} has invalid template: ${row['template']}`; + } + } + + if (errorMessage) { + return res.status(400).json({ error: errorMessage }); + } + + casesToCreate.push({ + folderId: folderId, + title: row['title'], + description: row['description'] || '', + state: 0, // default state + priority: priorityIndex, + type: typeIndex, + preConditions: row['preConditions'], + expectedResults: row['expectedResults'], + automationStatus: automationStatusIndex, + template: templateIndex, + }); + } + + const createdCases = await Case.bulkCreate(casesToCreate); + res.json(createdCases); + } catch (error) { + console.error(error); + res.status(500).send('Internal Server Error'); + } + } + ); + + return router; +} diff --git a/backend/server.js b/backend/server.js index ea60146..db432b8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -100,6 +100,7 @@ import casesNewRoute from './routes/cases/new.js'; import casesEditRoute from './routes/cases/edit.js'; import casesDeleteRoute from './routes/cases/delete.js'; import casesCloneRoute from './routes/cases/clone.js'; +import casesImportRoute from './routes/cases/import.js'; app.use('/cases', casesDownloadRoute(sequelize)); app.use('/cases', casesMoveRoute(sequelize)); app.use('/cases', casesIndexRoute(sequelize)); @@ -109,6 +110,7 @@ app.use('/cases', casesNewRoute(sequelize)); app.use('/cases', casesEditRoute(sequelize)); app.use('/cases', casesDeleteRoute(sequelize)); app.use('/cases', casesCloneRoute(sequelize)); +app.use('/cases', casesImportRoute(sequelize)); // "/steps" import stepsEditRoute from './routes/steps/edit.js';