455 lines
17 KiB
TypeScript
455 lines
17 KiB
TypeScript
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;
|
||
}
|
||
}
|
||
}
|