Prepojenie na externu DB, projekt-zakazky
This commit is contained in:
@@ -23,6 +23,13 @@ export const env = {
|
||||
UPLOAD_DIR: process.env.UPLOAD_DIR || './uploads',
|
||||
MAX_FILE_SIZE: parseInt(process.env.MAX_FILE_SIZE || '10485760', 10),
|
||||
|
||||
// External Database (zakazky)
|
||||
EXTERNAL_DB_HOST: process.env.EXTERNAL_DB_HOST || '',
|
||||
EXTERNAL_DB_PORT: parseInt(process.env.EXTERNAL_DB_PORT || '5432', 10),
|
||||
EXTERNAL_DB_NAME: process.env.EXTERNAL_DB_NAME || '',
|
||||
EXTERNAL_DB_USER: process.env.EXTERNAL_DB_USER || '',
|
||||
EXTERNAL_DB_PASSWORD: process.env.EXTERNAL_DB_PASSWORD || '',
|
||||
|
||||
// Helpers
|
||||
isDev: process.env.NODE_ENV === 'development',
|
||||
isProd: process.env.NODE_ENV === 'production',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Response } from 'express';
|
||||
import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { movePendingFilesToEntity } from './upload.controller';
|
||||
|
||||
export const getEquipment = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
@@ -112,6 +113,11 @@ export const createEquipment = async (req: AuthRequest, res: Response): Promise<
|
||||
},
|
||||
});
|
||||
|
||||
// Move pending files if tempId provided
|
||||
if (req.body.tempId) {
|
||||
await movePendingFilesToEntity(req.body.tempId, 'equipment', equipment.id);
|
||||
}
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('CREATE', 'Equipment', equipment.id, { name: equipment.name });
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { configService } from '../services/config.service';
|
||||
import { movePendingFilesToEntity } from './upload.controller';
|
||||
|
||||
async function generateRMANumber(): Promise<string> {
|
||||
const today = new Date();
|
||||
@@ -192,6 +193,11 @@ export const createRMA = async (req: AuthRequest, res: Response): Promise<void>
|
||||
},
|
||||
});
|
||||
|
||||
// Move pending files if tempId provided
|
||||
if (req.body.tempId) {
|
||||
await movePendingFilesToEntity(req.body.tempId, 'rma', rma.id);
|
||||
}
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('CREATE', 'RMA', rma.id, { rmaNumber });
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, paginatedResponse, parseQueryInt, getParam, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { configService } from '../services/config.service';
|
||||
import { movePendingFilesToEntity } from './upload.controller';
|
||||
|
||||
export const getTasks = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
@@ -168,6 +169,11 @@ export const createTask = async (req: AuthRequest, res: Response): Promise<void>
|
||||
});
|
||||
}
|
||||
|
||||
// Move pending files if tempId provided
|
||||
if (req.body.tempId) {
|
||||
await movePendingFilesToEntity(req.body.tempId, 'task', task.id);
|
||||
}
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('CREATE', 'Task', task.id, { title: task.title });
|
||||
}
|
||||
|
||||
586
backend/src/controllers/upload.controller.ts
Normal file
586
backend/src/controllers/upload.controller.ts
Normal file
@@ -0,0 +1,586 @@
|
||||
import { Response } from 'express';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import prisma from '../config/database';
|
||||
import { successResponse, errorResponse, getParam } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { getFileUrl, getFilePath } from '../middleware/upload.middleware';
|
||||
import { env } from '../config/env';
|
||||
|
||||
// Pending file info stored in memory (for simplicity - in production use Redis)
|
||||
interface PendingFile {
|
||||
tempFilename: string;
|
||||
originalFilename: string;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
uploadedById: string;
|
||||
uploadedAt: Date;
|
||||
}
|
||||
|
||||
const pendingFiles = new Map<string, PendingFile[]>();
|
||||
|
||||
// Helper to get pending directory
|
||||
const getPendingDir = () => path.join(env.UPLOAD_DIR || 'uploads', 'pending');
|
||||
|
||||
// Pending uploads - for new entities that don't have ID yet
|
||||
export const uploadPendingFiles = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const tempId = getParam(req, 'tempId');
|
||||
const files = req.files as Express.Multer.File[];
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
errorResponse(res, 'Žiadne súbory neboli nahrané.', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get existing pending files for this tempId
|
||||
const existing = pendingFiles.get(tempId) || [];
|
||||
|
||||
// Add new files to pending
|
||||
const newPendingFiles: PendingFile[] = files.map((file) => ({
|
||||
tempFilename: file.filename,
|
||||
originalFilename: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
uploadedById: req.user!.userId,
|
||||
uploadedAt: new Date(),
|
||||
}));
|
||||
|
||||
pendingFiles.set(tempId, [...existing, ...newPendingFiles]);
|
||||
|
||||
// Return file info (without DB id since they're not saved yet)
|
||||
const response = newPendingFiles.map((f, index) => ({
|
||||
id: `pending-${tempId}-${Date.now()}-${index}`,
|
||||
filename: f.originalFilename,
|
||||
filepath: `/uploads/pending/${f.tempFilename}`,
|
||||
mimetype: f.mimetype,
|
||||
size: f.size,
|
||||
uploadedAt: f.uploadedAt.toISOString(),
|
||||
isPending: true,
|
||||
}));
|
||||
|
||||
successResponse(res, response, 'Súbory boli nahrané.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error uploading pending files:', error);
|
||||
errorResponse(res, 'Chyba pri nahrávaní súborov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPendingFiles = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const tempId = getParam(req, 'tempId');
|
||||
const files = pendingFiles.get(tempId) || [];
|
||||
|
||||
const response = files.map((f, index) => ({
|
||||
id: `pending-${tempId}-${index}`,
|
||||
filename: f.originalFilename,
|
||||
filepath: `/uploads/pending/${f.tempFilename}`,
|
||||
mimetype: f.mimetype,
|
||||
size: f.size,
|
||||
uploadedAt: f.uploadedAt.toISOString(),
|
||||
isPending: true,
|
||||
}));
|
||||
|
||||
successResponse(res, response);
|
||||
} catch (error) {
|
||||
console.error('Error getting pending files:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní súborov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePendingFile = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const tempId = getParam(req, 'tempId');
|
||||
const filename = getParam(req, 'filename');
|
||||
|
||||
const files = pendingFiles.get(tempId) || [];
|
||||
const fileIndex = files.findIndex((f) => f.tempFilename === filename || f.originalFilename === filename);
|
||||
|
||||
if (fileIndex === -1) {
|
||||
errorResponse(res, 'Súbor nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files[fileIndex];
|
||||
|
||||
// Delete physical file
|
||||
const filePath = path.join(getPendingDir(), file.tempFilename);
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch {
|
||||
console.warn(`Pending file not found: ${filePath}`);
|
||||
}
|
||||
|
||||
// Remove from pending list
|
||||
files.splice(fileIndex, 1);
|
||||
if (files.length === 0) {
|
||||
pendingFiles.delete(tempId);
|
||||
} else {
|
||||
pendingFiles.set(tempId, files);
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Súbor bol vymazaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting pending file:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní súboru.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to move pending files to entity
|
||||
export const movePendingFilesToEntity = async (
|
||||
tempId: string,
|
||||
entityType: 'equipment' | 'rma' | 'task',
|
||||
entityId: string
|
||||
): Promise<void> => {
|
||||
const files = pendingFiles.get(tempId);
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const targetDir = path.join(env.UPLOAD_DIR || 'uploads', entityType);
|
||||
|
||||
// Ensure target directory exists
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(getPendingDir(), file.tempFilename);
|
||||
const targetPath = path.join(targetDir, file.tempFilename);
|
||||
|
||||
// Move file
|
||||
if (fs.existsSync(sourcePath)) {
|
||||
fs.renameSync(sourcePath, targetPath);
|
||||
|
||||
// Create DB record
|
||||
if (entityType === 'equipment') {
|
||||
await prisma.equipmentAttachment.create({
|
||||
data: {
|
||||
equipmentId: entityId,
|
||||
filename: file.originalFilename,
|
||||
filepath: getFileUrl('equipment', file.tempFilename),
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
uploadedById: file.uploadedById,
|
||||
},
|
||||
});
|
||||
} else if (entityType === 'rma') {
|
||||
await prisma.rMAAttachment.create({
|
||||
data: {
|
||||
rmaId: entityId,
|
||||
filename: file.originalFilename,
|
||||
filepath: getFileUrl('rma', file.tempFilename),
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
uploadedById: file.uploadedById,
|
||||
},
|
||||
});
|
||||
} else if (entityType === 'task') {
|
||||
await prisma.taskAttachment.create({
|
||||
data: {
|
||||
taskId: entityId,
|
||||
filename: file.originalFilename,
|
||||
filepath: getFileUrl('task', file.tempFilename),
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
uploadedById: file.uploadedById,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear pending files
|
||||
pendingFiles.delete(tempId);
|
||||
};
|
||||
|
||||
// Equipment Attachments
|
||||
export const uploadEquipmentFiles = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const equipmentId = getParam(req, 'id');
|
||||
const files = req.files as Express.Multer.File[];
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
errorResponse(res, 'Žiadne súbory neboli nahrané.', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skontrolovať, či equipment existuje
|
||||
const equipment = await prisma.equipment.findUnique({
|
||||
where: { id: equipmentId },
|
||||
});
|
||||
|
||||
if (!equipment) {
|
||||
// Vymazať nahrané súbory
|
||||
files.forEach((file) => {
|
||||
fs.unlink(file.path, () => {});
|
||||
});
|
||||
errorResponse(res, 'Zariadenie nebolo nájdené.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Uložiť záznamy do databázy
|
||||
const attachments = await Promise.all(
|
||||
files.map((file) =>
|
||||
prisma.equipmentAttachment.create({
|
||||
data: {
|
||||
equipmentId,
|
||||
filename: file.originalname,
|
||||
filepath: getFileUrl('equipment', file.filename),
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
uploadedById: req.user!.userId,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPLOAD', 'Equipment', equipmentId, {
|
||||
files: attachments.map((a) => a.filename),
|
||||
});
|
||||
}
|
||||
|
||||
successResponse(res, attachments, 'Súbory boli nahrané.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error uploading equipment files:', error);
|
||||
errorResponse(res, 'Chyba pri nahrávaní súborov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getEquipmentFiles = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const equipmentId = getParam(req, 'id');
|
||||
|
||||
const attachments = await prisma.equipmentAttachment.findMany({
|
||||
where: { equipmentId },
|
||||
orderBy: { uploadedAt: 'desc' },
|
||||
include: {
|
||||
uploadedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
successResponse(res, attachments);
|
||||
} catch (error) {
|
||||
console.error('Error fetching equipment files:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní súborov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteEquipmentFile = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const equipmentId = getParam(req, 'id');
|
||||
const fileId = getParam(req, 'fileId');
|
||||
|
||||
const attachment = await prisma.equipmentAttachment.findFirst({
|
||||
where: { id: fileId, equipmentId },
|
||||
});
|
||||
|
||||
if (!attachment) {
|
||||
errorResponse(res, 'Súbor nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vymazať fyzický súbor
|
||||
const filename = path.basename(attachment.filepath);
|
||||
const filePath = getFilePath('equipment', filename);
|
||||
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch {
|
||||
// Súbor možno už neexistuje
|
||||
console.warn(`File not found: ${filePath}`);
|
||||
}
|
||||
|
||||
// Vymazať záznam z databázy
|
||||
await prisma.equipmentAttachment.delete({
|
||||
where: { id: fileId },
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('DELETE_FILE', 'Equipment', equipmentId, {
|
||||
filename: attachment.filename,
|
||||
});
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Súbor bol vymazaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting equipment file:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní súboru.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// RMA Attachments
|
||||
export const uploadRMAFiles = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const rmaId = getParam(req, 'id');
|
||||
const files = req.files as Express.Multer.File[];
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
errorResponse(res, 'Žiadne súbory neboli nahrané.', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skontrolovať, či RMA existuje
|
||||
const rma = await prisma.rMA.findUnique({
|
||||
where: { id: rmaId },
|
||||
});
|
||||
|
||||
if (!rma) {
|
||||
// Vymazať nahrané súbory
|
||||
files.forEach((file) => {
|
||||
fs.unlink(file.path, () => {});
|
||||
});
|
||||
errorResponse(res, 'RMA nebola nájdená.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Uložiť záznamy do databázy
|
||||
const attachments = await Promise.all(
|
||||
files.map((file) =>
|
||||
prisma.rMAAttachment.create({
|
||||
data: {
|
||||
rmaId,
|
||||
filename: file.originalname,
|
||||
filepath: getFileUrl('rma', file.filename),
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
uploadedById: req.user!.userId,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPLOAD', 'RMA', rmaId, {
|
||||
files: attachments.map((a) => a.filename),
|
||||
});
|
||||
}
|
||||
|
||||
successResponse(res, attachments, 'Súbory boli nahrané.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error uploading RMA files:', error);
|
||||
errorResponse(res, 'Chyba pri nahrávaní súborov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getRMAFiles = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const rmaId = getParam(req, 'id');
|
||||
|
||||
const attachments = await prisma.rMAAttachment.findMany({
|
||||
where: { rmaId },
|
||||
orderBy: { uploadedAt: 'desc' },
|
||||
include: {
|
||||
uploadedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
successResponse(res, attachments);
|
||||
} catch (error) {
|
||||
console.error('Error fetching RMA files:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní súborov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteRMAFile = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const rmaId = getParam(req, 'id');
|
||||
const fileId = getParam(req, 'fileId');
|
||||
|
||||
const attachment = await prisma.rMAAttachment.findFirst({
|
||||
where: { id: fileId, rmaId },
|
||||
});
|
||||
|
||||
if (!attachment) {
|
||||
errorResponse(res, 'Súbor nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vymazať fyzický súbor
|
||||
const filename = path.basename(attachment.filepath);
|
||||
const filePath = getFilePath('rma', filename);
|
||||
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch {
|
||||
// Súbor možno už neexistuje
|
||||
console.warn(`File not found: ${filePath}`);
|
||||
}
|
||||
|
||||
// Vymazať záznam z databázy
|
||||
await prisma.rMAAttachment.delete({
|
||||
where: { id: fileId },
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('DELETE_FILE', 'RMA', rmaId, {
|
||||
filename: attachment.filename,
|
||||
});
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Súbor bol vymazaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting RMA file:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní súboru.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// Task Attachments
|
||||
export const uploadTaskFiles = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const taskId = getParam(req, 'id');
|
||||
const files = req.files as Express.Multer.File[];
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
errorResponse(res, 'Žiadne súbory neboli nahrané.', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skontrolovať, či task existuje
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id: taskId },
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
// Vymazať nahrané súbory
|
||||
files.forEach((file) => {
|
||||
fs.unlink(file.path, () => {});
|
||||
});
|
||||
errorResponse(res, 'Úloha nebola nájdená.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Uložiť záznamy do databázy
|
||||
const attachments = await Promise.all(
|
||||
files.map((file) =>
|
||||
prisma.taskAttachment.create({
|
||||
data: {
|
||||
taskId,
|
||||
filename: file.originalname,
|
||||
filepath: getFileUrl('task', file.filename),
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
uploadedById: req.user!.userId,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('UPLOAD', 'Task', taskId, {
|
||||
files: attachments.map((a) => a.filename),
|
||||
});
|
||||
}
|
||||
|
||||
successResponse(res, attachments, 'Súbory boli nahrané.', 201);
|
||||
} catch (error) {
|
||||
console.error('Error uploading task files:', error);
|
||||
errorResponse(res, 'Chyba pri nahrávaní súborov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTaskFiles = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const taskId = getParam(req, 'id');
|
||||
|
||||
const attachments = await prisma.taskAttachment.findMany({
|
||||
where: { taskId },
|
||||
orderBy: { uploadedAt: 'desc' },
|
||||
include: {
|
||||
uploadedBy: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
successResponse(res, attachments);
|
||||
} catch (error) {
|
||||
console.error('Error fetching task files:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní súborov.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTaskFile = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const taskId = getParam(req, 'id');
|
||||
const fileId = getParam(req, 'fileId');
|
||||
|
||||
const attachment = await prisma.taskAttachment.findFirst({
|
||||
where: { id: fileId, taskId },
|
||||
});
|
||||
|
||||
if (!attachment) {
|
||||
errorResponse(res, 'Súbor nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vymazať fyzický súbor
|
||||
const filename = path.basename(attachment.filepath);
|
||||
const filePath = getFilePath('task', filename);
|
||||
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch {
|
||||
// Súbor možno už neexistuje
|
||||
console.warn(`File not found: ${filePath}`);
|
||||
}
|
||||
|
||||
// Vymazať záznam z databázy
|
||||
await prisma.taskAttachment.delete({
|
||||
where: { id: fileId },
|
||||
});
|
||||
|
||||
if (req.logActivity) {
|
||||
await req.logActivity('DELETE_FILE', 'Task', taskId, {
|
||||
filename: attachment.filename,
|
||||
});
|
||||
}
|
||||
|
||||
successResponse(res, null, 'Súbor bol vymazaný.');
|
||||
} catch (error) {
|
||||
console.error('Error deleting task file:', error);
|
||||
errorResponse(res, 'Chyba pri mazaní súboru.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// Download file (universal)
|
||||
export const downloadFile = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
const fileId = getParam(req, 'fileId');
|
||||
const entityType = getParam(req, 'entityType'); // 'equipment', 'rma', or 'task'
|
||||
|
||||
let attachment: { filepath: string; filename: string } | null = null;
|
||||
|
||||
if (entityType === 'equipment') {
|
||||
attachment = await prisma.equipmentAttachment.findUnique({
|
||||
where: { id: fileId },
|
||||
select: { filepath: true, filename: true },
|
||||
});
|
||||
} else if (entityType === 'rma') {
|
||||
attachment = await prisma.rMAAttachment.findUnique({
|
||||
where: { id: fileId },
|
||||
select: { filepath: true, filename: true },
|
||||
});
|
||||
} else if (entityType === 'task') {
|
||||
attachment = await prisma.taskAttachment.findUnique({
|
||||
where: { id: fileId },
|
||||
select: { filepath: true, filename: true },
|
||||
});
|
||||
}
|
||||
|
||||
if (!attachment) {
|
||||
errorResponse(res, 'Súbor nebol nájdený.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = path.basename(attachment.filepath);
|
||||
const filePath = getFilePath(entityType, filename);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
errorResponse(res, 'Súbor nebol nájdený na serveri.', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
res.download(filePath, attachment.filename);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
errorResponse(res, 'Chyba pri sťahovaní súboru.', 500);
|
||||
}
|
||||
};
|
||||
48
backend/src/controllers/zakazky.controller.ts
Normal file
48
backend/src/controllers/zakazky.controller.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Response } from 'express';
|
||||
import { successResponse, errorResponse, parseQueryInt, getQueryString } from '../utils/helpers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { externalDbService } from '../services/externalDb.service';
|
||||
|
||||
// Check if external DB is configured
|
||||
export const checkConfiguration = async (_req: AuthRequest, res: Response): Promise<void> => {
|
||||
const isConfigured = externalDbService.isConfigured();
|
||||
successResponse(res, { configured: isConfigured });
|
||||
};
|
||||
|
||||
// Get zakazky by year
|
||||
export const getZakazky = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!externalDbService.isConfigured()) {
|
||||
errorResponse(res, 'Externá databáza nie je nakonfigurovaná.', 503);
|
||||
return;
|
||||
}
|
||||
|
||||
const rok = parseQueryInt(req.query.rok, new Date().getFullYear());
|
||||
const search = getQueryString(req, 'search');
|
||||
|
||||
let zakazky;
|
||||
|
||||
if (search) {
|
||||
zakazky = await externalDbService.searchZakazky(rok, search);
|
||||
} else {
|
||||
zakazky = await externalDbService.getZakazkyByYear(rok);
|
||||
}
|
||||
|
||||
successResponse(res, zakazky);
|
||||
} catch (error) {
|
||||
console.error('Error fetching zakazky:', error);
|
||||
errorResponse(res, 'Chyba pri načítaní zákaziek z externej databázy.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// Get available years (current year and 5 years back)
|
||||
export const getAvailableYears = async (_req: AuthRequest, res: Response): Promise<void> => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const years = [];
|
||||
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
years.push(currentYear - i);
|
||||
}
|
||||
|
||||
successResponse(res, years);
|
||||
};
|
||||
@@ -2,15 +2,40 @@ import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import morgan from 'morgan';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { env } from './config/env';
|
||||
import routes from './routes';
|
||||
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
|
||||
import prisma from './config/database';
|
||||
|
||||
// Ensure upload directories exist
|
||||
const ensureUploadDirectories = () => {
|
||||
const uploadDir = env.UPLOAD_DIR || 'uploads';
|
||||
const directories = [
|
||||
uploadDir,
|
||||
path.join(uploadDir, 'equipment'),
|
||||
path.join(uploadDir, 'rma'),
|
||||
path.join(uploadDir, 'task'),
|
||||
path.join(uploadDir, 'pending'),
|
||||
];
|
||||
|
||||
directories.forEach((dir) => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
console.log(`Created directory: ${dir}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ensureUploadDirectories();
|
||||
|
||||
const app = express();
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(helmet({
|
||||
crossOriginResourcePolicy: { policy: 'cross-origin' }, // Pre static files z iného originu
|
||||
}));
|
||||
app.use(cors({
|
||||
origin: env.FRONTEND_URL,
|
||||
credentials: true,
|
||||
@@ -20,6 +45,10 @@ app.use(cors({
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Static files - uploads
|
||||
const uploadsPath = path.resolve(env.UPLOAD_DIR);
|
||||
app.use('/uploads', express.static(uploadsPath));
|
||||
|
||||
// Logging
|
||||
if (env.isDev) {
|
||||
app.use(morgan('dev'));
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AuthRequest } from './auth.middleware';
|
||||
import prisma from '../config/database';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
type ActionType = 'CREATE' | 'UPDATE' | 'DELETE' | 'STATUS_CHANGE' | 'LOGIN' | 'LOGOUT';
|
||||
type ActionType = 'CREATE' | 'UPDATE' | 'DELETE' | 'STATUS_CHANGE' | 'LOGIN' | 'LOGOUT' | 'UPLOAD' | 'DELETE_FILE';
|
||||
type EntityType = 'User' | 'Project' | 'Task' | 'Customer' | 'Equipment' | 'Revision' | 'RMA';
|
||||
|
||||
export const logActivity = async (
|
||||
|
||||
164
backend/src/middleware/upload.middleware.ts
Normal file
164
backend/src/middleware/upload.middleware.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import { Request } from 'express';
|
||||
import { env } from '../config/env';
|
||||
|
||||
// Povolené MIME typy
|
||||
const ALLOWED_MIME_TYPES = [
|
||||
// Obrázky
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
// Dokumenty
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
];
|
||||
|
||||
// Povolené prípony
|
||||
const ALLOWED_EXTENSIONS = [
|
||||
'.jpg', '.jpeg', '.png', '.gif', '.webp',
|
||||
'.pdf', '.doc', '.docx', '.xls', '.xlsx',
|
||||
'.txt', '.csv',
|
||||
];
|
||||
|
||||
// Maximálna veľkosť súboru (default 10MB)
|
||||
const MAX_FILE_SIZE = parseInt(process.env.MAX_FILE_SIZE || '10485760', 10);
|
||||
|
||||
// Storage konfigurácia
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req: Request, _file, cb) => {
|
||||
// Určenie cieľového priečinka podľa entity type
|
||||
const entityType = req.params.entityType || req.body.entityType || 'general';
|
||||
const uploadPath = path.join(env.UPLOAD_DIR || 'uploads', entityType);
|
||||
cb(null, uploadPath);
|
||||
},
|
||||
filename: (_req, file, cb) => {
|
||||
// Generovanie unikátneho názvu: timestamp-random.extension
|
||||
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
cb(null, `${uniqueSuffix}${ext}`);
|
||||
},
|
||||
});
|
||||
|
||||
// File filter
|
||||
const fileFilter = (
|
||||
_req: Request,
|
||||
file: Express.Multer.File,
|
||||
cb: multer.FileFilterCallback
|
||||
) => {
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
|
||||
// Kontrola MIME typu
|
||||
if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) {
|
||||
cb(new Error(`Nepodporovaný typ súboru: ${file.mimetype}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Kontrola prípony
|
||||
if (!ALLOWED_EXTENSIONS.includes(ext)) {
|
||||
cb(new Error(`Nepodporovaná prípona súboru: ${ext}`));
|
||||
return;
|
||||
}
|
||||
|
||||
cb(null, true);
|
||||
};
|
||||
|
||||
// Základná multer inštancia
|
||||
export const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: MAX_FILE_SIZE,
|
||||
files: 10, // Max počet súborov naraz
|
||||
},
|
||||
});
|
||||
|
||||
// Pre-configured uploaders pre rôzne entity
|
||||
export const equipmentUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (_req, _file, cb) => {
|
||||
cb(null, path.join(env.UPLOAD_DIR || 'uploads', 'equipment'));
|
||||
},
|
||||
filename: (_req, file, cb) => {
|
||||
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
cb(null, `${uniqueSuffix}${ext}`);
|
||||
},
|
||||
}),
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: MAX_FILE_SIZE,
|
||||
files: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export const rmaUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (_req, _file, cb) => {
|
||||
cb(null, path.join(env.UPLOAD_DIR || 'uploads', 'rma'));
|
||||
},
|
||||
filename: (_req, file, cb) => {
|
||||
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
cb(null, `${uniqueSuffix}${ext}`);
|
||||
},
|
||||
}),
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: MAX_FILE_SIZE,
|
||||
files: 10,
|
||||
},
|
||||
});
|
||||
|
||||
// Pending upload - for files before entity is created
|
||||
export const pendingUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (_req, _file, cb) => {
|
||||
cb(null, path.join(env.UPLOAD_DIR || 'uploads', 'pending'));
|
||||
},
|
||||
filename: (_req, file, cb) => {
|
||||
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
cb(null, `${uniqueSuffix}${ext}`);
|
||||
},
|
||||
}),
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: MAX_FILE_SIZE,
|
||||
files: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export const taskUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (_req, _file, cb) => {
|
||||
cb(null, path.join(env.UPLOAD_DIR || 'uploads', 'task'));
|
||||
},
|
||||
filename: (_req, file, cb) => {
|
||||
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
cb(null, `${uniqueSuffix}${ext}`);
|
||||
},
|
||||
}),
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: MAX_FILE_SIZE,
|
||||
files: 10,
|
||||
},
|
||||
});
|
||||
|
||||
// Helper pre získanie URL súboru
|
||||
export const getFileUrl = (entityType: string, filename: string): string => {
|
||||
return `/uploads/${entityType}/${filename}`;
|
||||
};
|
||||
|
||||
// Helper pre získanie cesty súboru
|
||||
export const getFilePath = (entityType: string, filename: string): string => {
|
||||
return path.join(env.UPLOAD_DIR || 'uploads', entityType, filename);
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import * as equipmentController from '../controllers/equipment.controller';
|
||||
import * as uploadController from '../controllers/upload.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware';
|
||||
import { activityLogger } from '../middleware/activityLog.middleware';
|
||||
import { validate } from '../middleware/validate.middleware';
|
||||
import { equipmentSchema } from '../utils/validators';
|
||||
import { equipmentUpload } from '../middleware/upload.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -22,4 +24,9 @@ router.delete('/:id', canDelete('equipment'), equipmentController.deleteEquipmen
|
||||
router.get('/:id/revisions', canRead('equipment'), equipmentController.getEquipmentRevisions);
|
||||
router.post('/:id/revisions', canCreate('equipment'), equipmentController.createEquipmentRevision);
|
||||
|
||||
// File attachments
|
||||
router.get('/:id/files', canRead('equipment'), uploadController.getEquipmentFiles);
|
||||
router.post('/:id/files', canCreate('equipment'), equipmentUpload.array('files', 10), uploadController.uploadEquipmentFiles);
|
||||
router.delete('/:id/files/:fileId', canDelete('equipment'), uploadController.deleteEquipmentFile);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -8,6 +8,8 @@ import equipmentRoutes from './equipment.routes';
|
||||
import rmaRoutes from './rma.routes';
|
||||
import settingsRoutes from './settings.routes';
|
||||
import dashboardRoutes from './dashboard.routes';
|
||||
import uploadRoutes from './upload.routes';
|
||||
import zakazkyRoutes from './zakazky.routes';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -20,5 +22,7 @@ router.use('/equipment', equipmentRoutes);
|
||||
router.use('/rma', rmaRoutes);
|
||||
router.use('/settings', settingsRoutes);
|
||||
router.use('/dashboard', dashboardRoutes);
|
||||
router.use('/files', uploadRoutes);
|
||||
router.use('/zakazky', zakazkyRoutes);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import * as rmaController from '../controllers/rma.controller';
|
||||
import * as uploadController from '../controllers/upload.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { canRead, canCreate, canUpdate, canDelete, isAdmin } from '../middleware/rbac.middleware';
|
||||
import { activityLogger } from '../middleware/activityLog.middleware';
|
||||
import { validate } from '../middleware/validate.middleware';
|
||||
import { rmaSchema } from '../utils/validators';
|
||||
import { rmaUpload } from '../middleware/upload.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -27,4 +29,9 @@ router.patch('/:id/approve', isAdmin, rmaController.approveRMA);
|
||||
// Comments
|
||||
router.post('/:id/comments', canRead('rma'), rmaController.addRMAComment);
|
||||
|
||||
// File attachments
|
||||
router.get('/:id/files', canRead('rma'), uploadController.getRMAFiles);
|
||||
router.post('/:id/files', canCreate('rma'), rmaUpload.array('files', 10), uploadController.uploadRMAFiles);
|
||||
router.delete('/:id/files/:fileId', canDelete('rma'), uploadController.deleteRMAFile);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import * as tasksController from '../controllers/tasks.controller';
|
||||
import * as uploadController from '../controllers/upload.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { canRead, canCreate, canUpdate, canDelete } from '../middleware/rbac.middleware';
|
||||
import { activityLogger } from '../middleware/activityLog.middleware';
|
||||
import { validate } from '../middleware/validate.middleware';
|
||||
import { taskSchema } from '../utils/validators';
|
||||
import { taskUpload } from '../middleware/upload.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -28,4 +30,9 @@ router.delete('/:id/assignees/:userId', canUpdate('tasks'), tasksController.remo
|
||||
router.get('/:id/comments', canRead('tasks'), tasksController.getTaskComments);
|
||||
router.post('/:id/comments', canRead('tasks'), tasksController.addTaskComment);
|
||||
|
||||
// Files
|
||||
router.post('/:id/files', canUpdate('tasks'), taskUpload.array('files', 10), uploadController.uploadTaskFiles);
|
||||
router.get('/:id/files', canRead('tasks'), uploadController.getTaskFiles);
|
||||
router.delete('/:id/files/:fileId', canUpdate('tasks'), uploadController.deleteTaskFile);
|
||||
|
||||
export default router;
|
||||
|
||||
20
backend/src/routes/upload.routes.ts
Normal file
20
backend/src/routes/upload.routes.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Router } from 'express';
|
||||
import * as uploadController from '../controllers/upload.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { activityLogger } from '../middleware/activityLog.middleware';
|
||||
import { pendingUpload } from '../middleware/upload.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.use(activityLogger);
|
||||
|
||||
// Pending uploads - for new entities before they are saved
|
||||
router.post('/pending/:tempId', pendingUpload.array('files', 10), uploadController.uploadPendingFiles);
|
||||
router.get('/pending/:tempId', uploadController.getPendingFiles);
|
||||
router.delete('/pending/:tempId/:filename', uploadController.deletePendingFile);
|
||||
|
||||
// Download file by entity type and file ID
|
||||
router.get('/:entityType/:fileId/download', uploadController.downloadFile);
|
||||
|
||||
export default router;
|
||||
21
backend/src/routes/zakazky.routes.ts
Normal file
21
backend/src/routes/zakazky.routes.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Router } from 'express';
|
||||
import * as zakazkyController from '../controllers/zakazky.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { canRead } from '../middleware/rbac.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.use(authenticate);
|
||||
|
||||
// Check if external DB is configured
|
||||
router.get('/status', zakazkyController.checkConfiguration);
|
||||
|
||||
// Get available years
|
||||
router.get('/years', zakazkyController.getAvailableYears);
|
||||
|
||||
// Get zakazky by year (with optional search)
|
||||
// Query params: rok (year), search (optional)
|
||||
router.get('/', canRead('projects'), zakazkyController.getZakazky);
|
||||
|
||||
export default router;
|
||||
173
backend/src/services/externalDb.service.ts
Normal file
173
backend/src/services/externalDb.service.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { Pool, PoolClient } from 'pg';
|
||||
import { env } from '../config/env';
|
||||
|
||||
// External database connection pool - READ ONLY
|
||||
let pool: Pool | null = null;
|
||||
|
||||
// Forbidden SQL keywords for safety
|
||||
const FORBIDDEN_KEYWORDS = [
|
||||
'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'TRUNCATE',
|
||||
'GRANT', 'REVOKE', 'EXECUTE', 'CALL', 'MERGE', 'REPLACE'
|
||||
];
|
||||
|
||||
// Validate query is read-only
|
||||
const validateReadOnlyQuery = (query: string): void => {
|
||||
const upperQuery = query.toUpperCase().trim();
|
||||
|
||||
for (const keyword of FORBIDDEN_KEYWORDS) {
|
||||
// Check if query starts with forbidden keyword or contains it as a statement
|
||||
if (upperQuery.startsWith(keyword) || upperQuery.includes(`;${keyword}`) || upperQuery.includes(`; ${keyword}`)) {
|
||||
throw new Error(`Forbidden operation: ${keyword} is not allowed on external database. Read-only access only.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Must start with SELECT or be a function call that returns data
|
||||
if (!upperQuery.startsWith('SELECT')) {
|
||||
throw new Error('Only SELECT queries are allowed on external database.');
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize connection pool
|
||||
const getPool = (): Pool => {
|
||||
if (!pool) {
|
||||
if (!env.EXTERNAL_DB_HOST || !env.EXTERNAL_DB_NAME) {
|
||||
throw new Error('External database not configured. Check EXTERNAL_DB_* environment variables.');
|
||||
}
|
||||
|
||||
pool = new Pool({
|
||||
host: env.EXTERNAL_DB_HOST,
|
||||
port: env.EXTERNAL_DB_PORT,
|
||||
database: env.EXTERNAL_DB_NAME,
|
||||
user: env.EXTERNAL_DB_USER,
|
||||
password: env.EXTERNAL_DB_PASSWORD,
|
||||
max: 5, // Max connections in pool
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
});
|
||||
|
||||
// Handle pool errors
|
||||
pool.on('error', (err) => {
|
||||
console.error('External DB pool error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
return pool;
|
||||
};
|
||||
|
||||
// Safe read-only query executor
|
||||
const executeReadOnlyQuery = async <T>(query: string, params?: unknown[]): Promise<T[]> => {
|
||||
// Validate query before execution
|
||||
validateReadOnlyQuery(query);
|
||||
|
||||
const dbPool = getPool();
|
||||
let client: PoolClient | null = null;
|
||||
|
||||
try {
|
||||
client = await dbPool.connect();
|
||||
|
||||
// Set transaction to read-only for extra safety
|
||||
await client.query('SET TRANSACTION READ ONLY');
|
||||
|
||||
const result = await client.query(query, params);
|
||||
return result.rows as T[];
|
||||
} finally {
|
||||
if (client) {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check if external DB is configured
|
||||
export const isExternalDbConfigured = (): boolean => {
|
||||
return !!(env.EXTERNAL_DB_HOST && env.EXTERNAL_DB_NAME);
|
||||
};
|
||||
|
||||
// Zakazka interface based on function output
|
||||
export interface Zakazka {
|
||||
id: number;
|
||||
id_stav_zakazky: number;
|
||||
cislo: string;
|
||||
datum_vystavenia: Date | null;
|
||||
datum_ukoncenia: Date | null;
|
||||
customer: string;
|
||||
nazov: string;
|
||||
poznamka: string | null;
|
||||
vystavil: string;
|
||||
uzavreta: boolean;
|
||||
}
|
||||
|
||||
// Raw row interface from DB function (id without underscore, rest with underscore)
|
||||
interface ZakazkaRow {
|
||||
id: number;
|
||||
_id_stav_zakazky: number;
|
||||
_cislo: string;
|
||||
_datum_vystavenia: Date | null;
|
||||
_datum_ukoncenia: Date | null;
|
||||
_customer: string;
|
||||
_nazov: string;
|
||||
_poznamka: string | null;
|
||||
_vystavil: string;
|
||||
_uzavreta: boolean;
|
||||
}
|
||||
|
||||
// Get zakazky by year - READ ONLY
|
||||
export const getZakazkyByYear = async (rok: number): Promise<Zakazka[]> => {
|
||||
try {
|
||||
// Call the stored function using safe read-only executor
|
||||
const rows = await executeReadOnlyQuery<ZakazkaRow>(
|
||||
'SELECT * FROM da.zakazky_select_all($1)',
|
||||
[rok]
|
||||
);
|
||||
|
||||
// Map the result to our interface
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
id_stav_zakazky: row._id_stav_zakazky,
|
||||
cislo: row._cislo,
|
||||
datum_vystavenia: row._datum_vystavenia,
|
||||
datum_ukoncenia: row._datum_ukoncenia,
|
||||
customer: row._customer,
|
||||
nazov: row._nazov,
|
||||
poznamka: row._poznamka,
|
||||
vystavil: row._vystavil,
|
||||
uzavreta: row._uzavreta,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error fetching zakazky:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Get single zakazka by ID
|
||||
export const getZakazkaById = async (rok: number, id: number): Promise<Zakazka | null> => {
|
||||
const zakazky = await getZakazkyByYear(rok);
|
||||
return zakazky.find((z) => z.id === id) || null;
|
||||
};
|
||||
|
||||
// Search zakazky
|
||||
export const searchZakazky = async (rok: number, search: string): Promise<Zakazka[]> => {
|
||||
const zakazky = await getZakazkyByYear(rok);
|
||||
const searchLower = search.toLowerCase();
|
||||
|
||||
return zakazky.filter((z) =>
|
||||
z.cislo.toLowerCase().includes(searchLower) ||
|
||||
z.nazov.toLowerCase().includes(searchLower) ||
|
||||
z.customer.toLowerCase().includes(searchLower)
|
||||
);
|
||||
};
|
||||
|
||||
// Close pool (for graceful shutdown)
|
||||
export const closeExternalDb = async (): Promise<void> => {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
}
|
||||
};
|
||||
|
||||
export const externalDbService = {
|
||||
isConfigured: isExternalDbConfigured,
|
||||
getZakazkyByYear,
|
||||
getZakazkaById,
|
||||
searchZakazky,
|
||||
close: closeExternalDb,
|
||||
};
|
||||
Reference in New Issue
Block a user