import json import os from typing import List, Dict, Any, Optional, Union import logging from datetime import datetime logger = logging.getLogger(__name__) class DatabaseConnector: """ A connector for JSON-based data storage. Provides generic database operations. """ def __init__(self, db_host: str, db_database: str, db_user: str = None, db_password: str = None, mandate_id: int = None, user_id: int = None): """ Initializes the JSON database connector. Args: db_host: Directory for the JSON files db_database = Database name db_user: Username for authentication (optional) db_password: API key for authentication (optional) mandate_id: Context parameter for the tenant user_id: Context parameter for the user """ # Store the input parameters self.db_host = db_host self.db_database = db_database self.db_user = db_user self.db_password = db_password # Check if context parameters are set if mandate_id is None or user_id is None: raise ValueError("mandate_id and user_id must be set") # Ensure the database directory exists self.db_folder=os.path.join(self.db_host,self.db_database) os.makedirs(self.db_folder, exist_ok=True) # Cache for loaded data self._tables_cache = {} # Initialize system table self._system_table_name = "_system" self._initialize_system_table() # Temporarily store mandate_id and user_id self._mandate_id = mandate_id self._user_id = user_id # If mandate_id or user_id are 0, try to use the initial IDs if mandate_id == 0: initial_mandate_id = self.get_initial_id("mandates") if initial_mandate_id is not None: self._mandate_id = initial_mandate_id logger.info(f"Using initial mandate_id: {initial_mandate_id} instead of 0") if user_id == 0: initial_user_id = self.get_initial_id("users") if initial_user_id is not None: self._user_id = initial_user_id logger.info(f"Using initial user_id: {initial_user_id} instead of 0") # Set the effective IDs as properties self.mandate_id = self._mandate_id self.user_id = self._user_id logger.info(f"DatabaseConnector initialized for directory: {self.db_folder}") logger.debug(f"Context: mandate_id={self.mandate_id}, user_id={self.user_id}") def _initialize_system_table(self): """Initializes the system table if it doesn't exist yet.""" system_table_path = self._get_table_path(self._system_table_name) if not os.path.exists(system_table_path): empty_system_table = {} self._save_system_table(empty_system_table) logger.info(f"System table initialized in {system_table_path}") def _load_system_table(self) -> Dict[str, int]: """Loads the system table with the initial IDs.""" system_table_path = self._get_table_path(self._system_table_name) try: if os.path.exists(system_table_path): with open(system_table_path, 'r', encoding='utf-8') as f: return json.load(f) else: return {} except Exception as e: logger.error(f"Error loading the system table: {e}") return {} def _save_system_table(self, data: Dict[str, int]) -> bool: """Saves the system table with the initial IDs.""" system_table_path = self._get_table_path(self._system_table_name) try: with open(system_table_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) return True except Exception as e: logger.error(f"Error saving the system table: {e}") return False def _get_table_path(self, table: str) -> str: """Returns the full path to a table file""" return os.path.join(self.db_folder, f"{table}.json") def _load_table(self, table: str) -> List[Dict[str, Any]]: """Loads a table from the corresponding JSON file""" path = self._get_table_path(table) # If the table is the system table, load it directly if table == self._system_table_name: return [] # The system table is not treated like normal tables # If the table is already in the cache, use the cache if table in self._tables_cache: # logger.info(f"Loading table {table} from cache") return self._tables_cache[table] # Otherwise load the file try: if os.path.exists(path): # logger.info(f"Loading table {table} from JSON {path}") with open(path, 'r', encoding='utf-8') as f: data = json.load(f) self._tables_cache[table] = data # If data was loaded and no initial ID is registered yet, # register the ID of the first record (if available) if data and not self.has_initial_id(table): if "id" in data[0]: self._register_initial_id(table, data[0]["id"]) logger.info(f"Initial ID {data[0]['id']} for table {table} retroactively registered") return data else: # If the file doesn't exist, create an empty table logger.info(f"New table {table}") self._tables_cache[table] = [] self._save_table(table, []) return [] except Exception as e: logger.error(f"Error loading table {table}: {e}") return [] def _save_table(self, table: str, data: List[Dict[str, Any]]) -> bool: """Saves a table to the corresponding JSON file""" # The system table is handled specially if table == self._system_table_name: return False path = self._get_table_path(table) try: with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) # Update the cache self._tables_cache[table] = data return True except Exception as e: logger.error(f"Error saving table {table}: {e}") return False def _filter_by_context(self, records: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """ Filters records by tenant and user context, if these fields exist in the record. """ filtered_records = [] for record in records: # Check if mandate_id exists in the record and is not null has_mandate = "mandate_id" in record and record["mandate_id"] is not None and record["mandate_id"] != "" # Check if user_id exists in the record and is not null has_user = "user_id" in record and record["user_id"] is not None and record["user_id"] != "" # If both exist, filter accordingly if has_mandate and has_user: if record["mandate_id"] == self.mandate_id: filtered_records.append(record) # If only mandate_id exists elif has_mandate and not has_user: if record["mandate_id"] == self.mandate_id: filtered_records.append(record) # If neither mandate_id nor user_id exist, add the record elif not has_mandate and not has_user: filtered_records.append(record) return filtered_records def _apply_record_filter(self, records: List[Dict[str, Any]], record_filter: Dict[str, Any] = None) -> List[Dict[str, Any]]: """Applies a record filter to the records""" if not record_filter: return records filtered_records = [] for record in records: match = True for field, value in record_filter.items(): # Check if the field exists if field not in record: match = False break # If the filter value is an integer string and the record field is an integer if isinstance(value, str) and value.isdigit() and isinstance(record[field], int): if record[field] != int(value): match = False break # Otherwise direct comparison elif record[field] != value: match = False break if match: filtered_records.append(record) return filtered_records def _register_initial_id(self, table: str, initial_id: int) -> bool: """ Registers the initial ID for a table. Args: table: Name of the table initial_id: The initial ID Returns: True on success, False on error """ try: # Load the current system table system_data = self._load_system_table() # Only register if not already present if table not in system_data: system_data[table] = initial_id success = self._save_system_table(system_data) if success: logger.info(f"Initial ID {initial_id} for table {table} registered") return success return True # If already present, this is not an error except Exception as e: logger.error(f"Error registering the initial ID for table {table}: {e}") return False def _remove_initial_id(self, table: str) -> bool: """ Removes the initial ID for a table from the system table. Args: table: Name of the table Returns: True on success, False on error """ try: # Load the current system table system_data = self._load_system_table() # Remove the entry if it exists if table in system_data: del system_data[table] success = self._save_system_table(system_data) if success: logger.info(f"Initial ID for table {table} removed from system table") return success return True # If not present, this is not an error except Exception as e: logger.error(f"Error removing initial ID for table {table}: {e}") return False # Public API def get_tables(self, filter_criteria: Dict[str, Any] = None) -> List[str]: """ Returns a list of all available tables. Args: filter_criteria: Optional filter criteria (not implemented) Returns: List of table names """ tables = [] try: for filename in os.listdir(self.db_folder): if filename.endswith('.json') and not filename.startswith('_'): table_name = filename[:-5] # Remove the .json extension tables.append(table_name) except Exception as e: logger.error(f"Error reading the database directory: {e}") return tables def get_fields(self, table: str, filter_criteria: Dict[str, Any] = None) -> List[str]: """ Returns a list of all fields in a table. Args: table: Name of the table filter_criteria: Optional filter criteria (not implemented) Returns: List of field names """ # Load the table data data = self._load_table(table) if not data: return [] # Take the first record as a reference for the fields fields = list(data[0].keys()) if data else [] return fields def get_schema(self, table: str, language: str = None, filter_criteria: Dict[str, Any] = None) -> Dict[str, Dict[str, Any]]: """ Returns a schema object for a table with data types and labels. Args: table: Name of the table language: Language for the labels (optional) filter_criteria: Optional filter criteria (not implemented) Returns: Schema object with fields, data types and labels """ # Load the table data data = self._load_table(table) schema = {} if not data: return schema # Take the first record as a reference for the fields and data types first_record = data[0] for field, value in first_record.items(): # Determine the data type data_type = type(value).__name__ # Create label (default is the field name) label = field # If model_info is available, try to get the label from the model # Implementation depends on the actual model schema[field] = { "type": data_type, "label": label } return schema def get_recordset(self, table: str, field_filter: Dict[str, Any] = None, record_filter: Dict[str, Any] = None) -> List[Dict[str, Any]]: """ Returns a list of records from a table, filtered by criteria. Args: table: Name of the table field_filter: Filter for fields (which fields should be returned) record_filter: Filter for records (which records should be returned) Returns: List of filtered records """ # Load the table data data = self._load_table(table) # Filter by tenant and user context filtered_data = self._filter_by_context(data) # Apply record_filter if available if record_filter: filtered_data = self._apply_record_filter(filtered_data, record_filter) # If field_filter is available, reduce the fields if field_filter and isinstance(field_filter, list): result = [] for record in filtered_data: filtered_record = {} for field in field_filter: if field in record: filtered_record[field] = record[field] result.append(filtered_record) return result return filtered_data def record_create(self, table: str, record_data: Dict[str, Any]) -> Dict[str, Any]: """ Creates a new record in the table. Args: table: Name of the table record_data: Data for the new record Returns: The created record """ # Load the table data data = self._load_table(table) # Add mandate_id and user_id if not present or 0 if "mandate_id" not in record_data or record_data["mandate_id"] == 0: record_data["mandate_id"] = self.mandate_id if "user_id" not in record_data or record_data["user_id"] == 0: record_data["user_id"] = self.user_id # Determine the next ID if not present if "id" not in record_data: next_id = 1 if data: next_id = max(record["id"] for record in data if "id" in record) + 1 record_data["id"] = next_id # If the table is empty and a system ID should be registered if not data: self._register_initial_id(table, record_data["id"]) logger.info(f"Initial ID {record_data['id']} for table {table} has been registered") # Add the new record data.append(record_data) # Save the updated table if self._save_table(table, data): return record_data else: raise ValueError(f"Error creating the record in table {table}") def record_delete(self, table: str, record_id: Union[str, int]) -> bool: """ Deletes a record from the table. Args: table: Name of the table record_id: ID of the record to delete Returns: True on success, False on error """ # Load table data data = self._load_table(table) # Search for the record for i, record in enumerate(data): if "id" in record and record["id"] == record_id: # Check if the record belongs to the current mandate if "mandate_id" in record and record["mandate_id"] != self.mandate_id: raise ValueError("Not your mandate") # Check if it's an initial record initial_id = self.get_initial_id(table) if initial_id is not None and initial_id == record_id: # Remove this entry from the system table self._remove_initial_id(table) logger.info(f"Initial ID {record_id} for table {table} has been removed from the system table") # Delete the record del data[i] # Save the updated table return self._save_table(table, data) # Record not found return False def record_modify(self, table: str, record_id: Union[str, int], record_data: Dict[str, Any]) -> Dict[str, Any]: """ Modifies a record in the table. Args: table: Name of the table record_id: ID of the record to modify record_data: New data for the record Returns: The updated record """ # Load table data data = self._load_table(table) # Search for the record for i, record in enumerate(data): if "id" in record and record["id"] == record_id: # Check if the record belongs to the current mandate if "mandate_id" in record and record["mandate_id"] != self.mandate_id: raise ValueError("Not your mandate") # Prevent changing the ID if "id" in record_data and record_data["id"] != record_id: raise ValueError(f"The ID of a record in table {table} cannot be changed") # Update the record for key, value in record_data.items(): data[i][key] = value # Save the updated table if self._save_table(table, data): return data[i] else: raise ValueError(f"Error updating record in table {table}") # Record not found raise ValueError(f"Record with ID {record_id} not found in table {table}") def has_initial_id(self, table: str) -> bool: """ Checks if an initial ID is registered for a table. Args: table: Name of the table Returns: True if an initial ID is registered, otherwise False """ system_data = self._load_system_table() return table in system_data def get_initial_id(self, table: str) -> Optional[int]: """ Returns the initial ID for a table. Args: table: Name of the table Returns: The initial ID or None if not present """ system_data = self._load_system_table() initial_id = system_data.get(table) if initial_id is None: logger.debug(f"No initial ID found for table {table}") return initial_id def get_all_initial_ids(self) -> Dict[str, int]: """ Returns all registered initial IDs. Returns: Dictionary with table names as keys and initial IDs as values """ system_data = self._load_system_table() return system_data.copy() # Return a copy to protect the original