app/src/services/graph/userNeoDBService.ts
2025-11-14 14:47:26 +00:00

455 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

import axiosInstance from '../../axiosConfig';
import { formatEmailForDatabase } from './neoDBService';
import { CCUserNodeProps, CCTeacherNodeProps, CCCalendarNodeProps, CCUserTeacherTimetableNodeProps } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-types';
import { NavigationNode, NodeContext } from '../../types/navigation';
import { TLBinding, TLShapeId } from '@tldraw/tldraw';
import { logger } from '../../debugConfig';
import { useNavigationStore } from '../../stores/navigationStore';
import { DatabaseNameService } from './databaseNameService';
// Dev configuration - only hardcoded value we need
const DEV_SCHOOL_NAME = 'default';
const DEV_SCHOOL_GROUP = 'development'
const ADMIN_USER_NAME = 'kcar';
const ADMIN_USER_GROUP = 'admin';
interface ShapeState {
parentId: TLShapeId | null;
isPageChild: boolean | null;
hasChildren: boolean | null;
bindings: TLBinding[] | null;
}
interface NodeResponse {
status: string;
nodes: {
userNode: CCUserNodeProps;
calendarNode: CCCalendarNodeProps;
teacherNode: CCTeacherNodeProps;
timetableNode: CCUserTeacherTimetableNodeProps;
};
}
interface NodeDataResponse {
__primarylabel__: string;
uuid_string: string;
node_storage_path: string;
created: string;
merged: string;
state: ShapeState | null;
defaultComponent: boolean | null;
user_name?: string;
user_email?: string;
user_type?: string;
user_id?: string;
worker_node_data?: string;
[key: string]: string | number | boolean | null | ShapeState | undefined;
}
interface DefaultNodeResponse {
status: string;
node: {
id: string;
node_storage_path: string;
type: string;
label: string;
data: NodeDataResponse;
};
}
export interface ProcessedUserNodes {
privateUserNode: CCUserNodeProps;
connectedNodes: {
calendar?: CCCalendarNodeProps;
teacher?: CCTeacherNodeProps;
timetable?: CCUserTeacherTimetableNodeProps;
};
}
export interface CalendarStructureResponse {
status: string;
data: {
currentDay: string;
days: Record<string, {
id: string;
date: string;
title: string;
}>;
weeks: Record<string, {
id: string;
title: string;
days: { id: string }[];
startDate: string;
endDate: string;
}>;
months: Record<string, {
id: string;
title: string;
days: { id: string }[];
weeks: { id: string }[];
year: string;
month: string;
}>;
years: {
id: string;
title: string;
months: { id: string }[];
year: string;
}[];
};
}
export interface WorkerStructureResponse {
status: string;
data: {
timetables: Record<string, Array<{
id: string;
title: string;
type: string;
startTime: string;
endTime: string;
}>>;
classes: Record<string, Array<{
id: string;
title: string;
type: string;
}>>;
lessons: Record<string, Array<{
id: string;
title: string;
type: string;
}>>;
journals: Record<string, Array<{
id: string;
title: string;
}>>;
planners: Record<string, Array<{
id: string;
title: string;
}>>;
};
}
export class UserNeoDBService {
static async fetchUserNodesData(
email: string,
userDbName?: string,
workerDbName?: string
): Promise<ProcessedUserNodes | null> {
try {
if (!userDbName) {
logger.error('neo4j-service', '❌ Attempted to fetch nodes without database name');
return null;
}
const formattedEmail = formatEmailForDatabase(email);
const uniqueId = `User_${formattedEmail}`;
logger.debug('neo4j-service', '🔄 Fetching user nodes data', {
email,
formattedEmail,
userDbName,
workerDbName,
uniqueId
});
// First get the user node from profile context
const userNode = await this.getDefaultNode('profile', userDbName);
if (!userNode || !userNode.data) {
throw new Error('Failed to fetch user node or node data missing');
}
logger.debug('neo4j-service', '✅ Found user node', {
nodeId: userNode.id,
type: userNode.type,
hasData: !!userNode.data,
userDbName,
workerDbName
});
// Initialize result structure
const processedNodes: ProcessedUserNodes = {
privateUserNode: {
...userNode.data,
__primarylabel__: 'User' as const,
title: userNode.data.user_email || 'User',
w: 200,
h: 200,
headerColor: '#3e6589',
backgroundColor: '#f0f0f0',
isLocked: false
} as CCUserNodeProps,
connectedNodes: {}
};
try {
// Get calendar node from calendar context
const calendarNode = await this.getDefaultNode('calendar', userDbName);
if (calendarNode?.data) {
processedNodes.connectedNodes.calendar = {
...calendarNode.data,
__primarylabel__: 'Calendar' as const,
title: calendarNode.data.calendar_name || 'Calendar',
w: 200,
h: 200,
headerColor: '#3e6589',
backgroundColor: '#f0f0f0',
isLocked: false
} as CCCalendarNodeProps;
logger.debug('neo4j-service', '✅ Found calendar node', {
nodeId: calendarNode.id,
node_storage_path: calendarNode.data.node_storage_path
});
} else {
logger.debug('neo4j-service', ' No calendar node found');
}
} catch (error) {
logger.warn('neo4j-service', '⚠️ Failed to fetch calendar node:', error);
// Continue without calendar node
}
// Get teacher node from teaching context if worker database is available
if (workerDbName) {
try {
const teacherNode = await this.getDefaultNode('teaching', userDbName);
if (teacherNode?.data) {
processedNodes.connectedNodes.teacher = {
...teacherNode.data,
__primarylabel__: 'Teacher' as const,
title: teacherNode.data.teacher_name_formal || 'Teacher',
w: 200,
h: 200,
headerColor: '#3e6589',
backgroundColor: '#f0f0f0',
isLocked: false,
user_db_name: userDbName,
school_db_name: workerDbName
} as CCTeacherNodeProps;
logger.debug('neo4j-service', '✅ Found teacher node', {
nodeId: teacherNode.id,
node_storage_path: teacherNode.data.node_storage_path,
userDbName,
workerDbName
});
} else {
logger.debug('neo4j-service', ' No teacher node found');
}
} catch (error) {
logger.warn('neo4j-service', '⚠️ Failed to fetch teacher node:', error);
// Continue without teacher node
}
}
logger.debug('neo4j-service', '✅ Processed all user nodes', {
hasUserNode: !!processedNodes.privateUserNode,
hasCalendar: !!processedNodes.connectedNodes.calendar,
hasTeacher: !!processedNodes.connectedNodes.teacher,
teacherData: processedNodes.connectedNodes.teacher ? {
uuid_string: processedNodes.connectedNodes.teacher.uuid_string,
school_db_name: processedNodes.connectedNodes.teacher.school_db_name,
node_storage_path: processedNodes.connectedNodes.teacher.node_storage_path
} : null
});
return processedNodes;
} catch (error: unknown) {
if (error instanceof Error) {
logger.error('neo4j-service', '❌ Failed to fetch user nodes:', error.message);
} else {
logger.error('neo4j-service', '❌ Failed to fetch user nodes:', String(error));
}
throw error;
}
}
static getUserDatabaseName(userType: string, identifier: string): string {
return DatabaseNameService.getUserPrivateDB(userType, identifier);
}
static getSchoolDatabaseName(schoolId: string): string {
return DatabaseNameService.getSchoolPrivateDB(schoolId);
}
static getDefaultSchoolDatabaseName(): string {
return DatabaseNameService.getStoredSchoolDatabase() || '';
}
static async fetchNodeData(nodeId: string, dbName: string): Promise<{ node_type: string; node_data: NodeDataResponse } | null> {
try {
logger.debug('neo4j-service', '🔄 Fetching node data', { nodeId, dbName });
const response = await axiosInstance.get<{
status: string;
node: {
node_type: string;
node_data: NodeDataResponse;
};
}>('/database/tools/get-node', {
params: {
uuid_string: nodeId,
db_name: dbName
}
});
if (response.data?.status === 'success' && response.data.node) {
return response.data.node;
}
return null;
} catch (error) {
logger.error('neo4j-service', '❌ Failed to fetch node data:', error);
throw error;
}
}
static getNodeDatabaseName(node: NavigationNode): string {
// Validate that node and node_storage_path exist
if (!node || !node.node_storage_path) {
logger.error('neo4j-service', '❌ Invalid node or missing node_storage_path', {
node: node ? { id: node.id, type: node.type, label: node.label } : null,
hasStoragePath: !!node?.node_storage_path
});
throw new Error('Node is missing required storage path information');
}
// If the node path starts with /node_filesystem/users/, it's in a user database
if (node.node_storage_path.startsWith('users/')) {
const parts = node.node_storage_path.split('/');
const databaseIndex = parts.indexOf('databases');
if (databaseIndex >= 0 && parts.length > databaseIndex + 1) {
return parts[databaseIndex + 1];
}
// parts[3] should be the database name (e.g., cc.users.surfacedashdev3atkevlaraidotcom)
if (parts.length >= 4) {
return parts[3];
}
logger.warn('neo4j-service', '⚠️ Unexpected user path format', { path: node.node_storage_path });
return 'cc.users';
}
// For Supabase Storage paths (cc.public.snapshots/...), determine database based on node type
if (node.node_storage_path.startsWith('cc.public.snapshots/')) {
const parts = node.node_storage_path.split('/');
const nodeType = parts[1]; // e.g., 'User', 'Teacher', 'School'
if (nodeType === 'User') {
return DatabaseNameService.getStoredUserDatabase() || 'cc.users';
} else if (nodeType === 'Teacher' || nodeType === 'Student') {
return DatabaseNameService.getStoredSchoolDatabase() || 'cc.institutes';
} else if (nodeType === 'School') {
return DatabaseNameService.getStoredSchoolDatabase() || 'cc.institutes';
}
}
// For school/worker nodes, extract from the path or use a default
if (node.node_storage_path.startsWith('schools/')) {
const parts = node.node_storage_path.split('/');
const databaseIndex = parts.indexOf('databases');
if (databaseIndex >= 0 && parts.length > databaseIndex + 1) {
return parts[databaseIndex + 1];
}
if (parts.length >= 4) {
return parts[3];
}
const storedSchoolDb = DatabaseNameService.getStoredSchoolDatabase();
if (storedSchoolDb) {
logger.warn('neo4j-service', '⚠️ Falling back to stored school database name', {
path: node.node_storage_path,
storedSchoolDb
});
return storedSchoolDb;
}
logger.warn('neo4j-service', '⚠️ Could not determine school database from path', { path: node.node_storage_path });
return DatabaseNameService.getStoredSchoolDatabase() || 'cc.institutes';
}
// Try to extract from path, but provide fallback
const parts = node.node_storage_path.split('/');
if (parts.length >= 4) {
return parts[3];
}
// Default fallback
logger.warn('neo4j-service', '⚠️ Using fallback database name', {
path: node.node_storage_path,
nodeType: node.type
});
return 'cc.users'; //TODO: remove hard-coding
}
static async getDefaultNode(context: NodeContext, dbName: string): Promise<NavigationNode | null> {
try {
logger.debug('neo4j-service', '🔄 Fetching default node', { context, dbName });
// For overview context, we need to extract the base context from the current navigation state
const params: Record<string, string> = { db_name: dbName };
if (context === 'overview') {
// Get the current base context from the navigation store
const navigationStore = useNavigationStore.getState();
params.base_context = navigationStore.context.base;
}
const response = await axiosInstance.get<DefaultNodeResponse>(
`/database/tools/get-default-node/${context}`,
{ params }
);
if (response.data?.status === 'success' && response.data.node) {
return {
id: response.data.node.id,
node_storage_path: response.data.node.node_storage_path,
type: response.data.node.type,
label: response.data.node.label,
data: response.data.node.data
};
}
return null;
} catch (error) {
logger.error('neo4j-service', '❌ Failed to fetch default node:', error);
throw error;
}
}
static async fetchCalendarStructure(dbName: string): Promise<CalendarStructureResponse['data']> {
try {
logger.debug('navigation', '🔄 Fetching calendar structure', { dbName });
const response = await axiosInstance.get<CalendarStructureResponse>(
`/database/calendar-structure/get-calendar-structure?db_name=${dbName}`
);
if (response.data.status === 'success') {
logger.info('navigation', '✅ Calendar structure fetched successfully');
return response.data.data;
}
throw new Error('Failed to fetch calendar structure');
} catch (error) {
logger.error('navigation', '❌ Failed to fetch calendar structure:', error);
throw error;
}
}
static async fetchWorkerStructure(dbName: string): Promise<WorkerStructureResponse['data']> {
try {
logger.debug('navigation', '🔄 Fetching worker structure', { dbName });
const response = await axiosInstance.get<WorkerStructureResponse>(
`/database/worker-structure/get-worker-structure?db_name=${dbName}`
);
if (response.data.status === 'success') {
logger.info('navigation', '✅ Worker structure fetched successfully');
return response.data.data;
}
throw new Error('Failed to fetch worker structure');
} catch (error) {
logger.error('navigation', '❌ Failed to fetch worker structure:', error);
throw error;
}
}
}