CentralBackend/repositories/matrix/MatrixRepository.js
2025-11-30 12:20:36 +01:00

945 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 }) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
// Ensure columns for multi-instance (idempotent)
try { await conn.query(`ALTER TABLE user_tree_edges ADD COLUMN matrix_instance_id INT NULL`); } catch (_) {}
try { await conn.query(`ALTER TABLE user_tree_edges ADD COLUMN rogue_user BOOLEAN DEFAULT FALSE`); } catch (_) {}
try { await conn.query(`ALTER TABLE user_tree_closure ADD COLUMN matrix_instance_id INT NOT NULL`); } catch (_) {}
try { await conn.query(`ALTER TABLE user_matrix_metadata ADD COLUMN matrix_instance_id INT NOT NULL`); } catch (_) {}
const topUser = await ensureUserExistsByEmail(conn, topNodeEmail);
// Create new matrix instance
const [instRes] = await conn.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(), 0, NULL)`, // CHANGED: first_free_position NULL at root
[topUser.id, name]
);
const matrixInstanceId = instRes.insertId;
// Insert metadata row (primary keyed by matrix_instance_id)
await conn.query(
`INSERT INTO user_matrix_metadata
(matrix_instance_id, root_user_id, ego_activated_at, last_bfs_fill_at, immediate_children_count,
first_free_position, name, is_active, max_depth)
VALUES (?, ?, NOW(), NULL, 0, 1, ?, TRUE, NULL)`,
[matrixInstanceId, topUser.id, name]
);
// Self closure scoped to instance
await conn.query(
`INSERT IGNORE INTO user_tree_closure
(matrix_instance_id, ancestor_user_id, descendant_user_id, depth)
VALUES (?, ?, ?, 0)`,
[matrixInstanceId, topUser.id, topUser.id]
);
await conn.commit();
return {
name,
matrixInstanceId,
masterTopUserId: topUser.id,
masterTopUserEmail: topUser.email,
rootUserId: topUser.id
};
} catch (e) {
await conn.rollback();
throw e;
} 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, max_depth)
VALUES
(?, NOW(), NULL, ?, ?, 5)
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),
max_depth = 5,
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 {
const [matRows] = await conn.query(`
SELECT mi.id AS matrixInstanceId,
mi.root_user_id,
mi.name,
mi.is_active,
mi.ego_activated_at,
mi.max_depth,
u.email
FROM matrix_instances mi
JOIN users u ON u.id = mi.root_user_id
ORDER BY mi.id ASC
`);
if (!matRows.length) return {
activeMatrices: 0,
totalMatrices: 0,
totalUsersSubscribed: 0,
matrices: []
};
const instanceIds = matRows.map(r => r.matrixInstanceId);
// Count users per instance honoring max_depth
const [cntRows] = await conn.query(`
SELECT c.matrix_instance_id AS mid, COUNT(*) AS cnt
FROM user_tree_closure c
JOIN matrix_instances mi ON mi.id = c.matrix_instance_id
WHERE c.matrix_instance_id IN (?)
AND c.depth BETWEEN 0 AND COALESCE(mi.max_depth, 2147483647)
GROUP BY c.matrix_instance_id
`, [instanceIds]);
const countsMap = new Map(cntRows.map(r => [Number(r.mid), Number(r.cnt)]));
// Rogue counts
const [rogueRows] = await conn.query(`
SELECT matrix_instance_id AS mid, COUNT(*) AS rogueCnt
FROM user_tree_edges
WHERE matrix_instance_id IN (?)
AND rogue_user = TRUE
`, [instanceIds]);
const rogueMap = new Map(rogueRows.map(r => [Number(r.mid), Number(r.rogueCnt)]));
// Distinct subscribed users across active matrices
const [distinctRows] = await conn.query(`
SELECT COUNT(DISTINCT c.descendant_user_id) AS total
FROM user_tree_closure c
JOIN matrix_instances mi ON mi.id = c.matrix_instance_id
WHERE mi.is_active = TRUE
AND c.matrix_instance_id IN (?)
AND c.depth BETWEEN 0 AND COALESCE(mi.max_depth, 2147483647)
`, [instanceIds]);
const totalDistinct = Number(distinctRows[0]?.total || 0);
const activeMatrices = matRows.filter(r => !!r.is_active).length;
const matrices = matRows.map(r => ({
matrixInstanceId: Number(r.matrixInstanceId),
rootUserId: Number(r.root_user_id),
name: r.name || null,
isActive: !!r.is_active,
usersCount: countsMap.get(Number(r.matrixInstanceId)) || 0,
rogueUsersCount: rogueMap.get(Number(r.matrixInstanceId)) || 0,
createdAt: r.ego_activated_at || null,
topNodeEmail: r.email
}));
return {
activeMatrices,
totalMatrices: matRows.length,
totalUsersSubscribed: totalDistinct,
matrices
};
} finally {
conn.release();
}
}
async function resolveRootUserId({ rootUserId, matrixId, matrixInstanceId, topNodeEmail }) {
const conn = await pool.getConnection();
try {
// Prefer explicit matrixInstanceId (matrixId backward compatible)
const midRaw = matrixInstanceId || matrixId;
const mid = Number.parseInt(midRaw, 10);
if (Number.isFinite(mid) && mid > 0) {
const [rows] = await conn.query(
`SELECT id, root_user_id FROM matrix_instances WHERE id = ? LIMIT 1`,
[mid]
);
if (!rows.length) {
const err = new Error('Matrix instance not found');
err.status = 404;
throw err;
}
return { rootUserId: Number(rows[0].root_user_id), matrixInstanceId: Number(rows[0].id) };
}
// Direct root user id (find first instance with that root)
const rid = Number.parseInt(rootUserId, 10);
if (Number.isFinite(rid) && rid > 0) {
const [rows] = await conn.query(
`SELECT id FROM matrix_instances WHERE root_user_id = ? ORDER BY id ASC LIMIT 1`,
[rid]
);
if (rows.length) {
return { rootUserId: rid, matrixInstanceId: Number(rows[0].id) };
}
}
// Resolve by email
const email = (topNodeEmail || '').trim();
if (email) {
const [uRows] = await conn.query(
`SELECT id FROM users WHERE LOWER(email)=LOWER(?) LIMIT 1`,
[email]
);
if (!uRows.length) {
const err = new Error('Top node user not found');
err.status = 404;
throw err;
}
const uid = Number(uRows[0].id);
const [instRows] = await conn.query(
`SELECT id FROM matrix_instances WHERE root_user_id = ? ORDER BY id ASC LIMIT 1`,
[uid]
);
if (instRows.length) {
return { rootUserId: uid, matrixInstanceId: Number(instRows[0].id) };
}
}
// Fallback: pick first existing instance
const [firstInst] = await conn.query(`SELECT id, root_user_id FROM matrix_instances ORDER BY id ASC LIMIT 1`);
if (firstInst.length) {
return { rootUserId: Number(firstInst[0].root_user_id), matrixInstanceId: Number(firstInst[0].id) };
}
const err = new Error('Matrix resolver parameters required');
err.status = 400;
throw err;
} finally {
conn.release();
}
}
// NEW: search user candidates to add to a matrix
async function getUserSearchCandidates({ q, type = 'all', rootUserId, matrixInstanceId, limit = 20, offset = 0 }) {
const conn = await pool.getConnection();
try {
const whereParts = [];
const params = [];
// Exclude users already in selected matrix instance
whereParts.push(`u.id NOT IN (
SELECT descendant_user_id
FROM user_tree_closure
WHERE matrix_instance_id = ?
AND ancestor_user_id = ?
)`);
params.push(Number(matrixInstanceId), Number(rootUserId));
whereParts.push(`u.role = 'user'`);
if (type === 'personal' || type === 'company') {
whereParts.push(`u.user_type = ?`);
params.push(type);
}
const like = `%${q.toLowerCase()}%`;
whereParts.push(`(
LOWER(u.email) LIKE ?
OR LOWER(CONCAT(COALESCE(p.first_name,''),' ',COALESCE(p.last_name,''))) LIKE ?
OR LOWER(COALESCE(c.company_name,'')) LIKE ?
)`);
params.push(like, like, like);
const whereSql = `WHERE ${whereParts.join(' AND ')}`;
const [countRows] = await conn.query(`
SELECT COUNT(*) AS total
FROM users u
LEFT JOIN personal_profiles p ON p.user_id = u.id
LEFT JOIN company_profiles c ON c.user_id = u.id
${whereSql}
`, params);
const total = Number(countRows[0]?.total || 0);
const [rows] = await conn.query(`
SELECT u.id AS userId, u.email, u.user_type AS userType,
CASE
WHEN u.user_type='personal' THEN TRIM(CONCAT(COALESCE(p.first_name,''),' ',COALESCE(p.last_name,'')))
WHEN u.user_type='company' THEN COALESCE(c.company_name,'')
ELSE ''
END AS name
FROM users u
LEFT JOIN personal_profiles p ON p.user_id = u.id
LEFT JOIN company_profiles c ON c.user_id = u.id
${whereSql}
ORDER BY u.created_at DESC, u.email ASC
LIMIT ? OFFSET ?
`, params.concat([Number(limit), Number(offset)]));
return {
total,
items: rows.map(r => ({
userId: Number(r.userId),
email: r.email,
userType: r.userType,
name: r.name || ''
}))
};
} finally {
conn.release();
}
}
// NEW: fetch matrix users (descendants) under a root
async function getMatrixUsers({ rootUserId, matrixInstanceId, maxDepth = 5, limit = 100, offset = 0, includeRoot = false, rogueOnly = false }) {
const conn = await pool.getConnection();
try {
let policyDepth = null;
try {
const [pRows] = await conn.query(
`SELECT max_depth FROM matrix_instances WHERE id = ? LIMIT 1`,
[matrixInstanceId]
);
if (pRows.length) policyDepth = pRows[0].max_depth == null ? null : Number(pRows[0].max_depth);
} catch (_) {}
let requestedDepth = Number(maxDepth);
if (!Number.isFinite(requestedDepth) || requestedDepth < 0) requestedDepth = 0;
const depthLimit = policyDepth == null ? requestedDepth : Math.min(requestedDepth, policyDepth);
const startDepth = includeRoot ? 0 : 1;
if (startDepth > depthLimit) return [];
const rogueClause = rogueOnly ? 'AND (e.rogue_user = TRUE)' : '';
const [rows] = await conn.query(`
SELECT
c.descendant_user_id AS userId,
c.depth,
u.email,
u.user_type AS userType,
u.role,
u.created_at AS createdAt,
e.parent_user_id AS parentUserId,
e.position,
e.rogue_user AS rogueUser,
CASE
WHEN u.user_type='personal' THEN TRIM(CONCAT(COALESCE(pp.first_name,''),' ',COALESCE(pp.last_name,'')))
WHEN u.user_type='company' THEN COALESCE(cp.company_name,'')
ELSE ''
END AS name
FROM user_tree_closure c
JOIN users u ON u.id = c.descendant_user_id
LEFT JOIN user_tree_edges e
ON e.child_user_id = c.descendant_user_id
AND e.matrix_instance_id = c.matrix_instance_id
AND e.parent_user_id != e.child_user_id
LEFT JOIN personal_profiles pp ON pp.user_id = u.id
LEFT JOIN company_profiles cp ON cp.user_id = u.id
WHERE c.matrix_instance_id = ?
AND c.ancestor_user_id = ?
AND c.depth BETWEEN ? AND ?
${rogueClause}
ORDER BY c.depth ASC, u.created_at ASC, u.id ASC
LIMIT ? OFFSET ?
`, [matrixInstanceId, rootUserId, startDepth, depthLimit, Number(limit), Number(offset)]);
return rows.map(r => ({
userId: Number(r.userId),
email: r.email,
userType: r.userType,
role: r.role,
depth: Number(r.depth),
level: Number(r.depth),
parentUserId: r.parentUserId ? Number(r.parentUserId) : null,
position: r.position != null ? Number(r.position) : null,
rogueUser: !!r.rogueUser,
name: r.name || r.email,
createdAt: r.createdAt
}));
} finally {
conn.release();
}
}
async function addUserToMatrix({
rootUserId,
matrixInstanceId,
matrixId,
topNodeEmail,
childUserId,
forceParentFallback = false,
parentUserId,
actorUserId
}) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
// Resolve (matrixId backward compatible)
const resolved = await resolveRootUserId({
rootUserId,
matrixId: matrixId || matrixInstanceId,
matrixInstanceId,
topNodeEmail
});
const rid = resolved.rootUserId;
const mid = resolved.matrixInstanceId;
// PREVENT additions to inactive instances
const [miRows] = await conn.query(
`SELECT is_active FROM matrix_instances WHERE id = ? LIMIT 1`,
[mid]
);
if (!miRows.length) {
const err = new Error('Matrix instance not found');
err.status = 404;
throw err;
}
if (!miRows[0].is_active) {
const err = new Error('Matrix instance is inactive');
err.status = 409;
throw err;
}
const cId = Number(childUserId);
if (!Number.isFinite(cId) || cId <= 0) {
const err = new Error('Invalid childUserId');
err.status = 400;
throw err;
}
const [childRows] = await conn.query(`SELECT id FROM users WHERE id = ? LIMIT 1`, [cId]);
if (!childRows.length) {
const err = new Error('Child user not found');
err.status = 404;
throw err;
}
let parentId = Number(parentUserId) > 0 ? Number(parentUserId) : null;
let explicitRootChoice = false;
if (parentId === rid) {
explicitRootChoice = true; // admin explicitly chose root
}
if (!parentId) {
// referral parent discovery
const [refRows] = await conn.query(
`
SELECT t.created_by_user_id AS parent_user_id
FROM referral_token_usage u
JOIN referral_tokens t ON t.id = u.referral_token_id
WHERE u.used_by_user_id = ?
ORDER BY u.id ASC
LIMIT 1
`,
[cId]
);
if (refRows.length) parentId = Number(refRows[0].parent_user_id);
}
let fallback_reason = null;
let rogueFlag = false;
// Check parent within matrix instance (when parentId exists and is not root)
if (parentId && parentId !== rid) {
const [inMatrixRows] = await conn.query(
`
SELECT depth FROM user_tree_closure
WHERE matrix_instance_id = ? AND ancestor_user_id = ? AND descendant_user_id = ?
LIMIT 1
`,
[mid, rid, parentId]
);
if (!inMatrixRows.length) {
// Referrer not in matrix -> fallback decision
if (forceParentFallback) {
fallback_reason = 'referrer_not_in_matrix';
parentId = rid;
rogueFlag = true;
} else {
const err = new Error('Referrer is not part of this matrix');
err.status = 400;
throw err;
}
}
}
if (!parentId) {
// No referral parent found
fallback_reason = 'referrer_not_in_matrix';
parentId = rid;
rogueFlag = true;
}
// Duplicate check (already in matrix)
const [dupRows] = await conn.query(
`
SELECT 1 FROM user_tree_closure
WHERE matrix_instance_id = ? AND ancestor_user_id = ? AND descendant_user_id = ?
LIMIT 1
`,
[mid, rid, cId]
);
if (dupRows.length) {
const err = new Error('User already in matrix');
err.status = 409;
throw err;
}
// Determine policy depth
let maxDepthPolicy = null;
try {
const [pRows] = await conn.query(`SELECT max_depth FROM matrix_instances WHERE id = ? LIMIT 1`, [mid]);
if (pRows.length) maxDepthPolicy = pRows[0].max_depth == null ? null : Number(pRows[0].max_depth);
} catch (_) {}
let assignPos = null;
if (parentId === rid) {
// ROOT: unlimited children, sequential position
// Do not mark rogue if admin explicitly chose root
if (explicitRootChoice) {
rogueFlag = false;
fallback_reason = null;
}
const [rootPosRows] = await conn.query(
`SELECT MAX(position) AS maxPos FROM user_tree_edges WHERE matrix_instance_id = ? AND parent_user_id = ?`,
[mid, rid]
);
const maxPos = Number(rootPosRows[0]?.maxPos || 0);
assignPos = maxPos + 1; // sequential position under root
} else {
// NON-ROOT: enforce 15
const [childPosRows] = await conn.query(
`SELECT position FROM user_tree_edges WHERE matrix_instance_id = ? AND parent_user_id = ? ORDER BY position ASC`,
[mid, parentId]
);
const used = new Set(childPosRows.map(r => Number(r.position)));
for (let i = 1; i <= 5; i++) if (!used.has(i)) { assignPos = i; break; }
if (!assignPos) {
// BFS within the chosen parent subtree only
const [candRows] = await conn.query(
`
SELECT d.descendant_user_id AS candidate_id
FROM user_tree_closure AS d
WHERE d.matrix_instance_id = ?
AND d.ancestor_user_id = ?
AND d.descendant_user_id != ?
ORDER BY d.depth ASC, d.descendant_user_id ASC
`,
[mid, parentId, parentId]
);
for (const row of candRows) {
const candidateParentId = Number(row.candidate_id);
// skip root in BFS
if (candidateParentId === rid) continue;
// ensure candidate within max depth policy (parent depth + 1)
if (maxDepthPolicy != null) {
const [depthRows] = await conn.query(
`SELECT depth FROM user_tree_closure WHERE matrix_instance_id = ? AND ancestor_user_id = ? AND descendant_user_id = ? LIMIT 1`,
[mid, rid, candidateParentId]
);
const candDepthFromRoot = Number(depthRows[0]?.depth ?? 0);
if (candDepthFromRoot + 1 > maxDepthPolicy) continue;
}
const [candPosRows] = await conn.query(
`SELECT position FROM user_tree_edges WHERE matrix_instance_id = ? AND parent_user_id = ? ORDER BY position ASC`,
[mid, candidateParentId]
);
const candUsed = new Set(candPosRows.map(r => Number(r.position)));
for (let i = 1; i <= 5; i++) if (!candUsed.has(i)) { assignPos = i; break; }
if (assignPos) {
parentId = candidateParentId;
break;
}
}
if (!assignPos) {
// no slot in subtree -> decide fallback
if (forceParentFallback) {
fallback_reason = 'referrer_full';
parentId = rid;
rogueFlag = true;
// root sequential
const [rootPosRows] = await conn.query(
`SELECT MAX(position) AS maxPos FROM user_tree_edges WHERE matrix_instance_id = ? AND parent_user_id = ?`,
[mid, rid]
);
const maxPos = Number(rootPosRows[0]?.maxPos || 0);
assignPos = maxPos + 1;
} else {
const err = new Error('Parent subtree is full (5-ary); no available position');
err.status = 409;
throw err;
}
}
}
}
// Insert edge
await conn.query(
`INSERT INTO user_tree_edges
(matrix_instance_id, parent_user_id, child_user_id, position, rogue_user)
VALUES (?,?,?,?,?)`,
[mid, parentId, cId, assignPos, rogueFlag]
);
// Self closure for child
await conn.query(
`INSERT IGNORE INTO user_tree_closure
(matrix_instance_id, ancestor_user_id, descendant_user_id, depth)
VALUES (?,?,?,0)`,
[mid, cId, cId]
);
// Ancestor closure rows
const [ancestorRows] = await conn.query(
`SELECT ancestor_user_id, depth
FROM user_tree_closure
WHERE matrix_instance_id = ? AND descendant_user_id = ?
ORDER BY depth ASC`,
[mid, parentId]
);
if (ancestorRows.length) {
const values = ancestorRows
.map(r => `(${mid}, ${Number(r.ancestor_user_id)}, ${cId}, ${Number(r.depth) + 1})`)
.join(',');
await conn.query(
`INSERT IGNORE INTO user_tree_closure
(matrix_instance_id, ancestor_user_id, descendant_user_id, depth)
VALUES ${values}`
);
}
// Update instance metadata: immediate children count for root; do not compute first_free_position for root
if (parentId === rid) {
const [rootChildrenRows] = await conn.query(
`SELECT COUNT(*) AS cnt FROM user_tree_edges WHERE matrix_instance_id = ? AND parent_user_id = ?`,
[mid, rid]
);
await conn.query(
`UPDATE matrix_instances
SET immediate_children_count = ?
WHERE id = ?`,
[Number(rootChildrenRows[0]?.cnt || 0), mid]
);
}
// Log
try {
await conn.query(
`
INSERT INTO user_action_logs
(action, performed_by_user_id, affected_user_id, details, created_at)
VALUES
('matrix_add_user', ?, ?, ?, NOW())
`,
[
Number(actorUserId) || null,
cId,
JSON.stringify({
matrixInstanceId: mid,
rootUserId: rid,
parentUserId: parentId,
position: assignPos,
rogue_user: rogueFlag,
fallback_reason
})
]
);
} catch (_) {}
await conn.commit();
return {
matrixInstanceId: mid,
rootUserId: rid,
parentUserId: parentId,
childUserId: cId,
position: assignPos,
rogue_user: rogueFlag,
fallback_reason
};
} catch (e) {
try { await conn.rollback(); } catch (_) {}
throw e;
} finally {
conn.release();
}
}
// NEW: resolve instance containing a given user via closure
async function resolveInstanceForUser(userId) {
const conn = await pool.getConnection();
try {
const [rows] = await conn.query(
`SELECT matrix_instance_id AS mid
FROM user_tree_closure
WHERE descendant_user_id = ?
ORDER BY matrix_instance_id ASC
LIMIT 1`,
[Number(userId)]
);
if (!rows.length) return null;
return { matrixInstanceId: Number(rows[0].mid) };
} finally {
conn.release();
}
}
// NEW: fetch instance basic info
async function getInstanceInfo(instanceId) {
const conn = await pool.getConnection();
try {
const [rows] = await conn.query(
`SELECT id, root_user_id, max_depth
FROM matrix_instances
WHERE id = ? LIMIT 1`,
[Number(instanceId)]
);
return rows.length ? rows[0] : null;
} finally {
conn.release();
}
}
// NEW: deactivate a matrix instance by id (idempotent)
async function deactivateInstance(instanceId) {
const conn = await pool.getConnection();
try {
const id = Number(instanceId);
if (!Number.isFinite(id) || id <= 0) {
const err = new Error('Invalid matrix instance id');
err.status = 400;
throw err;
}
const [rows] = await conn.query(
`SELECT id, is_active FROM matrix_instances WHERE id = ? LIMIT 1`,
[id]
);
if (!rows.length) {
const err = new Error('Matrix instance not found');
err.status = 404;
throw err;
}
const wasActive = !!rows[0].is_active;
// Try to set deactivated_at if column exists; fallback otherwise
try {
await conn.query(
`UPDATE matrix_instances
SET is_active = FALSE,
deactivated_at = COALESCE(deactivated_at, NOW()),
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[id]
);
} catch (e) {
if (e && e.code === 'ER_BAD_FIELD_ERROR') {
await conn.query(
`UPDATE matrix_instances
SET is_active = FALSE,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[id]
);
} else {
throw e;
}
}
return {
matrixInstanceId: id,
wasActive,
isActive: false,
status: wasActive ? 'deactivated' : 'already_inactive'
};
} finally {
conn.release();
}
}
// NEW: activate a matrix instance by id (idempotent)
async function activateInstance(instanceId) {
const conn = await pool.getConnection();
try {
const id = Number(instanceId);
if (!Number.isFinite(id) || id <= 0) {
const err = new Error('Invalid matrix instance id');
err.status = 400;
throw err;
}
const [rows] = await conn.query(
`SELECT id, is_active FROM matrix_instances WHERE id = ? LIMIT 1`,
[id]
);
if (!rows.length) {
const err = new Error('Matrix instance not found');
err.status = 404;
throw err;
}
const wasActive = !!rows[0].is_active;
if (!wasActive) {
// Try to clear deactivated_at if column exists
try {
await conn.query(
`UPDATE matrix_instances
SET is_active = TRUE,
deactivated_at = NULL,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[id]
);
} catch (e) {
if (e && e.code === 'ER_BAD_FIELD_ERROR') {
await conn.query(
`UPDATE matrix_instances
SET is_active = TRUE,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[id]
);
} else {
throw e;
}
}
}
return {
matrixInstanceId: id,
wasActive,
isActive: true,
status: wasActive ? 'already_active' : 'activated'
};
} finally {
conn.release();
}
}
module.exports = {
createMatrix,
ensureUserExistsByEmail,
activateEgoMatrix,
getMatrixStats,
resolveRootUserId,
getUserSearchCandidates,
getMatrixUsers,
addUserToMatrix,
resolveInstanceForUser, // NEW
getInstanceInfo, // NEW
deactivateInstance, // NEW
activateInstance // NEW
};