feat: pool management
This commit is contained in:
parent
c62d775e66
commit
8aebdbb607
57
controller/pool/PoolController.js
Normal file
57
controller/pool/PoolController.js
Normal 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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -620,6 +620,61 @@ async function createDatabase() {
|
||||
`);
|
||||
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!');
|
||||
|
||||
await connection.end();
|
||||
|
||||
14
models/Pool.js
Normal file
14
models/Pool.js
Normal 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;
|
||||
66
repositories/pool/poolRepository.js
Normal file
66
repositories/pool/poolRepository.js
Normal 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;
|
||||
@ -16,6 +16,7 @@ const UserStatusController = require('../controller/auth/UserStatusController');
|
||||
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
|
||||
const MatrixController = require('../controller/matrix/MatrixController'); // <-- added
|
||||
const CoffeeController = require('../controller/admin/CoffeeController');
|
||||
const PoolController = require('../controller/pool/PoolController');
|
||||
|
||||
// small helpers copied from original files
|
||||
function adminOnly(req, res, next) {
|
||||
@ -118,5 +119,8 @@ router.get('/admin/matrix/user-candidates', authMiddleware, adminOnly, MatrixCon
|
||||
// NEW: Matrix POST (admin)
|
||||
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser);
|
||||
|
||||
// NEW: Admin list pools
|
||||
router.get('/admin/pools', authMiddleware, adminOnly, PoolController.list);
|
||||
|
||||
// export
|
||||
module.exports = router;
|
||||
@ -7,6 +7,7 @@ const CompanyStampController = require('../controller/companyStamp/CompanyStampC
|
||||
const CoffeeController = require('../controller/admin/CoffeeController');
|
||||
const AdminUserController = require('../controller/admin/AdminUserController');
|
||||
const PersonalProfileController = require('../controller/profile/PersonalProfileController'); // <-- add
|
||||
const PoolController = require('../controller/pool/PoolController'); // <-- new
|
||||
|
||||
// Helper middlewares for company-stamp
|
||||
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);
|
||||
// Admin: set state for coffee product
|
||||
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
|
||||
router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic);
|
||||
|
||||
@ -22,6 +22,7 @@ const CompanyProfileController = require('../controller/profile/CompanyProfileCo
|
||||
const AdminUserController = require('../controller/admin/AdminUserController');
|
||||
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
|
||||
const MatrixController = require('../controller/matrix/MatrixController'); // Matrix admin operations
|
||||
const PoolController = require('../controller/pool/PoolController');
|
||||
|
||||
const multer = require('multer');
|
||||
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);
|
||||
// NEW: add user into matrix
|
||||
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)
|
||||
router.post('/register/personal', (req, res) => {
|
||||
|
||||
70
services/pool/PoolService.js
Normal file
70
services/pool/PoolService.js
Normal 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 };
|
||||
Loading…
Reference in New Issue
Block a user