feat: matrix adjustments

This commit is contained in:
DeathKaioken 2025-11-30 12:20:36 +01:00
parent 1fce1f1831
commit 9f5458f0a8
7 changed files with 1043 additions and 507 deletions

View File

@ -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
};

View File

@ -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!');

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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
};