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
|
// NEW: admin-only list of matrix users under a root
|
||||||
async function getMatrixUserforAdmin(req, res) {
|
async function getMatrixUserforAdmin(req, res) {
|
||||||
try {
|
try {
|
||||||
const { rootUserId, depth, limit, offset, includeRoot } = req.query;
|
const { rootUserId, depth, limit, offset, includeRoot, rogueOnly } = req.query;
|
||||||
// aliases accepted by backend for convenience
|
const matrixInstanceId = req.query.matrixInstanceId || req.query.matrixId || req.query.id;
|
||||||
const matrixId = req.query.matrixId || req.query.id;
|
|
||||||
const topNodeEmail = req.query.topNodeEmail || req.query.email;
|
const topNodeEmail = req.query.topNodeEmail || req.query.email;
|
||||||
|
|
||||||
const data = await MatrixService.getUsers({
|
const data = await MatrixService.getUsers({
|
||||||
@ -40,8 +39,10 @@ async function getMatrixUserforAdmin(req, res) {
|
|||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
includeRoot,
|
includeRoot,
|
||||||
|
rogueOnly: String(rogueOnly || '').toLowerCase() === 'true',
|
||||||
actorUser: req.user,
|
actorUser: req.user,
|
||||||
matrixId,
|
matrixInstanceId,
|
||||||
|
matrixId: matrixInstanceId, // backward alias
|
||||||
topNodeEmail
|
topNodeEmail
|
||||||
});
|
});
|
||||||
return res.json({ success: true, data });
|
return res.json({ success: true, data });
|
||||||
@ -55,20 +56,19 @@ async function getMatrixUserforAdmin(req, res) {
|
|||||||
async function searchCandidates(req, res) {
|
async function searchCandidates(req, res) {
|
||||||
try {
|
try {
|
||||||
const { q, type, limit, offset, rootUserId } = req.query;
|
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 topNodeEmail = req.query.topNodeEmail || req.query.email;
|
||||||
|
|
||||||
const data = await MatrixService.getUserCandidates({
|
const data = await MatrixService.getUserCandidates({
|
||||||
q,
|
q,
|
||||||
type,
|
type,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
rootUserId,
|
rootUserId,
|
||||||
matrixId,
|
matrixInstanceId,
|
||||||
|
matrixId: matrixInstanceId,
|
||||||
topNodeEmail,
|
topNodeEmail,
|
||||||
actorUser: req.user
|
actorUser: req.user
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.json({ success: true, data });
|
return res.json({ success: true, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const status = err.status || 500;
|
const status = err.status || 500;
|
||||||
@ -80,15 +80,18 @@ async function addUser(req, res) {
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
rootUserId,
|
rootUserId,
|
||||||
|
matrixInstanceId,
|
||||||
matrixId,
|
matrixId,
|
||||||
topNodeEmail,
|
topNodeEmail,
|
||||||
childUserId,
|
childUserId,
|
||||||
forceParentFallback,
|
forceParentFallback,
|
||||||
parentUserId
|
parentUserId
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
const resolvedMatrixId = matrixInstanceId || matrixId;
|
||||||
const data = await MatrixService.addUserToMatrix({
|
const data = await MatrixService.addUserToMatrix({
|
||||||
rootUserId,
|
rootUserId,
|
||||||
matrixId,
|
matrixInstanceId: resolvedMatrixId,
|
||||||
|
matrixId: resolvedMatrixId,
|
||||||
topNodeEmail,
|
topNodeEmail,
|
||||||
childUserId,
|
childUserId,
|
||||||
forceParentFallback,
|
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 = {
|
module.exports = {
|
||||||
create,
|
create,
|
||||||
stats,
|
stats,
|
||||||
getMatrixUserforAdmin,
|
getMatrixUserforAdmin,
|
||||||
searchCandidates,
|
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 (
|
CREATE TABLE IF NOT EXISTS matrix_config (
|
||||||
id TINYINT PRIMARY KEY DEFAULT 1,
|
id TINYINT PRIMARY KEY DEFAULT 1,
|
||||||
master_top_user_id INT NOT NULL,
|
master_top_user_id INT NOT NULL,
|
||||||
|
name VARCHAR(255) NULL, -- ADDED (was missing, caused Unknown column 'name')
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE 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 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)
|
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');
|
console.log('✅ Matrix config table created/verified');
|
||||||
|
|
||||||
await connection.query(`
|
await connection.query(`
|
||||||
@ -578,101 +588,196 @@ async function createDatabase() {
|
|||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
parent_user_id INT NOT NULL,
|
parent_user_id INT NOT NULL,
|
||||||
child_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,
|
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_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 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_child UNIQUE (child_user_id),
|
||||||
CONSTRAINT uq_parent_position UNIQUE (parent_user_id, position),
|
CONSTRAINT uq_parent_position UNIQUE (parent_user_id, position)
|
||||||
CONSTRAINT chk_position CHECK (position BETWEEN 1 AND 5)
|
-- REMOVED: chk_position CHECK (position BETWEEN 1 AND 5)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
console.log('✅ User tree edges table created/verified');
|
console.log('✅ User tree edges table created/verified');
|
||||||
|
|
||||||
await connection.query(`
|
// --- Migration: relax position constraints on existing schemas ---
|
||||||
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
|
|
||||||
try {
|
try {
|
||||||
await connection.query(`
|
await connection.query(`ALTER TABLE user_tree_edges MODIFY COLUMN position INT NOT NULL`);
|
||||||
ALTER TABLE pools
|
console.log('🛠️ user_tree_edges.position changed to INT');
|
||||||
MODIFY COLUMN state ENUM('active','inactive','archived') DEFAULT 'active'
|
|
||||||
`);
|
|
||||||
console.log('🆙 Pools.state updated to include archived');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('ℹ️ Pools.state alteration skipped:', e.message);
|
console.log('ℹ️ position type change skipped:', e.message);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await connection.query(`ALTER TABLE pools ADD COLUMN created_by INT NOT NULL`);
|
await connection.query(`ALTER TABLE user_tree_edges DROP CHECK chk_position`);
|
||||||
await connection.query(`CREATE INDEX idx_pool_created_by ON pools (created_by)`);
|
console.log('🧹 Dropped CHECK constraint chk_position on user_tree_edges');
|
||||||
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) {
|
} 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 {
|
try {
|
||||||
await connection.query(`ALTER TABLE pools ADD COLUMN updated_by INT NULL`);
|
const [legacyCols] = await connection.query(`SHOW COLUMNS FROM user_tree_edges LIKE 'matrix_id'`);
|
||||||
await connection.query(`CREATE INDEX idx_pool_updated_by ON pools (updated_by)`);
|
if (legacyCols.length) {
|
||||||
await connection.query(`
|
console.log('🧹 Found legacy user_tree_edges.matrix_id; dropping old FK & column');
|
||||||
ALTER TABLE pools ADD CONSTRAINT fk_pools_updated_by
|
try { await connection.query(`ALTER TABLE user_tree_edges DROP FOREIGN KEY fk_edges_matrix`); } catch (e) {
|
||||||
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
|
console.log('ℹ️ fk_edges_matrix drop skipped:', e.message);
|
||||||
`);
|
}
|
||||||
console.log('🆕 Pools.updated_by added with FK and index');
|
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) {
|
} 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!');
|
console.log('🎉 Normalized database schema created/updated successfully!');
|
||||||
|
|||||||
@ -9,7 +9,8 @@ class Matrix {
|
|||||||
egoActivatedAt,
|
egoActivatedAt,
|
||||||
lastBfsFillAt,
|
lastBfsFillAt,
|
||||||
immediateChildrenCount,
|
immediateChildrenCount,
|
||||||
firstFreePosition
|
firstFreePosition,
|
||||||
|
matrixInstanceId // ADDED (was missing → ReferenceError)
|
||||||
}) {
|
}) {
|
||||||
this.name = typeof name === 'string' ? name : null;
|
this.name = typeof name === 'string' ? name : null;
|
||||||
this.masterTopUserId = masterTopUserId !== undefined ? Number(masterTopUserId) : null;
|
this.masterTopUserId = masterTopUserId !== undefined ? Number(masterTopUserId) : null;
|
||||||
@ -22,6 +23,7 @@ class Matrix {
|
|||||||
this.firstFreePosition = firstFreePosition !== undefined
|
this.firstFreePosition = firstFreePosition !== undefined
|
||||||
? (firstFreePosition === null ? null : Number(firstFreePosition))
|
? (firstFreePosition === null ? null : Number(firstFreePosition))
|
||||||
: null;
|
: 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/users', authMiddleware, adminOnly, MatrixController.getMatrixUserforAdmin);
|
||||||
router.get('/admin/matrix/user-candidates', authMiddleware, adminOnly, MatrixController.searchCandidates);
|
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)
|
// NEW: Matrix POST (admin)
|
||||||
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser);
|
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 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
|
const PoolController = require('../controller/pool/PoolController'); // <-- new
|
||||||
|
const MatrixController = require('../controller/matrix/MatrixController'); // <-- new
|
||||||
|
|
||||||
// Helper middlewares for company-stamp
|
// Helper middlewares for company-stamp
|
||||||
function adminOnly(req, res, next) {
|
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);
|
router.patch('/admin/coffee/:id/state', authMiddleware, adminOnly, CoffeeController.setState);
|
||||||
// NEW: Admin pool state update
|
// NEW: Admin pool state update
|
||||||
router.patch('/admin/pools/:id/state', authMiddleware, adminOnly, PoolController.updateState);
|
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
|
// Personal profile (self-service) - no admin guard
|
||||||
router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic);
|
router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic);
|
||||||
|
|||||||
@ -19,7 +19,7 @@ function toBool(value, defaultVal = false) {
|
|||||||
return defaultVal;
|
return defaultVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create({ name, topNodeEmail, force = false, actorUser }) {
|
async function create({ name, topNodeEmail, actorUser }) { // force removed (new instance each time)
|
||||||
if (!actorUser) {
|
if (!actorUser) {
|
||||||
const err = new Error('Unauthorized');
|
const err = new Error('Unauthorized');
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
@ -48,14 +48,13 @@ async function create({ name, topNodeEmail, force = false, actorUser }) {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await MatrixRepository.createMatrix({ name: trimmedName, topNodeEmail: email, force });
|
const res = await MatrixRepository.createMatrix({ name: trimmedName, topNodeEmail: email });
|
||||||
|
|
||||||
return new Matrix({
|
return new Matrix({
|
||||||
name: res.name,
|
name: res.name,
|
||||||
masterTopUserId: res.masterTopUserId,
|
masterTopUserId: res.masterTopUserId,
|
||||||
masterTopUserEmail: res.masterTopUserEmail,
|
masterTopUserEmail: res.masterTopUserEmail,
|
||||||
// also expose root user id for frontend
|
rootUserId: res.rootUserId,
|
||||||
rootUserId: res.masterTopUserId
|
matrixInstanceId: res.matrixInstanceId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,29 +77,22 @@ async function getStats({ actorUser }) {
|
|||||||
activeMatrices: stats.activeMatrices,
|
activeMatrices: stats.activeMatrices,
|
||||||
totalMatrices: stats.totalMatrices,
|
totalMatrices: stats.totalMatrices,
|
||||||
totalUsersSubscribed: stats.totalUsersSubscribed,
|
totalUsersSubscribed: stats.totalUsersSubscribed,
|
||||||
matrices: stats.matrices.map(m => {
|
matrices: stats.matrices.map(m => ({
|
||||||
const rootId = Number(m.rootUserId);
|
matrixInstanceId: Number(m.matrixInstanceId),
|
||||||
const matrixConfigId = m.matrixConfigId != null ? Number(m.matrixConfigId) : null;
|
rootUserId: Number(m.rootUserId),
|
||||||
const matrixId = matrixConfigId != null ? matrixConfigId : rootId; // prefer config id when available
|
name: m.name,
|
||||||
return {
|
isActive: !!m.isActive,
|
||||||
// primary fields
|
usersCount: m.usersCount,
|
||||||
rootUserId: rootId,
|
rogueUsersCount: m.rogueUsersCount,
|
||||||
name: m.name,
|
createdAt: m.createdAt,
|
||||||
isActive: !!m.isActive,
|
topNodeEmail: m.topNodeEmail,
|
||||||
usersCount: m.usersCount,
|
matrixId: Number(m.matrixInstanceId), // backward compatibility
|
||||||
createdAt: m.createdAt, // equals ego_activated_at
|
id: Number(m.matrixInstanceId)
|
||||||
topNodeEmail: m.topNodeEmail,
|
}))
|
||||||
// compatibility and routing aliases for frontend
|
|
||||||
root_user_id: rootId,
|
|
||||||
matrixConfigId,
|
|
||||||
matrixId,
|
|
||||||
id: matrixId
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (!actorUser) {
|
||||||
const err = new Error('Unauthorized');
|
const err = new Error('Unauthorized');
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
@ -112,11 +104,9 @@ async function getUsers({ rootUserId, maxDepth, limit, offset, includeRoot, acto
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve root id from any of the accepted identifiers
|
const resolved = await MatrixRepository.resolveRootUserId({ rootUserId, matrixId, matrixInstanceId, topNodeEmail });
|
||||||
let rid = Number.parseInt(rootUserId, 10);
|
const rid = resolved.rootUserId;
|
||||||
if (!Number.isFinite(rid) || rid <= 0) {
|
const mid = resolved.matrixInstanceId;
|
||||||
rid = await MatrixRepository.resolveRootUserId({ rootUserId, matrixId, topNodeEmail });
|
|
||||||
}
|
|
||||||
|
|
||||||
// NEW: do not clamp to 5 globally; repository will enforce per-root policy
|
// NEW: do not clamp to 5 globally; repository will enforce per-root policy
|
||||||
let depth = Number.parseInt(maxDepth ?? 5, 10);
|
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({
|
const users = await MatrixRepository.getMatrixUsers({
|
||||||
rootUserId: rid,
|
rootUserId: rid,
|
||||||
|
matrixInstanceId: mid,
|
||||||
maxDepth: depth,
|
maxDepth: depth,
|
||||||
limit: lim,
|
limit: lim,
|
||||||
offset: off,
|
offset: off,
|
||||||
includeRoot: incRoot
|
includeRoot: incRoot,
|
||||||
|
rogueOnly: !!rogueOnly
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
matrixInstanceId: mid,
|
||||||
rootUserId: rid,
|
rootUserId: rid,
|
||||||
maxDepth: depth,
|
maxDepth: depth,
|
||||||
limit: lim,
|
limit: lim,
|
||||||
offset: off,
|
offset: off,
|
||||||
includeRoot: incRoot,
|
includeRoot: incRoot,
|
||||||
|
rogueOnly: !!rogueOnly,
|
||||||
users
|
users
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW: search user candidates to add into a matrix
|
// 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) {
|
if (!actorUser) {
|
||||||
const err = new Error('Unauthorized');
|
const err = new Error('Unauthorized');
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
@ -181,23 +175,26 @@ async function getUserCandidates({ q, type, limit, offset, rootUserId, matrixId,
|
|||||||
q: query,
|
q: query,
|
||||||
type: normType,
|
type: normType,
|
||||||
rootUserId: null,
|
rootUserId: null,
|
||||||
|
matrixInstanceId: null, // ADDED for consistency
|
||||||
limit: lim,
|
limit: lim,
|
||||||
offset: off,
|
offset: off,
|
||||||
total: 0,
|
total: 0,
|
||||||
items: []
|
items: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Always resolve (covers legacy rootUserId only case)
|
||||||
// Resolve root id if not a valid positive number
|
const { rootUserId: rid, matrixInstanceId: mid } = await MatrixRepository.resolveRootUserId({
|
||||||
let rid = Number.parseInt(rootUserId, 10);
|
rootUserId,
|
||||||
if (!Number.isFinite(rid) || rid <= 0) {
|
matrixId,
|
||||||
rid = await MatrixRepository.resolveRootUserId({ rootUserId, matrixId, topNodeEmail });
|
matrixInstanceId,
|
||||||
}
|
topNodeEmail
|
||||||
|
});
|
||||||
|
|
||||||
const { total, items } = await MatrixRepository.getUserSearchCandidates({
|
const { total, items } = await MatrixRepository.getUserSearchCandidates({
|
||||||
q: query,
|
q: query,
|
||||||
type: normType,
|
type: normType,
|
||||||
rootUserId: rid,
|
rootUserId: rid,
|
||||||
|
matrixInstanceId: mid,
|
||||||
limit: lim,
|
limit: lim,
|
||||||
offset: off
|
offset: off
|
||||||
});
|
});
|
||||||
@ -206,6 +203,7 @@ async function getUserCandidates({ q, type, limit, offset, rootUserId, matrixId,
|
|||||||
q: query,
|
q: query,
|
||||||
type: normType,
|
type: normType,
|
||||||
rootUserId: rid,
|
rootUserId: rid,
|
||||||
|
matrixInstanceId: mid,
|
||||||
limit: lim,
|
limit: lim,
|
||||||
offset: off,
|
offset: off,
|
||||||
total,
|
total,
|
||||||
@ -216,6 +214,7 @@ async function getUserCandidates({ q, type, limit, offset, rootUserId, matrixId,
|
|||||||
async function addUserToMatrix({
|
async function addUserToMatrix({
|
||||||
rootUserId,
|
rootUserId,
|
||||||
matrixId,
|
matrixId,
|
||||||
|
matrixInstanceId,
|
||||||
topNodeEmail,
|
topNodeEmail,
|
||||||
childUserId,
|
childUserId,
|
||||||
forceParentFallback,
|
forceParentFallback,
|
||||||
@ -244,16 +243,17 @@ async function addUserToMatrix({
|
|||||||
const summary = await MatrixRepository.addUserToMatrix({
|
const summary = await MatrixRepository.addUserToMatrix({
|
||||||
rootUserId,
|
rootUserId,
|
||||||
matrixId,
|
matrixId,
|
||||||
|
matrixInstanceId,
|
||||||
topNodeEmail,
|
topNodeEmail,
|
||||||
childUserId: cId,
|
childUserId: cId,
|
||||||
forceParentFallback: fallback,
|
forceParentFallback: fallback, // respected by repository for root fallback
|
||||||
parentUserId: parentOverride,
|
parentUserId: parentOverride,
|
||||||
actorUserId: actorUser.id
|
actorUserId: actorUser.id
|
||||||
});
|
});
|
||||||
|
|
||||||
// fetch a small updated list (depth 2, limit 25)
|
|
||||||
const users = await MatrixRepository.getMatrixUsers({
|
const users = await MatrixRepository.getMatrixUsers({
|
||||||
rootUserId: summary.rootUserId,
|
rootUserId: summary.rootUserId,
|
||||||
|
matrixInstanceId: summary.matrixInstanceId,
|
||||||
maxDepth: 2,
|
maxDepth: 2,
|
||||||
limit: 25,
|
limit: 25,
|
||||||
offset: 0,
|
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 = {
|
module.exports = {
|
||||||
create,
|
create,
|
||||||
getStats,
|
getStats,
|
||||||
getUsers,
|
getUsers,
|
||||||
getUserCandidates,
|
getUserCandidates,
|
||||||
addUserToMatrix // NEW
|
addUserToMatrix,
|
||||||
|
getMyOverview, // NEW
|
||||||
|
deactivate, // NEW
|
||||||
|
activate // NEW
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user