refactor: switch database to postgres and update project service
This commit is contained in:
@@ -1,46 +1,26 @@
|
|||||||
import mongoose from 'mongoose';
|
import pg from 'pg';
|
||||||
import { GridFSBucket } from 'mongodb';
|
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 () => {
|
export const connectDB = async () => {
|
||||||
try {
|
try {
|
||||||
const uri = process.env.MONGODB_URI;
|
const client = await pool.connect();
|
||||||
if (!uri) {
|
console.log('✅ Postgres (Supabase) connected successfully');
|
||||||
throw new Error('MONGODB_URI is not defined in environment variables');
|
client.release();
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ MongoDB connection error:', error);
|
console.error('❌ Postgres connection error:', error);
|
||||||
console.warn('⚠️ Server will continue running for debugging, but database features will be unavailable.');
|
|
||||||
// process.exit(1);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const query = (text: string, params?: any[]) => pool.query(text, params);
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import Project from '../models/Project.js';
|
import { query } from '../config/database.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';
|
|
||||||
|
|
||||||
interface ProjectData {
|
interface ProjectData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -15,207 +11,72 @@ interface ProjectData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const createProject = async (data: ProjectData & { organizationId?: string }) => {
|
export const createProject = async (data: ProjectData & { organizationId?: string }) => {
|
||||||
const newProject = new Project({
|
const res = await query(
|
||||||
name: data.name,
|
'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 *',
|
||||||
client: data.client,
|
[data.organizationId, data.name, data.client, data.startDate, data.endDate, data.technician, data.environment, data.weightKg]
|
||||||
startDate: data.startDate ? new Date(data.startDate) : null,
|
);
|
||||||
endDate: data.endDate ? new Date(data.endDate) : null,
|
return res.rows[0];
|
||||||
technician: data.technician,
|
|
||||||
environment: data.environment,
|
|
||||||
organizationId: data.organizationId,
|
|
||||||
weightKg: data.weightKg
|
|
||||||
});
|
|
||||||
return await newProject.save();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDashboardProjects = async (organizationId?: string) => {
|
export const getDashboardProjects = async (organizationId?: string) => {
|
||||||
const matchStage = organizationId ? { organizationId } : {};
|
const sql = `
|
||||||
|
SELECT p.*,
|
||||||
const projects = await Project.aggregate([
|
(SELECT json_agg(s) FROM gpi.painting_schemes s WHERE s.project_id = p.id) as schemes,
|
||||||
{ $match: matchStage },
|
(SELECT SUM(weight_kg) FROM gpi.inspections i WHERE i.project_id = p.id) as painted_weight
|
||||||
{ $sort: { name: 1 } },
|
FROM gpi.projects p
|
||||||
{
|
WHERE p.organization_id = $1 OR $1 IS NULL
|
||||||
$lookup: {
|
ORDER BY p.name ASC
|
||||||
from: 'paintingschemes',
|
`;
|
||||||
localField: '_id',
|
const res = await query(sql, [organizationId || null]);
|
||||||
foreignField: 'projectId',
|
return res.rows;
|
||||||
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() }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllProjects = async (organizationId?: string, isGlobalAdmin: boolean = false, status: string = 'active') => {
|
export const getAllProjects = async (organizationId?: string, isGlobalAdmin: boolean = false, status: string = 'active') => {
|
||||||
const statusQuery = status === 'active'
|
let sql = 'SELECT * FROM gpi.projects WHERE status = $1';
|
||||||
? { status: { $ne: 'archived' } }
|
const params: any[] = [status];
|
||||||
: { status: 'archived' };
|
|
||||||
|
|
||||||
const matchQuery: Record<string, unknown> = isGlobalAdmin
|
if (!isGlobalAdmin) {
|
||||||
? { ...statusQuery }
|
sql += ' AND (organization_id = $2 OR organization_id IS NULL)';
|
||||||
: {
|
params.push(organizationId);
|
||||||
...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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newStatus = project.status === 'active' ? 'archived' : 'active';
|
sql += ' ORDER BY name ASC';
|
||||||
const updated = await Project.findByIdAndUpdate(id, { status: newStatus }, { new: true }).lean();
|
const res = await query(sql, params);
|
||||||
return updated;
|
return res.rows;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProjectById = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
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');
|
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.organization_id && project.organization_id !== organizationId) {
|
||||||
if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) {
|
|
||||||
throw new Error('Acesso negado a este projeto');
|
throw new Error('Acesso negado a este projeto');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [parts, schemes, records, inspections] = await Promise.all([
|
const [parts, schemes, records, inspections] = await Promise.all([
|
||||||
Part.find({ projectId: id }).lean(),
|
query('SELECT * FROM gpi.parts WHERE project_id = $1', [id]),
|
||||||
PaintingScheme.find({ projectId: id }).populate('paintId thinnerId').lean(),
|
query('SELECT * FROM gpi.painting_schemes WHERE project_id = $1', [id]),
|
||||||
ApplicationRecord.find({ projectId: id }).lean(),
|
query('SELECT * FROM gpi.application_records WHERE project_id = $1', [id]), // Assuming this table exists in gpi
|
||||||
Inspection.find({ projectId: id })
|
query('SELECT * FROM gpi.inspections WHERE project_id = $1', [id])
|
||||||
.populate({
|
|
||||||
path: 'stockItemId',
|
|
||||||
select: 'batchNumber dataSheetId',
|
|
||||||
populate: { path: 'dataSheetId', select: 'name' }
|
|
||||||
})
|
|
||||||
.lean()
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
id: project._id.toString(),
|
parts: parts.rows,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
paintingSchemes: schemes.rows,
|
||||||
parts: parts.map((p: any) => ({ ...p, id: p._id.toString() })),
|
applicationRecords: records.rows,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
inspections: inspections.rows
|
||||||
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() }))
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateProject = async (id: string, data: Partial<ProjectData>, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
export const updateProject = async (id: string, data: Partial<ProjectData>, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||||
const existing = await Project.findById(id);
|
// Basic update logic
|
||||||
if (!existing) return null;
|
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];
|
||||||
// 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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteProject = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
export const deleteProject = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
|
||||||
const project = await Project.findById(id);
|
await query('DELETE FROM gpi.projects WHERE id = $1', [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 })
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user