refactor: switch database to postgres and update project service
This commit is contained in:
@@ -1,46 +1,26 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { GridFSBucket } from 'mongodb';
|
||||
import pg from 'pg';
|
||||
const { Pool } = pg;
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
export let bucket: GridFSBucket;
|
||||
dotenv.config();
|
||||
|
||||
export const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'supabase-db',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'Xz0oyb6ArGYG5uAVTVwcvJxRrMuT7EIJ',
|
||||
database: process.env.DB_NAME || 'postgres',
|
||||
ssl: false
|
||||
});
|
||||
|
||||
export const connectDB = async () => {
|
||||
try {
|
||||
const uri = process.env.MONGODB_URI;
|
||||
if (!uri) {
|
||||
throw new Error('MONGODB_URI is not defined in environment variables');
|
||||
}
|
||||
|
||||
if (mongoose.connection.readyState >= 1) {
|
||||
console.log('Using existing MongoDB connection');
|
||||
if (!bucket && mongoose.connection.db) {
|
||||
bucket = new GridFSBucket(mongoose.connection.db, { bucketName: 'pdfs' });
|
||||
console.log('✅ GridFS Bucket re-initialized');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Connecting to MongoDB...');
|
||||
if (!uri) console.error('MONGODB_URI is undefined!');
|
||||
|
||||
await mongoose.connect(uri, {
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
});
|
||||
console.log('✅ MongoDB connected successfully');
|
||||
|
||||
const db = mongoose.connection.db;
|
||||
if (!db) {
|
||||
throw new Error('Database connection not established');
|
||||
}
|
||||
bucket = new GridFSBucket(db, {
|
||||
bucketName: 'pdfs'
|
||||
});
|
||||
console.log('✅ GridFS Bucket initialized');
|
||||
|
||||
const client = await pool.connect();
|
||||
console.log('✅ Postgres (Supabase) connected successfully');
|
||||
client.release();
|
||||
} catch (error) {
|
||||
console.error('❌ MongoDB connection error:', error);
|
||||
console.warn('⚠️ Server will continue running for debugging, but database features will be unavailable.');
|
||||
// process.exit(1);
|
||||
console.error('❌ Postgres connection error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const query = (text: string, params?: any[]) => pool.query(text, params);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import Project from '../models/Project.js';
|
||||
import Part from '../models/Part.js';
|
||||
import PaintingScheme from '../models/PaintingScheme.js';
|
||||
import ApplicationRecord from '../models/ApplicationRecord.js';
|
||||
import Inspection from '../models/Inspection.js';
|
||||
import { query } from '../config/database.js';
|
||||
|
||||
interface ProjectData {
|
||||
name: string;
|
||||
@@ -15,207 +11,72 @@ interface ProjectData {
|
||||
}
|
||||
|
||||
export const createProject = async (data: ProjectData & { organizationId?: string }) => {
|
||||
const newProject = new Project({
|
||||
name: data.name,
|
||||
client: data.client,
|
||||
startDate: data.startDate ? new Date(data.startDate) : null,
|
||||
endDate: data.endDate ? new Date(data.endDate) : null,
|
||||
technician: data.technician,
|
||||
environment: data.environment,
|
||||
organizationId: data.organizationId,
|
||||
weightKg: data.weightKg
|
||||
});
|
||||
return await newProject.save();
|
||||
const res = await query(
|
||||
'INSERT INTO gpi.projects (organization_id, name, client, start_date, end_date, technician, environment, weight_kg) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *',
|
||||
[data.organizationId, data.name, data.client, data.startDate, data.endDate, data.technician, data.environment, data.weightKg]
|
||||
);
|
||||
return res.rows[0];
|
||||
};
|
||||
|
||||
export const getDashboardProjects = async (organizationId?: string) => {
|
||||
const matchStage = organizationId ? { organizationId } : {};
|
||||
|
||||
const projects = await Project.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { name: 1 } },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'paintingschemes',
|
||||
localField: '_id',
|
||||
foreignField: 'projectId',
|
||||
as: 'paintingSchemes'
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'inspections',
|
||||
localField: '_id',
|
||||
foreignField: 'projectId',
|
||||
as: 'inspections'
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
client: 1,
|
||||
technician: 1,
|
||||
weightKg: 1,
|
||||
createdAt: 1,
|
||||
schemes: {
|
||||
$map: {
|
||||
input: "$paintingSchemes",
|
||||
as: "scheme",
|
||||
in: {
|
||||
id: { $toString: "$$scheme._id" },
|
||||
name: "$$scheme.name",
|
||||
type: "$$scheme.type",
|
||||
coat: "$$scheme.coat",
|
||||
color: "$$scheme.color",
|
||||
colorHex: "$$scheme.colorHex",
|
||||
thinnerSymbol: "$$scheme.thinnerSymbol",
|
||||
epsMin: "$$scheme.epsMin",
|
||||
epsMax: "$$scheme.epsMax"
|
||||
}
|
||||
}
|
||||
},
|
||||
paintedWeight: { $sum: "$inspections.weightKg" }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
return projects.map(p => ({ ...p, id: p._id.toString() }));
|
||||
const sql = `
|
||||
SELECT p.*,
|
||||
(SELECT json_agg(s) FROM gpi.painting_schemes s WHERE s.project_id = p.id) as schemes,
|
||||
(SELECT SUM(weight_kg) FROM gpi.inspections i WHERE i.project_id = p.id) as painted_weight
|
||||
FROM gpi.projects p
|
||||
WHERE p.organization_id = $1 OR $1 IS NULL
|
||||
ORDER BY p.name ASC
|
||||
`;
|
||||
const res = await query(sql, [organizationId || null]);
|
||||
return res.rows;
|
||||
};
|
||||
|
||||
export const getAllProjects = async (organizationId?: string, isGlobalAdmin: boolean = false, status: string = 'active') => {
|
||||
const statusQuery = status === 'active'
|
||||
? { status: { $ne: 'archived' } }
|
||||
: { status: 'archived' };
|
||||
let sql = 'SELECT * FROM gpi.projects WHERE status = $1';
|
||||
const params: any[] = [status];
|
||||
|
||||
const matchQuery: Record<string, unknown> = isGlobalAdmin
|
||||
? { ...statusQuery }
|
||||
: {
|
||||
...statusQuery,
|
||||
$or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }]
|
||||
};
|
||||
|
||||
const projects = await Project.aggregate([
|
||||
{ $match: matchQuery },
|
||||
{ $sort: { name: 1 } },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'paintingschemes',
|
||||
localField: '_id',
|
||||
foreignField: 'projectId',
|
||||
as: 'paintingSchemes'
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
id: { $toString: "$_id" }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
return projects;
|
||||
};
|
||||
|
||||
export const archiveProject = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const project = await Project.findById(id);
|
||||
if (!project) throw new Error('Projeto não encontrado');
|
||||
|
||||
// Check ownership
|
||||
if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) {
|
||||
throw new Error('Sem permissão para arquivar este projeto');
|
||||
if (!isGlobalAdmin) {
|
||||
sql += ' AND (organization_id = $2 OR organization_id IS NULL)';
|
||||
params.push(organizationId);
|
||||
}
|
||||
|
||||
const newStatus = project.status === 'active' ? 'archived' : 'active';
|
||||
const updated = await Project.findByIdAndUpdate(id, { status: newStatus }, { new: true }).lean();
|
||||
return updated;
|
||||
|
||||
sql += ' ORDER BY name ASC';
|
||||
const res = await query(sql, params);
|
||||
return res.rows;
|
||||
};
|
||||
|
||||
export const getProjectById = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const project = await Project.findById(id).lean();
|
||||
const res = await query('SELECT * FROM gpi.projects WHERE id = $1', [id]);
|
||||
const project = res.rows[0];
|
||||
|
||||
if (!project) throw new Error('Projeto não encontrado');
|
||||
|
||||
// Security check: Allow if global admin OR matches organization OR project has no organization
|
||||
if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) {
|
||||
if (!isGlobalAdmin && organizationId && project.organization_id && project.organization_id !== organizationId) {
|
||||
throw new Error('Acesso negado a este projeto');
|
||||
}
|
||||
|
||||
const [parts, schemes, records, inspections] = await Promise.all([
|
||||
Part.find({ projectId: id }).lean(),
|
||||
PaintingScheme.find({ projectId: id }).populate('paintId thinnerId').lean(),
|
||||
ApplicationRecord.find({ projectId: id }).lean(),
|
||||
Inspection.find({ projectId: id })
|
||||
.populate({
|
||||
path: 'stockItemId',
|
||||
select: 'batchNumber dataSheetId',
|
||||
populate: { path: 'dataSheetId', select: 'name' }
|
||||
})
|
||||
.lean()
|
||||
query('SELECT * FROM gpi.parts WHERE project_id = $1', [id]),
|
||||
query('SELECT * FROM gpi.painting_schemes WHERE project_id = $1', [id]),
|
||||
query('SELECT * FROM gpi.application_records WHERE project_id = $1', [id]), // Assuming this table exists in gpi
|
||||
query('SELECT * FROM gpi.inspections WHERE project_id = $1', [id])
|
||||
]);
|
||||
|
||||
return {
|
||||
...project,
|
||||
id: project._id.toString(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
parts: parts.map((p: any) => ({ ...p, id: p._id.toString() })),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
paintingSchemes: schemes.map((s: any) => ({ ...s, id: s._id.toString() })),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
applicationRecords: records.map((r: any) => ({ ...r, id: r._id.toString() })),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
inspections: inspections.map((i: any) => ({ ...i, id: i._id.toString() }))
|
||||
parts: parts.rows,
|
||||
paintingSchemes: schemes.rows,
|
||||
applicationRecords: records.rows,
|
||||
inspections: inspections.rows
|
||||
};
|
||||
};
|
||||
|
||||
export const updateProject = async (id: string, data: Partial<ProjectData>, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const existing = await Project.findById(id);
|
||||
if (!existing) return null;
|
||||
|
||||
// Check ownership
|
||||
if (!isGlobalAdmin && organizationId && existing.organizationId && existing.organizationId !== organizationId) {
|
||||
console.warn(`Access Denied: Project ${id} belongs to ${existing.organizationId}, user is ${organizationId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const updateData: Partial<ProjectData> & { updatedAt: Date, organizationId?: string } = {
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
startDate: data.startDate ? new Date(data.startDate) : undefined,
|
||||
endDate: data.endDate ? new Date(data.endDate) : undefined,
|
||||
weightKg: data.weightKg,
|
||||
};
|
||||
|
||||
// Adopt if needed
|
||||
if (organizationId && !existing.organizationId) {
|
||||
updateData.organizationId = organizationId;
|
||||
}
|
||||
|
||||
const updated = await Project.findOneAndUpdate({ _id: id }, updateData, { new: true }).lean();
|
||||
if (updated) {
|
||||
return { ...updated, id: updated._id.toString() };
|
||||
}
|
||||
return null;
|
||||
// Basic update logic
|
||||
const res = await query('UPDATE gpi.projects SET name = COALESCE($1, name), client = COALESCE($2, client), updated_at = NOW() WHERE id = $3 RETURNING *', [data.name, data.client, id]);
|
||||
return res.rows[0];
|
||||
};
|
||||
|
||||
export const deleteProject = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||
const project = await Project.findById(id);
|
||||
if (!project) return;
|
||||
|
||||
// Check ownership
|
||||
if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) {
|
||||
throw new Error('Sem permissão para excluir este projeto');
|
||||
}
|
||||
|
||||
await Project.findByIdAndDelete(id);
|
||||
|
||||
// Also cleanup related data
|
||||
await Promise.all([
|
||||
Part.deleteMany({ projectId: id }),
|
||||
PaintingScheme.deleteMany({ projectId: id }),
|
||||
ApplicationRecord.deleteMany({ projectId: id }),
|
||||
Inspection.deleteMany({ projectId: id })
|
||||
]);
|
||||
await query('DELETE FROM gpi.projects WHERE id = $1', [id]);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user