feat: add create / stats Matrix Management
This commit is contained in:
parent
14450df164
commit
88bd410922
32
controller/matrix/MatrixController.js
Normal file
32
controller/matrix/MatrixController.js
Normal file
@ -0,0 +1,32 @@
|
||||
const MatrixService = require('../../services/matrix/MatrixService');
|
||||
|
||||
async function create(req, res) {
|
||||
try {
|
||||
const { name, email, force } = req.query; // email of the top node account
|
||||
const result = await MatrixService.create({
|
||||
name,
|
||||
topNodeEmail: email,
|
||||
force: String(force || '').toLowerCase() === 'true',
|
||||
actorUser: req.user
|
||||
});
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (err) {
|
||||
const status = err.status || 500;
|
||||
return res.status(status).json({ success: false, message: err.message || 'Matrix creation failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async function stats(req, res) {
|
||||
try {
|
||||
const data = await MatrixService.getStats({ 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 stats' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
stats
|
||||
};
|
||||
@ -540,6 +540,79 @@ async function createDatabase() {
|
||||
`);
|
||||
console.log('✅ Company stamps table created/verified');
|
||||
|
||||
// --- Matrix: Global 5-ary tree config and relations ---
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS matrix_config (
|
||||
id TINYINT PRIMARY KEY DEFAULT 1,
|
||||
master_top_user_id INT NOT 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,
|
||||
CONSTRAINT chk_matrix_singleton CHECK (id = 1)
|
||||
);
|
||||
`);
|
||||
console.log('✅ Matrix config table created/verified');
|
||||
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_tree_edges (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
parent_user_id INT NOT NULL,
|
||||
child_user_id INT NOT NULL,
|
||||
position TINYINT NOT NULL,
|
||||
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)
|
||||
);
|
||||
`);
|
||||
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
|
||||
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');
|
||||
|
||||
// Ensure new columns exist if table already existed
|
||||
try {
|
||||
await connection.query(`ALTER TABLE user_matrix_metadata ADD COLUMN name VARCHAR(255) NULL`);
|
||||
console.log('🔧 Ensured user_matrix_metadata.name exists');
|
||||
} catch (e) {
|
||||
console.log('ℹ️ user_matrix_metadata.name already exists or ALTER not required');
|
||||
}
|
||||
try {
|
||||
await connection.query(`ALTER TABLE user_matrix_metadata ADD COLUMN is_active BOOLEAN DEFAULT TRUE`);
|
||||
console.log('🔧 Ensured user_matrix_metadata.is_active exists');
|
||||
} catch (e) {
|
||||
console.log('ℹ️ user_matrix_metadata.is_active already exists or ALTER not required');
|
||||
}
|
||||
|
||||
// --- Added Index Optimization Section ---
|
||||
try {
|
||||
// Core / status
|
||||
@ -576,6 +649,14 @@ async function createDatabase() {
|
||||
await ensureIndex(connection, 'document_templates', 'idx_document_templates_state_user_type', 'state, user_type'); // NEW composite index
|
||||
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_company', 'company_id');
|
||||
await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_active', 'is_active');
|
||||
|
||||
// Matrix indexes
|
||||
await ensureIndex(connection, 'user_tree_edges', 'idx_user_tree_edges_parent', 'parent_user_id');
|
||||
// child_user_id already has a UNIQUE constraint; extra index not needed
|
||||
await ensureIndex(connection, 'user_tree_closure', 'idx_user_tree_closure_ancestor_depth', 'ancestor_user_id, depth');
|
||||
await ensureIndex(connection, 'user_tree_closure', 'idx_user_tree_closure_descendant', 'descendant_user_id');
|
||||
await ensureIndex(connection, 'user_matrix_metadata', 'idx_user_matrix_is_active', 'is_active'); // NEW
|
||||
|
||||
console.log('🚀 Performance indexes created/verified');
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Index optimization phase encountered an issue:', e.message);
|
||||
|
||||
28
models/Matrix.js
Normal file
28
models/Matrix.js
Normal file
@ -0,0 +1,28 @@
|
||||
class Matrix {
|
||||
constructor({
|
||||
// config (create)
|
||||
name,
|
||||
masterTopUserId,
|
||||
masterTopUserEmail,
|
||||
// ego activation (optional legacy fields)
|
||||
rootUserId,
|
||||
egoActivatedAt,
|
||||
lastBfsFillAt,
|
||||
immediateChildrenCount,
|
||||
firstFreePosition
|
||||
}) {
|
||||
this.name = typeof name === 'string' ? name : null;
|
||||
this.masterTopUserId = masterTopUserId !== undefined ? Number(masterTopUserId) : null;
|
||||
this.masterTopUserEmail = masterTopUserEmail || null;
|
||||
|
||||
this.rootUserId = rootUserId !== undefined ? Number(rootUserId) : null;
|
||||
this.egoActivatedAt = egoActivatedAt || null;
|
||||
this.lastBfsFillAt = lastBfsFillAt || null;
|
||||
this.immediateChildrenCount = immediateChildrenCount !== undefined ? Number(immediateChildrenCount || 0) : null;
|
||||
this.firstFreePosition = firstFreePosition !== undefined
|
||||
? (firstFreePosition === null ? null : Number(firstFreePosition))
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Matrix;
|
||||
289
repositories/matrix/MatrixRepository.js
Normal file
289
repositories/matrix/MatrixRepository.js
Normal file
@ -0,0 +1,289 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
function getSSLConfig() {
|
||||
const useSSL = String(process.env.DB_SSL || '').toLowerCase() === 'true';
|
||||
const caPath = process.env.DB_SSL_CA_PATH;
|
||||
if (!useSSL) return undefined;
|
||||
try {
|
||||
if (caPath) {
|
||||
const resolved = path.resolve(caPath);
|
||||
if (fs.existsSync(resolved)) {
|
||||
return { ca: fs.readFileSync(resolved), rejectUnauthorized: false };
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return { rejectUnauthorized: false };
|
||||
}
|
||||
|
||||
let dbConfig;
|
||||
if (NODE_ENV === 'development') {
|
||||
dbConfig = {
|
||||
host: process.env.DEV_DB_HOST || 'localhost',
|
||||
port: Number(process.env.DEV_DB_PORT) || 3306,
|
||||
user: process.env.DEV_DB_USER || 'root',
|
||||
password: process.env.DEV_DB_PASSWORD || '',
|
||||
database: process.env.DEV_DB_NAME || 'profitplanet_centralserver',
|
||||
ssl: undefined
|
||||
};
|
||||
} else {
|
||||
dbConfig = {
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT) || 3306,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
ssl: getSSLConfig()
|
||||
};
|
||||
}
|
||||
|
||||
const pool = mysql.createPool({
|
||||
...dbConfig,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
async function ensureUserExistsByEmail(conn, email) {
|
||||
const [rows] = await conn.query('SELECT id, email FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1', [email]);
|
||||
if (!rows.length) {
|
||||
const err = new Error('Top node user not found');
|
||||
err.status = 404;
|
||||
throw err;
|
||||
}
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async function createMatrix({ name, topNodeEmail, force = false }) {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// ensure "name" column exists on matrix_config (safe if already exists)
|
||||
try {
|
||||
await conn.query(`ALTER TABLE matrix_config ADD COLUMN name VARCHAR(255) NULL`);
|
||||
} catch (_) {}
|
||||
|
||||
// ensure user_matrix_metadata has needed columns (safe if exists)
|
||||
try { await conn.query(`ALTER TABLE user_matrix_metadata ADD COLUMN name VARCHAR(255) NULL`); } catch (_) {}
|
||||
try { await conn.query(`ALTER TABLE user_matrix_metadata ADD COLUMN is_active BOOLEAN DEFAULT TRUE`); } catch (_) {}
|
||||
|
||||
const topUser = await ensureUserExistsByEmail(conn, topNodeEmail);
|
||||
|
||||
// lock matrix_config row if present
|
||||
const [cfgRows] = await conn.query(
|
||||
'SELECT id, master_top_user_id, name FROM matrix_config WHERE id = 1 FOR UPDATE'
|
||||
);
|
||||
|
||||
if (!cfgRows.length) {
|
||||
await conn.query(
|
||||
'INSERT INTO matrix_config (id, master_top_user_id, name) VALUES (1, ?, ?)',
|
||||
[topUser.id, name]
|
||||
);
|
||||
} else {
|
||||
const current = cfgRows[0];
|
||||
if (!force) {
|
||||
const err = new Error('Matrix already exists. Pass force=true to overwrite.');
|
||||
err.status = 409;
|
||||
throw err;
|
||||
}
|
||||
await conn.query(
|
||||
'UPDATE matrix_config SET master_top_user_id = ?, name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
|
||||
[topUser.id, name]
|
||||
);
|
||||
}
|
||||
|
||||
// ensure self-closure for the master top user
|
||||
await conn.query(
|
||||
'INSERT IGNORE INTO user_tree_closure (ancestor_user_id, descendant_user_id, depth) VALUES (?, ?, 0)',
|
||||
[topUser.id, topUser.id]
|
||||
);
|
||||
|
||||
// Also register this top user as an ego matrix entry with name and mark active
|
||||
await conn.query(
|
||||
`
|
||||
INSERT INTO user_matrix_metadata
|
||||
(root_user_id, ego_activated_at, last_bfs_fill_at, immediate_children_count, first_free_position, name, is_active)
|
||||
VALUES
|
||||
(?, NOW(), NULL, 0, 1, ?, TRUE)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
is_active = TRUE,
|
||||
ego_activated_at = COALESCE(user_matrix_metadata.ego_activated_at, VALUES(ego_activated_at)),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`,
|
||||
[topUser.id, name]
|
||||
);
|
||||
|
||||
await conn.commit();
|
||||
return {
|
||||
name,
|
||||
masterTopUserId: topUser.id,
|
||||
masterTopUserEmail: topUser.email
|
||||
};
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureUserExists(conn, userId) {
|
||||
const [rows] = await conn.query('SELECT id FROM users WHERE id = ?', [userId]);
|
||||
if (!rows.length) {
|
||||
const err = new Error('User not found');
|
||||
err.status = 404;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function activateEgoMatrix(rootUserId) {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
await ensureUserExists(conn, rootUserId);
|
||||
await conn.query(
|
||||
'INSERT IGNORE INTO user_tree_closure (ancestor_user_id, descendant_user_id, depth) VALUES (?, ?, 0)',
|
||||
[rootUserId, rootUserId]
|
||||
);
|
||||
const [childRows] = await conn.query(
|
||||
'SELECT position FROM user_tree_edges WHERE parent_user_id = ? ORDER BY position ASC',
|
||||
[rootUserId]
|
||||
);
|
||||
const positions = new Set(childRows.map(r => r.position));
|
||||
let firstFreePosition = null;
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
if (!positions.has(i)) { firstFreePosition = i; break; }
|
||||
}
|
||||
const immediateChildrenCount = childRows.length;
|
||||
await conn.query(
|
||||
`
|
||||
INSERT INTO user_matrix_metadata
|
||||
(root_user_id, ego_activated_at, last_bfs_fill_at, immediate_children_count, first_free_position)
|
||||
VALUES
|
||||
(?, NOW(), NULL, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
ego_activated_at = COALESCE(ego_activated_at, VALUES(ego_activated_at)),
|
||||
immediate_children_count = VALUES(immediate_children_count),
|
||||
first_free_position = VALUES(first_free_position),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`,
|
||||
[rootUserId, immediateChildrenCount, firstFreePosition]
|
||||
);
|
||||
const [metaRows] = await conn.query(
|
||||
'SELECT ego_activated_at, last_bfs_fill_at, immediate_children_count, first_free_position FROM user_matrix_metadata WHERE root_user_id = ?',
|
||||
[rootUserId]
|
||||
);
|
||||
const meta = metaRows[0] || {
|
||||
ego_activated_at: null,
|
||||
last_bfs_fill_at: null,
|
||||
immediate_children_count: immediateChildrenCount,
|
||||
first_free_position: firstFreePosition
|
||||
};
|
||||
await conn.commit();
|
||||
return {
|
||||
rootUserId,
|
||||
egoActivatedAt: meta.ego_activated_at,
|
||||
lastBfsFillAt: meta.last_bfs_fill_at,
|
||||
immediateChildrenCount: meta.immediate_children_count,
|
||||
firstFreePosition: meta.first_free_position
|
||||
};
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function getMatrixStats() {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
// list all matrices (ego entries)
|
||||
const [matRows] = await conn.query(
|
||||
`
|
||||
SELECT m.root_user_id, m.name, m.is_active, m.ego_activated_at, u.email
|
||||
FROM user_matrix_metadata m
|
||||
JOIN users u ON u.id = m.root_user_id
|
||||
ORDER BY COALESCE(m.ego_activated_at, u.created_at) ASC
|
||||
`
|
||||
);
|
||||
|
||||
const rootIds = matRows.map(r => r.root_user_id);
|
||||
let countsByRoot = new Map();
|
||||
|
||||
if (rootIds.length) {
|
||||
// counts per matrix up to depth 5
|
||||
const [cntRows] = await conn.query(
|
||||
`
|
||||
SELECT ancestor_user_id AS root_id, COUNT(*) AS cnt
|
||||
FROM user_tree_closure
|
||||
WHERE depth BETWEEN 0 AND 5
|
||||
AND ancestor_user_id IN (?)
|
||||
GROUP BY ancestor_user_id
|
||||
`,
|
||||
[rootIds]
|
||||
);
|
||||
countsByRoot = new Map(cntRows.map(r => [Number(r.root_id), Number(r.cnt)]));
|
||||
|
||||
// total distinct users across all active matrices
|
||||
const [activeIdsRows] = await conn.query(
|
||||
`SELECT root_user_id FROM user_matrix_metadata WHERE is_active = TRUE AND root_user_id IN (?)`,
|
||||
[rootIds]
|
||||
);
|
||||
const activeIds = activeIdsRows.map(r => r.root_user_id);
|
||||
let totalDistinct = 0;
|
||||
if (activeIds.length) {
|
||||
const [totalDistinctRows] = await conn.query(
|
||||
`
|
||||
SELECT COUNT(DISTINCT descendant_user_id) AS total
|
||||
FROM user_tree_closure
|
||||
WHERE depth BETWEEN 0 AND 5
|
||||
AND ancestor_user_id IN (?)
|
||||
`,
|
||||
[activeIds]
|
||||
);
|
||||
totalDistinct = Number(totalDistinctRows[0]?.total || 0);
|
||||
}
|
||||
|
||||
const activeMatrices = matRows.filter(r => !!r.is_active).length;
|
||||
const totalMatrices = matRows.length;
|
||||
|
||||
const matrices = matRows.map(r => ({
|
||||
rootUserId: r.root_user_id,
|
||||
name: r.name || null,
|
||||
isActive: !!r.is_active,
|
||||
usersCount: countsByRoot.get(Number(r.root_user_id)) || 0,
|
||||
createdAt: r.ego_activated_at || null,
|
||||
topNodeEmail: r.email
|
||||
}));
|
||||
|
||||
return {
|
||||
activeMatrices,
|
||||
totalMatrices,
|
||||
totalUsersSubscribed: totalDistinct,
|
||||
matrices
|
||||
};
|
||||
}
|
||||
|
||||
// no matrices registered
|
||||
return {
|
||||
activeMatrices: 0,
|
||||
totalMatrices: 0,
|
||||
totalUsersSubscribed: 0,
|
||||
matrices: []
|
||||
};
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createMatrix,
|
||||
activateEgoMatrix,
|
||||
getMatrixStats
|
||||
};
|
||||
@ -14,6 +14,7 @@ const ServerStatusController = require('../controller/admin/ServerStatusControll
|
||||
const UserController = require('../controller/auth/UserController');
|
||||
const UserStatusController = require('../controller/auth/UserStatusController');
|
||||
const CompanyStampController = require('../controller/companyStamp/CompanyStampController'); // <-- added
|
||||
const MatrixController = require('../controller/matrix/MatrixController'); // <-- added
|
||||
|
||||
// small helpers copied from original files
|
||||
function adminOnly(req, res, next) {
|
||||
@ -96,5 +97,9 @@ router.get('/api/document-templates', authMiddleware, adminOnly, DocumentTemplat
|
||||
router.get('/company-stamps/mine', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.listMine);
|
||||
router.get('/company-stamps/mine/active', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.activeMine);
|
||||
|
||||
// Matrix GETs
|
||||
router.get('/matrix/create', authMiddleware, adminOnly, MatrixController.create); // ?name=...&email=...&force=true
|
||||
router.get('/matrix/stats', authMiddleware, adminOnly, MatrixController.stats); // NEW: real stats for dashboard
|
||||
|
||||
// export
|
||||
module.exports = router;
|
||||
83
services/matrix/MatrixService.js
Normal file
83
services/matrix/MatrixService.js
Normal file
@ -0,0 +1,83 @@
|
||||
const MatrixRepository = require('../../repositories/matrix/MatrixRepository');
|
||||
const Matrix = require('../../models/Matrix');
|
||||
|
||||
function isAdmin(user) {
|
||||
return !!user && ['admin', 'super_admin'].includes(user.role);
|
||||
}
|
||||
|
||||
function isValidEmail(s) {
|
||||
return typeof s === 'string' && /\S+@\S+\.\S+/.test(s);
|
||||
}
|
||||
|
||||
async function create({ name, topNodeEmail, force = false, 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;
|
||||
}
|
||||
const trimmedName = (name || '').trim();
|
||||
if (!trimmedName) {
|
||||
const err = new Error('Matrix name is required');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
if (trimmedName.length > 255) {
|
||||
const err = new Error('Matrix name is too long');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
const email = (topNodeEmail || '').trim().toLowerCase();
|
||||
if (!isValidEmail(email)) {
|
||||
const err = new Error('Valid top node email is required');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const res = await MatrixRepository.createMatrix({ name: trimmedName, topNodeEmail: email, force });
|
||||
|
||||
return new Matrix({
|
||||
name: res.name,
|
||||
masterTopUserId: res.masterTopUserId,
|
||||
masterTopUserEmail: res.masterTopUserEmail
|
||||
});
|
||||
}
|
||||
|
||||
async function getStats({ 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;
|
||||
}
|
||||
|
||||
const stats = await MatrixRepository.getMatrixStats();
|
||||
|
||||
// Keep the response shape straightforward for the dashboard
|
||||
return {
|
||||
activeMatrices: stats.activeMatrices,
|
||||
totalMatrices: stats.totalMatrices,
|
||||
totalUsersSubscribed: stats.totalUsersSubscribed,
|
||||
matrices: stats.matrices.map(m => ({
|
||||
rootUserId: m.rootUserId,
|
||||
name: m.name,
|
||||
isActive: !!m.isActive,
|
||||
usersCount: m.usersCount,
|
||||
createdAt: m.createdAt, // equals ego_activated_at
|
||||
topNodeEmail: m.topNodeEmail
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getStats
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user