refactor: switch database to postgres and update project service

This commit is contained in:
2026-03-16 07:55:17 -03:00
parent ede8c21c8d
commit 8c247d8afd
2 changed files with 59 additions and 218 deletions

View File

@@ -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);

View File

@@ -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]);
};