feat: matrix adjustments
This commit is contained in:
parent
1fce1f1831
commit
9f5458f0a8
@ -29,9 +29,8 @@ async function stats(req, res) {
|
||||
// NEW: admin-only list of matrix users under a root
|
||||
async function getMatrixUserforAdmin(req, res) {
|
||||
try {
|
||||
const { rootUserId, depth, limit, offset, includeRoot } = req.query;
|
||||
// aliases accepted by backend for convenience
|
||||
const matrixId = req.query.matrixId || req.query.id;
|
||||
const { rootUserId, depth, limit, offset, includeRoot, rogueOnly } = req.query;
|
||||
const matrixInstanceId = req.query.matrixInstanceId || req.query.matrixId || req.query.id;
|
||||
const topNodeEmail = req.query.topNodeEmail || req.query.email;
|
||||
|
||||
const data = await MatrixService.getUsers({
|
||||
@ -40,8 +39,10 @@ async function getMatrixUserforAdmin(req, res) {
|
||||
limit,
|
||||
offset,
|
||||
includeRoot,
|
||||
rogueOnly: String(rogueOnly || '').toLowerCase() === 'true',
|
||||
actorUser: req.user,
|
||||
matrixId,
|
||||
matrixInstanceId,
|
||||
matrixId: matrixInstanceId, // backward alias
|
||||
topNodeEmail
|
||||
});
|
||||
return res.json({ success: true, data });
|
||||
@ -55,20 +56,19 @@ async function getMatrixUserforAdmin(req, res) {
|
||||
async function searchCandidates(req, res) {
|
||||
try {
|
||||
const { q, type, limit, offset, rootUserId } = req.query;
|
||||
const matrixId = req.query.matrixId || req.query.id;
|
||||
const matrixInstanceId = req.query.matrixInstanceId || req.query.matrixId || req.query.id;
|
||||
const topNodeEmail = req.query.topNodeEmail || req.query.email;
|
||||
|
||||
const data = await MatrixService.getUserCandidates({
|
||||
q,
|
||||
type,
|
||||
limit,
|
||||
offset,
|
||||
rootUserId,
|
||||
matrixId,
|
||||
matrixInstanceId,
|
||||
matrixId: matrixInstanceId,
|
||||
topNodeEmail,
|
||||
actorUser: req.user
|
||||
});
|
||||
|
||||
return res.json({ success: true, data });
|
||||
} catch (err) {
|
||||
const status = err.status || 500;
|
||||
@ -80,15 +80,18 @@ async function addUser(req, res) {
|
||||
try {
|
||||
const {
|
||||
rootUserId,
|
||||
matrixInstanceId,
|
||||
matrixId,
|
||||
topNodeEmail,
|
||||
childUserId,
|
||||
forceParentFallback,
|
||||
parentUserId
|
||||
} = req.body;
|
||||
const resolvedMatrixId = matrixInstanceId || matrixId;
|
||||
const data = await MatrixService.addUserToMatrix({
|
||||
rootUserId,
|
||||
matrixId,
|
||||
matrixInstanceId: resolvedMatrixId,
|
||||
matrixId: resolvedMatrixId,
|
||||
topNodeEmail,
|
||||
childUserId,
|
||||
forceParentFallback,
|
||||
@ -102,10 +105,67 @@ async function addUser(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: user-facing matrix overview anchored at requester
|
||||
async function getMyOverview(req, res) {
|
||||
try {
|
||||
const userId = req.user?.id ?? req.user?.userId;
|
||||
const data = await MatrixService.getMyOverview({ userId, actorUser: req.user });
|
||||
return res.json({ success: true, data });
|
||||
} catch (err) {
|
||||
const status = err.status || 500;
|
||||
return res.status(status).json({ success: false, message: err.message || 'Could not load matrix overview' });
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: admin-only deactivate a matrix instance
|
||||
async function deactivate(req, res) {
|
||||
try {
|
||||
const matrixInstanceId =
|
||||
req.params.id ||
|
||||
req.body?.matrixInstanceId ||
|
||||
req.body?.matrixId ||
|
||||
req.query?.matrixInstanceId ||
|
||||
req.query?.matrixId;
|
||||
|
||||
const data = await MatrixService.deactivate({
|
||||
matrixInstanceId,
|
||||
actorUser: req.user
|
||||
});
|
||||
return res.json({ success: true, data });
|
||||
} catch (err) {
|
||||
const status = err.status || 500;
|
||||
return res.status(status).json({ success: false, message: err.message || 'Could not deactivate matrix' });
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: admin-only activate a matrix instance
|
||||
async function activate(req, res) {
|
||||
try {
|
||||
const matrixInstanceId =
|
||||
req.params.id ||
|
||||
req.body?.matrixInstanceId ||
|
||||
req.body?.matrixId ||
|
||||
req.query?.matrixInstanceId ||
|
||||
req.query?.matrixId;
|
||||
|
||||
const data = await MatrixService.activate({
|
||||
matrixInstanceId,
|
||||
actorUser: req.user
|
||||
});
|
||||
return res.json({ success: true, data });
|
||||
} catch (err) {
|
||||
const status = err.status || 500;
|
||||
return res.status(status).json({ success: false, message: err.message || 'Could not activate matrix' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
stats,
|
||||
getMatrixUserforAdmin,
|
||||
searchCandidates,
|
||||
addUser // NEW
|
||||
addUser, // NEW
|
||||
getMyOverview, // NEW
|
||||
deactivate, // NEW
|
||||
activate // NEW
|
||||
};
|
||||
|
||||
@ -565,12 +565,22 @@ async function createDatabase() {
|
||||
CREATE TABLE IF NOT EXISTS matrix_config (
|
||||
id TINYINT PRIMARY KEY DEFAULT 1,
|
||||
master_top_user_id INT NOT NULL,
|
||||
name VARCHAR(255) NULL, -- ADDED (was missing, caused Unknown column 'name')
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_matrix_config_master FOREIGN KEY (master_top_user_id) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT chk_matrix_singleton CHECK (id = 1)
|
||||
);
|
||||
`);
|
||||
// Safeguard: if table pre-existed without name column, add it
|
||||
try {
|
||||
await connection.query(`ALTER TABLE matrix_config ADD COLUMN name VARCHAR(255) NULL`);
|
||||
console.log('🆕 Added missing matrix_config.name column');
|
||||
} catch (e) {
|
||||
if (!/Duplicate column/i.test(e.message)) {
|
||||
console.log('ℹ️ matrix_config.name alter skipped:', e.message);
|
||||
}
|
||||
}
|
||||
console.log('✅ Matrix config table created/verified');
|
||||
|
||||
await connection.query(`
|
||||
@ -578,101 +588,196 @@ async function createDatabase() {
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
parent_user_id INT NOT NULL,
|
||||
child_user_id INT NOT NULL,
|
||||
position TINYINT NOT NULL,
|
||||
position INT NOT NULL, -- CHANGED: allow unlimited positions
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_edges_parent FOREIGN KEY (parent_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_edges_child FOREIGN KEY (child_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT uq_child UNIQUE (child_user_id),
|
||||
CONSTRAINT uq_parent_position UNIQUE (parent_user_id, position),
|
||||
CONSTRAINT chk_position CHECK (position BETWEEN 1 AND 5)
|
||||
CONSTRAINT uq_parent_position UNIQUE (parent_user_id, position)
|
||||
-- REMOVED: chk_position CHECK (position BETWEEN 1 AND 5)
|
||||
);
|
||||
`);
|
||||
console.log('✅ User tree edges table created/verified');
|
||||
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_tree_closure (
|
||||
ancestor_user_id INT NOT NULL,
|
||||
descendant_user_id INT NOT NULL,
|
||||
depth INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_closure PRIMARY KEY (ancestor_user_id, descendant_user_id),
|
||||
CONSTRAINT fk_closure_ancestor FOREIGN KEY (ancestor_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_closure_descendant FOREIGN KEY (descendant_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT chk_depth_nonneg CHECK (depth >= 0)
|
||||
);
|
||||
`);
|
||||
console.log('✅ User tree closure table created/verified');
|
||||
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_matrix_metadata (
|
||||
root_user_id INT PRIMARY KEY,
|
||||
ego_activated_at TIMESTAMP NULL,
|
||||
last_bfs_fill_at TIMESTAMP NULL,
|
||||
immediate_children_count INT DEFAULT 0,
|
||||
first_free_position TINYINT NULL,
|
||||
name VARCHAR(255) NULL, -- NEW: matrix display name
|
||||
is_active BOOLEAN DEFAULT TRUE, -- NEW: activation flag
|
||||
max_depth INT NULL, -- NEW: NULL=unlimited; otherwise enforce per root
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_matrix_meta_root FOREIGN KEY (root_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT chk_first_free_position CHECK (first_free_position IS NULL OR (first_free_position BETWEEN 1 AND 5))
|
||||
);
|
||||
`);
|
||||
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
|
||||
// --- Migration: relax position constraints on existing schemas ---
|
||||
try {
|
||||
await connection.query(`
|
||||
ALTER TABLE pools
|
||||
MODIFY COLUMN state ENUM('active','inactive','archived') DEFAULT 'active'
|
||||
`);
|
||||
console.log('🆙 Pools.state updated to include archived');
|
||||
await connection.query(`ALTER TABLE user_tree_edges MODIFY COLUMN position INT NOT NULL`);
|
||||
console.log('🛠️ user_tree_edges.position changed to INT');
|
||||
} catch (e) {
|
||||
console.log('ℹ️ Pools.state alteration skipped:', e.message);
|
||||
console.log('ℹ️ position type change 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');
|
||||
await connection.query(`ALTER TABLE user_tree_edges DROP CHECK chk_position`);
|
||||
console.log('🧹 Dropped CHECK constraint chk_position on user_tree_edges');
|
||||
} catch (e) {
|
||||
console.log('ℹ️ Pools.created_by add skipped:', e.message);
|
||||
// MySQL versions or engines may report different messages if CHECK is not enforced or named differently
|
||||
console.log('ℹ️ DROP CHECK chk_position skipped:', e.message);
|
||||
}
|
||||
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS matrix_instances (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
root_user_id INT NOT NULL,
|
||||
name VARCHAR(255) NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
max_depth INT NULL,
|
||||
ego_activated_at TIMESTAMP NULL,
|
||||
immediate_children_count INT DEFAULT 0,
|
||||
first_free_position TINYINT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_matrix_instances_root FOREIGN KEY (root_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT chk_first_free_pos CHECK (first_free_position IS NULL OR (first_free_position BETWEEN 1 AND 5))
|
||||
);
|
||||
`);
|
||||
console.log('✅ matrix_instances table created/verified');
|
||||
|
||||
// Legacy: ensure legacy matrix_config exists (still needed short-term for migration)
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS matrix_config (
|
||||
id TINYINT PRIMARY KEY DEFAULT 1,
|
||||
master_top_user_id INT NOT NULL,
|
||||
name VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_matrix_config_master FOREIGN KEY (master_top_user_id) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
`);
|
||||
console.log('ℹ️ matrix_config (legacy) verified');
|
||||
|
||||
// --- user_matrix_metadata: add matrix_instance_id + alter PK ---
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_matrix_metadata (
|
||||
matrix_instance_id INT PRIMARY KEY,
|
||||
root_user_id INT NOT NULL,
|
||||
ego_activated_at TIMESTAMP NULL,
|
||||
last_bfs_fill_at TIMESTAMP NULL,
|
||||
immediate_children_count INT DEFAULT 0,
|
||||
first_free_position TINYINT NULL,
|
||||
name VARCHAR(255) NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
max_depth INT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_meta_instance FOREIGN KEY (matrix_instance_id) REFERENCES matrix_instances(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_meta_root FOREIGN KEY (root_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT chk_meta_first_free CHECK (first_free_position IS NULL OR (first_free_position BETWEEN 1 AND 5)),
|
||||
INDEX idx_meta_root (root_user_id)
|
||||
);
|
||||
`);
|
||||
console.log('✅ user_matrix_metadata (multi) created/verified');
|
||||
|
||||
// Migration: if legacy data without matrix_instances rows
|
||||
const [instCheck] = await connection.query(`SELECT COUNT(*) AS cnt FROM matrix_instances`);
|
||||
if (Number(instCheck[0].cnt) === 0) {
|
||||
const [legacyCfg] = await connection.query(`SELECT master_top_user_id, name FROM matrix_config WHERE id=1`);
|
||||
if (legacyCfg.length) {
|
||||
const legacyRoot = legacyCfg[0].master_top_user_id;
|
||||
const legacyName = legacyCfg[0].name || null;
|
||||
// Determine existing root children stats
|
||||
const [legacyEdges] = await connection.query(`SELECT position FROM user_tree_edges WHERE parent_user_id = ?`, [legacyRoot]);
|
||||
const usedRootPos = new Set(legacyEdges.map(r => Number(r.position)));
|
||||
let firstFree = null;
|
||||
for (let i = 1; i <= 5; i++) { if (!usedRootPos.has(i)) { firstFree = i; break; } }
|
||||
// Create initial instance
|
||||
const [instRes] = await connection.query(`
|
||||
INSERT INTO matrix_instances
|
||||
(root_user_id, name, is_active, max_depth, ego_activated_at, immediate_children_count, first_free_position)
|
||||
VALUES (?, ?, TRUE, NULL, NOW(), ?, NULL) -- CHANGED: first_free_position NULL for root
|
||||
`, [legacyRoot, legacyName, legacyEdges.length, /* firstFree removed */ null]);
|
||||
const firstInstanceId = instRes.insertId;
|
||||
// Backfill metadata
|
||||
await connection.query(`
|
||||
INSERT INTO user_matrix_metadata
|
||||
(matrix_instance_id, root_user_id, ego_activated_at, immediate_children_count, first_free_position, name, is_active, max_depth)
|
||||
VALUES (?, ?, NOW(), ?, ?, ?, TRUE, NULL)
|
||||
`, [firstInstanceId, legacyRoot, legacyEdges.length, firstFree, legacyName]);
|
||||
console.log('🧩 Migration: created first matrix_instance id=', firstInstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Legacy cleanup: remove old matrix_id / fk_edges_matrix referencing obsolete `matrices` table ---
|
||||
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');
|
||||
const [legacyCols] = await connection.query(`SHOW COLUMNS FROM user_tree_edges LIKE 'matrix_id'`);
|
||||
if (legacyCols.length) {
|
||||
console.log('🧹 Found legacy user_tree_edges.matrix_id; dropping old FK & column');
|
||||
try { await connection.query(`ALTER TABLE user_tree_edges DROP FOREIGN KEY fk_edges_matrix`); } catch (e) {
|
||||
console.log('ℹ️ fk_edges_matrix drop skipped:', e.message);
|
||||
}
|
||||
try { await connection.query(`ALTER TABLE user_tree_edges DROP COLUMN matrix_id`); } catch (e) {
|
||||
console.log('ℹ️ matrix_id column drop skipped:', e.message);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('ℹ️ Pools.updated_by add skipped:', e.message);
|
||||
console.log('ℹ️ Legacy matrix_id cleanup check failed:', e.message);
|
||||
}
|
||||
|
||||
// --- Ensure multi-instance columns (idempotent) ---
|
||||
try { await connection.query(`ALTER TABLE user_tree_edges ADD COLUMN matrix_instance_id INT NULL`); } catch (_) {}
|
||||
try { await connection.query(`ALTER TABLE user_tree_edges ADD COLUMN rogue_user BOOLEAN DEFAULT FALSE`); } catch (_) {}
|
||||
try {
|
||||
await connection.query(`
|
||||
ALTER TABLE user_tree_edges
|
||||
ADD CONSTRAINT fk_edges_instance FOREIGN KEY (matrix_instance_id)
|
||||
REFERENCES matrix_instances(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
`);
|
||||
console.log('🆕 FK fk_edges_instance added');
|
||||
} catch (e) {
|
||||
console.log('ℹ️ fk_edges_instance already exists or failed:', e.message);
|
||||
}
|
||||
|
||||
// Backfill matrix_instance_id for existing edges
|
||||
const [firstInstRow] = await connection.query(`SELECT id FROM matrix_instances ORDER BY id ASC LIMIT 1`);
|
||||
const firstInstanceId = firstInstRow[0]?.id;
|
||||
if (firstInstanceId) {
|
||||
await connection.query(`UPDATE user_tree_edges SET matrix_instance_id = ? WHERE matrix_instance_id IS NULL`, [firstInstanceId]);
|
||||
}
|
||||
|
||||
// Indexes (idempotent)
|
||||
try { await connection.query(`CREATE INDEX idx_edges_instance_parent ON user_tree_edges (matrix_instance_id, parent_user_id)`); } catch (_) {}
|
||||
try { await connection.query(`CREATE INDEX idx_edges_instance_child ON user_tree_edges (matrix_instance_id, child_user_id)`); } catch (_) {}
|
||||
try { await connection.query(`CREATE INDEX idx_edges_rogue ON user_tree_edges (matrix_instance_id, rogue_user)`); } catch (_) {}
|
||||
|
||||
// --- Alter user_tree_closure: add matrix_instance_id ---
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_tree_closure (
|
||||
matrix_instance_id INT NOT NULL,
|
||||
ancestor_user_id INT NOT NULL,
|
||||
descendant_user_id INT NOT NULL,
|
||||
depth INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_closure PRIMARY KEY (matrix_instance_id, ancestor_user_id, descendant_user_id),
|
||||
CONSTRAINT fk_closure_instance FOREIGN KEY (matrix_instance_id) REFERENCES matrix_instances(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_closure_ancestor FOREIGN KEY (ancestor_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT fk_closure_descendant FOREIGN KEY (descendant_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT chk_depth_nonneg CHECK (depth >= 0)
|
||||
);
|
||||
`);
|
||||
console.log('✅ user_tree_closure (multi) created/verified');
|
||||
|
||||
// If legacy closure rows without matrix_instance_id (older separate table definition), attempt add column & backfill
|
||||
try {
|
||||
await connection.query(`ALTER TABLE user_tree_closure ADD COLUMN matrix_instance_id INT NOT NULL`);
|
||||
console.log('🆕 Added matrix_instance_id column to existing user_tree_closure');
|
||||
} catch (e) {
|
||||
// already integrated new definition
|
||||
}
|
||||
if (firstInstanceId) {
|
||||
try {
|
||||
await connection.query(`UPDATE user_tree_closure SET matrix_instance_id = ? WHERE matrix_instance_id IS NULL`, [firstInstanceId]);
|
||||
console.log('🧩 Backfilled closure matrix_instance_id');
|
||||
} catch (e) {
|
||||
console.log('ℹ️ Closure backfill skipped:', e.message);
|
||||
}
|
||||
}
|
||||
try { await connection.query(`CREATE INDEX idx_closure_instance_depth ON user_tree_closure (matrix_instance_id, depth)`); } catch (_) {}
|
||||
try { await connection.query(`CREATE INDEX idx_closure_instance_ancestor ON user_tree_closure (matrix_instance_id, ancestor_user_id)`); } catch (_) {}
|
||||
|
||||
// Remove singleton constraint if present (best effort)
|
||||
try {
|
||||
await connection.query(`ALTER TABLE matrix_config DROP CHECK chk_matrix_singleton`);
|
||||
console.log('🧹 Dropped chk_matrix_singleton');
|
||||
} catch (e) {
|
||||
console.log('ℹ️ chk_matrix_singleton drop skipped:', e.message);
|
||||
}
|
||||
|
||||
console.log('🎉 Normalized database schema created/updated successfully!');
|
||||
|
||||
@ -9,7 +9,8 @@ class Matrix {
|
||||
egoActivatedAt,
|
||||
lastBfsFillAt,
|
||||
immediateChildrenCount,
|
||||
firstFreePosition
|
||||
firstFreePosition,
|
||||
matrixInstanceId // ADDED (was missing → ReferenceError)
|
||||
}) {
|
||||
this.name = typeof name === 'string' ? name : null;
|
||||
this.masterTopUserId = masterTopUserId !== undefined ? Number(masterTopUserId) : null;
|
||||
@ -22,6 +23,7 @@ class Matrix {
|
||||
this.firstFreePosition = firstFreePosition !== undefined
|
||||
? (firstFreePosition === null ? null : Number(firstFreePosition))
|
||||
: null;
|
||||
this.matrixInstanceId = matrixInstanceId !== undefined ? Number(matrixInstanceId) : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -116,6 +116,9 @@ router.get('/matrix/stats', authMiddleware, adminOnly, MatrixController.stats);
|
||||
router.get('/admin/matrix/users', authMiddleware, adminOnly, MatrixController.getMatrixUserforAdmin);
|
||||
router.get('/admin/matrix/user-candidates', authMiddleware, adminOnly, MatrixController.searchCandidates);
|
||||
|
||||
// NEW: Matrix overview for authenticated user
|
||||
router.get('/matrix/me/overview', authMiddleware, MatrixController.getMyOverview);
|
||||
|
||||
// NEW: Matrix POST (admin)
|
||||
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser);
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ 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
|
||||
const MatrixController = require('../controller/matrix/MatrixController'); // <-- new
|
||||
|
||||
// Helper middlewares for company-stamp
|
||||
function adminOnly(req, res, next) {
|
||||
@ -39,6 +40,10 @@ router.patch('/admin/update-user-status/:id', authMiddleware, adminOnly, AdminUs
|
||||
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);
|
||||
// NEW: deactivate a matrix instance (admin-only)
|
||||
router.patch('/admin/matrix/:id/deactivate', authMiddleware, adminOnly, MatrixController.deactivate);
|
||||
// NEW: activate a matrix instance (admin-only)
|
||||
router.patch('/admin/matrix/:id/activate', authMiddleware, adminOnly, MatrixController.activate);
|
||||
|
||||
// Personal profile (self-service) - no admin guard
|
||||
router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic);
|
||||
|
||||
@ -19,7 +19,7 @@ function toBool(value, defaultVal = false) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
async function create({ name, topNodeEmail, force = false, actorUser }) {
|
||||
async function create({ name, topNodeEmail, actorUser }) { // force removed (new instance each time)
|
||||
if (!actorUser) {
|
||||
const err = new Error('Unauthorized');
|
||||
err.status = 401;
|
||||
@ -48,14 +48,13 @@ async function create({ name, topNodeEmail, force = false, actorUser }) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const res = await MatrixRepository.createMatrix({ name: trimmedName, topNodeEmail: email, force });
|
||||
|
||||
const res = await MatrixRepository.createMatrix({ name: trimmedName, topNodeEmail: email });
|
||||
return new Matrix({
|
||||
name: res.name,
|
||||
masterTopUserId: res.masterTopUserId,
|
||||
masterTopUserEmail: res.masterTopUserEmail,
|
||||
// also expose root user id for frontend
|
||||
rootUserId: res.masterTopUserId
|
||||
rootUserId: res.rootUserId,
|
||||
matrixInstanceId: res.matrixInstanceId
|
||||
});
|
||||
}
|
||||
|
||||
@ -78,29 +77,22 @@ async function getStats({ actorUser }) {
|
||||
activeMatrices: stats.activeMatrices,
|
||||
totalMatrices: stats.totalMatrices,
|
||||
totalUsersSubscribed: stats.totalUsersSubscribed,
|
||||
matrices: stats.matrices.map(m => {
|
||||
const rootId = Number(m.rootUserId);
|
||||
const matrixConfigId = m.matrixConfigId != null ? Number(m.matrixConfigId) : null;
|
||||
const matrixId = matrixConfigId != null ? matrixConfigId : rootId; // prefer config id when available
|
||||
return {
|
||||
// primary fields
|
||||
rootUserId: rootId,
|
||||
name: m.name,
|
||||
isActive: !!m.isActive,
|
||||
usersCount: m.usersCount,
|
||||
createdAt: m.createdAt, // equals ego_activated_at
|
||||
topNodeEmail: m.topNodeEmail,
|
||||
// compatibility and routing aliases for frontend
|
||||
root_user_id: rootId,
|
||||
matrixConfigId,
|
||||
matrixId,
|
||||
id: matrixId
|
||||
};
|
||||
})
|
||||
matrices: stats.matrices.map(m => ({
|
||||
matrixInstanceId: Number(m.matrixInstanceId),
|
||||
rootUserId: Number(m.rootUserId),
|
||||
name: m.name,
|
||||
isActive: !!m.isActive,
|
||||
usersCount: m.usersCount,
|
||||
rogueUsersCount: m.rogueUsersCount,
|
||||
createdAt: m.createdAt,
|
||||
topNodeEmail: m.topNodeEmail,
|
||||
matrixId: Number(m.matrixInstanceId), // backward compatibility
|
||||
id: Number(m.matrixInstanceId)
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
async function getUsers({ rootUserId, maxDepth, limit, offset, includeRoot, actorUser, matrixId, topNodeEmail }) {
|
||||
async function getUsers({ rootUserId, maxDepth, limit, offset, includeRoot, actorUser, matrixId, matrixInstanceId, topNodeEmail, rogueOnly }) {
|
||||
if (!actorUser) {
|
||||
const err = new Error('Unauthorized');
|
||||
err.status = 401;
|
||||
@ -112,11 +104,9 @@ async function getUsers({ rootUserId, maxDepth, limit, offset, includeRoot, acto
|
||||
throw err;
|
||||
}
|
||||
|
||||
// resolve root id from any of the accepted identifiers
|
||||
let rid = Number.parseInt(rootUserId, 10);
|
||||
if (!Number.isFinite(rid) || rid <= 0) {
|
||||
rid = await MatrixRepository.resolveRootUserId({ rootUserId, matrixId, topNodeEmail });
|
||||
}
|
||||
const resolved = await MatrixRepository.resolveRootUserId({ rootUserId, matrixId, matrixInstanceId, topNodeEmail });
|
||||
const rid = resolved.rootUserId;
|
||||
const mid = resolved.matrixInstanceId;
|
||||
|
||||
// NEW: do not clamp to 5 globally; repository will enforce per-root policy
|
||||
let depth = Number.parseInt(maxDepth ?? 5, 10);
|
||||
@ -134,24 +124,28 @@ async function getUsers({ rootUserId, maxDepth, limit, offset, includeRoot, acto
|
||||
|
||||
const users = await MatrixRepository.getMatrixUsers({
|
||||
rootUserId: rid,
|
||||
matrixInstanceId: mid,
|
||||
maxDepth: depth,
|
||||
limit: lim,
|
||||
offset: off,
|
||||
includeRoot: incRoot
|
||||
includeRoot: incRoot,
|
||||
rogueOnly: !!rogueOnly
|
||||
});
|
||||
|
||||
return {
|
||||
matrixInstanceId: mid,
|
||||
rootUserId: rid,
|
||||
maxDepth: depth,
|
||||
limit: lim,
|
||||
offset: off,
|
||||
includeRoot: incRoot,
|
||||
rogueOnly: !!rogueOnly,
|
||||
users
|
||||
};
|
||||
}
|
||||
|
||||
// NEW: search user candidates to add into a matrix
|
||||
async function getUserCandidates({ q, type, limit, offset, rootUserId, matrixId, topNodeEmail, actorUser }) {
|
||||
async function getUserCandidates({ q, type, limit, offset, rootUserId, matrixId, matrixInstanceId, topNodeEmail, actorUser }) {
|
||||
if (!actorUser) {
|
||||
const err = new Error('Unauthorized');
|
||||
err.status = 401;
|
||||
@ -181,23 +175,26 @@ async function getUserCandidates({ q, type, limit, offset, rootUserId, matrixId,
|
||||
q: query,
|
||||
type: normType,
|
||||
rootUserId: null,
|
||||
matrixInstanceId: null, // ADDED for consistency
|
||||
limit: lim,
|
||||
offset: off,
|
||||
total: 0,
|
||||
items: []
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve root id if not a valid positive number
|
||||
let rid = Number.parseInt(rootUserId, 10);
|
||||
if (!Number.isFinite(rid) || rid <= 0) {
|
||||
rid = await MatrixRepository.resolveRootUserId({ rootUserId, matrixId, topNodeEmail });
|
||||
}
|
||||
// Always resolve (covers legacy rootUserId only case)
|
||||
const { rootUserId: rid, matrixInstanceId: mid } = await MatrixRepository.resolveRootUserId({
|
||||
rootUserId,
|
||||
matrixId,
|
||||
matrixInstanceId,
|
||||
topNodeEmail
|
||||
});
|
||||
|
||||
const { total, items } = await MatrixRepository.getUserSearchCandidates({
|
||||
q: query,
|
||||
type: normType,
|
||||
rootUserId: rid,
|
||||
matrixInstanceId: mid,
|
||||
limit: lim,
|
||||
offset: off
|
||||
});
|
||||
@ -206,6 +203,7 @@ async function getUserCandidates({ q, type, limit, offset, rootUserId, matrixId,
|
||||
q: query,
|
||||
type: normType,
|
||||
rootUserId: rid,
|
||||
matrixInstanceId: mid,
|
||||
limit: lim,
|
||||
offset: off,
|
||||
total,
|
||||
@ -216,6 +214,7 @@ async function getUserCandidates({ q, type, limit, offset, rootUserId, matrixId,
|
||||
async function addUserToMatrix({
|
||||
rootUserId,
|
||||
matrixId,
|
||||
matrixInstanceId,
|
||||
topNodeEmail,
|
||||
childUserId,
|
||||
forceParentFallback,
|
||||
@ -244,16 +243,17 @@ async function addUserToMatrix({
|
||||
const summary = await MatrixRepository.addUserToMatrix({
|
||||
rootUserId,
|
||||
matrixId,
|
||||
matrixInstanceId,
|
||||
topNodeEmail,
|
||||
childUserId: cId,
|
||||
forceParentFallback: fallback,
|
||||
forceParentFallback: fallback, // respected by repository for root fallback
|
||||
parentUserId: parentOverride,
|
||||
actorUserId: actorUser.id
|
||||
});
|
||||
|
||||
// fetch a small updated list (depth 2, limit 25)
|
||||
const users = await MatrixRepository.getMatrixUsers({
|
||||
rootUserId: summary.rootUserId,
|
||||
matrixInstanceId: summary.matrixInstanceId,
|
||||
maxDepth: 2,
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
@ -266,10 +266,205 @@ async function addUserToMatrix({
|
||||
};
|
||||
}
|
||||
|
||||
function maskName(name, email) {
|
||||
const safe = (s) => (typeof s === 'string' ? s.trim() : '');
|
||||
const n = safe(name);
|
||||
if (n) {
|
||||
const parts = n.split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 1) {
|
||||
const fi = parts[0][0]?.toLowerCase() || '';
|
||||
return fi ? `${fi}…` : '…';
|
||||
}
|
||||
const first = parts[0][0]?.toLowerCase() || '';
|
||||
const last = parts[parts.length - 1][0]?.toLowerCase() || '';
|
||||
return `${first}… ${last}…`.trim();
|
||||
}
|
||||
const em = safe(email);
|
||||
if (em && em.includes('@')) {
|
||||
const [local, domain] = em.split('@');
|
||||
const fi = local?.[0]?.toLowerCase() || '';
|
||||
const li = domain?.[0]?.toLowerCase() || '';
|
||||
return `${fi}… ${li}…`.trim();
|
||||
}
|
||||
return '…';
|
||||
}
|
||||
|
||||
// NEW: user-facing overview anchored at requester
|
||||
async function getMyOverview({ userId, actorUser }) {
|
||||
if (!actorUser) {
|
||||
const err = new Error('Unauthorized');
|
||||
err.status = 401;
|
||||
throw err;
|
||||
}
|
||||
const uid = Number(userId);
|
||||
if (!Number.isFinite(uid) || uid <= 0) {
|
||||
const err = new Error('Invalid user');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Resolve the matrix instance that includes this user
|
||||
const resolved = await MatrixRepository.resolveInstanceForUser(uid);
|
||||
if (!resolved) {
|
||||
return {
|
||||
matrixInstanceId: null,
|
||||
totalUsersUnderMe: 0,
|
||||
levelsFilled: 0,
|
||||
immediateChildrenCount: 0,
|
||||
rootSlotsRemaining: null,
|
||||
level1: [],
|
||||
level2Plus: []
|
||||
};
|
||||
}
|
||||
const mid = resolved.matrixInstanceId;
|
||||
|
||||
// Load instance policy and root user
|
||||
const instanceInfo = await MatrixRepository.getInstanceInfo(mid); // helper added below
|
||||
const policyDepth = instanceInfo?.max_depth == null ? 5 : Number(instanceInfo.max_depth);
|
||||
const rootUserId = Number(instanceInfo?.root_user_id || 0);
|
||||
const isRoot = rootUserId === uid;
|
||||
|
||||
// Fetch descendants anchored at requester
|
||||
const users = await MatrixRepository.getMatrixUsers({
|
||||
rootUserId: uid,
|
||||
matrixInstanceId: mid,
|
||||
maxDepth: policyDepth,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
includeRoot: false
|
||||
});
|
||||
|
||||
// Bucket by depth
|
||||
const depthBuckets = new Map();
|
||||
for (const u of users) {
|
||||
const d = Number(u.depth);
|
||||
if (!depthBuckets.has(d)) depthBuckets.set(d, []);
|
||||
depthBuckets.get(d).push(u);
|
||||
}
|
||||
|
||||
const immediateChildren = depthBuckets.get(1) || [];
|
||||
// Order level1 by position asc, then createdAt
|
||||
immediateChildren.sort((a, b) => {
|
||||
const pa = (a.position ?? 999999);
|
||||
const pb = (b.position ?? 999999);
|
||||
if (pa !== pb) return pa - pb;
|
||||
return (new Date(a.createdAt) - new Date(b.createdAt));
|
||||
});
|
||||
const level1 = immediateChildren.slice(0, 5).map(u => ({
|
||||
userId: u.userId,
|
||||
email: u.email,
|
||||
name: u.name || u.email,
|
||||
position: u.position ?? null
|
||||
}));
|
||||
|
||||
const level2PlusRaw = users.filter(u => u.depth >= 2);
|
||||
const level2Plus = level2PlusRaw.map(u => ({
|
||||
userId: u.userId,
|
||||
depth: u.depth,
|
||||
name: maskName(u.name, u.email)
|
||||
}));
|
||||
|
||||
// levelsFilled = highest depth having at least one user (bounded by policyDepth)
|
||||
let levelsFilled = 0;
|
||||
for (let d = 1; d <= policyDepth; d++) {
|
||||
if ((depthBuckets.get(d) || []).length > 0) levelsFilled = d;
|
||||
}
|
||||
|
||||
// rootSlotsRemaining: only meaningful for root; compute among positions 1..5 under root
|
||||
let rootSlotsRemaining = null;
|
||||
if (isRoot) {
|
||||
const firstFiveUsed = immediateChildren
|
||||
.map(u => Number(u.position))
|
||||
.filter(p => Number.isFinite(p) && p >= 1 && p <= 5);
|
||||
const unique = new Set(firstFiveUsed);
|
||||
rootSlotsRemaining = Math.max(0, 5 - unique.size);
|
||||
}
|
||||
|
||||
return {
|
||||
matrixInstanceId: mid,
|
||||
totalUsersUnderMe: users.length,
|
||||
levelsFilled,
|
||||
immediateChildrenCount: immediateChildren.length,
|
||||
rootSlotsRemaining,
|
||||
level1,
|
||||
level2Plus
|
||||
};
|
||||
}
|
||||
|
||||
// NEW: deactivate a matrix instance (admin-only)
|
||||
async function deactivate({ matrixInstanceId, matrixId, rootUserId, topNodeEmail, actorUser }) {
|
||||
if (!actorUser) {
|
||||
const err = new Error('Unauthorized');
|
||||
err.status = 401;
|
||||
throw err;
|
||||
}
|
||||
if (!isAdmin(actorUser)) {
|
||||
const err = new Error('Forbidden: Admins only');
|
||||
err.status = 403;
|
||||
throw err;
|
||||
}
|
||||
|
||||
let instanceId = Number.parseInt(matrixInstanceId ?? matrixId, 10);
|
||||
if (!Number.isFinite(instanceId) || instanceId <= 0) {
|
||||
// Try to resolve via other hints
|
||||
const resolved = await MatrixRepository.resolveRootUserId({
|
||||
rootUserId,
|
||||
matrixId,
|
||||
matrixInstanceId,
|
||||
topNodeEmail
|
||||
});
|
||||
instanceId = resolved.matrixInstanceId;
|
||||
}
|
||||
|
||||
const res = await MatrixRepository.deactivateInstance(instanceId);
|
||||
return {
|
||||
matrixInstanceId: res.matrixInstanceId,
|
||||
wasActive: res.wasActive,
|
||||
isActive: res.isActive,
|
||||
status: res.status
|
||||
};
|
||||
}
|
||||
|
||||
// NEW: activate a matrix instance (admin-only)
|
||||
async function activate({ matrixInstanceId, matrixId, rootUserId, topNodeEmail, actorUser }) {
|
||||
if (!actorUser) {
|
||||
const err = new Error('Unauthorized');
|
||||
err.status = 401;
|
||||
throw err;
|
||||
}
|
||||
if (!isAdmin(actorUser)) {
|
||||
const err = new Error('Forbidden: Admins only');
|
||||
err.status = 403;
|
||||
throw err;
|
||||
}
|
||||
|
||||
let instanceId = Number.parseInt(matrixInstanceId ?? matrixId, 10);
|
||||
if (!Number.isFinite(instanceId) || instanceId <= 0) {
|
||||
const resolved = await MatrixRepository.resolveRootUserId({
|
||||
rootUserId,
|
||||
matrixId,
|
||||
matrixInstanceId,
|
||||
topNodeEmail
|
||||
});
|
||||
instanceId = resolved.matrixInstanceId;
|
||||
}
|
||||
|
||||
const res = await MatrixRepository.activateInstance(instanceId);
|
||||
return {
|
||||
matrixInstanceId: res.matrixInstanceId,
|
||||
wasActive: res.wasActive,
|
||||
isActive: res.isActive,
|
||||
status: res.status
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getStats,
|
||||
getUsers,
|
||||
getUserCandidates,
|
||||
addUserToMatrix // NEW
|
||||
addUserToMatrix,
|
||||
getMyOverview, // NEW
|
||||
deactivate, // NEW
|
||||
activate // NEW
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user