From ede8c21c8d46250e78efca5b3acd62e1715d08ec Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Mon, 16 Mar 2026 07:20:08 -0300 Subject: [PATCH] feat: add postgres config and migration route --- api/app.ts | 39 ++++++++- package-lock.json | 151 ++++++++++++++++++++++++++++++++++ package.json | 2 + src/server/app.ts | 28 +++++++ src/server/config/postgres.ts | 18 ++++ 5 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 src/server/config/postgres.ts diff --git a/api/app.ts b/api/app.ts index 17a3600..38cb311 100644 --- a/api/app.ts +++ b/api/app.ts @@ -35,7 +35,44 @@ app.use(extractUser); // Static Uploads app.use('/uploads', express.static(path.join(process.cwd(), 'uploads'))); -// Routes +import pool from '../src/server/config/postgres.js'; + +// ... (existing routes) + +app.get('/api/admin/migrate-to-gpi', async (req, res) => { + const TABLES = [ + "organizations", + "users", + "user_organizations", + "projects", + "parts", + "painting_schemes", + "inspections", + "instruments", + "stock_items", + "stock_movements" + ]; + + try { + const client = await pool.connect(); + await client.query("CREATE SCHEMA IF NOT EXISTS gpi;"); + + const results = []; + for (const table of TABLES) { + try { + await client.query(`ALTER TABLE public.${table} SET SCHEMA gpi;`); + results.push({ table, status: 'success' }); + } catch (err: any) { + results.push({ table, status: 'failed', error: err.message }); + } + } + client.release(); + res.json({ message: "Migration completed", results }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +}); + app.use('/api/auth', authRoutes); app.use('/api/users', userRoutes); app.use('/api/projects', projectRoutes); diff --git a/package-lock.json b/package-lock.json index 15c4901..fba9e31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "mongoose": "^9.1.5", "multer": "^2.0.2", "pdf-parse": "^1.1.1", + "pg": "^8.20.0", "prop-types": "^15.8.1", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -47,6 +48,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.0.0", "@types/node": "^24.10.1", + "@types/pg": "^8.18.0", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vercel/node": "^5.5.28", @@ -3492,6 +3494,18 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "dev": true, @@ -9059,6 +9073,95 @@ "ms": "^2.1.1" } }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" @@ -9117,6 +9220,45 @@ "dev": true, "license": "MIT" }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -10177,6 +10319,15 @@ "memory-pager": "^1.0.2" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/statuses": { "version": "2.0.2", "license": "MIT", diff --git a/package.json b/package.json index 42a3eee..6168e31 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "mongoose": "^9.1.5", "multer": "^2.0.2", "pdf-parse": "^1.1.1", + "pg": "^8.20.0", "prop-types": "^15.8.1", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -52,6 +53,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.0.0", "@types/node": "^24.10.1", + "@types/pg": "^8.18.0", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vercel/node": "^5.5.28", diff --git a/src/server/app.ts b/src/server/app.ts index f970589..795f32e 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -51,6 +51,34 @@ if (!fs.existsSync(uploadsPath)) { app.use('/uploads', express.static(uploadsPath)); +// Migration Route (Temporary) +import pool from './config/postgres.js'; +app.get('/api/admin/migrate-schema', async (req, res) => { + const TABLES = [ + "organizations", "users", "user_organizations", "projects", + "parts", "painting_schemes", "inspections", "instruments", + "stock_items", "stock_movements" + ]; + try { + const client = await pool.connect(); + await client.query("CREATE SCHEMA IF NOT EXISTS gpi;"); + const results = []; + for (const table of TABLES) { + try { + // Try to move from public to gpi + await client.query(`ALTER TABLE public."${table}" SET SCHEMA gpi;`); + results.push({ table, status: 'moved to gpi' }); + } catch (err: any) { + results.push({ table, status: 'error', error: err.message }); + } + } + client.release(); + res.json({ message: "Schema migration finished", results }); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } +}); + // Routes app.use('/api/auth', authRoutes); app.use('/api/users', userRoutes); diff --git a/src/server/config/postgres.ts b/src/server/config/postgres.ts new file mode 100644 index 0000000..e1557d1 --- /dev/null +++ b/src/server/config/postgres.ts @@ -0,0 +1,18 @@ +import pg from 'pg'; +const { Pool } = pg; +import dotenv from 'dotenv'; + +dotenv.config(); + +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 // Internal network usually doesn't need SSL +}); + +export const query = (text: string, params?: any[]) => pool.query(text, params); + +export default pool;