gateway/connectors/connector_db_json.py
2025-04-21 17:44:28 +02:00

557 lines
No EOL
20 KiB
Python

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