feat: pool management

This commit is contained in:
DeathKaioken 2025-11-29 13:14:10 +01:00
parent c62d775e66
commit 8aebdbb607
8 changed files with 272 additions and 0 deletions

View File

@ -0,0 +1,57 @@
const { createPool, listPools, updatePoolState } = require('../../services/pool/PoolService');
module.exports = {
async create(req, res) {
try {
const { name, description, state } = req.body || {};
const actorUserId = req.user && req.user.userId;
if (!name) return res.status(400).json({ success: false, message: 'name is required' });
if (state && !['active', 'inactive'].includes(state)) {
return res.status(400).json({ success: false, message: 'Invalid state. Allowed: active, inactive' });
}
const pool = await createPool({ name, description, state, actorUserId });
return res.status(201).json({ success: true, data: pool });
} catch (e) {
if (e && (e.code === 'ER_DUP_ENTRY' || e.errno === 1062)) {
return res.status(409).json({ success: false, message: 'Pool name already exists' });
}
if (e && e.status === 400) {
return res.status(400).json({ success: false, message: e.message });
}
console.error('[PoolController.create]', e);
return res.status(500).json({ success: false, message: 'Internal server error' });
}
},
async list(req, res) {
try {
console.debug('[PoolController.list] GET /api/admin/pools');
const pools = await listPools();
return res.status(200).json({ success: true, data: pools });
} catch (e) {
console.error('[PoolController.list] error', e);
return res.status(500).json({ success: false, message: 'Internal server error' });
}
},
// NEW: optional state update handler (route can be added later)
async updateState(req, res) {
try {
const { id } = req.params || {};
const { state } = req.body || {};
const actorUserId = req.user && req.user.userId;
if (!id) return res.status(400).json({ success: false, message: 'id is required' });
if (!['active', 'inactive', 'archived'].includes(state)) {
return res.status(400).json({ success: false, message: 'Invalid state. Allowed: active, inactive, archived' });
}
const updated = await updatePoolState(id, state, actorUserId);
return res.status(200).json({ success: true, data: updated });
} catch (e) {
if (e && e.status === 400) {
return res.status(400).json({ success: false, message: e.message });
}
console.error('[PoolController.updateState]', e);
return res.status(500).json({ success: false, message: 'Internal server error' });
}
}
};

View File

@ -620,6 +620,61 @@ async function createDatabase() {
`); `);
console.log('✅ User matrix metadata table created/verified'); console.log('✅ User matrix metadata table created/verified');
// --- Pools (NEW) ---
await connection.query(`
CREATE TABLE IF NOT EXISTS pools (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT NULL,
state ENUM('active','inactive','archived') DEFAULT 'active',
created_by INT NOT NULL,
updated_by INT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_pool_name (name),
INDEX idx_pool_name (name),
INDEX idx_pool_state (state),
INDEX idx_pool_created_by (created_by),
INDEX idx_pool_updated_by (updated_by),
CONSTRAINT fk_pools_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT fk_pools_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
);
`);
console.log('✅ Pools table created/verified');
// Migrations for existing databases: add 'archived' and actor columns if missing
try {
await connection.query(`
ALTER TABLE pools
MODIFY COLUMN state ENUM('active','inactive','archived') DEFAULT 'active'
`);
console.log('🆙 Pools.state updated to include archived');
} catch (e) {
console.log(' Pools.state alteration skipped:', e.message);
}
try {
await connection.query(`ALTER TABLE pools ADD COLUMN created_by INT NOT NULL`);
await connection.query(`CREATE INDEX idx_pool_created_by ON pools (created_by)`);
await connection.query(`
ALTER TABLE pools ADD CONSTRAINT fk_pools_created_by
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE
`);
console.log('🆕 Pools.created_by added with FK and index');
} catch (e) {
console.log(' Pools.created_by add skipped:', e.message);
}
try {
await connection.query(`ALTER TABLE pools ADD COLUMN updated_by INT NULL`);
await connection.query(`CREATE INDEX idx_pool_updated_by ON pools (updated_by)`);
await connection.query(`
ALTER TABLE pools ADD CONSTRAINT fk_pools_updated_by
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
`);
console.log('🆕 Pools.updated_by added with FK and index');
} catch (e) {
console.log(' Pools.updated_by add skipped:', e.message);
}
console.log('🎉 Normalized database schema created/updated successfully!'); console.log('🎉 Normalized database schema created/updated successfully!');
await connection.end(); await connection.end();

14
models/Pool.js Normal file
View File

@ -0,0 +1,14 @@
class Pool {
constructor({ id = null, name, description = null, state = 'active', created_by = null, updated_by = null, created_at = null, updated_at = null }) {
this.id = id;
this.name = name;
this.description = description;
this.state = state;
this.created_by = created_by;
this.updated_by = updated_by;
this.created_at = created_at;
this.updated_at = updated_at;
}
}
module.exports = Pool;

View File

@ -0,0 +1,66 @@
const Pool = require('../../models/Pool');
class PoolRepository {
constructor(uow) {
this.uow = uow;
}
async create({ name, description = null, state = 'active', created_by }) {
const conn = this.uow.connection;
const [res] = await conn.execute(
`INSERT INTO pools (name, description, state, created_by, updated_by)
VALUES (?, ?, ?, ?, NULL)`,
[name, description, state, created_by]
);
return new Pool({ id: res.insertId, name, description, state, created_by, updated_by: null });
}
async findAll() {
const conn = this.uow.connection; // switched to connection
try {
console.debug('[PoolRepository.findAll] querying pools');
const [rows] = await conn.execute(
`SELECT id, name, description, state, created_at, updated_at
FROM pools
ORDER BY created_at DESC`
);
console.debug('[PoolRepository.findAll] rows fetched', { count: rows.length });
return rows.map(r => new Pool(r));
} catch (err) {
console.error('[PoolRepository.findAll] query failed', err);
// Surface a consistent error up the stack
const e = new Error('Failed to fetch pools');
e.status = 500;
e.cause = err;
throw e;
}
}
// NEW: update state with enforced transitions (active <-> inactive -> archived also supported)
async updateState(id, nextState, updated_by) {
const conn = this.uow.connection;
const [rows] = await conn.execute(`SELECT id, state FROM pools WHERE id = ?`, [id]);
if (!rows || rows.length === 0) {
const err = new Error('Pool not found');
err.status = 404;
throw err;
}
const current = rows[0].state;
const allowed = ['active', 'inactive', 'archived'];
if (!allowed.includes(current) || !allowed.includes(nextState)) {
const err = new Error('Invalid state transition');
err.status = 400;
throw err;
}
if (current === nextState) {
return new Pool({ id, state: current, updated_by }); // no-op
}
await conn.execute(
`UPDATE pools SET state = ?, updated_by = ?, updated_at = NOW() WHERE id = ?`,
[nextState, updated_by, id]
);
return new Pool({ id, state: nextState, updated_by });
}
}
module.exports = PoolRepository;

View File

@ -16,6 +16,7 @@ const UserStatusController = require('../controller/auth/UserStatusController');
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
const MatrixController = require('../controller/matrix/MatrixController'); // <-- added const MatrixController = require('../controller/matrix/MatrixController'); // <-- added
const CoffeeController = require('../controller/admin/CoffeeController'); const CoffeeController = require('../controller/admin/CoffeeController');
const PoolController = require('../controller/pool/PoolController');
// small helpers copied from original files // small helpers copied from original files
function adminOnly(req, res, next) { function adminOnly(req, res, next) {
@ -118,5 +119,8 @@ router.get('/admin/matrix/user-candidates', authMiddleware, adminOnly, MatrixCon
// NEW: Matrix POST (admin) // NEW: Matrix POST (admin)
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser); router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser);
// NEW: Admin list pools
router.get('/admin/pools', authMiddleware, adminOnly, PoolController.list);
// export // export
module.exports = router; module.exports = router;

View File

@ -7,6 +7,7 @@ const CompanyStampController = require('../controller/companyStamp/CompanyStampC
const CoffeeController = require('../controller/admin/CoffeeController'); const CoffeeController = require('../controller/admin/CoffeeController');
const AdminUserController = require('../controller/admin/AdminUserController'); const AdminUserController = require('../controller/admin/AdminUserController');
const PersonalProfileController = require('../controller/profile/PersonalProfileController'); // <-- add const PersonalProfileController = require('../controller/profile/PersonalProfileController'); // <-- add
const PoolController = require('../controller/pool/PoolController'); // <-- new
// Helper middlewares for company-stamp // Helper middlewares for company-stamp
function adminOnly(req, res, next) { function adminOnly(req, res, next) {
@ -36,6 +37,8 @@ router.patch('/admin/update-user-profile/:id', authMiddleware, adminOnly, AdminU
router.patch('/admin/update-user-status/:id', authMiddleware, adminOnly, AdminUserController.updateUserStatus); router.patch('/admin/update-user-status/:id', authMiddleware, adminOnly, AdminUserController.updateUserStatus);
// Admin: set state for coffee product // Admin: set state for coffee product
router.patch('/admin/coffee/:id/state', authMiddleware, adminOnly, CoffeeController.setState); router.patch('/admin/coffee/:id/state', authMiddleware, adminOnly, CoffeeController.setState);
// NEW: Admin pool state update
router.patch('/admin/pools/:id/state', authMiddleware, adminOnly, PoolController.updateState);
// Personal profile (self-service) - no admin guard // Personal profile (self-service) - no admin guard
router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic); router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic);

View File

@ -22,6 +22,7 @@ const CompanyProfileController = require('../controller/profile/CompanyProfileCo
const AdminUserController = require('../controller/admin/AdminUserController'); const AdminUserController = require('../controller/admin/AdminUserController');
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
const MatrixController = require('../controller/matrix/MatrixController'); // Matrix admin operations const MatrixController = require('../controller/matrix/MatrixController'); // Matrix admin operations
const PoolController = require('../controller/pool/PoolController');
const multer = require('multer'); const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
@ -121,6 +122,8 @@ router.post('/company-stamps', authMiddleware, adminOnly, forceCompanyForAdmin,
router.post('/admin/coffee', authMiddleware, adminOnly, upload.single('picture'), CoffeeController.create); router.post('/admin/coffee', authMiddleware, adminOnly, upload.single('picture'), CoffeeController.create);
// NEW: add user into matrix // NEW: add user into matrix
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser); // already added router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser); // already added
// NEW: Admin create pool
router.post('/admin/pools', authMiddleware, adminOnly, PoolController.create);
// Existing registration handlers (keep) // Existing registration handlers (keep)
router.post('/register/personal', (req, res) => { router.post('/register/personal', (req, res) => {

View File

@ -0,0 +1,70 @@
const UnitOfWork = require('../../database/UnitOfWork');
const PoolRepository = require('../../repositories/pool/poolRepository');
function isValidState(state) {
return state === undefined || state === null || state === 'active' || state === 'inactive' || state === 'archived';
}
async function createPool({ name, description = null, state = 'active', actorUserId }) {
if (!isValidState(state)) {
const err = new Error('Invalid state. Allowed: active, inactive, archived');
err.status = 400;
throw err;
}
const uow = new UnitOfWork();
try {
console.debug('[PoolService.createPool] start', { name, state });
await uow.start();
const repo = new PoolRepository(uow);
const pool = await repo.create({ name, description, state, created_by: actorUserId });
await uow.commit();
console.debug('[PoolService.createPool] success', { id: pool.id });
return pool;
} catch (err) {
console.error('[PoolService.createPool] error', err);
try { await uow.rollback(); } catch (_) { console.warn('[PoolService.createPool] rollback failed'); }
throw err;
}
}
async function listPools() {
const uow = new UnitOfWork();
try {
console.debug('[PoolService.listPools] start');
// Ensure connection exists even for read-only
await uow.start();
const repo = new PoolRepository(uow);
const pools = await repo.findAll();
await uow.commit(); // harmless commit; ensures proper cleanup if UnitOfWork requires it
console.debug('[PoolService.listPools] success', { count: pools.length });
return pools;
} catch (err) {
console.error('[PoolService.listPools] error', err);
try { await uow.rollback(); } catch (_) { console.warn('[PoolService.listPools] rollback failed'); }
throw err;
}
}
async function updatePoolState(id, nextState, actorUserId) {
if (!isValidState(nextState) || !['active', 'inactive', 'archived'].includes(nextState)) {
const err = new Error('Invalid state. Allowed: active, inactive, archived');
err.status = 400;
throw err;
}
const uow = new UnitOfWork();
try {
console.debug('[PoolService.updatePoolState] start', { id, nextState });
await uow.start();
const repo = new PoolRepository(uow);
const updated = await repo.updateState(id, nextState, actorUserId);
await uow.commit();
console.debug('[PoolService.updatePoolState] success', { id, state: nextState });
return updated;
} catch (err) {
console.error('[PoolService.updatePoolState] error', err);
try { await uow.rollback(); } catch (_) { console.warn('[PoolService.updatePoolState] rollback failed'); }
throw err;
}
}
module.exports = { createPool, listPools, updatePoolState };