This commit is contained in:
2025-11-14 14:47:26 +00:00
parent 69ecf2c7c1
commit 3b4876793e
104 changed files with 231517 additions and 1029 deletions
+60 -11
View File
@@ -1,14 +1,35 @@
import { logger } from '../../debugConfig';
import { storageService, StorageKeys } from '../auth/localStorageService';
export class DatabaseNameService {
static readonly CC_USERS = 'cc.users';
static readonly CC_SCHOOLS = 'cc.institutes';
private static remember(key: StorageKeys, value?: string | null) {
if (!value) {
return;
}
storageService.set(key, value);
}
private static recall<T extends StorageKeys>(key: T): string | null {
return storageService.get(key);
}
static getUserPrivateDB(userType: string, username: string): string {
const dbName = `${this.CC_USERS}.${userType}.${username}`;
private static sanitizeComponent(component: string, fallback = 'user'): string {
const cleaned = (component || fallback)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '');
return cleaned || fallback;
}
static getUserPrivateDB(userType: string, identifier: string): string {
const role = this.sanitizeComponent(userType || 'standard', 'standard');
const idComponent = this.sanitizeComponent(identifier, 'user');
const dbName = `${this.CC_USERS}.${role}.${idComponent}`;
logger.debug('database-name-service', '📥 Generating user private DB name', {
userType,
username,
identifier,
dbName
});
return dbName;
@@ -24,18 +45,46 @@ export class DatabaseNameService {
}
static getDevelopmentSchoolDB(): string {
const dbName = `${this.CC_SCHOOLS}.development.default`;
logger.debug('database-name-service', '📥 Getting default school DB name', {
dbName
const stored = this.recall(StorageKeys.NEO4J_WORKER_DB);
if (stored && stored !== `${this.CC_SCHOOLS}.development.default`) {
logger.debug('database-name-service', '📥 Using stored school DB name', {
dbName: stored
});
return stored;
}
if (stored) {
logger.warn('database-name-service', '⚠️ Ignoring legacy stored school DB name', {
dbName: stored
});
} else {
logger.warn('database-name-service', '⚠️ No stored school DB name available; returning empty string');
}
return '';
}
static rememberDatabaseNames({ userDbName, schoolDbName }: { userDbName?: string | null; schoolDbName?: string | null }) {
this.remember(StorageKeys.NEO4J_USER_DB, userDbName ?? null);
this.remember(StorageKeys.NEO4J_WORKER_DB, schoolDbName ?? null);
logger.debug('database-name-service', '💾 Remembered database names', {
userDbName,
schoolDbName
});
return dbName;
}
static getStoredUserDatabase(): string | null {
return this.recall(StorageKeys.NEO4J_USER_DB);
}
static getStoredSchoolDatabase(): string | null {
return this.recall(StorageKeys.NEO4J_WORKER_DB);
}
static getContextDatabase(context: string, userType: string, username: string): string {
static getContextDatabase(context: string, userType: string, identifier: string): string {
logger.debug('database-name-service', '📥 Resolving context database', {
context,
userType,
username
identifier
});
// For school-related contexts, use the schools database
@@ -48,11 +97,11 @@ export class DatabaseNameService {
}
// For user-specific contexts, use their private database
const userDb = this.getUserPrivateDB(userType, username);
const userDb = this.getUserPrivateDB(userType, identifier);
logger.debug('database-name-service', '✅ Using user private database for context', {
context,
dbName: userDb
});
return userDb;
}
}
}
+29 -14
View File
@@ -8,20 +8,20 @@ import { logger } from '../../debugConfig';
export class GraphNeoDBService {
static async fetchConnectedNodesAndEdges(
unique_id: string,
uuid_string: string,
db_name: string,
editor: Editor
) {
try {
logger.debug('graph-service', '📤 Fetching connected nodes', {
unique_id,
uuid_string,
db_name
});
const response = await axios.get<ConnectedNodesResponse>(
'/database/tools/get-connected-nodes-and-edges', {
params: {
unique_id,
uuid_string,
db_name
}
}
@@ -61,8 +61,8 @@ export class GraphNeoDBService {
if (isValidNodeType(connectedNode.type)) {
// Convert the simplified node structure to node_data format
const nodeData = {
unique_id: connectedNode.id,
tldraw_snapshot: connectedNode.tldraw_snapshot,
uuid_string: connectedNode.id,
node_storage_path: connectedNode.node_storage_path,
name: connectedNode.label,
__primarylabel__: connectedNode.type as keyof CCNodeTypes,
created: new Date().toISOString(),
@@ -82,7 +82,7 @@ export class GraphNeoDBService {
for (const nodeData of nodesToProcess) {
await this.createOrUpdateNode(nodeData);
logger.debug('graph-service', '📝 Processed node', {
nodeId: nodeData.unique_id,
nodeId: nodeData.uuid_string,
nodeType: nodeData.__primarylabel__
});
}
@@ -105,7 +105,7 @@ export class GraphNeoDBService {
private static async createOrUpdateNode(
nodeData: NodeResponse['node_data']
) {
const uniqueId = nodeData.unique_id;
const uniqueId = nodeData.uuid_string;
const nodeType = nodeData.__primarylabel__;
if (!isValidNodeType(nodeType)) {
@@ -126,6 +126,27 @@ export class GraphNeoDBService {
const defaultProps = shapeUtil.prototype.getDefaultProps();
// Create the shape with proper typing based on the node type
// Filter out properties that TLDraw doesn't expect
const { path, cc_username, user_db_name, ...filteredNodeData } = nodeData;
// Map backend properties to TLDraw shape properties
const mappedProps = {
...defaultProps,
...filteredNodeData,
__primarylabel__: nodeData.__primarylabel__,
uuid_string: nodeData.uuid_string,
node_storage_path: nodeData.node_storage_path as string || '',
};
// Add missing properties for cc-user-node
if (shapeType === 'cc-user-node') {
mappedProps.user_id = nodeData.uuid_string; // Use uuid_string as user_id
mappedProps.worker_node_data = JSON.stringify({
cc_username: nodeData.cc_username || '',
user_db_name: nodeData.user_db_name || ''
});
}
const shape = {
id: createShapeId(uniqueId),
type: shapeType,
@@ -137,13 +158,7 @@ export class GraphNeoDBService {
isLocked: false,
opacity: 1,
meta: {},
props: {
...defaultProps,
...nodeData,
__primarylabel__: nodeData.__primarylabel__,
unique_id: nodeData.unique_id,
tldraw_snapshot: nodeData.path as string || '',
}
props: mappedProps
};
// Add to graphState
+1 -1
View File
@@ -2,7 +2,7 @@ import { CCNodeTypes } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-types'
import { logger } from '../../debugConfig';
export interface BaseNodeData {
unique_id: string;
uuid_string: string;
path: string;
__primarylabel__: string;
[key: string]: unknown;
+10 -10
View File
@@ -46,10 +46,10 @@ class NeoRegistrationService {
// Add school data if we have a school node
if (schoolNode) {
formData.append('school_uuid', schoolNode.school_uuid);
formData.append('school_name', schoolNode.school_name);
formData.append('school_website', schoolNode.school_website);
formData.append('school_tldraw_snapshot', schoolNode.tldraw_snapshot);
formData.append('school_uuid_string', schoolNode.uuid_string);
formData.append('school_name', schoolNode.name);
formData.append('school_website', schoolNode.website);
formData.append('school_node_storage_path', schoolNode.node_storage_path);
// Add worker data based on role
const workerData = role.includes('teacher') ? {
@@ -72,8 +72,8 @@ class NeoRegistrationService {
userName: username,
userEmail: user.email,
schoolNode: schoolNode ? {
uuid: schoolNode.school_uuid,
name: schoolNode.school_name
uuid_string: schoolNode.uuid_string,
name: schoolNode.name
} : null
});
@@ -105,7 +105,7 @@ class NeoRegistrationService {
logger.info('neo4j-service', '✅ Neo4j user registration successful', {
userId: user.id,
nodeId: userNode.unique_id,
nodeId: userNode.uuid_string,
hasCalendar: !!response.data.data.calendar_nodes
});
@@ -133,11 +133,11 @@ class NeoRegistrationService {
}
}
async fetchSchoolNode(schoolUuid: string): Promise<CCSchoolNodeProps> {
logger.debug('neo4j-service', '🔄 Fetching school node', { schoolUuid });
async fetchSchoolNode(schoolUrn: string): Promise<CCSchoolNodeProps> {
logger.debug('neo4j-service', '🔄 Fetching school node', { schoolUrn });
try {
const response = await axiosInstance.get(`/database/tools/get-school-node?school_uuid=${schoolUuid}`);
const response = await axiosInstance.get(`/database/tools/get-school-node?school_urn=${schoolUrn}`);
if (response.data?.status === 'success' && response.data.school_node) {
logger.info('neo4j-service', '✅ School node fetched successfully');
+14 -4
View File
@@ -33,9 +33,10 @@ export class NeoShapeService {
const width = 500;
const height = 350;
// Process the node data
// Process the node data - filter out properties that TLDraw doesn't expect
const { cc_username, user_db_name, path, ...filteredNodeData } = nodeData;
const processedProps = {
...this.processDateTimeFields(nodeData),
...this.processDateTimeFields(filteredNodeData),
title: nodeData.title || node.label,
w: width,
h: height,
@@ -49,10 +50,19 @@ export class NeoShapeService {
backgroundColor: theme.backgroundColor,
isLocked: false,
__primarylabel__: node.type,
unique_id: node.id,
tldraw_snapshot: node.tldraw_snapshot
uuid_string: node.id,
node_storage_path: node.node_storage_path
};
// Add missing properties for cc-user-node
if (shapeType === 'cc-user-node') {
processedProps.user_id = node.id; // Use node.id as user_id
processedProps.worker_node_data = JSON.stringify({
cc_username: nodeData.cc_username || '',
user_db_name: nodeData.user_db_name || ''
});
}
logger.debug('neo-shape-service', '📄 Created shape configuration', {
nodeId: node.id,
shapeType,
+10 -10
View File
@@ -22,7 +22,7 @@ export interface TeacherTimetableEvent {
subjectClass: string;
color: string;
periodCode: string;
tldraw_snapshot?: string;
node_storage_path?: string;
};
}
@@ -42,21 +42,21 @@ export class TimetableNeoDBService {
const formData = new FormData();
formData.append('file', file);
formData.append('user_node', JSON.stringify({
unique_id: userNode.unique_id,
uuid_string: userNode.uuid_string,
user_id: userNode.user_id,
user_type: userNode.user_type,
user_name: userNode.user_name,
user_email: userNode.user_email,
tldraw_snapshot: userNode.tldraw_snapshot,
node_storage_path: userNode.node_storage_path,
worker_node_data: userNode.worker_node_data
}));
formData.append('worker_node', JSON.stringify({
unique_id: workerNode.unique_id,
uuid_string: workerNode.uuid_string,
teacher_code: workerNode.teacher_code,
teacher_name_formal: workerNode.teacher_name_formal,
teacher_email: workerNode.teacher_email,
tldraw_snapshot: workerNode.tldraw_snapshot,
node_storage_path: workerNode.node_storage_path,
worker_db_name: workerNode.school_db_name,
user_db_name: workerNode.user_db_name
}));
@@ -92,18 +92,18 @@ export class TimetableNeoDBService {
}
static async fetchTeacherTimetableEvents(
unique_id: string,
uuid_string: string,
school_db_name: string
): Promise<TeacherTimetableEvent[]> {
try {
logger.debug('timetable-service', '📤 Fetching timetable events', {
unique_id,
uuid_string,
school_db_name
});
const response = await axios.get('/calendar/get_teacher_timetable_events', {
params: {
unique_id,
uuid_string,
school_db_name
}
});
@@ -216,8 +216,8 @@ export class TimetableNeoDBService {
}
// Validate worker node has required fields
const requiredWorkerFields = ['unique_id', 'teacher_code', 'teacher_name_formal', 'teacher_email', 'worker_db_name', 'path'];
const requiredUserFields = ['unique_id', 'user_id', 'user_type', 'user_name', 'user_email', 'path', 'worker_node_data'];
const requiredWorkerFields = ['uuid_string', 'teacher_code', 'teacher_name_formal', 'teacher_email', 'worker_db_name', 'path'];
const requiredUserFields = ['uuid_string', 'user_id', 'user_type', 'user_name', 'user_email', 'path', 'worker_node_data'];
const missingWorkerFields = requiredWorkerFields.filter(field => !(field in workerNode));
const missingUserFields = requiredUserFields.filter(field => !(field in userNode));
+88 -24
View File
@@ -8,7 +8,11 @@ import { useNavigationStore } from '../../stores/navigationStore';
import { DatabaseNameService } from './databaseNameService';
// Dev configuration - only hardcoded value we need
const DEV_SCHOOL_UUID = 'kevlarai';
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;
@@ -29,8 +33,8 @@ interface NodeResponse {
interface NodeDataResponse {
__primarylabel__: string;
unique_id: string;
tldraw_snapshot: string;
uuid_string: string;
node_storage_path: string;
created: string;
merged: string;
state: ShapeState | null;
@@ -47,7 +51,7 @@ interface DefaultNodeResponse {
status: string;
node: {
id: string;
tldraw_snapshot: string;
node_storage_path: string;
type: string;
label: string;
data: NodeDataResponse;
@@ -195,7 +199,7 @@ export class UserNeoDBService {
} as CCCalendarNodeProps;
logger.debug('neo4j-service', '✅ Found calendar node', {
nodeId: calendarNode.id,
tldraw_snapshot: calendarNode.data.tldraw_snapshot
node_storage_path: calendarNode.data.node_storage_path
});
} else {
logger.debug('neo4j-service', '️ No calendar node found');
@@ -224,7 +228,7 @@ export class UserNeoDBService {
} as CCTeacherNodeProps;
logger.debug('neo4j-service', '✅ Found teacher node', {
nodeId: teacherNode.id,
tldraw_snapshot: teacherNode.data.tldraw_snapshot,
node_storage_path: teacherNode.data.node_storage_path,
userDbName,
workerDbName
});
@@ -242,9 +246,9 @@ export class UserNeoDBService {
hasCalendar: !!processedNodes.connectedNodes.calendar,
hasTeacher: !!processedNodes.connectedNodes.teacher,
teacherData: processedNodes.connectedNodes.teacher ? {
unique_id: processedNodes.connectedNodes.teacher.unique_id,
uuid_string: processedNodes.connectedNodes.teacher.uuid_string,
school_db_name: processedNodes.connectedNodes.teacher.school_db_name,
tldraw_snapshot: processedNodes.connectedNodes.teacher.tldraw_snapshot
node_storage_path: processedNodes.connectedNodes.teacher.node_storage_path
} : null
});
@@ -259,8 +263,8 @@ export class UserNeoDBService {
}
}
static getUserDatabaseName(userType: string, username: string): string {
return DatabaseNameService.getUserPrivateDB(userType, username);
static getUserDatabaseName(userType: string, identifier: string): string {
return DatabaseNameService.getUserPrivateDB(userType, identifier);
}
static getSchoolDatabaseName(schoolId: string): string {
@@ -268,10 +272,10 @@ export class UserNeoDBService {
}
static getDefaultSchoolDatabaseName(): string {
return DatabaseNameService.getDevelopmentSchoolDB();
return DatabaseNameService.getStoredSchoolDatabase() || '';
}
static async fetchNodeData(nodeId: string, dbName: string): Promise<{ node_type: string; node_data: NodeResponse['nodes']['userNode'] } | null> {
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 });
@@ -279,11 +283,11 @@ export class UserNeoDBService {
status: string;
node: {
node_type: string;
node_data: NodeResponse['nodes']['userNode'];
node_data: NodeDataResponse;
};
}>('/database/tools/get-node', {
params: {
unique_id: nodeId,
uuid_string: nodeId,
db_name: dbName
}
});
@@ -300,18 +304,78 @@ export class UserNeoDBService {
}
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.tldraw_snapshot.startsWith('/node_filesystem/users/')) {
const parts = node.tldraw_snapshot.split('/');
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];
}
// For school/worker nodes, extract from the path or use a default
if (node.tldraw_snapshot.includes('/schools/')) {
return `cc.institutes.${DEV_SCHOOL_UUID}`;
}
// Default to user database if we can't determine
return node.tldraw_snapshot.split('/')[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> {
@@ -334,7 +398,7 @@ export class UserNeoDBService {
if (response.data?.status === 'success' && response.data.node) {
return {
id: response.data.node.id,
tldraw_snapshot: response.data.node.tldraw_snapshot,
node_storage_path: response.data.node.node_storage_path,
type: response.data.node.type,
label: response.data.node.label,
data: response.data.node.data
@@ -387,4 +451,4 @@ export class UserNeoDBService {
throw error;
}
}
}
}