From 8c247d8afd314652b29837f5217149290e609ad2 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Mon, 16 Mar 2026 07:55:17 -0300 Subject: [PATCH] refactor: switch database to postgres and update project service --- src/server/config/database.ts | 58 +++---- src/server/services/projectService.ts | 219 +++++--------------------- 2 files changed, 59 insertions(+), 218 deletions(-) diff --git a/src/server/config/database.ts b/src/server/config/database.ts index 74a1f4a..1c8c166 100644 --- a/src/server/config/database.ts +++ b/src/server/config/database.ts @@ -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); diff --git a/src/server/services/projectService.ts b/src/server/services/projectService.ts index ba5e804..e66778a 100644 --- a/src/server/services/projectService.ts +++ b/src/server/services/projectService.ts @@ -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 = 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, 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 & { 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]); }; - - -