API and persisted records use PowerOnModel system fields: - sysCreatedAt, sysCreatedBy, sysModifiedAt, sysModifiedBy Removed legacy JSON/DB field names: - _createdAt, _createdBy, _modifiedAt, _modifiedBy Frontend (frontend_nyla) and gateway call sites were updated accordingly. Database: - Bootstrap runs idempotent backfill (_migrateSystemFieldColumns) from old underscore columns and selected business duplicates into sys* where sys* IS NULL. - Re-run app bootstrap against each PostgreSQL database after deploy. - Optional: DROP INDEX IF EXISTS "idx_invitation_createdby" if an old index remains; new index: idx_invitation_syscreatedby on Invitation(sysCreatedBy). Tests: - RBAC integration tests aligned with current GROUP mandate filter and UserMandate-based UserConnection GROUP clause; buildRbacWhereClause(..., mandateId=...) must be passed explicitly (same as production request context).
100 lines
6 KiB
Python
100 lines
6 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""File-related datamodels: FileItem, FilePreview, FileData."""
|
|
|
|
from typing import Dict, Any, List, Optional, Union
|
|
from pydantic import BaseModel, Field
|
|
from modules.datamodels.datamodelBase import PowerOnModel
|
|
from modules.shared.attributeUtils import registerModelLabels
|
|
import uuid
|
|
import base64
|
|
|
|
|
|
class FileItem(PowerOnModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
|
mandateId: Optional[str] = Field(default="", description="ID of the mandate this file belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
|
featureInstanceId: Optional[str] = Field(default="", description="ID of the feature instance this file belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_fk_source": "/api/features/instances", "frontend_fk_display_field": "label"})
|
|
fileName: str = Field(description="Name of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
|
|
mimeType: str = Field(description="MIME type of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
|
fileHash: str = Field(description="Hash of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
|
fileSize: int = Field(description="Size of the file in bytes", json_schema_extra={"frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
|
|
tags: Optional[List[str]] = Field(default=None, description="Tags for categorization and search", json_schema_extra={"frontend_type": "tags", "frontend_readonly": False, "frontend_required": False})
|
|
folderId: Optional[str] = Field(default=None, description="ID of the parent folder", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
|
|
description: Optional[str] = Field(default=None, description="User-provided description of the file", json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False})
|
|
status: Optional[str] = Field(default=None, description="Processing status: pending, extracted, embedding, indexed, failed", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
|
scope: str = Field(
|
|
default="personal",
|
|
description="Data visibility scope: personal, featureInstance, mandate, global",
|
|
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
|
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
|
|
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
|
|
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
|
|
{"value": "global", "label": {"en": "Global", "de": "Global"}},
|
|
]}
|
|
)
|
|
neutralize: bool = Field(
|
|
default=False,
|
|
description="Whether this file should be neutralized before AI processing",
|
|
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
|
)
|
|
|
|
registerModelLabels(
|
|
"FileItem",
|
|
{"en": "File Item", "fr": "Élément de fichier"},
|
|
{
|
|
"id": {"en": "ID", "fr": "ID"},
|
|
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
|
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité"},
|
|
"fileName": {"en": "fileName", "fr": "Nom de fichier"},
|
|
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
|
"fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
|
|
"fileSize": {"en": "File Size", "fr": "Taille du fichier"},
|
|
"tags": {"en": "Tags", "fr": "Tags"},
|
|
"folderId": {"en": "Folder ID", "fr": "ID du dossier"},
|
|
"description": {"en": "Description", "fr": "Description"},
|
|
"status": {"en": "Status", "fr": "Statut"},
|
|
"scope": {"en": "Scope", "de": "Sichtbarkeit"},
|
|
"neutralize": {"en": "Neutralize", "de": "Neutralisieren"},
|
|
},
|
|
)
|
|
|
|
class FilePreview(BaseModel):
|
|
content: Union[str, bytes] = Field(description="File content (text or binary)")
|
|
mimeType: str = Field(description="MIME type of the file")
|
|
fileName: str = Field(description="Original fileName")
|
|
isText: bool = Field(description="Whether the content is text (True) or binary (False)")
|
|
encoding: Optional[str] = Field(None, description="Text encoding if content is text")
|
|
size: int = Field(description="Size of the content in bytes")
|
|
|
|
def toDictWithBase64Encoding(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary with base64 encoding for binary content."""
|
|
data = self.model_dump()
|
|
if isinstance(data.get("content"), bytes):
|
|
data["content"] = base64.b64encode(data["content"]).decode("utf-8")
|
|
return data
|
|
registerModelLabels(
|
|
"FilePreview",
|
|
{"en": "File Preview", "fr": "Aperçu du fichier"},
|
|
{
|
|
"content": {"en": "Content", "fr": "Contenu"},
|
|
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
|
"fileName": {"en": "fileName", "fr": "Nom de fichier"},
|
|
"isText": {"en": "Is Text", "fr": "Est du texte"},
|
|
"encoding": {"en": "Encoding", "fr": "Encodage"},
|
|
"size": {"en": "Size", "fr": "Taille"},
|
|
},
|
|
)
|
|
|
|
class FileData(PowerOnModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
|
data: str = Field(description="File data content")
|
|
base64Encoded: bool = Field(description="Whether the data is base64 encoded")
|
|
registerModelLabels(
|
|
"FileData",
|
|
{"en": "File Data", "fr": "Données de fichier"},
|
|
{
|
|
"id": {"en": "ID", "fr": "ID"},
|
|
"data": {"en": "Data", "fr": "Données"},
|
|
"base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"},
|
|
},
|
|
)
|