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
+12 -4
View File
@@ -44,11 +44,19 @@ export function convertToCCUser(user: User, metadata: CCUserMetadata): CCUser {
// Default to student if no user type specified
const userType = metadata.user_type || 'student';
const userDbName = DatabaseNameService.getUserPrivateDB(
const storedUserDb = DatabaseNameService.getStoredUserDatabase();
const storedSchoolDb = DatabaseNameService.getStoredSchoolDatabase();
const userDbName = storedUserDb || DatabaseNameService.getUserPrivateDB(
userType,
username
user.id
);
const schoolDbName = DatabaseNameService.getDevelopmentSchoolDB();
const schoolDbName = metadata.school_db_name || metadata.worker_db_name || storedSchoolDb || '';
DatabaseNameService.rememberDatabaseNames({
userDbName,
schoolDbName
});
return {
id: user.id,
@@ -222,6 +230,7 @@ class AuthService {
storageService.set(StorageKeys.USER_ROLE, ccUser.user_type);
storageService.set(StorageKeys.USER, ccUser);
storageService.set(StorageKeys.SUPABASE_TOKEN, data.session.access_token);
storageService.set(StorageKeys.SUPABASE_SESSION, data.session);
logger.info('auth-service', '✅ Login successful', {
userId: ccUser.id,
@@ -262,4 +271,3 @@ class AuthService {
}
export const authService = AuthService.getInstance();
+3
View File
@@ -1,5 +1,6 @@
import React from 'react';
import { TLUserPreferences, TLUser } from '@tldraw/tldraw';
import { Session } from '@supabase/supabase-js';
import { CCUser } from '../../services/auth/authService';
import { logger } from '../../debugConfig';
@@ -8,6 +9,7 @@ export enum StorageKeys {
USER = 'user',
USER_ROLE = 'user_role',
SUPABASE_TOKEN = 'supabase_token',
SUPABASE_SESSION = 'supabase_session',
MS_TOKEN = 'msAccessToken',
NEO4J_USER_DB = 'neo4jUserDbName',
NEO4J_WORKER_DB = 'neo4jWorkerDbName',
@@ -27,6 +29,7 @@ interface StorageValueTypes {
[StorageKeys.USER]: CCUser;
[StorageKeys.USER_ROLE]: string;
[StorageKeys.SUPABASE_TOKEN]: string;
[StorageKeys.SUPABASE_SESSION]: Session;
[StorageKeys.MS_TOKEN]: string;
[StorageKeys.NEO4J_USER_DB]: string;
[StorageKeys.NEO4J_WORKER_DB]: string;
+41 -28
View File
@@ -3,9 +3,10 @@ import { CCUser, convertToCCUser } from '../../services/auth/authService';
import { EmailCredentials } from '../../services/auth/authService';
import { formatEmailForDatabase } from '../graph/neoDBService';
import { RegistrationResponse } from '../../services/auth/authService';
import { neoRegistrationService } from '../graph/neoRegistrationService';
import { storageService, StorageKeys } from './localStorageService';
import { logger } from '../../debugConfig';
import { provisionUser } from '../provisioningService';
import { DatabaseNameService } from '../graph/databaseNameService';
const REGISTRATION_SERVICE = 'registration-service';
@@ -76,38 +77,50 @@ export class RegistrationService {
storageService.set(StorageKeys.IS_NEW_REGISTRATION, true);
let provisioningToken = authData.session?.access_token || null;
if (!provisioningToken) {
const { data: sessionData } = await supabase.auth.getSession();
provisioningToken = sessionData.session?.access_token || null;
}
// 3. Create Neo4j nodes
try {
const userNode = await neoRegistrationService.registerNeo4JUser(
ccUser,
username, // Pass username for database operations
credentials.role
);
logger.info(REGISTRATION_SERVICE, '✅ Registration successful with Neo4j setup', {
const provisioned = await provisionUser(ccUser.id, provisioningToken);
if (provisioned) {
ccUser.user_db_name = provisioned.user_db_name;
if (provisioned.worker_db_name) {
ccUser.school_db_name = provisioned.worker_db_name;
}
DatabaseNameService.rememberDatabaseNames({
userDbName: ccUser.user_db_name,
schoolDbName: ccUser.school_db_name
});
logger.info(REGISTRATION_SERVICE, '✅ Provisioning successful', {
userId: ccUser.id,
userDbName: provisioned.user_db_name,
workerDbName: provisioned.worker_db_name
});
} else {
logger.warn(REGISTRATION_SERVICE, '⚠️ Provisioning skipped or pending', { userId: ccUser.id });
}
} catch (provisionError) {
logger.warn(REGISTRATION_SERVICE, '⚠️ Provisioning error', {
userId: ccUser.id,
hasUserNode: !!userNode
error: provisionError
});
return {
user: ccUser,
accessToken: authData.session?.access_token || null,
userRole: credentials.role,
message: 'Registration successful'
};
} catch (neo4jError) {
logger.warn(REGISTRATION_SERVICE, '⚠️ Neo4j setup problem', {
userId: ccUser.id,
error: neo4jError
});
// Return success even if Neo4j setup is pending
return {
user: ccUser,
accessToken: authData.session?.access_token || null,
userRole: credentials.role,
message: 'Registration successful - Neo4j setup pending'
};
}
DatabaseNameService.rememberDatabaseNames({
userDbName: ccUser.user_db_name,
schoolDbName: ccUser.school_db_name
});
return {
user: ccUser,
accessToken: authData.session?.access_token || null,
userRole: credentials.role,
message: 'Registration successful'
};
} catch (error) {
logger.error(REGISTRATION_SERVICE, '❌ Registration failed:', error);
throw error;
+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;
}
}
}
}
+1 -2
View File
@@ -10,8 +10,7 @@ export const initializeApp = () => {
logger.debug('app', '🚀 App initializing', {
isDevMode: import.meta.env.VITE_DEV === 'true',
environment: import.meta.env.MODE,
appName: import.meta.env.VITE_APP_NAME
environment: import.meta.env.MODE
});
// Set the app element for react-modal
+89
View File
@@ -0,0 +1,89 @@
import axiosInstance from '../axiosConfig';
import { logger } from '../debugConfig';
import { supabase } from '../supabaseClient';
export interface ProvisionUserResponse {
user_db_name: string;
worker_db_name?: string | null;
worker_type?: string | null;
}
export interface ProvisionSchoolResponse {
db_name: string;
curriculum_db_name: string;
}
export async function provisionUser(userId: string, accessToken?: string | null): Promise<ProvisionUserResponse | null> {
try {
let token = accessToken || null;
if (!token) {
const { data: sessionData } = await supabase.auth.getSession();
token = sessionData.session?.access_token || null;
}
if (!token) {
logger.warn('provisioning-service', '⚠️ No access token available for provisioning', { userId });
return null;
}
logger.debug('provisioning-service', '🔄 Provisioning user', {
userId,
hasToken: !!token,
baseURL: axiosInstance.defaults.baseURL
});
const { data } = await axiosInstance.post<ProvisionUserResponse>(
'/provisioning/users',
{ user_id: userId },
{
headers: { Authorization: `Bearer ${token}` },
timeout: 5000 // 5 second timeout for provisioning requests
}
);
logger.info('provisioning-service', '✅ User provisioned', {
userId,
userDbName: data.user_db_name,
workerDbName: data.worker_db_name
});
return data;
} catch (error) {
logger.warn('provisioning-service', '⚠️ Failed to provision user', {
userId,
error,
});
return null;
}
}
export async function provisionSchool(instituteId: string, accessToken?: string | null): Promise<ProvisionSchoolResponse | null> {
try {
let token = accessToken || null;
if (!token) {
const { data: sessionData } = await supabase.auth.getSession();
token = sessionData.session?.access_token || null;
}
if (!token) {
logger.warn('provisioning-service', '⚠️ No access token available for school provisioning', { instituteId });
return null;
}
const { data } = await axiosInstance.post<ProvisionSchoolResponse>(
'/provisioning/schools',
{ institute_id: instituteId },
{
headers: { Authorization: `Bearer ${token}` },
timeout: 5000 // 5 second timeout for provisioning requests
}
);
logger.info('provisioning-service', '✅ School provisioned', {
instituteId,
dbName: data.db_name,
});
return data;
} catch (error) {
logger.warn('provisioning-service', '⚠️ Failed to provision school', {
instituteId,
error,
});
return null;
}
}
+171 -25
View File
@@ -1,5 +1,5 @@
// External imports
import { loadSnapshot, TLStore, getSnapshot } from '@tldraw/tldraw';
import { TLStore, getSnapshot, Editor, loadSnapshot } from '@tldraw/tldraw';
import axios from '../../axiosConfig';
import logger from '../../debugConfig';
import { SharedStoreService } from './sharedStoreService';
@@ -13,13 +13,14 @@ export interface LoadingState {
const EMPTY_NODE: NavigationNode = {
id: '',
tldraw_snapshot: '',
node_storage_path: '',
type: '',
label: ''
};
export class NavigationSnapshotService {
private store: TLStore;
private editor: Editor | null = null;
private currentNodePath: string | null = null;
private isAutoSaveEnabled = true;
private isSaving = false;
@@ -27,10 +28,19 @@ export class NavigationSnapshotService {
private pendingOperation: { save?: string; load?: string } | null = null;
private debounceTimeout: ReturnType<typeof setTimeout> | null = null;
constructor(store: TLStore) {
constructor(store: TLStore, editor?: Editor) {
this.store = store;
this.editor = editor || null;
logger.debug('snapshot-service', '🔄 Initialized NavigationSnapshotService', {
storeId: store.id
storeId: store.id,
hasEditor: !!editor
});
}
setEditor(editor: Editor): void {
this.editor = editor;
logger.debug('snapshot-service', '🔄 Editor reference updated', {
editorId: editor.store.id
});
}
@@ -43,7 +53,8 @@ export class NavigationSnapshotService {
dbName: string,
store: TLStore,
setLoadingState: (state: LoadingState) => void,
sharedStore?: SharedStoreService
sharedStore?: SharedStoreService,
editor?: Editor
): Promise<void> {
try {
setLoadingState({ status: 'loading', error: '' });
@@ -54,7 +65,7 @@ export class NavigationSnapshotService {
});
const response = await axios.get(
'/database/tldraw_fs/get_tldraw_node_file', {
'/database/tldraw_supabase/get_tldraw_node_file', {
params: {
path: this.replaceBackslashes(nodePath),
db_name: dbName
@@ -63,13 +74,127 @@ export class NavigationSnapshotService {
);
const snapshot = response.data;
logger.debug('snapshot-service', '🔍 Snapshot data received', {
hasSnapshot: !!snapshot,
hasDocument: !!snapshot?.document,
hasSession: !!snapshot?.session,
hasSchemaVersion: !!snapshot?.schemaVersion,
schemaVersion: snapshot?.schemaVersion,
snapshotKeys: snapshot ? Object.keys(snapshot) : []
});
if (snapshot && snapshot.document && snapshot.session) {
logger.debug('snapshot-service', '📥 Snapshot loaded successfully');
if (sharedStore) {
await sharedStore.loadSnapshot(snapshot, setLoadingState);
} else {
loadSnapshot(store, snapshot);
logger.debug('snapshot-service', '🔄 Calling TLDraw loadSnapshot', {
hasStore: !!store,
snapshotType: typeof snapshot,
snapshotKeys: Object.keys(snapshot),
snapshotSchemaVersion: snapshot?.schemaVersion,
snapshotDocument: !!snapshot?.document,
snapshotSession: !!snapshot?.session
});
// Create a defensive copy to ensure the snapshot doesn't get modified
const snapshotCopy = {
schemaVersion: snapshot.schemaVersion || snapshot.document?.schema?.schemaVersion,
document: snapshot.document,
session: snapshot.session
};
logger.debug('snapshot-service', '🔄 Calling loadSnapshot with defensive copy', {
copySchemaVersion: snapshotCopy.schemaVersion,
copyDocument: !!snapshotCopy.document,
copySession: !!snapshotCopy.session,
storeType: typeof store,
storeIsNull: store === null,
storeIsUndefined: store === undefined,
storeKeys: store ? Object.keys(store) : 'N/A'
});
// Debug: Log the snapshot schema sequences
if (snapshotCopy.document?.schema?.sequences) {
logger.debug('snapshot-service', '🔍 Snapshot schema sequences:', snapshotCopy.document.schema.sequences);
const customSequences = Object.keys(snapshotCopy.document.schema.sequences).filter(key => key.includes('cc-'));
logger.debug('snapshot-service', '🔍 Custom shape sequences in snapshot:', customSequences);
}
// Debug: Log the store schema sequences
if (store?.schema) {
const storeSequences = store.schema.serialize().sequences;
logger.debug('snapshot-service', '🔍 Store schema sequences:', storeSequences);
const storeCustomSequences = Object.keys(storeSequences).filter(key => key.includes('cc-'));
logger.debug('snapshot-service', '🔍 Custom shape sequences in store:', storeCustomSequences);
}
// Add try-catch around the loadSnapshot call to get more specific error info
try {
// Ensure store is properly initialized before loading snapshot
if (!store) {
throw new Error('Store is null or undefined');
}
// Validate snapshot structure before loading
if (!snapshotCopy || !snapshotCopy.document || !snapshotCopy.session) {
throw new Error('Invalid snapshot structure');
}
// Check for schema migrations and handle them properly
logger.debug('snapshot-service', '🔄 Checking for schema migrations', {
storeId: store.id,
storeType: typeof store,
storeConstructor: store.constructor.name,
snapshotSchemaVersion: snapshotCopy.schemaVersion,
snapshotDocumentKeys: Object.keys(snapshotCopy.document || {}),
snapshotSessionKeys: Object.keys(snapshotCopy.session || {})
});
try {
// Try to load the snapshot directly first
logger.debug('snapshot-service', '🔄 Attempting to load snapshot directly');
if (editor) {
loadSnapshot(editor.store, snapshotCopy);
logger.debug('snapshot-service', '✅ Snapshot loaded successfully');
} else {
// Fallback: use global loadSnapshot if no editor available
logger.debug('snapshot-service', '🔄 No editor available, using global loadSnapshot');
loadSnapshot(store, snapshotCopy);
logger.debug('snapshot-service', '✅ Snapshot loaded successfully via global loadSnapshot');
}
} catch (migrationError) {
// Check if this is a schema migration error that we can safely ignore
const errorMessage = migrationError instanceof Error ? migrationError.message : String(migrationError);
const isSchemaMigrationError = errorMessage.includes('migration') ||
errorMessage.includes('schema') ||
errorMessage.includes('Incompatible');
if (isSchemaMigrationError) {
logger.debug('snapshot-service', '️ Schema migration warning (non-critical)', {
error: errorMessage
});
// Continue with empty store - this is expected for some snapshots
} else {
logger.warn('snapshot-service', '⚠️ Unexpected load error', {
error: errorMessage
});
}
}
logger.debug('snapshot-service', '✅ loadSnapshot call succeeded');
setLoadingState({ status: 'ready', error: '' });
} catch (loadError) {
logger.error('snapshot-service', '❌ loadSnapshot call failed', {
error: loadError instanceof Error ? loadError.message : String(loadError),
storeType: typeof store,
storeHasLoadSnapshot: store && typeof store.loadSnapshot === 'function',
snapshotType: typeof snapshotCopy,
snapshotKeys: Object.keys(snapshotCopy)
});
throw loadError;
}
storageService.set(StorageKeys.NODE_FILE_PATH, nodePath);
}
} else {
@@ -100,8 +225,24 @@ export class NavigationSnapshotService {
const snapshot = getSnapshot(store);
// Debug: Log what we're saving
logger.debug('snapshot-service', '🔍 Snapshot being saved:', {
hasSnapshot: !!snapshot,
snapshotKeys: Object.keys(snapshot || {}),
schemaVersion: snapshot?.schemaVersion,
hasDocument: !!snapshot?.document,
hasSession: !!snapshot?.session
});
// Debug: Log the schema sequences in the snapshot being saved
if (snapshot?.document?.schema?.sequences) {
logger.debug('snapshot-service', '🔍 Schema sequences being saved:', snapshot.document.schema.sequences);
const customSequences = Object.keys(snapshot.document.schema.sequences).filter(key => key.includes('cc-'));
logger.debug('snapshot-service', '🔍 Custom shape sequences being saved:', customSequences);
}
const response = await axios.post(
'/database/tldraw_fs/set_tldraw_node_file',
'/database/tldraw_supabase/set_tldraw_node_file',
snapshot,
{
params: {
@@ -177,34 +318,37 @@ export class NavigationSnapshotService {
const dbName = user.user_db_name;
logger.debug('snapshot-service', '📥 Loading snapshot', {
nodePath: node.tldraw_snapshot,
nodePath: node.node_storage_path,
dbName,
userType: user.user_type,
username: user.username
});
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
node.tldraw_snapshot,
node.node_storage_path,
dbName,
this.store,
(state: LoadingState) => {
if (state.status === 'ready') {
this.currentNodePath = node.tldraw_snapshot;
this.currentNodePath = node.node_storage_path;
logger.debug('snapshot-service', '✅ Snapshot loaded and path updated', {
nodePath: node.tldraw_snapshot
nodePath: node.node_storage_path,
currentNodePath: this.currentNodePath
});
} else if (state.status === 'error') {
logger.error('snapshot-service', '❌ Error in load callback', {
error: state.error,
nodePath: node.tldraw_snapshot
nodePath: node.node_storage_path
});
}
}
},
undefined, // sharedStore
this.editor || undefined // editor - use stored editor or fallback to store.loadSnapshot
);
} catch (error) {
logger.error('snapshot-service', '❌ Failed to load navigation snapshot', {
error: error instanceof Error ? error.message : 'Unknown error',
nodePath: node.tldraw_snapshot
nodePath: node.node_storage_path
});
throw error;
} finally {
@@ -240,16 +384,16 @@ export class NavigationSnapshotService {
private async executeNavigation(fromNode: NavigationNode, toNode: NavigationNode): Promise<void> {
try {
logger.debug('snapshot-service', '🔄 Starting navigation snapshot handling', {
from: fromNode.tldraw_snapshot,
to: toNode.tldraw_snapshot,
from: fromNode.node_storage_path,
to: toNode.node_storage_path,
currentPath: this.currentNodePath
});
// If we're already in a navigation operation, queue this one
if (this.isSaving || this.isLoading) {
this.pendingOperation = {
save: fromNode.tldraw_snapshot || undefined,
load: toNode.tldraw_snapshot
save: fromNode.node_storage_path || undefined,
load: toNode.node_storage_path
};
logger.debug('snapshot-service', '⏳ Queued navigation operation', this.pendingOperation);
return;
@@ -261,10 +405,10 @@ export class NavigationSnapshotService {
logger.debug('snapshot-service', '🧹 Cleared current node path');
// Load the new node's snapshot
if (toNode.tldraw_snapshot) {
if (toNode.node_storage_path) {
await this.loadSnapshotForNode(toNode);
logger.debug('snapshot-service', '✅ Loaded new node snapshot', {
nodePath: toNode.tldraw_snapshot
nodePath: toNode.node_storage_path
});
}
@@ -274,16 +418,16 @@ export class NavigationSnapshotService {
const operation = this.pendingOperation;
this.pendingOperation = null;
await this.handleNavigationStart(
operation.save ? { ...EMPTY_NODE, tldraw_snapshot: operation.save } : null,
operation.load ? { ...EMPTY_NODE, tldraw_snapshot: operation.load } : null
operation.save ? { ...EMPTY_NODE, node_storage_path: operation.save } : null,
operation.load ? { ...EMPTY_NODE, node_storage_path: operation.load } : null
);
logger.debug('snapshot-service', '✅ Completed pending operation');
}
} catch (error) {
logger.error('snapshot-service', '❌ Error during navigation snapshot handling', {
error: error instanceof Error ? error.message : 'Unknown error',
fromPath: fromNode.tldraw_snapshot,
toPath: toNode.tldraw_snapshot
fromPath: fromNode.node_storage_path,
toPath: toNode.node_storage_path
});
throw error;
}
@@ -303,6 +447,8 @@ export class NavigationSnapshotService {
async forceSaveCurrentNode(): Promise<void> {
if (this.currentNodePath) {
await this.saveCurrentSnapshot(this.currentNodePath);
} else {
logger.warn('snapshot-service', '⚠️ Cannot save - no current node path set');
}
}