refac
This commit is contained in:
@@ -19,28 +19,24 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccessGrant(Base):
|
||||
__tablename__ = "access_grant"
|
||||
__tablename__ = 'access_grant'
|
||||
|
||||
id = Column(Text, primary_key=True)
|
||||
resource_type = Column(
|
||||
Text, nullable=False
|
||||
) # "knowledge", "model", "prompt", "tool", "note", "channel", "file"
|
||||
resource_type = Column(Text, nullable=False) # "knowledge", "model", "prompt", "tool", "note", "channel", "file"
|
||||
resource_id = Column(Text, nullable=False)
|
||||
principal_type = Column(Text, nullable=False) # "user" or "group"
|
||||
principal_id = Column(
|
||||
Text, nullable=False
|
||||
) # user_id, group_id, or "*" (wildcard for public)
|
||||
principal_id = Column(Text, nullable=False) # user_id, group_id, or "*" (wildcard for public)
|
||||
permission = Column(Text, nullable=False) # "read" or "write"
|
||||
created_at = Column(BigInteger, nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"resource_type",
|
||||
"resource_id",
|
||||
"principal_type",
|
||||
"principal_id",
|
||||
"permission",
|
||||
name="uq_access_grant_grant",
|
||||
'resource_type',
|
||||
'resource_id',
|
||||
'principal_type',
|
||||
'principal_id',
|
||||
'permission',
|
||||
name='uq_access_grant_grant',
|
||||
),
|
||||
)
|
||||
|
||||
@@ -66,7 +62,7 @@ class AccessGrantResponse(BaseModel):
|
||||
permission: str
|
||||
|
||||
@classmethod
|
||||
def from_grant(cls, grant: "AccessGrantModel") -> "AccessGrantResponse":
|
||||
def from_grant(cls, grant: 'AccessGrantModel') -> 'AccessGrantResponse':
|
||||
return cls(
|
||||
id=grant.id,
|
||||
principal_type=grant.principal_type,
|
||||
@@ -100,14 +96,14 @@ def access_control_to_grants(
|
||||
if access_control is None:
|
||||
# NULL → public read (user:* for read)
|
||||
# Exception: files with NULL are private (owner-only), no grants needed
|
||||
if resource_type != "file":
|
||||
if resource_type != 'file':
|
||||
grants.append(
|
||||
{
|
||||
"resource_type": resource_type,
|
||||
"resource_id": resource_id,
|
||||
"principal_type": "user",
|
||||
"principal_id": "*",
|
||||
"permission": "read",
|
||||
'resource_type': resource_type,
|
||||
'resource_id': resource_id,
|
||||
'principal_type': 'user',
|
||||
'principal_id': '*',
|
||||
'permission': 'read',
|
||||
}
|
||||
)
|
||||
return grants
|
||||
@@ -117,30 +113,30 @@ def access_control_to_grants(
|
||||
return grants
|
||||
|
||||
# Parse structured permissions
|
||||
for permission in ["read", "write"]:
|
||||
for permission in ['read', 'write']:
|
||||
perm_data = access_control.get(permission, {})
|
||||
if not perm_data:
|
||||
continue
|
||||
|
||||
for group_id in perm_data.get("group_ids", []):
|
||||
for group_id in perm_data.get('group_ids', []):
|
||||
grants.append(
|
||||
{
|
||||
"resource_type": resource_type,
|
||||
"resource_id": resource_id,
|
||||
"principal_type": "group",
|
||||
"principal_id": group_id,
|
||||
"permission": permission,
|
||||
'resource_type': resource_type,
|
||||
'resource_id': resource_id,
|
||||
'principal_type': 'group',
|
||||
'principal_id': group_id,
|
||||
'permission': permission,
|
||||
}
|
||||
)
|
||||
|
||||
for user_id in perm_data.get("user_ids", []):
|
||||
for user_id in perm_data.get('user_ids', []):
|
||||
grants.append(
|
||||
{
|
||||
"resource_type": resource_type,
|
||||
"resource_id": resource_id,
|
||||
"principal_type": "user",
|
||||
"principal_id": user_id,
|
||||
"permission": permission,
|
||||
'resource_type': resource_type,
|
||||
'resource_id': resource_id,
|
||||
'principal_type': 'user',
|
||||
'principal_id': user_id,
|
||||
'permission': permission,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -164,27 +160,23 @@ def normalize_access_grants(access_grants: Optional[list]) -> list[dict]:
|
||||
if not isinstance(grant, dict):
|
||||
continue
|
||||
|
||||
principal_type = grant.get("principal_type")
|
||||
principal_id = grant.get("principal_id")
|
||||
permission = grant.get("permission")
|
||||
principal_type = grant.get('principal_type')
|
||||
principal_id = grant.get('principal_id')
|
||||
permission = grant.get('permission')
|
||||
|
||||
if principal_type not in ("user", "group"):
|
||||
if principal_type not in ('user', 'group'):
|
||||
continue
|
||||
if permission not in ("read", "write"):
|
||||
if permission not in ('read', 'write'):
|
||||
continue
|
||||
if not isinstance(principal_id, str) or not principal_id:
|
||||
continue
|
||||
|
||||
key = (principal_type, principal_id, permission)
|
||||
deduped[key] = {
|
||||
"id": (
|
||||
grant.get("id")
|
||||
if isinstance(grant.get("id"), str) and grant.get("id")
|
||||
else str(uuid.uuid4())
|
||||
),
|
||||
"principal_type": principal_type,
|
||||
"principal_id": principal_id,
|
||||
"permission": permission,
|
||||
'id': (grant.get('id') if isinstance(grant.get('id'), str) and grant.get('id') else str(uuid.uuid4())),
|
||||
'principal_type': principal_type,
|
||||
'principal_id': principal_id,
|
||||
'permission': permission,
|
||||
}
|
||||
|
||||
return list(deduped.values())
|
||||
@@ -195,11 +187,7 @@ def has_public_read_access_grant(access_grants: Optional[list]) -> bool:
|
||||
Returns True when a direct grant list includes wildcard public-read.
|
||||
"""
|
||||
for grant in normalize_access_grants(access_grants):
|
||||
if (
|
||||
grant["principal_type"] == "user"
|
||||
and grant["principal_id"] == "*"
|
||||
and grant["permission"] == "read"
|
||||
):
|
||||
if grant['principal_type'] == 'user' and grant['principal_id'] == '*' and grant['permission'] == 'read':
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -209,7 +197,7 @@ def has_user_access_grant(access_grants: Optional[list]) -> bool:
|
||||
Returns True when a direct grant list includes any non-wildcard user grant.
|
||||
"""
|
||||
for grant in normalize_access_grants(access_grants):
|
||||
if grant["principal_type"] == "user" and grant["principal_id"] != "*":
|
||||
if grant['principal_type'] == 'user' and grant['principal_id'] != '*':
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -225,18 +213,9 @@ def strip_user_access_grants(access_grants: Optional[list]) -> list:
|
||||
grant
|
||||
for grant in access_grants
|
||||
if not (
|
||||
(
|
||||
grant.get("principal_type")
|
||||
if isinstance(grant, dict)
|
||||
else getattr(grant, "principal_type", None)
|
||||
)
|
||||
== "user"
|
||||
and (
|
||||
grant.get("principal_id")
|
||||
if isinstance(grant, dict)
|
||||
else getattr(grant, "principal_id", None)
|
||||
)
|
||||
!= "*"
|
||||
(grant.get('principal_type') if isinstance(grant, dict) else getattr(grant, 'principal_type', None))
|
||||
== 'user'
|
||||
and (grant.get('principal_id') if isinstance(grant, dict) else getattr(grant, 'principal_id', None)) != '*'
|
||||
)
|
||||
]
|
||||
|
||||
@@ -260,29 +239,25 @@ def grants_to_access_control(grants: list) -> Optional[dict]:
|
||||
return {} # No grants = private/owner-only
|
||||
|
||||
result = {
|
||||
"read": {"group_ids": [], "user_ids": []},
|
||||
"write": {"group_ids": [], "user_ids": []},
|
||||
'read': {'group_ids': [], 'user_ids': []},
|
||||
'write': {'group_ids': [], 'user_ids': []},
|
||||
}
|
||||
|
||||
is_public = False
|
||||
for grant in grants:
|
||||
if (
|
||||
grant.principal_type == "user"
|
||||
and grant.principal_id == "*"
|
||||
and grant.permission == "read"
|
||||
):
|
||||
if grant.principal_type == 'user' and grant.principal_id == '*' and grant.permission == 'read':
|
||||
is_public = True
|
||||
continue # Don't add wildcard to user_ids list
|
||||
|
||||
if grant.permission not in ("read", "write"):
|
||||
if grant.permission not in ('read', 'write'):
|
||||
continue
|
||||
|
||||
if grant.principal_type == "group":
|
||||
if grant.principal_id not in result[grant.permission]["group_ids"]:
|
||||
result[grant.permission]["group_ids"].append(grant.principal_id)
|
||||
elif grant.principal_type == "user":
|
||||
if grant.principal_id not in result[grant.permission]["user_ids"]:
|
||||
result[grant.permission]["user_ids"].append(grant.principal_id)
|
||||
if grant.principal_type == 'group':
|
||||
if grant.principal_id not in result[grant.permission]['group_ids']:
|
||||
result[grant.permission]['group_ids'].append(grant.principal_id)
|
||||
elif grant.principal_type == 'user':
|
||||
if grant.principal_id not in result[grant.permission]['user_ids']:
|
||||
result[grant.permission]['user_ids'].append(grant.principal_id)
|
||||
|
||||
if is_public:
|
||||
return None # Public read access
|
||||
@@ -399,9 +374,7 @@ class AccessGrantsTable:
|
||||
).delete()
|
||||
|
||||
# Convert JSON to grant dicts
|
||||
grant_dicts = access_control_to_grants(
|
||||
resource_type, resource_id, access_control
|
||||
)
|
||||
grant_dicts = access_control_to_grants(resource_type, resource_id, access_control)
|
||||
|
||||
# Insert new grants
|
||||
results = []
|
||||
@@ -442,9 +415,9 @@ class AccessGrantsTable:
|
||||
id=str(uuid.uuid4()),
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
principal_type=grant_dict["principal_type"],
|
||||
principal_id=grant_dict["principal_id"],
|
||||
permission=grant_dict["permission"],
|
||||
principal_type=grant_dict['principal_type'],
|
||||
principal_id=grant_dict['principal_id'],
|
||||
permission=grant_dict['permission'],
|
||||
created_at=int(time.time()),
|
||||
)
|
||||
db.add(grant)
|
||||
@@ -511,9 +484,7 @@ class AccessGrantsTable:
|
||||
)
|
||||
.all()
|
||||
)
|
||||
result: dict[str, list[AccessGrantModel]] = {
|
||||
rid: [] for rid in resource_ids
|
||||
}
|
||||
result: dict[str, list[AccessGrantModel]] = {rid: [] for rid in resource_ids}
|
||||
for g in grants:
|
||||
result[g.resource_id].append(AccessGrantModel.model_validate(g))
|
||||
return result
|
||||
@@ -523,7 +494,7 @@ class AccessGrantsTable:
|
||||
user_id: str,
|
||||
resource_type: str,
|
||||
resource_id: str,
|
||||
permission: str = "read",
|
||||
permission: str = 'read',
|
||||
user_group_ids: Optional[set[str]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> bool:
|
||||
@@ -540,12 +511,12 @@ class AccessGrantsTable:
|
||||
conditions = [
|
||||
# Public access
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_id == "*",
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == '*',
|
||||
),
|
||||
# Direct user access
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == user_id,
|
||||
),
|
||||
]
|
||||
@@ -560,7 +531,7 @@ class AccessGrantsTable:
|
||||
if user_group_ids:
|
||||
conditions.append(
|
||||
and_(
|
||||
AccessGrant.principal_type == "group",
|
||||
AccessGrant.principal_type == 'group',
|
||||
AccessGrant.principal_id.in_(user_group_ids),
|
||||
)
|
||||
)
|
||||
@@ -582,7 +553,7 @@ class AccessGrantsTable:
|
||||
user_id: str,
|
||||
resource_type: str,
|
||||
resource_ids: list[str],
|
||||
permission: str = "read",
|
||||
permission: str = 'read',
|
||||
user_group_ids: Optional[set[str]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> set[str]:
|
||||
@@ -597,11 +568,11 @@ class AccessGrantsTable:
|
||||
with get_db_context(db) as db:
|
||||
conditions = [
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_id == "*",
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == '*',
|
||||
),
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == user_id,
|
||||
),
|
||||
]
|
||||
@@ -615,7 +586,7 @@ class AccessGrantsTable:
|
||||
if user_group_ids:
|
||||
conditions.append(
|
||||
and_(
|
||||
AccessGrant.principal_type == "group",
|
||||
AccessGrant.principal_type == 'group',
|
||||
AccessGrant.principal_id.in_(user_group_ids),
|
||||
)
|
||||
)
|
||||
@@ -637,7 +608,7 @@ class AccessGrantsTable:
|
||||
self,
|
||||
resource_type: str,
|
||||
resource_id: str,
|
||||
permission: str = "read",
|
||||
permission: str = 'read',
|
||||
db: Optional[Session] = None,
|
||||
) -> list:
|
||||
"""
|
||||
@@ -660,19 +631,17 @@ class AccessGrantsTable:
|
||||
|
||||
# Check for public access
|
||||
for grant in grants:
|
||||
if grant.principal_type == "user" and grant.principal_id == "*":
|
||||
result = Users.get_users(filter={"roles": ["!pending"]}, db=db)
|
||||
return result.get("users", [])
|
||||
if grant.principal_type == 'user' and grant.principal_id == '*':
|
||||
result = Users.get_users(filter={'roles': ['!pending']}, db=db)
|
||||
return result.get('users', [])
|
||||
|
||||
user_ids_with_access = set()
|
||||
|
||||
for grant in grants:
|
||||
if grant.principal_type == "user":
|
||||
if grant.principal_type == 'user':
|
||||
user_ids_with_access.add(grant.principal_id)
|
||||
elif grant.principal_type == "group":
|
||||
group_user_ids = Groups.get_group_user_ids_by_id(
|
||||
grant.principal_id, db=db
|
||||
)
|
||||
elif grant.principal_type == 'group':
|
||||
group_user_ids = Groups.get_group_user_ids_by_id(grant.principal_id, db=db)
|
||||
if group_user_ids:
|
||||
user_ids_with_access.update(group_user_ids)
|
||||
|
||||
@@ -688,20 +657,18 @@ class AccessGrantsTable:
|
||||
DocumentModel,
|
||||
filter: dict,
|
||||
resource_type: str,
|
||||
permission: str = "read",
|
||||
permission: str = 'read',
|
||||
):
|
||||
"""
|
||||
Apply access control filtering to a SQLAlchemy query by JOINing with access_grant.
|
||||
|
||||
This replaces the old JSON-column-based filtering with a proper relational JOIN.
|
||||
"""
|
||||
group_ids = filter.get("group_ids", [])
|
||||
user_id = filter.get("user_id")
|
||||
group_ids = filter.get('group_ids', [])
|
||||
user_id = filter.get('user_id')
|
||||
|
||||
if permission == "read_only":
|
||||
return self._has_read_only_permission_filter(
|
||||
db, query, DocumentModel, filter, resource_type
|
||||
)
|
||||
if permission == 'read_only':
|
||||
return self._has_read_only_permission_filter(db, query, DocumentModel, filter, resource_type)
|
||||
|
||||
# Build principal conditions
|
||||
principal_conditions = []
|
||||
@@ -710,8 +677,8 @@ class AccessGrantsTable:
|
||||
# Public access: user:* read
|
||||
principal_conditions.append(
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_id == "*",
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == '*',
|
||||
)
|
||||
)
|
||||
|
||||
@@ -722,7 +689,7 @@ class AccessGrantsTable:
|
||||
# Direct user grant
|
||||
principal_conditions.append(
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == user_id,
|
||||
)
|
||||
)
|
||||
@@ -731,7 +698,7 @@ class AccessGrantsTable:
|
||||
# Group grants
|
||||
principal_conditions.append(
|
||||
and_(
|
||||
AccessGrant.principal_type == "group",
|
||||
AccessGrant.principal_type == 'group',
|
||||
AccessGrant.principal_id.in_(group_ids),
|
||||
)
|
||||
)
|
||||
@@ -751,13 +718,13 @@ class AccessGrantsTable:
|
||||
AccessGrant.permission == permission,
|
||||
or_(
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_id == "*",
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == '*',
|
||||
),
|
||||
*(
|
||||
[
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == user_id,
|
||||
)
|
||||
]
|
||||
@@ -767,7 +734,7 @@ class AccessGrantsTable:
|
||||
*(
|
||||
[
|
||||
and_(
|
||||
AccessGrant.principal_type == "group",
|
||||
AccessGrant.principal_type == 'group',
|
||||
AccessGrant.principal_id.in_(group_ids),
|
||||
)
|
||||
]
|
||||
@@ -800,8 +767,8 @@ class AccessGrantsTable:
|
||||
Filter for items where user has read BUT NOT write access.
|
||||
Public items are NOT considered read_only.
|
||||
"""
|
||||
group_ids = filter.get("group_ids", [])
|
||||
user_id = filter.get("user_id")
|
||||
group_ids = filter.get('group_ids', [])
|
||||
user_id = filter.get('user_id')
|
||||
|
||||
from sqlalchemy import exists as sa_exists, select
|
||||
|
||||
@@ -811,12 +778,12 @@ class AccessGrantsTable:
|
||||
.where(
|
||||
AccessGrant.resource_type == resource_type,
|
||||
AccessGrant.resource_id == DocumentModel.id,
|
||||
AccessGrant.permission == "read",
|
||||
AccessGrant.permission == 'read',
|
||||
or_(
|
||||
*(
|
||||
[
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == user_id,
|
||||
)
|
||||
]
|
||||
@@ -826,7 +793,7 @@ class AccessGrantsTable:
|
||||
*(
|
||||
[
|
||||
and_(
|
||||
AccessGrant.principal_type == "group",
|
||||
AccessGrant.principal_type == 'group',
|
||||
AccessGrant.principal_id.in_(group_ids),
|
||||
)
|
||||
]
|
||||
@@ -845,12 +812,12 @@ class AccessGrantsTable:
|
||||
.where(
|
||||
AccessGrant.resource_type == resource_type,
|
||||
AccessGrant.resource_id == DocumentModel.id,
|
||||
AccessGrant.permission == "write",
|
||||
AccessGrant.permission == 'write',
|
||||
or_(
|
||||
*(
|
||||
[
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == user_id,
|
||||
)
|
||||
]
|
||||
@@ -860,7 +827,7 @@ class AccessGrantsTable:
|
||||
*(
|
||||
[
|
||||
and_(
|
||||
AccessGrant.principal_type == "group",
|
||||
AccessGrant.principal_type == 'group',
|
||||
AccessGrant.principal_id.in_(group_ids),
|
||||
)
|
||||
]
|
||||
@@ -879,9 +846,9 @@ class AccessGrantsTable:
|
||||
.where(
|
||||
AccessGrant.resource_type == resource_type,
|
||||
AccessGrant.resource_id == DocumentModel.id,
|
||||
AccessGrant.permission == "read",
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_id == "*",
|
||||
AccessGrant.permission == 'read',
|
||||
AccessGrant.principal_type == 'user',
|
||||
AccessGrant.principal_id == '*',
|
||||
)
|
||||
.correlate(DocumentModel)
|
||||
.exists()
|
||||
|
||||
@@ -17,7 +17,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Auth(Base):
|
||||
__tablename__ = "auth"
|
||||
__tablename__ = 'auth'
|
||||
|
||||
id = Column(String, primary_key=True, unique=True)
|
||||
email = Column(String)
|
||||
@@ -73,9 +73,9 @@ class SignupForm(BaseModel):
|
||||
name: str
|
||||
email: str
|
||||
password: str
|
||||
profile_image_url: Optional[str] = "/user.png"
|
||||
profile_image_url: Optional[str] = '/user.png'
|
||||
|
||||
@field_validator("profile_image_url")
|
||||
@field_validator('profile_image_url')
|
||||
@classmethod
|
||||
def check_profile_image_url(cls, v: Optional[str]) -> Optional[str]:
|
||||
if v is not None:
|
||||
@@ -84,7 +84,7 @@ class SignupForm(BaseModel):
|
||||
|
||||
|
||||
class AddUserForm(SignupForm):
|
||||
role: Optional[str] = "pending"
|
||||
role: Optional[str] = 'pending'
|
||||
|
||||
|
||||
class AuthsTable:
|
||||
@@ -93,25 +93,21 @@ class AuthsTable:
|
||||
email: str,
|
||||
password: str,
|
||||
name: str,
|
||||
profile_image_url: str = "/user.png",
|
||||
role: str = "pending",
|
||||
profile_image_url: str = '/user.png',
|
||||
role: str = 'pending',
|
||||
oauth: Optional[dict] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[UserModel]:
|
||||
with get_db_context(db) as db:
|
||||
log.info("insert_new_auth")
|
||||
log.info('insert_new_auth')
|
||||
|
||||
id = str(uuid.uuid4())
|
||||
|
||||
auth = AuthModel(
|
||||
**{"id": id, "email": email, "password": password, "active": True}
|
||||
)
|
||||
auth = AuthModel(**{'id': id, 'email': email, 'password': password, 'active': True})
|
||||
result = Auth(**auth.model_dump())
|
||||
db.add(result)
|
||||
|
||||
user = Users.insert_new_user(
|
||||
id, name, email, profile_image_url, role, oauth=oauth, db=db
|
||||
)
|
||||
user = Users.insert_new_user(id, name, email, profile_image_url, role, oauth=oauth, db=db)
|
||||
|
||||
db.commit()
|
||||
db.refresh(result)
|
||||
@@ -124,7 +120,7 @@ class AuthsTable:
|
||||
def authenticate_user(
|
||||
self, email: str, verify_password: callable, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
log.info(f"authenticate_user: {email}")
|
||||
log.info(f'authenticate_user: {email}')
|
||||
|
||||
user = Users.get_user_by_email(email, db=db)
|
||||
if not user:
|
||||
@@ -143,10 +139,8 @@ class AuthsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def authenticate_user_by_api_key(
|
||||
self, api_key: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
log.info(f"authenticate_user_by_api_key")
|
||||
def authenticate_user_by_api_key(self, api_key: str, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
log.info(f'authenticate_user_by_api_key')
|
||||
# if no api_key, return None
|
||||
if not api_key:
|
||||
return None
|
||||
@@ -157,10 +151,8 @@ class AuthsTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def authenticate_user_by_email(
|
||||
self, email: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
log.info(f"authenticate_user_by_email: {email}")
|
||||
def authenticate_user_by_email(self, email: str, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
log.info(f'authenticate_user_by_email: {email}')
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
# Single JOIN query instead of two separate queries
|
||||
@@ -177,28 +169,22 @@ class AuthsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_password_by_id(
|
||||
self, id: str, new_password: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def update_user_password_by_id(self, id: str, new_password: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
result = (
|
||||
db.query(Auth).filter_by(id=id).update({"password": new_password})
|
||||
)
|
||||
result = db.query(Auth).filter_by(id=id).update({'password': new_password})
|
||||
db.commit()
|
||||
return True if result == 1 else False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def update_email_by_id(
|
||||
self, id: str, email: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def update_email_by_id(self, id: str, email: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
result = db.query(Auth).filter_by(id=id).update({"email": email})
|
||||
result = db.query(Auth).filter_by(id=id).update({'email': email})
|
||||
db.commit()
|
||||
if result == 1:
|
||||
Users.update_user_by_id(id, {"email": email}, db=db)
|
||||
Users.update_user_by_id(id, {'email': email}, db=db)
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
|
||||
@@ -37,7 +37,7 @@ from sqlalchemy.sql import exists
|
||||
|
||||
|
||||
class Channel(Base):
|
||||
__tablename__ = "channel"
|
||||
__tablename__ = 'channel'
|
||||
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
user_id = Column(Text)
|
||||
@@ -94,7 +94,7 @@ class ChannelModel(BaseModel):
|
||||
|
||||
|
||||
class ChannelMember(Base):
|
||||
__tablename__ = "channel_member"
|
||||
__tablename__ = 'channel_member'
|
||||
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
channel_id = Column(Text, nullable=False)
|
||||
@@ -154,25 +154,19 @@ class ChannelMemberModel(BaseModel):
|
||||
|
||||
|
||||
class ChannelFile(Base):
|
||||
__tablename__ = "channel_file"
|
||||
__tablename__ = 'channel_file'
|
||||
|
||||
id = Column(Text, unique=True, primary_key=True)
|
||||
user_id = Column(Text, nullable=False)
|
||||
|
||||
channel_id = Column(
|
||||
Text, ForeignKey("channel.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
message_id = Column(
|
||||
Text, ForeignKey("message.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
file_id = Column(Text, ForeignKey("file.id", ondelete="CASCADE"), nullable=False)
|
||||
channel_id = Column(Text, ForeignKey('channel.id', ondelete='CASCADE'), nullable=False)
|
||||
message_id = Column(Text, ForeignKey('message.id', ondelete='CASCADE'), nullable=True)
|
||||
file_id = Column(Text, ForeignKey('file.id', ondelete='CASCADE'), nullable=False)
|
||||
|
||||
created_at = Column(BigInteger, nullable=False)
|
||||
updated_at = Column(BigInteger, nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("channel_id", "file_id", name="uq_channel_file_channel_file"),
|
||||
)
|
||||
__table_args__ = (UniqueConstraint('channel_id', 'file_id', name='uq_channel_file_channel_file'),)
|
||||
|
||||
|
||||
class ChannelFileModel(BaseModel):
|
||||
@@ -189,7 +183,7 @@ class ChannelFileModel(BaseModel):
|
||||
|
||||
|
||||
class ChannelWebhook(Base):
|
||||
__tablename__ = "channel_webhook"
|
||||
__tablename__ = 'channel_webhook'
|
||||
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
channel_id = Column(Text, nullable=False)
|
||||
@@ -235,7 +229,7 @@ class ChannelResponse(ChannelModel):
|
||||
|
||||
|
||||
class ChannelForm(BaseModel):
|
||||
name: str = ""
|
||||
name: str = ''
|
||||
description: Optional[str] = None
|
||||
is_private: Optional[bool] = None
|
||||
data: Optional[dict] = None
|
||||
@@ -255,10 +249,8 @@ class ChannelWebhookForm(BaseModel):
|
||||
|
||||
|
||||
class ChannelTable:
|
||||
def _get_access_grants(
|
||||
self, channel_id: str, db: Optional[Session] = None
|
||||
) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource("channel", channel_id, db=db)
|
||||
def _get_access_grants(self, channel_id: str, db: Optional[Session] = None) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource('channel', channel_id, db=db)
|
||||
|
||||
def _to_channel_model(
|
||||
self,
|
||||
@@ -266,13 +258,9 @@ class ChannelTable:
|
||||
access_grants: Optional[list[AccessGrantModel]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> ChannelModel:
|
||||
channel_data = ChannelModel.model_validate(channel).model_dump(
|
||||
exclude={"access_grants"}
|
||||
)
|
||||
channel_data["access_grants"] = (
|
||||
access_grants
|
||||
if access_grants is not None
|
||||
else self._get_access_grants(channel_data["id"], db=db)
|
||||
channel_data = ChannelModel.model_validate(channel).model_dump(exclude={'access_grants'})
|
||||
channel_data['access_grants'] = (
|
||||
access_grants if access_grants is not None else self._get_access_grants(channel_data['id'], db=db)
|
||||
)
|
||||
return ChannelModel.model_validate(channel_data)
|
||||
|
||||
@@ -313,20 +301,20 @@ class ChannelTable:
|
||||
for uid in user_ids:
|
||||
model = ChannelMemberModel(
|
||||
**{
|
||||
"id": str(uuid.uuid4()),
|
||||
"channel_id": channel_id,
|
||||
"user_id": uid,
|
||||
"status": "joined",
|
||||
"is_active": True,
|
||||
"is_channel_muted": False,
|
||||
"is_channel_pinned": False,
|
||||
"invited_at": now,
|
||||
"invited_by": invited_by,
|
||||
"joined_at": now,
|
||||
"left_at": None,
|
||||
"last_read_at": now,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
'id': str(uuid.uuid4()),
|
||||
'channel_id': channel_id,
|
||||
'user_id': uid,
|
||||
'status': 'joined',
|
||||
'is_active': True,
|
||||
'is_channel_muted': False,
|
||||
'is_channel_pinned': False,
|
||||
'invited_at': now,
|
||||
'invited_by': invited_by,
|
||||
'joined_at': now,
|
||||
'left_at': None,
|
||||
'last_read_at': now,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
}
|
||||
)
|
||||
memberships.append(ChannelMember(**model.model_dump()))
|
||||
@@ -339,19 +327,19 @@ class ChannelTable:
|
||||
with get_db_context(db) as db:
|
||||
channel = ChannelModel(
|
||||
**{
|
||||
**form_data.model_dump(exclude={"access_grants"}),
|
||||
"type": form_data.type if form_data.type else None,
|
||||
"name": form_data.name.lower(),
|
||||
"id": str(uuid.uuid4()),
|
||||
"user_id": user_id,
|
||||
"created_at": int(time.time_ns()),
|
||||
"updated_at": int(time.time_ns()),
|
||||
"access_grants": [],
|
||||
**form_data.model_dump(exclude={'access_grants'}),
|
||||
'type': form_data.type if form_data.type else None,
|
||||
'name': form_data.name.lower(),
|
||||
'id': str(uuid.uuid4()),
|
||||
'user_id': user_id,
|
||||
'created_at': int(time.time_ns()),
|
||||
'updated_at': int(time.time_ns()),
|
||||
'access_grants': [],
|
||||
}
|
||||
)
|
||||
new_channel = Channel(**channel.model_dump(exclude={"access_grants"}))
|
||||
new_channel = Channel(**channel.model_dump(exclude={'access_grants'}))
|
||||
|
||||
if form_data.type in ["group", "dm"]:
|
||||
if form_data.type in ['group', 'dm']:
|
||||
users = self._collect_unique_user_ids(
|
||||
invited_by=user_id,
|
||||
user_ids=form_data.user_ids,
|
||||
@@ -366,18 +354,14 @@ class ChannelTable:
|
||||
db.add_all(memberships)
|
||||
db.add(new_channel)
|
||||
db.commit()
|
||||
AccessGrants.set_access_grants(
|
||||
"channel", new_channel.id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('channel', new_channel.id, form_data.access_grants, db=db)
|
||||
return self._to_channel_model(new_channel, db=db)
|
||||
|
||||
def get_channels(self, db: Optional[Session] = None) -> list[ChannelModel]:
|
||||
with get_db_context(db) as db:
|
||||
channels = db.query(Channel).all()
|
||||
channel_ids = [channel.id for channel in channels]
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"channel", channel_ids, db=db
|
||||
)
|
||||
grants_map = AccessGrants.get_grants_by_resources('channel', channel_ids, db=db)
|
||||
return [
|
||||
self._to_channel_model(
|
||||
channel,
|
||||
@@ -387,23 +371,19 @@ class ChannelTable:
|
||||
for channel in channels
|
||||
]
|
||||
|
||||
def _has_permission(self, db, query, filter: dict, permission: str = "read"):
|
||||
def _has_permission(self, db, query, filter: dict, permission: str = 'read'):
|
||||
return AccessGrants.has_permission_filter(
|
||||
db=db,
|
||||
query=query,
|
||||
DocumentModel=Channel,
|
||||
filter=filter,
|
||||
resource_type="channel",
|
||||
resource_type='channel',
|
||||
permission=permission,
|
||||
)
|
||||
|
||||
def get_channels_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[ChannelModel]:
|
||||
def get_channels_by_user_id(self, user_id: str, db: Optional[Session] = None) -> list[ChannelModel]:
|
||||
with get_db_context(db) as db:
|
||||
user_group_ids = [
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
]
|
||||
user_group_ids = [group.id for group in Groups.get_groups_by_member_id(user_id, db=db)]
|
||||
|
||||
membership_channels = (
|
||||
db.query(Channel)
|
||||
@@ -411,7 +391,7 @@ class ChannelTable:
|
||||
.filter(
|
||||
Channel.deleted_at.is_(None),
|
||||
Channel.archived_at.is_(None),
|
||||
Channel.type.in_(["group", "dm"]),
|
||||
Channel.type.in_(['group', 'dm']),
|
||||
ChannelMember.user_id == user_id,
|
||||
ChannelMember.is_active.is_(True),
|
||||
)
|
||||
@@ -423,29 +403,20 @@ class ChannelTable:
|
||||
Channel.archived_at.is_(None),
|
||||
or_(
|
||||
Channel.type.is_(None), # True NULL/None
|
||||
Channel.type == "", # Empty string
|
||||
and_(Channel.type != "group", Channel.type != "dm"),
|
||||
Channel.type == '', # Empty string
|
||||
and_(Channel.type != 'group', Channel.type != 'dm'),
|
||||
),
|
||||
)
|
||||
query = self._has_permission(
|
||||
db, query, {"user_id": user_id, "group_ids": user_group_ids}
|
||||
)
|
||||
query = self._has_permission(db, query, {'user_id': user_id, 'group_ids': user_group_ids})
|
||||
|
||||
standard_channels = query.all()
|
||||
|
||||
all_channels = membership_channels + standard_channels
|
||||
channel_ids = [c.id for c in all_channels]
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"channel", channel_ids, db=db
|
||||
)
|
||||
return [
|
||||
self._to_channel_model(c, access_grants=grants_map.get(c.id, []), db=db)
|
||||
for c in all_channels
|
||||
]
|
||||
grants_map = AccessGrants.get_grants_by_resources('channel', channel_ids, db=db)
|
||||
return [self._to_channel_model(c, access_grants=grants_map.get(c.id, []), db=db) for c in all_channels]
|
||||
|
||||
def get_dm_channel_by_user_ids(
|
||||
self, user_ids: list[str], db: Optional[Session] = None
|
||||
) -> Optional[ChannelModel]:
|
||||
def get_dm_channel_by_user_ids(self, user_ids: list[str], db: Optional[Session] = None) -> Optional[ChannelModel]:
|
||||
with get_db_context(db) as db:
|
||||
# Ensure uniqueness in case a list with duplicates is passed
|
||||
unique_user_ids = list(set(user_ids))
|
||||
@@ -471,7 +442,7 @@ class ChannelTable:
|
||||
db.query(Channel)
|
||||
.filter(
|
||||
Channel.id.in_(subquery),
|
||||
Channel.type == "dm",
|
||||
Channel.type == 'dm',
|
||||
)
|
||||
.first()
|
||||
)
|
||||
@@ -488,32 +459,23 @@ class ChannelTable:
|
||||
) -> list[ChannelMemberModel]:
|
||||
with get_db_context(db) as db:
|
||||
# 1. Collect all user_ids including groups + inviter
|
||||
requested_users = self._collect_unique_user_ids(
|
||||
invited_by, user_ids, group_ids
|
||||
)
|
||||
requested_users = self._collect_unique_user_ids(invited_by, user_ids, group_ids)
|
||||
|
||||
existing_users = {
|
||||
row.user_id
|
||||
for row in db.query(ChannelMember.user_id)
|
||||
.filter(ChannelMember.channel_id == channel_id)
|
||||
.all()
|
||||
for row in db.query(ChannelMember.user_id).filter(ChannelMember.channel_id == channel_id).all()
|
||||
}
|
||||
|
||||
new_user_ids = requested_users - existing_users
|
||||
if not new_user_ids:
|
||||
return [] # Nothing to add
|
||||
|
||||
new_memberships = self._create_membership_models(
|
||||
channel_id, invited_by, new_user_ids
|
||||
)
|
||||
new_memberships = self._create_membership_models(channel_id, invited_by, new_user_ids)
|
||||
|
||||
db.add_all(new_memberships)
|
||||
db.commit()
|
||||
|
||||
return [
|
||||
ChannelMemberModel.model_validate(membership)
|
||||
for membership in new_memberships
|
||||
]
|
||||
return [ChannelMemberModel.model_validate(membership) for membership in new_memberships]
|
||||
|
||||
def remove_members_from_channel(
|
||||
self,
|
||||
@@ -533,9 +495,7 @@ class ChannelTable:
|
||||
db.commit()
|
||||
return result # number of rows deleted
|
||||
|
||||
def is_user_channel_manager(
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def is_user_channel_manager(self, channel_id: str, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
# Check if the user is the creator of the channel
|
||||
# or has a 'manager' role in ChannelMember
|
||||
@@ -548,15 +508,13 @@ class ChannelTable:
|
||||
.filter(
|
||||
ChannelMember.channel_id == channel_id,
|
||||
ChannelMember.user_id == user_id,
|
||||
ChannelMember.role == "manager",
|
||||
ChannelMember.role == 'manager',
|
||||
)
|
||||
.first()
|
||||
)
|
||||
return membership is not None
|
||||
|
||||
def join_channel(
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelMemberModel]:
|
||||
def join_channel(self, channel_id: str, user_id: str, db: Optional[Session] = None) -> Optional[ChannelMemberModel]:
|
||||
with get_db_context(db) as db:
|
||||
# Check if the membership already exists
|
||||
existing_membership = (
|
||||
@@ -573,18 +531,18 @@ class ChannelTable:
|
||||
# Create new membership
|
||||
channel_member = ChannelMemberModel(
|
||||
**{
|
||||
"id": str(uuid.uuid4()),
|
||||
"channel_id": channel_id,
|
||||
"user_id": user_id,
|
||||
"status": "joined",
|
||||
"is_active": True,
|
||||
"is_channel_muted": False,
|
||||
"is_channel_pinned": False,
|
||||
"joined_at": int(time.time_ns()),
|
||||
"left_at": None,
|
||||
"last_read_at": int(time.time_ns()),
|
||||
"created_at": int(time.time_ns()),
|
||||
"updated_at": int(time.time_ns()),
|
||||
'id': str(uuid.uuid4()),
|
||||
'channel_id': channel_id,
|
||||
'user_id': user_id,
|
||||
'status': 'joined',
|
||||
'is_active': True,
|
||||
'is_channel_muted': False,
|
||||
'is_channel_pinned': False,
|
||||
'joined_at': int(time.time_ns()),
|
||||
'left_at': None,
|
||||
'last_read_at': int(time.time_ns()),
|
||||
'created_at': int(time.time_ns()),
|
||||
'updated_at': int(time.time_ns()),
|
||||
}
|
||||
)
|
||||
new_membership = ChannelMember(**channel_member.model_dump())
|
||||
@@ -593,9 +551,7 @@ class ChannelTable:
|
||||
db.commit()
|
||||
return channel_member
|
||||
|
||||
def leave_channel(
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def leave_channel(self, channel_id: str, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
@@ -608,7 +564,7 @@ class ChannelTable:
|
||||
if not membership:
|
||||
return False
|
||||
|
||||
membership.status = "left"
|
||||
membership.status = 'left'
|
||||
membership.is_active = False
|
||||
membership.left_at = int(time.time_ns())
|
||||
membership.updated_at = int(time.time_ns())
|
||||
@@ -630,19 +586,10 @@ class ChannelTable:
|
||||
)
|
||||
return ChannelMemberModel.model_validate(membership) if membership else None
|
||||
|
||||
def get_members_by_channel_id(
|
||||
self, channel_id: str, db: Optional[Session] = None
|
||||
) -> list[ChannelMemberModel]:
|
||||
def get_members_by_channel_id(self, channel_id: str, db: Optional[Session] = None) -> list[ChannelMemberModel]:
|
||||
with get_db_context(db) as db:
|
||||
memberships = (
|
||||
db.query(ChannelMember)
|
||||
.filter(ChannelMember.channel_id == channel_id)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
ChannelMemberModel.model_validate(membership)
|
||||
for membership in memberships
|
||||
]
|
||||
memberships = db.query(ChannelMember).filter(ChannelMember.channel_id == channel_id).all()
|
||||
return [ChannelMemberModel.model_validate(membership) for membership in memberships]
|
||||
|
||||
def pin_channel(
|
||||
self,
|
||||
@@ -669,9 +616,7 @@ class ChannelTable:
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def update_member_last_read_at(
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def update_member_last_read_at(self, channel_id: str, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
@@ -715,9 +660,7 @@ class ChannelTable:
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def is_user_channel_member(
|
||||
self, channel_id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def is_user_channel_member(self, channel_id: str, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
@@ -729,9 +672,7 @@ class ChannelTable:
|
||||
)
|
||||
return membership is not None
|
||||
|
||||
def get_channel_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelModel]:
|
||||
def get_channel_by_id(self, id: str, db: Optional[Session] = None) -> Optional[ChannelModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
channel = db.query(Channel).filter(Channel.id == id).first()
|
||||
@@ -739,18 +680,12 @@ class ChannelTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_channels_by_file_id(
|
||||
self, file_id: str, db: Optional[Session] = None
|
||||
) -> list[ChannelModel]:
|
||||
def get_channels_by_file_id(self, file_id: str, db: Optional[Session] = None) -> list[ChannelModel]:
|
||||
with get_db_context(db) as db:
|
||||
channel_files = (
|
||||
db.query(ChannelFile).filter(ChannelFile.file_id == file_id).all()
|
||||
)
|
||||
channel_files = db.query(ChannelFile).filter(ChannelFile.file_id == file_id).all()
|
||||
channel_ids = [cf.channel_id for cf in channel_files]
|
||||
channels = db.query(Channel).filter(Channel.id.in_(channel_ids)).all()
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"channel", channel_ids, db=db
|
||||
)
|
||||
grants_map = AccessGrants.get_grants_by_resources('channel', channel_ids, db=db)
|
||||
return [
|
||||
self._to_channel_model(
|
||||
channel,
|
||||
@@ -765,9 +700,7 @@ class ChannelTable:
|
||||
) -> list[ChannelModel]:
|
||||
with get_db_context(db) as db:
|
||||
# 1. Determine which channels have this file
|
||||
channel_file_rows = (
|
||||
db.query(ChannelFile).filter(ChannelFile.file_id == file_id).all()
|
||||
)
|
||||
channel_file_rows = db.query(ChannelFile).filter(ChannelFile.file_id == file_id).all()
|
||||
channel_ids = [row.channel_id for row in channel_file_rows]
|
||||
|
||||
if not channel_ids:
|
||||
@@ -787,15 +720,13 @@ class ChannelTable:
|
||||
return []
|
||||
|
||||
# Preload user's group membership
|
||||
user_group_ids = [
|
||||
g.id for g in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
]
|
||||
user_group_ids = [g.id for g in Groups.get_groups_by_member_id(user_id, db=db)]
|
||||
|
||||
allowed_channels = []
|
||||
|
||||
for channel in channels:
|
||||
# --- Case A: group or dm => user must be an active member ---
|
||||
if channel.type in ["group", "dm"]:
|
||||
if channel.type in ['group', 'dm']:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
.filter(
|
||||
@@ -815,8 +746,8 @@ class ChannelTable:
|
||||
query = self._has_permission(
|
||||
db,
|
||||
query,
|
||||
{"user_id": user_id, "group_ids": user_group_ids},
|
||||
permission="read",
|
||||
{'user_id': user_id, 'group_ids': user_group_ids},
|
||||
permission='read',
|
||||
)
|
||||
|
||||
allowed = query.first()
|
||||
@@ -844,7 +775,7 @@ class ChannelTable:
|
||||
return None
|
||||
|
||||
# If the channel is a group or dm, read access requires membership (active)
|
||||
if channel.type in ["group", "dm"]:
|
||||
if channel.type in ['group', 'dm']:
|
||||
membership = (
|
||||
db.query(ChannelMember)
|
||||
.filter(
|
||||
@@ -863,24 +794,18 @@ class ChannelTable:
|
||||
query = db.query(Channel).filter(Channel.id == id)
|
||||
|
||||
# Determine user groups
|
||||
user_group_ids = [
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
]
|
||||
user_group_ids = [group.id for group in Groups.get_groups_by_member_id(user_id, db=db)]
|
||||
|
||||
# Apply ACL rules
|
||||
query = self._has_permission(
|
||||
db,
|
||||
query,
|
||||
{"user_id": user_id, "group_ids": user_group_ids},
|
||||
permission="read",
|
||||
{'user_id': user_id, 'group_ids': user_group_ids},
|
||||
permission='read',
|
||||
)
|
||||
|
||||
channel_allowed = query.first()
|
||||
return (
|
||||
self._to_channel_model(channel_allowed, db=db)
|
||||
if channel_allowed
|
||||
else None
|
||||
)
|
||||
return self._to_channel_model(channel_allowed, db=db) if channel_allowed else None
|
||||
|
||||
def update_channel_by_id(
|
||||
self, id: str, form_data: ChannelForm, db: Optional[Session] = None
|
||||
@@ -898,9 +823,7 @@ class ChannelTable:
|
||||
channel.meta = form_data.meta
|
||||
|
||||
if form_data.access_grants is not None:
|
||||
AccessGrants.set_access_grants(
|
||||
"channel", id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('channel', id, form_data.access_grants, db=db)
|
||||
channel.updated_at = int(time.time_ns())
|
||||
|
||||
db.commit()
|
||||
@@ -912,12 +835,12 @@ class ChannelTable:
|
||||
with get_db_context(db) as db:
|
||||
channel_file = ChannelFileModel(
|
||||
**{
|
||||
"id": str(uuid.uuid4()),
|
||||
"channel_id": channel_id,
|
||||
"file_id": file_id,
|
||||
"user_id": user_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
'id': str(uuid.uuid4()),
|
||||
'channel_id': channel_id,
|
||||
'file_id': file_id,
|
||||
'user_id': user_id,
|
||||
'created_at': int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -942,11 +865,7 @@ class ChannelTable:
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
channel_file = (
|
||||
db.query(ChannelFile)
|
||||
.filter_by(channel_id=channel_id, file_id=file_id)
|
||||
.first()
|
||||
)
|
||||
channel_file = db.query(ChannelFile).filter_by(channel_id=channel_id, file_id=file_id).first()
|
||||
if not channel_file:
|
||||
return False
|
||||
|
||||
@@ -958,14 +877,10 @@ class ChannelTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def remove_file_from_channel_by_id(
|
||||
self, channel_id: str, file_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def remove_file_from_channel_by_id(self, channel_id: str, file_id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
db.query(ChannelFile).filter_by(
|
||||
channel_id=channel_id, file_id=file_id
|
||||
).delete()
|
||||
db.query(ChannelFile).filter_by(channel_id=channel_id, file_id=file_id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
@@ -973,7 +888,7 @@ class ChannelTable:
|
||||
|
||||
def delete_channel_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
AccessGrants.revoke_all_access("channel", id, db=db)
|
||||
AccessGrants.revoke_all_access('channel', id, db=db)
|
||||
db.query(Channel).filter(Channel.id == id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
@@ -1005,24 +920,14 @@ class ChannelTable:
|
||||
db.commit()
|
||||
return webhook
|
||||
|
||||
def get_webhooks_by_channel_id(
|
||||
self, channel_id: str, db: Optional[Session] = None
|
||||
) -> list[ChannelWebhookModel]:
|
||||
def get_webhooks_by_channel_id(self, channel_id: str, db: Optional[Session] = None) -> list[ChannelWebhookModel]:
|
||||
with get_db_context(db) as db:
|
||||
webhooks = (
|
||||
db.query(ChannelWebhook)
|
||||
.filter(ChannelWebhook.channel_id == channel_id)
|
||||
.all()
|
||||
)
|
||||
webhooks = db.query(ChannelWebhook).filter(ChannelWebhook.channel_id == channel_id).all()
|
||||
return [ChannelWebhookModel.model_validate(w) for w in webhooks]
|
||||
|
||||
def get_webhook_by_id(
|
||||
self, webhook_id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChannelWebhookModel]:
|
||||
def get_webhook_by_id(self, webhook_id: str, db: Optional[Session] = None) -> Optional[ChannelWebhookModel]:
|
||||
with get_db_context(db) as db:
|
||||
webhook = (
|
||||
db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first()
|
||||
)
|
||||
webhook = db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first()
|
||||
return ChannelWebhookModel.model_validate(webhook) if webhook else None
|
||||
|
||||
def get_webhook_by_id_and_token(
|
||||
@@ -1046,9 +951,7 @@ class ChannelTable:
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[ChannelWebhookModel]:
|
||||
with get_db_context(db) as db:
|
||||
webhook = (
|
||||
db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first()
|
||||
)
|
||||
webhook = db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first()
|
||||
if not webhook:
|
||||
return None
|
||||
webhook.name = form_data.name
|
||||
@@ -1057,28 +960,18 @@ class ChannelTable:
|
||||
db.commit()
|
||||
return ChannelWebhookModel.model_validate(webhook)
|
||||
|
||||
def update_webhook_last_used_at(
|
||||
self, webhook_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def update_webhook_last_used_at(self, webhook_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
webhook = (
|
||||
db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first()
|
||||
)
|
||||
webhook = db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first()
|
||||
if not webhook:
|
||||
return False
|
||||
webhook.last_used_at = int(time.time_ns())
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def delete_webhook_by_id(
|
||||
self, webhook_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_webhook_by_id(self, webhook_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
result = (
|
||||
db.query(ChannelWebhook)
|
||||
.filter(ChannelWebhook.id == webhook_id)
|
||||
.delete()
|
||||
)
|
||||
result = db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).delete()
|
||||
db.commit()
|
||||
return result > 0
|
||||
|
||||
|
||||
@@ -47,13 +47,11 @@ def _normalize_timestamp(timestamp: int) -> float:
|
||||
|
||||
|
||||
class ChatMessage(Base):
|
||||
__tablename__ = "chat_message"
|
||||
__tablename__ = 'chat_message'
|
||||
|
||||
# Identity
|
||||
id = Column(Text, primary_key=True)
|
||||
chat_id = Column(
|
||||
Text, ForeignKey("chat.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
chat_id = Column(Text, ForeignKey('chat.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||
user_id = Column(Text, index=True)
|
||||
|
||||
# Structure
|
||||
@@ -85,9 +83,9 @@ class ChatMessage(Base):
|
||||
updated_at = Column(BigInteger)
|
||||
|
||||
__table_args__ = (
|
||||
Index("chat_message_chat_parent_idx", "chat_id", "parent_id"),
|
||||
Index("chat_message_model_created_idx", "model_id", "created_at"),
|
||||
Index("chat_message_user_created_idx", "user_id", "created_at"),
|
||||
Index('chat_message_chat_parent_idx', 'chat_id', 'parent_id'),
|
||||
Index('chat_message_model_created_idx', 'model_id', 'created_at'),
|
||||
Index('chat_message_user_created_idx', 'user_id', 'created_at'),
|
||||
)
|
||||
|
||||
|
||||
@@ -135,43 +133,41 @@ class ChatMessageTable:
|
||||
"""Insert or update a chat message."""
|
||||
with get_db_context(db) as db:
|
||||
now = int(time.time())
|
||||
timestamp = data.get("timestamp", now)
|
||||
timestamp = data.get('timestamp', now)
|
||||
|
||||
# Use composite ID: {chat_id}-{message_id}
|
||||
composite_id = f"{chat_id}-{message_id}"
|
||||
composite_id = f'{chat_id}-{message_id}'
|
||||
|
||||
existing = db.get(ChatMessage, composite_id)
|
||||
if existing:
|
||||
# Update existing
|
||||
if "role" in data:
|
||||
existing.role = data["role"]
|
||||
if "parent_id" in data:
|
||||
existing.parent_id = data.get("parent_id") or data.get("parentId")
|
||||
if "content" in data:
|
||||
existing.content = data.get("content")
|
||||
if "output" in data:
|
||||
existing.output = data.get("output")
|
||||
if "model_id" in data or "model" in data:
|
||||
existing.model_id = data.get("model_id") or data.get("model")
|
||||
if "files" in data:
|
||||
existing.files = data.get("files")
|
||||
if "sources" in data:
|
||||
existing.sources = data.get("sources")
|
||||
if "embeds" in data:
|
||||
existing.embeds = data.get("embeds")
|
||||
if "done" in data:
|
||||
existing.done = data.get("done", True)
|
||||
if "status_history" in data or "statusHistory" in data:
|
||||
existing.status_history = data.get("status_history") or data.get(
|
||||
"statusHistory"
|
||||
)
|
||||
if "error" in data:
|
||||
existing.error = data.get("error")
|
||||
if 'role' in data:
|
||||
existing.role = data['role']
|
||||
if 'parent_id' in data:
|
||||
existing.parent_id = data.get('parent_id') or data.get('parentId')
|
||||
if 'content' in data:
|
||||
existing.content = data.get('content')
|
||||
if 'output' in data:
|
||||
existing.output = data.get('output')
|
||||
if 'model_id' in data or 'model' in data:
|
||||
existing.model_id = data.get('model_id') or data.get('model')
|
||||
if 'files' in data:
|
||||
existing.files = data.get('files')
|
||||
if 'sources' in data:
|
||||
existing.sources = data.get('sources')
|
||||
if 'embeds' in data:
|
||||
existing.embeds = data.get('embeds')
|
||||
if 'done' in data:
|
||||
existing.done = data.get('done', True)
|
||||
if 'status_history' in data or 'statusHistory' in data:
|
||||
existing.status_history = data.get('status_history') or data.get('statusHistory')
|
||||
if 'error' in data:
|
||||
existing.error = data.get('error')
|
||||
# Extract usage - check direct field first, then info.usage
|
||||
usage = data.get("usage")
|
||||
usage = data.get('usage')
|
||||
if not usage:
|
||||
info = data.get("info", {})
|
||||
usage = info.get("usage") if info else None
|
||||
info = data.get('info', {})
|
||||
usage = info.get('usage') if info else None
|
||||
if usage:
|
||||
existing.usage = usage
|
||||
existing.updated_at = now
|
||||
@@ -181,26 +177,25 @@ class ChatMessageTable:
|
||||
else:
|
||||
# Insert new
|
||||
# Extract usage - check direct field first, then info.usage
|
||||
usage = data.get("usage")
|
||||
usage = data.get('usage')
|
||||
if not usage:
|
||||
info = data.get("info", {})
|
||||
usage = info.get("usage") if info else None
|
||||
info = data.get('info', {})
|
||||
usage = info.get('usage') if info else None
|
||||
message = ChatMessage(
|
||||
id=composite_id,
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
role=data.get("role", "user"),
|
||||
parent_id=data.get("parent_id") or data.get("parentId"),
|
||||
content=data.get("content"),
|
||||
output=data.get("output"),
|
||||
model_id=data.get("model_id") or data.get("model"),
|
||||
files=data.get("files"),
|
||||
sources=data.get("sources"),
|
||||
embeds=data.get("embeds"),
|
||||
done=data.get("done", True),
|
||||
status_history=data.get("status_history")
|
||||
or data.get("statusHistory"),
|
||||
error=data.get("error"),
|
||||
role=data.get('role', 'user'),
|
||||
parent_id=data.get('parent_id') or data.get('parentId'),
|
||||
content=data.get('content'),
|
||||
output=data.get('output'),
|
||||
model_id=data.get('model_id') or data.get('model'),
|
||||
files=data.get('files'),
|
||||
sources=data.get('sources'),
|
||||
embeds=data.get('embeds'),
|
||||
done=data.get('done', True),
|
||||
status_history=data.get('status_history') or data.get('statusHistory'),
|
||||
error=data.get('error'),
|
||||
usage=usage,
|
||||
created_at=timestamp,
|
||||
updated_at=now,
|
||||
@@ -210,23 +205,14 @@ class ChatMessageTable:
|
||||
db.refresh(message)
|
||||
return ChatMessageModel.model_validate(message)
|
||||
|
||||
def get_message_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatMessageModel]:
|
||||
def get_message_by_id(self, id: str, db: Optional[Session] = None) -> Optional[ChatMessageModel]:
|
||||
with get_db_context(db) as db:
|
||||
message = db.get(ChatMessage, id)
|
||||
return ChatMessageModel.model_validate(message) if message else None
|
||||
|
||||
def get_messages_by_chat_id(
|
||||
self, chat_id: str, db: Optional[Session] = None
|
||||
) -> list[ChatMessageModel]:
|
||||
def get_messages_by_chat_id(self, chat_id: str, db: Optional[Session] = None) -> list[ChatMessageModel]:
|
||||
with get_db_context(db) as db:
|
||||
messages = (
|
||||
db.query(ChatMessage)
|
||||
.filter_by(chat_id=chat_id)
|
||||
.order_by(ChatMessage.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
messages = db.query(ChatMessage).filter_by(chat_id=chat_id).order_by(ChatMessage.created_at.asc()).all()
|
||||
return [ChatMessageModel.model_validate(message) for message in messages]
|
||||
|
||||
def get_messages_by_user_id(
|
||||
@@ -262,12 +248,7 @@ class ChatMessageTable:
|
||||
query = query.filter(ChatMessage.created_at >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(ChatMessage.created_at <= end_date)
|
||||
messages = (
|
||||
query.order_by(ChatMessage.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
messages = query.order_by(ChatMessage.created_at.desc()).offset(skip).limit(limit).all()
|
||||
return [ChatMessageModel.model_validate(message) for message in messages]
|
||||
|
||||
def get_chat_ids_by_model_id(
|
||||
@@ -284,7 +265,7 @@ class ChatMessageTable:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(
|
||||
ChatMessage.chat_id,
|
||||
func.max(ChatMessage.created_at).label("last_message_at"),
|
||||
func.max(ChatMessage.created_at).label('last_message_at'),
|
||||
).filter(ChatMessage.model_id == model_id)
|
||||
if start_date:
|
||||
query = query.filter(ChatMessage.created_at >= start_date)
|
||||
@@ -303,9 +284,7 @@ class ChatMessageTable:
|
||||
)
|
||||
return [chat_id for chat_id, _ in chat_ids]
|
||||
|
||||
def delete_messages_by_chat_id(
|
||||
self, chat_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_messages_by_chat_id(self, chat_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
db.query(ChatMessage).filter_by(chat_id=chat_id).delete()
|
||||
db.commit()
|
||||
@@ -323,12 +302,10 @@ class ChatMessageTable:
|
||||
from sqlalchemy import func
|
||||
from open_webui.models.groups import GroupMember
|
||||
|
||||
query = db.query(
|
||||
ChatMessage.model_id, func.count(ChatMessage.id).label("count")
|
||||
).filter(
|
||||
ChatMessage.role == "assistant",
|
||||
query = db.query(ChatMessage.model_id, func.count(ChatMessage.id).label('count')).filter(
|
||||
ChatMessage.role == 'assistant',
|
||||
ChatMessage.model_id.isnot(None),
|
||||
~ChatMessage.user_id.like("shared-%"),
|
||||
~ChatMessage.user_id.like('shared-%'),
|
||||
)
|
||||
|
||||
if start_date:
|
||||
@@ -336,11 +313,7 @@ class ChatMessageTable:
|
||||
if end_date:
|
||||
query = query.filter(ChatMessage.created_at <= end_date)
|
||||
if group_id:
|
||||
group_users = (
|
||||
db.query(GroupMember.user_id)
|
||||
.filter(GroupMember.group_id == group_id)
|
||||
.subquery()
|
||||
)
|
||||
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
|
||||
query = query.filter(ChatMessage.user_id.in_(group_users))
|
||||
|
||||
results = query.group_by(ChatMessage.model_id).all()
|
||||
@@ -360,36 +333,32 @@ class ChatMessageTable:
|
||||
|
||||
dialect = db.bind.dialect.name
|
||||
|
||||
if dialect == "sqlite":
|
||||
input_tokens = cast(
|
||||
func.json_extract(ChatMessage.usage, "$.input_tokens"), Integer
|
||||
)
|
||||
output_tokens = cast(
|
||||
func.json_extract(ChatMessage.usage, "$.output_tokens"), Integer
|
||||
)
|
||||
elif dialect == "postgresql":
|
||||
if dialect == 'sqlite':
|
||||
input_tokens = cast(func.json_extract(ChatMessage.usage, '$.input_tokens'), Integer)
|
||||
output_tokens = cast(func.json_extract(ChatMessage.usage, '$.output_tokens'), Integer)
|
||||
elif dialect == 'postgresql':
|
||||
# Use json_extract_path_text for PostgreSQL JSON columns
|
||||
input_tokens = cast(
|
||||
func.json_extract_path_text(ChatMessage.usage, "input_tokens"),
|
||||
func.json_extract_path_text(ChatMessage.usage, 'input_tokens'),
|
||||
Integer,
|
||||
)
|
||||
output_tokens = cast(
|
||||
func.json_extract_path_text(ChatMessage.usage, "output_tokens"),
|
||||
func.json_extract_path_text(ChatMessage.usage, 'output_tokens'),
|
||||
Integer,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported dialect: {dialect}")
|
||||
raise NotImplementedError(f'Unsupported dialect: {dialect}')
|
||||
|
||||
query = db.query(
|
||||
ChatMessage.model_id,
|
||||
func.coalesce(func.sum(input_tokens), 0).label("input_tokens"),
|
||||
func.coalesce(func.sum(output_tokens), 0).label("output_tokens"),
|
||||
func.count(ChatMessage.id).label("message_count"),
|
||||
func.coalesce(func.sum(input_tokens), 0).label('input_tokens'),
|
||||
func.coalesce(func.sum(output_tokens), 0).label('output_tokens'),
|
||||
func.count(ChatMessage.id).label('message_count'),
|
||||
).filter(
|
||||
ChatMessage.role == "assistant",
|
||||
ChatMessage.role == 'assistant',
|
||||
ChatMessage.model_id.isnot(None),
|
||||
ChatMessage.usage.isnot(None),
|
||||
~ChatMessage.user_id.like("shared-%"),
|
||||
~ChatMessage.user_id.like('shared-%'),
|
||||
)
|
||||
|
||||
if start_date:
|
||||
@@ -397,21 +366,17 @@ class ChatMessageTable:
|
||||
if end_date:
|
||||
query = query.filter(ChatMessage.created_at <= end_date)
|
||||
if group_id:
|
||||
group_users = (
|
||||
db.query(GroupMember.user_id)
|
||||
.filter(GroupMember.group_id == group_id)
|
||||
.subquery()
|
||||
)
|
||||
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
|
||||
query = query.filter(ChatMessage.user_id.in_(group_users))
|
||||
|
||||
results = query.group_by(ChatMessage.model_id).all()
|
||||
|
||||
return {
|
||||
row.model_id: {
|
||||
"input_tokens": row.input_tokens,
|
||||
"output_tokens": row.output_tokens,
|
||||
"total_tokens": row.input_tokens + row.output_tokens,
|
||||
"message_count": row.message_count,
|
||||
'input_tokens': row.input_tokens,
|
||||
'output_tokens': row.output_tokens,
|
||||
'total_tokens': row.input_tokens + row.output_tokens,
|
||||
'message_count': row.message_count,
|
||||
}
|
||||
for row in results
|
||||
}
|
||||
@@ -430,36 +395,32 @@ class ChatMessageTable:
|
||||
|
||||
dialect = db.bind.dialect.name
|
||||
|
||||
if dialect == "sqlite":
|
||||
input_tokens = cast(
|
||||
func.json_extract(ChatMessage.usage, "$.input_tokens"), Integer
|
||||
)
|
||||
output_tokens = cast(
|
||||
func.json_extract(ChatMessage.usage, "$.output_tokens"), Integer
|
||||
)
|
||||
elif dialect == "postgresql":
|
||||
if dialect == 'sqlite':
|
||||
input_tokens = cast(func.json_extract(ChatMessage.usage, '$.input_tokens'), Integer)
|
||||
output_tokens = cast(func.json_extract(ChatMessage.usage, '$.output_tokens'), Integer)
|
||||
elif dialect == 'postgresql':
|
||||
# Use json_extract_path_text for PostgreSQL JSON columns
|
||||
input_tokens = cast(
|
||||
func.json_extract_path_text(ChatMessage.usage, "input_tokens"),
|
||||
func.json_extract_path_text(ChatMessage.usage, 'input_tokens'),
|
||||
Integer,
|
||||
)
|
||||
output_tokens = cast(
|
||||
func.json_extract_path_text(ChatMessage.usage, "output_tokens"),
|
||||
func.json_extract_path_text(ChatMessage.usage, 'output_tokens'),
|
||||
Integer,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported dialect: {dialect}")
|
||||
raise NotImplementedError(f'Unsupported dialect: {dialect}')
|
||||
|
||||
query = db.query(
|
||||
ChatMessage.user_id,
|
||||
func.coalesce(func.sum(input_tokens), 0).label("input_tokens"),
|
||||
func.coalesce(func.sum(output_tokens), 0).label("output_tokens"),
|
||||
func.count(ChatMessage.id).label("message_count"),
|
||||
func.coalesce(func.sum(input_tokens), 0).label('input_tokens'),
|
||||
func.coalesce(func.sum(output_tokens), 0).label('output_tokens'),
|
||||
func.count(ChatMessage.id).label('message_count'),
|
||||
).filter(
|
||||
ChatMessage.role == "assistant",
|
||||
ChatMessage.role == 'assistant',
|
||||
ChatMessage.user_id.isnot(None),
|
||||
ChatMessage.usage.isnot(None),
|
||||
~ChatMessage.user_id.like("shared-%"),
|
||||
~ChatMessage.user_id.like('shared-%'),
|
||||
)
|
||||
|
||||
if start_date:
|
||||
@@ -467,21 +428,17 @@ class ChatMessageTable:
|
||||
if end_date:
|
||||
query = query.filter(ChatMessage.created_at <= end_date)
|
||||
if group_id:
|
||||
group_users = (
|
||||
db.query(GroupMember.user_id)
|
||||
.filter(GroupMember.group_id == group_id)
|
||||
.subquery()
|
||||
)
|
||||
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
|
||||
query = query.filter(ChatMessage.user_id.in_(group_users))
|
||||
|
||||
results = query.group_by(ChatMessage.user_id).all()
|
||||
|
||||
return {
|
||||
row.user_id: {
|
||||
"input_tokens": row.input_tokens,
|
||||
"output_tokens": row.output_tokens,
|
||||
"total_tokens": row.input_tokens + row.output_tokens,
|
||||
"message_count": row.message_count,
|
||||
'input_tokens': row.input_tokens,
|
||||
'output_tokens': row.output_tokens,
|
||||
'total_tokens': row.input_tokens + row.output_tokens,
|
||||
'message_count': row.message_count,
|
||||
}
|
||||
for row in results
|
||||
}
|
||||
@@ -497,20 +454,16 @@ class ChatMessageTable:
|
||||
from sqlalchemy import func
|
||||
from open_webui.models.groups import GroupMember
|
||||
|
||||
query = db.query(
|
||||
ChatMessage.user_id, func.count(ChatMessage.id).label("count")
|
||||
).filter(~ChatMessage.user_id.like("shared-%"))
|
||||
query = db.query(ChatMessage.user_id, func.count(ChatMessage.id).label('count')).filter(
|
||||
~ChatMessage.user_id.like('shared-%')
|
||||
)
|
||||
|
||||
if start_date:
|
||||
query = query.filter(ChatMessage.created_at >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(ChatMessage.created_at <= end_date)
|
||||
if group_id:
|
||||
group_users = (
|
||||
db.query(GroupMember.user_id)
|
||||
.filter(GroupMember.group_id == group_id)
|
||||
.subquery()
|
||||
)
|
||||
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
|
||||
query = query.filter(ChatMessage.user_id.in_(group_users))
|
||||
|
||||
results = query.group_by(ChatMessage.user_id).all()
|
||||
@@ -527,20 +480,16 @@ class ChatMessageTable:
|
||||
from sqlalchemy import func
|
||||
from open_webui.models.groups import GroupMember
|
||||
|
||||
query = db.query(
|
||||
ChatMessage.chat_id, func.count(ChatMessage.id).label("count")
|
||||
).filter(~ChatMessage.user_id.like("shared-%"))
|
||||
query = db.query(ChatMessage.chat_id, func.count(ChatMessage.id).label('count')).filter(
|
||||
~ChatMessage.user_id.like('shared-%')
|
||||
)
|
||||
|
||||
if start_date:
|
||||
query = query.filter(ChatMessage.created_at >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(ChatMessage.created_at <= end_date)
|
||||
if group_id:
|
||||
group_users = (
|
||||
db.query(GroupMember.user_id)
|
||||
.filter(GroupMember.group_id == group_id)
|
||||
.subquery()
|
||||
)
|
||||
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
|
||||
query = query.filter(ChatMessage.user_id.in_(group_users))
|
||||
|
||||
results = query.group_by(ChatMessage.chat_id).all()
|
||||
@@ -559,9 +508,9 @@ class ChatMessageTable:
|
||||
from open_webui.models.groups import GroupMember
|
||||
|
||||
query = db.query(ChatMessage.created_at, ChatMessage.model_id).filter(
|
||||
ChatMessage.role == "assistant",
|
||||
ChatMessage.role == 'assistant',
|
||||
ChatMessage.model_id.isnot(None),
|
||||
~ChatMessage.user_id.like("shared-%"),
|
||||
~ChatMessage.user_id.like('shared-%'),
|
||||
)
|
||||
|
||||
if start_date:
|
||||
@@ -569,11 +518,7 @@ class ChatMessageTable:
|
||||
if end_date:
|
||||
query = query.filter(ChatMessage.created_at <= end_date)
|
||||
if group_id:
|
||||
group_users = (
|
||||
db.query(GroupMember.user_id)
|
||||
.filter(GroupMember.group_id == group_id)
|
||||
.subquery()
|
||||
)
|
||||
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
|
||||
query = query.filter(ChatMessage.user_id.in_(group_users))
|
||||
|
||||
results = query.all()
|
||||
@@ -581,21 +526,17 @@ class ChatMessageTable:
|
||||
# Group by date -> model -> count
|
||||
daily_counts: dict[str, dict[str, int]] = {}
|
||||
for timestamp, model_id in results:
|
||||
date_str = datetime.fromtimestamp(
|
||||
_normalize_timestamp(timestamp)
|
||||
).strftime("%Y-%m-%d")
|
||||
date_str = datetime.fromtimestamp(_normalize_timestamp(timestamp)).strftime('%Y-%m-%d')
|
||||
if date_str not in daily_counts:
|
||||
daily_counts[date_str] = {}
|
||||
daily_counts[date_str][model_id] = (
|
||||
daily_counts[date_str].get(model_id, 0) + 1
|
||||
)
|
||||
daily_counts[date_str][model_id] = daily_counts[date_str].get(model_id, 0) + 1
|
||||
|
||||
# Fill in missing days
|
||||
if start_date and end_date:
|
||||
current = datetime.fromtimestamp(_normalize_timestamp(start_date))
|
||||
end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date))
|
||||
while current <= end_dt:
|
||||
date_str = current.strftime("%Y-%m-%d")
|
||||
date_str = current.strftime('%Y-%m-%d')
|
||||
if date_str not in daily_counts:
|
||||
daily_counts[date_str] = {}
|
||||
current += timedelta(days=1)
|
||||
@@ -613,9 +554,9 @@ class ChatMessageTable:
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
query = db.query(ChatMessage.created_at, ChatMessage.model_id).filter(
|
||||
ChatMessage.role == "assistant",
|
||||
ChatMessage.role == 'assistant',
|
||||
ChatMessage.model_id.isnot(None),
|
||||
~ChatMessage.user_id.like("shared-%"),
|
||||
~ChatMessage.user_id.like('shared-%'),
|
||||
)
|
||||
|
||||
if start_date:
|
||||
@@ -628,23 +569,19 @@ class ChatMessageTable:
|
||||
# Group by hour -> model -> count
|
||||
hourly_counts: dict[str, dict[str, int]] = {}
|
||||
for timestamp, model_id in results:
|
||||
hour_str = datetime.fromtimestamp(
|
||||
_normalize_timestamp(timestamp)
|
||||
).strftime("%Y-%m-%d %H:00")
|
||||
hour_str = datetime.fromtimestamp(_normalize_timestamp(timestamp)).strftime('%Y-%m-%d %H:00')
|
||||
if hour_str not in hourly_counts:
|
||||
hourly_counts[hour_str] = {}
|
||||
hourly_counts[hour_str][model_id] = (
|
||||
hourly_counts[hour_str].get(model_id, 0) + 1
|
||||
)
|
||||
hourly_counts[hour_str][model_id] = hourly_counts[hour_str].get(model_id, 0) + 1
|
||||
|
||||
# Fill in missing hours
|
||||
if start_date and end_date:
|
||||
current = datetime.fromtimestamp(
|
||||
_normalize_timestamp(start_date)
|
||||
).replace(minute=0, second=0, microsecond=0)
|
||||
current = datetime.fromtimestamp(_normalize_timestamp(start_date)).replace(
|
||||
minute=0, second=0, microsecond=0
|
||||
)
|
||||
end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date))
|
||||
while current <= end_dt:
|
||||
hour_str = current.strftime("%Y-%m-%d %H:00")
|
||||
hour_str = current.strftime('%Y-%m-%d %H:00')
|
||||
if hour_str not in hourly_counts:
|
||||
hourly_counts[hour_str] = {}
|
||||
current += timedelta(hours=1)
|
||||
|
||||
+253
-420
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Feedback(Base):
|
||||
__tablename__ = "feedback"
|
||||
__tablename__ = 'feedback'
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
user_id = Column(Text)
|
||||
version = Column(BigInteger, default=0)
|
||||
@@ -81,7 +81,7 @@ class RatingData(BaseModel):
|
||||
sibling_model_ids: Optional[list[str]] = None
|
||||
reason: Optional[str] = None
|
||||
comment: Optional[str] = None
|
||||
model_config = ConfigDict(extra="allow", protected_namespaces=())
|
||||
model_config = ConfigDict(extra='allow', protected_namespaces=())
|
||||
|
||||
|
||||
class MetaData(BaseModel):
|
||||
@@ -89,12 +89,12 @@ class MetaData(BaseModel):
|
||||
chat_id: Optional[str] = None
|
||||
message_id: Optional[str] = None
|
||||
tags: Optional[list[str]] = None
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
class SnapshotData(BaseModel):
|
||||
chat: Optional[dict] = None
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
class FeedbackForm(BaseModel):
|
||||
@@ -102,14 +102,14 @@ class FeedbackForm(BaseModel):
|
||||
data: Optional[RatingData] = None
|
||||
meta: Optional[dict] = None
|
||||
snapshot: Optional[SnapshotData] = None
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
role: str = "pending"
|
||||
role: str = 'pending'
|
||||
|
||||
last_active_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
@@ -146,12 +146,12 @@ class FeedbackTable:
|
||||
id = str(uuid.uuid4())
|
||||
feedback = FeedbackModel(
|
||||
**{
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
"version": 0,
|
||||
'id': id,
|
||||
'user_id': user_id,
|
||||
'version': 0,
|
||||
**form_data.model_dump(),
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
'created_at': int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
try:
|
||||
@@ -164,12 +164,10 @@ class FeedbackTable:
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.exception(f"Error creating a new feedback: {e}")
|
||||
log.exception(f'Error creating a new feedback: {e}')
|
||||
return None
|
||||
|
||||
def get_feedback_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[FeedbackModel]:
|
||||
def get_feedback_by_id(self, id: str, db: Optional[Session] = None) -> Optional[FeedbackModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
feedback = db.query(Feedback).filter_by(id=id).first()
|
||||
@@ -191,16 +189,14 @@ class FeedbackTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_feedbacks_by_chat_id(
|
||||
self, chat_id: str, db: Optional[Session] = None
|
||||
) -> list[FeedbackModel]:
|
||||
def get_feedbacks_by_chat_id(self, chat_id: str, db: Optional[Session] = None) -> list[FeedbackModel]:
|
||||
"""Get all feedbacks for a specific chat."""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
# meta.chat_id stores the chat reference
|
||||
feedbacks = (
|
||||
db.query(Feedback)
|
||||
.filter(Feedback.meta["chat_id"].as_string() == chat_id)
|
||||
.filter(Feedback.meta['chat_id'].as_string() == chat_id)
|
||||
.order_by(Feedback.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
@@ -219,36 +215,28 @@ class FeedbackTable:
|
||||
query = db.query(Feedback, User).join(User, Feedback.user_id == User.id)
|
||||
|
||||
if filter:
|
||||
order_by = filter.get("order_by")
|
||||
direction = filter.get("direction")
|
||||
order_by = filter.get('order_by')
|
||||
direction = filter.get('direction')
|
||||
|
||||
if order_by == "username":
|
||||
if direction == "asc":
|
||||
if order_by == 'username':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(User.name.asc())
|
||||
else:
|
||||
query = query.order_by(User.name.desc())
|
||||
elif order_by == "model_id":
|
||||
elif order_by == 'model_id':
|
||||
# it's stored in feedback.data['model_id']
|
||||
if direction == "asc":
|
||||
query = query.order_by(
|
||||
Feedback.data["model_id"].as_string().asc()
|
||||
)
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Feedback.data['model_id'].as_string().asc())
|
||||
else:
|
||||
query = query.order_by(
|
||||
Feedback.data["model_id"].as_string().desc()
|
||||
)
|
||||
elif order_by == "rating":
|
||||
query = query.order_by(Feedback.data['model_id'].as_string().desc())
|
||||
elif order_by == 'rating':
|
||||
# it's stored in feedback.data['rating']
|
||||
if direction == "asc":
|
||||
query = query.order_by(
|
||||
Feedback.data["rating"].as_string().asc()
|
||||
)
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Feedback.data['rating'].as_string().asc())
|
||||
else:
|
||||
query = query.order_by(
|
||||
Feedback.data["rating"].as_string().desc()
|
||||
)
|
||||
elif order_by == "updated_at":
|
||||
if direction == "asc":
|
||||
query = query.order_by(Feedback.data['rating'].as_string().desc())
|
||||
elif order_by == 'updated_at':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Feedback.updated_at.asc())
|
||||
else:
|
||||
query = query.order_by(Feedback.updated_at.desc())
|
||||
@@ -270,9 +258,7 @@ class FeedbackTable:
|
||||
for feedback, user in items:
|
||||
feedback_model = FeedbackModel.model_validate(feedback)
|
||||
user_model = UserResponse.model_validate(user)
|
||||
feedbacks.append(
|
||||
FeedbackUserResponse(**feedback_model.model_dump(), user=user_model)
|
||||
)
|
||||
feedbacks.append(FeedbackUserResponse(**feedback_model.model_dump(), user=user_model))
|
||||
|
||||
return FeedbackListResponse(items=feedbacks, total=total)
|
||||
|
||||
@@ -280,14 +266,10 @@ class FeedbackTable:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FeedbackModel.model_validate(feedback)
|
||||
for feedback in db.query(Feedback)
|
||||
.order_by(Feedback.updated_at.desc())
|
||||
.all()
|
||||
for feedback in db.query(Feedback).order_by(Feedback.updated_at.desc()).all()
|
||||
]
|
||||
|
||||
def get_all_feedback_ids(
|
||||
self, db: Optional[Session] = None
|
||||
) -> list[FeedbackIdResponse]:
|
||||
def get_all_feedback_ids(self, db: Optional[Session] = None) -> list[FeedbackIdResponse]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FeedbackIdResponse(
|
||||
@@ -306,14 +288,11 @@ class FeedbackTable:
|
||||
.all()
|
||||
]
|
||||
|
||||
def get_feedbacks_for_leaderboard(
|
||||
self, db: Optional[Session] = None
|
||||
) -> list[LeaderboardFeedbackData]:
|
||||
def get_feedbacks_for_leaderboard(self, db: Optional[Session] = None) -> list[LeaderboardFeedbackData]:
|
||||
"""Fetch only id and data for leaderboard computation (excludes snapshot/meta)."""
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
LeaderboardFeedbackData(id=row.id, data=row.data)
|
||||
for row in db.query(Feedback.id, Feedback.data).all()
|
||||
LeaderboardFeedbackData(id=row.id, data=row.data) for row in db.query(Feedback.id, Feedback.data).all()
|
||||
]
|
||||
|
||||
def get_model_evaluation_history(
|
||||
@@ -333,30 +312,26 @@ class FeedbackTable:
|
||||
rows = db.query(Feedback.created_at, Feedback.data).all()
|
||||
else:
|
||||
cutoff = int(time.time()) - (days * 86400)
|
||||
rows = (
|
||||
db.query(Feedback.created_at, Feedback.data)
|
||||
.filter(Feedback.created_at >= cutoff)
|
||||
.all()
|
||||
)
|
||||
rows = db.query(Feedback.created_at, Feedback.data).filter(Feedback.created_at >= cutoff).all()
|
||||
|
||||
daily_counts = defaultdict(lambda: {"won": 0, "lost": 0})
|
||||
daily_counts = defaultdict(lambda: {'won': 0, 'lost': 0})
|
||||
first_date = None
|
||||
|
||||
for created_at, data in rows:
|
||||
if not data:
|
||||
continue
|
||||
if data.get("model_id") != model_id:
|
||||
if data.get('model_id') != model_id:
|
||||
continue
|
||||
|
||||
rating_str = str(data.get("rating", ""))
|
||||
if rating_str not in ("1", "-1"):
|
||||
rating_str = str(data.get('rating', ''))
|
||||
if rating_str not in ('1', '-1'):
|
||||
continue
|
||||
|
||||
date_str = datetime.fromtimestamp(created_at).strftime("%Y-%m-%d")
|
||||
if rating_str == "1":
|
||||
daily_counts[date_str]["won"] += 1
|
||||
date_str = datetime.fromtimestamp(created_at).strftime('%Y-%m-%d')
|
||||
if rating_str == '1':
|
||||
daily_counts[date_str]['won'] += 1
|
||||
else:
|
||||
daily_counts[date_str]["lost"] += 1
|
||||
daily_counts[date_str]['lost'] += 1
|
||||
|
||||
# Track first date for this model
|
||||
if first_date is None or date_str < first_date:
|
||||
@@ -368,7 +343,7 @@ class FeedbackTable:
|
||||
|
||||
if days == 0 and first_date:
|
||||
# All time: start from first feedback date
|
||||
start_date = datetime.strptime(first_date, "%Y-%m-%d").date()
|
||||
start_date = datetime.strptime(first_date, '%Y-%m-%d').date()
|
||||
num_days = (today - start_date).days + 1
|
||||
else:
|
||||
# Fixed range
|
||||
@@ -377,36 +352,24 @@ class FeedbackTable:
|
||||
|
||||
for i in range(num_days):
|
||||
d = start_date + timedelta(days=i)
|
||||
date_str = d.strftime("%Y-%m-%d")
|
||||
counts = daily_counts.get(date_str, {"won": 0, "lost": 0})
|
||||
result.append(
|
||||
ModelHistoryEntry(date=date_str, won=counts["won"], lost=counts["lost"])
|
||||
)
|
||||
date_str = d.strftime('%Y-%m-%d')
|
||||
counts = daily_counts.get(date_str, {'won': 0, 'lost': 0})
|
||||
result.append(ModelHistoryEntry(date=date_str, won=counts['won'], lost=counts['lost']))
|
||||
|
||||
return result
|
||||
|
||||
def get_feedbacks_by_type(
|
||||
self, type: str, db: Optional[Session] = None
|
||||
) -> list[FeedbackModel]:
|
||||
def get_feedbacks_by_type(self, type: str, db: Optional[Session] = None) -> list[FeedbackModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FeedbackModel.model_validate(feedback)
|
||||
for feedback in db.query(Feedback)
|
||||
.filter_by(type=type)
|
||||
.order_by(Feedback.updated_at.desc())
|
||||
.all()
|
||||
for feedback in db.query(Feedback).filter_by(type=type).order_by(Feedback.updated_at.desc()).all()
|
||||
]
|
||||
|
||||
def get_feedbacks_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[FeedbackModel]:
|
||||
def get_feedbacks_by_user_id(self, user_id: str, db: Optional[Session] = None) -> list[FeedbackModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FeedbackModel.model_validate(feedback)
|
||||
for feedback in db.query(Feedback)
|
||||
.filter_by(user_id=user_id)
|
||||
.order_by(Feedback.updated_at.desc())
|
||||
.all()
|
||||
for feedback in db.query(Feedback).filter_by(user_id=user_id).order_by(Feedback.updated_at.desc()).all()
|
||||
]
|
||||
|
||||
def update_feedback_by_id(
|
||||
@@ -462,9 +425,7 @@ class FeedbackTable:
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def delete_feedback_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_feedback_by_id_and_user_id(self, id: str, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first()
|
||||
if not feedback:
|
||||
@@ -473,9 +434,7 @@ class FeedbackTable:
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def delete_feedbacks_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_feedbacks_by_user_id(self, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
result = db.query(Feedback).filter_by(user_id=user_id).delete()
|
||||
db.commit()
|
||||
|
||||
@@ -16,7 +16,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class File(Base):
|
||||
__tablename__ = "file"
|
||||
__tablename__ = 'file'
|
||||
id = Column(String, primary_key=True, unique=True)
|
||||
user_id = Column(String)
|
||||
hash = Column(Text, nullable=True)
|
||||
@@ -58,9 +58,9 @@ class FileMeta(BaseModel):
|
||||
content_type: Optional[str] = None
|
||||
size: Optional[int] = None
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
@model_validator(mode="before")
|
||||
@model_validator(mode='before')
|
||||
@classmethod
|
||||
def sanitize_meta(cls, data):
|
||||
"""Sanitize metadata fields to handle malformed legacy data."""
|
||||
@@ -68,14 +68,12 @@ class FileMeta(BaseModel):
|
||||
return data
|
||||
|
||||
# Handle content_type that may be a list like ['application/pdf', None]
|
||||
content_type = data.get("content_type")
|
||||
content_type = data.get('content_type')
|
||||
if isinstance(content_type, list):
|
||||
# Extract first non-None string value
|
||||
data["content_type"] = next(
|
||||
(item for item in content_type if isinstance(item, str)), None
|
||||
)
|
||||
data['content_type'] = next((item for item in content_type if isinstance(item, str)), None)
|
||||
elif content_type is not None and not isinstance(content_type, str):
|
||||
data["content_type"] = None
|
||||
data['content_type'] = None
|
||||
|
||||
return data
|
||||
|
||||
@@ -92,7 +90,7 @@ class FileModelResponse(BaseModel):
|
||||
created_at: int # timestamp in epoch
|
||||
updated_at: Optional[int] = None # timestamp in epoch, optional for legacy files
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
class FileMetadataResponse(BaseModel):
|
||||
@@ -123,25 +121,22 @@ class FileUpdateForm(BaseModel):
|
||||
meta: Optional[dict] = None
|
||||
|
||||
|
||||
|
||||
class FilesTable:
|
||||
def insert_new_file(
|
||||
self, user_id: str, form_data: FileForm, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
def insert_new_file(self, user_id: str, form_data: FileForm, db: Optional[Session] = None) -> Optional[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
file_data = form_data.model_dump()
|
||||
|
||||
# Sanitize meta to remove non-JSON-serializable objects
|
||||
# (e.g. callable tool functions, MCP client instances from middleware)
|
||||
if file_data.get("meta"):
|
||||
file_data["meta"] = sanitize_metadata(file_data["meta"])
|
||||
if file_data.get('meta'):
|
||||
file_data['meta'] = sanitize_metadata(file_data['meta'])
|
||||
|
||||
file = FileModel(
|
||||
**{
|
||||
**file_data,
|
||||
"user_id": user_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
'user_id': user_id,
|
||||
'created_at': int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -155,12 +150,10 @@ class FilesTable:
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.exception(f"Error inserting a new file: {e}")
|
||||
log.exception(f'Error inserting a new file: {e}')
|
||||
return None
|
||||
|
||||
def get_file_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
def get_file_by_id(self, id: str, db: Optional[Session] = None) -> Optional[FileModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
@@ -171,9 +164,7 @@ class FilesTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_file_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
def get_file_by_id_and_user_id(self, id: str, user_id: str, db: Optional[Session] = None) -> Optional[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id, user_id=user_id).first()
|
||||
@@ -184,9 +175,7 @@ class FilesTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_file_metadata_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[FileMetadataResponse]:
|
||||
def get_file_metadata_by_id(self, id: str, db: Optional[Session] = None) -> Optional[FileMetadataResponse]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.get(File, id)
|
||||
@@ -204,9 +193,7 @@ class FilesTable:
|
||||
with get_db_context(db) as db:
|
||||
return [FileModel.model_validate(file) for file in db.query(File).all()]
|
||||
|
||||
def check_access_by_user_id(
|
||||
self, id, user_id, permission="write", db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def check_access_by_user_id(self, id, user_id, permission='write', db: Optional[Session] = None) -> bool:
|
||||
file = self.get_file_by_id(id, db=db)
|
||||
if not file:
|
||||
return False
|
||||
@@ -215,21 +202,14 @@ class FilesTable:
|
||||
# Implement additional access control logic here as needed
|
||||
return False
|
||||
|
||||
def get_files_by_ids(
|
||||
self, ids: list[str], db: Optional[Session] = None
|
||||
) -> list[FileModel]:
|
||||
def get_files_by_ids(self, ids: list[str], db: Optional[Session] = None) -> list[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FileModel.model_validate(file)
|
||||
for file in db.query(File)
|
||||
.filter(File.id.in_(ids))
|
||||
.order_by(File.updated_at.desc())
|
||||
.all()
|
||||
for file in db.query(File).filter(File.id.in_(ids)).order_by(File.updated_at.desc()).all()
|
||||
]
|
||||
|
||||
def get_file_metadatas_by_ids(
|
||||
self, ids: list[str], db: Optional[Session] = None
|
||||
) -> list[FileMetadataResponse]:
|
||||
def get_file_metadatas_by_ids(self, ids: list[str], db: Optional[Session] = None) -> list[FileMetadataResponse]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FileMetadataResponse(
|
||||
@@ -239,22 +219,15 @@ class FilesTable:
|
||||
created_at=file.created_at,
|
||||
updated_at=file.updated_at,
|
||||
)
|
||||
for file in db.query(
|
||||
File.id, File.hash, File.meta, File.created_at, File.updated_at
|
||||
)
|
||||
for file in db.query(File.id, File.hash, File.meta, File.created_at, File.updated_at)
|
||||
.filter(File.id.in_(ids))
|
||||
.order_by(File.updated_at.desc())
|
||||
.all()
|
||||
]
|
||||
|
||||
def get_files_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[FileModel]:
|
||||
def get_files_by_user_id(self, user_id: str, db: Optional[Session] = None) -> list[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FileModel.model_validate(file)
|
||||
for file in db.query(File).filter_by(user_id=user_id).all()
|
||||
]
|
||||
return [FileModel.model_validate(file) for file in db.query(File).filter_by(user_id=user_id).all()]
|
||||
|
||||
def get_file_list(
|
||||
self,
|
||||
@@ -262,7 +235,7 @@ class FilesTable:
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[Session] = None,
|
||||
) -> "FileListResponse":
|
||||
) -> 'FileListResponse':
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(File)
|
||||
if user_id:
|
||||
@@ -272,10 +245,7 @@ class FilesTable:
|
||||
|
||||
items = [
|
||||
FileModel.model_validate(file)
|
||||
for file in query.order_by(File.updated_at.desc(), File.id.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
for file in query.order_by(File.updated_at.desc(), File.id.desc()).offset(skip).limit(limit).all()
|
||||
]
|
||||
|
||||
return FileListResponse(items=items, total=total)
|
||||
@@ -296,17 +266,17 @@ class FilesTable:
|
||||
A SQL LIKE compatible pattern with proper escaping.
|
||||
"""
|
||||
# Escape SQL special characters first, then convert glob wildcards
|
||||
pattern = glob.replace("\\", "\\\\")
|
||||
pattern = pattern.replace("%", "\\%")
|
||||
pattern = pattern.replace("_", "\\_")
|
||||
pattern = pattern.replace("*", "%")
|
||||
pattern = pattern.replace("?", "_")
|
||||
pattern = glob.replace('\\', '\\\\')
|
||||
pattern = pattern.replace('%', '\\%')
|
||||
pattern = pattern.replace('_', '\\_')
|
||||
pattern = pattern.replace('*', '%')
|
||||
pattern = pattern.replace('?', '_')
|
||||
return pattern
|
||||
|
||||
def search_files(
|
||||
self,
|
||||
user_id: Optional[str] = None,
|
||||
filename: str = "*",
|
||||
filename: str = '*',
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Optional[Session] = None,
|
||||
@@ -331,15 +301,12 @@ class FilesTable:
|
||||
query = query.filter_by(user_id=user_id)
|
||||
|
||||
pattern = self._glob_to_like_pattern(filename)
|
||||
if pattern != "%":
|
||||
query = query.filter(File.filename.ilike(pattern, escape="\\"))
|
||||
if pattern != '%':
|
||||
query = query.filter(File.filename.ilike(pattern, escape='\\'))
|
||||
|
||||
return [
|
||||
FileModel.model_validate(file)
|
||||
for file in query.order_by(File.created_at.desc(), File.id.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
for file in query.order_by(File.created_at.desc(), File.id.desc()).offset(skip).limit(limit).all()
|
||||
]
|
||||
|
||||
def update_file_by_id(
|
||||
@@ -362,12 +329,10 @@ class FilesTable:
|
||||
db.commit()
|
||||
return FileModel.model_validate(file)
|
||||
except Exception as e:
|
||||
log.exception(f"Error updating file completely by id: {e}")
|
||||
log.exception(f'Error updating file completely by id: {e}')
|
||||
return None
|
||||
|
||||
def update_file_hash_by_id(
|
||||
self, id: str, hash: Optional[str], db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
def update_file_hash_by_id(self, id: str, hash: Optional[str], db: Optional[Session] = None) -> Optional[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id).first()
|
||||
@@ -379,9 +344,7 @@ class FilesTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_file_data_by_id(
|
||||
self, id: str, data: dict, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
def update_file_data_by_id(self, id: str, data: dict, db: Optional[Session] = None) -> Optional[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id).first()
|
||||
@@ -390,12 +353,9 @@ class FilesTable:
|
||||
db.commit()
|
||||
return FileModel.model_validate(file)
|
||||
except Exception as e:
|
||||
|
||||
return None
|
||||
|
||||
def update_file_metadata_by_id(
|
||||
self, id: str, meta: dict, db: Optional[Session] = None
|
||||
) -> Optional[FileModel]:
|
||||
def update_file_metadata_by_id(self, id: str, meta: dict, db: Optional[Session] = None) -> Optional[FileModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
file = db.query(File).filter_by(id=id).first()
|
||||
|
||||
@@ -20,7 +20,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Folder(Base):
|
||||
__tablename__ = "folder"
|
||||
__tablename__ = 'folder'
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
parent_id = Column(Text, nullable=True)
|
||||
user_id = Column(Text)
|
||||
@@ -72,14 +72,14 @@ class FolderForm(BaseModel):
|
||||
data: Optional[dict] = None
|
||||
meta: Optional[dict] = None
|
||||
parent_id: Optional[str] = None
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
class FolderUpdateForm(BaseModel):
|
||||
name: Optional[str] = None
|
||||
data: Optional[dict] = None
|
||||
meta: Optional[dict] = None
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
class FolderTable:
|
||||
@@ -94,12 +94,12 @@ class FolderTable:
|
||||
id = str(uuid.uuid4())
|
||||
folder = FolderModel(
|
||||
**{
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
'id': id,
|
||||
'user_id': user_id,
|
||||
**(form_data.model_dump(exclude_unset=True) or {}),
|
||||
"parent_id": parent_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
'parent_id': parent_id,
|
||||
'created_at': int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
try:
|
||||
@@ -112,7 +112,7 @@ class FolderTable:
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.exception(f"Error inserting a new folder: {e}")
|
||||
log.exception(f'Error inserting a new folder: {e}')
|
||||
return None
|
||||
|
||||
def get_folder_by_id_and_user_id(
|
||||
@@ -137,9 +137,7 @@ class FolderTable:
|
||||
folders = []
|
||||
|
||||
def get_children(folder):
|
||||
children = self.get_folders_by_parent_id_and_user_id(
|
||||
folder.id, user_id, db=db
|
||||
)
|
||||
children = self.get_folders_by_parent_id_and_user_id(folder.id, user_id, db=db)
|
||||
for child in children:
|
||||
get_children(child)
|
||||
folders.append(child)
|
||||
@@ -153,14 +151,9 @@ class FolderTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_folders_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[FolderModel]:
|
||||
def get_folders_by_user_id(self, user_id: str, db: Optional[Session] = None) -> list[FolderModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FolderModel.model_validate(folder)
|
||||
for folder in db.query(Folder).filter_by(user_id=user_id).all()
|
||||
]
|
||||
return [FolderModel.model_validate(folder) for folder in db.query(Folder).filter_by(user_id=user_id).all()]
|
||||
|
||||
def get_folder_by_parent_id_and_user_id_and_name(
|
||||
self,
|
||||
@@ -184,7 +177,7 @@ class FolderTable:
|
||||
|
||||
return FolderModel.model_validate(folder)
|
||||
except Exception as e:
|
||||
log.error(f"get_folder_by_parent_id_and_user_id_and_name: {e}")
|
||||
log.error(f'get_folder_by_parent_id_and_user_id_and_name: {e}')
|
||||
return None
|
||||
|
||||
def get_folders_by_parent_id_and_user_id(
|
||||
@@ -193,9 +186,7 @@ class FolderTable:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FolderModel.model_validate(folder)
|
||||
for folder in db.query(Folder)
|
||||
.filter_by(parent_id=parent_id, user_id=user_id)
|
||||
.all()
|
||||
for folder in db.query(Folder).filter_by(parent_id=parent_id, user_id=user_id).all()
|
||||
]
|
||||
|
||||
def update_folder_parent_id_by_id_and_user_id(
|
||||
@@ -219,7 +210,7 @@ class FolderTable:
|
||||
|
||||
return FolderModel.model_validate(folder)
|
||||
except Exception as e:
|
||||
log.error(f"update_folder: {e}")
|
||||
log.error(f'update_folder: {e}')
|
||||
return
|
||||
|
||||
def update_folder_by_id_and_user_id(
|
||||
@@ -241,7 +232,7 @@ class FolderTable:
|
||||
existing_folder = (
|
||||
db.query(Folder)
|
||||
.filter_by(
|
||||
name=form_data.get("name"),
|
||||
name=form_data.get('name'),
|
||||
parent_id=folder.parent_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
@@ -251,17 +242,17 @@ class FolderTable:
|
||||
if existing_folder and existing_folder.id != id:
|
||||
return None
|
||||
|
||||
folder.name = form_data.get("name", folder.name)
|
||||
if "data" in form_data:
|
||||
folder.name = form_data.get('name', folder.name)
|
||||
if 'data' in form_data:
|
||||
folder.data = {
|
||||
**(folder.data or {}),
|
||||
**form_data["data"],
|
||||
**form_data['data'],
|
||||
}
|
||||
|
||||
if "meta" in form_data:
|
||||
if 'meta' in form_data:
|
||||
folder.meta = {
|
||||
**(folder.meta or {}),
|
||||
**form_data["meta"],
|
||||
**form_data['meta'],
|
||||
}
|
||||
|
||||
folder.updated_at = int(time.time())
|
||||
@@ -269,7 +260,7 @@ class FolderTable:
|
||||
|
||||
return FolderModel.model_validate(folder)
|
||||
except Exception as e:
|
||||
log.error(f"update_folder: {e}")
|
||||
log.error(f'update_folder: {e}')
|
||||
return
|
||||
|
||||
def update_folder_is_expanded_by_id_and_user_id(
|
||||
@@ -289,12 +280,10 @@ class FolderTable:
|
||||
|
||||
return FolderModel.model_validate(folder)
|
||||
except Exception as e:
|
||||
log.error(f"update_folder: {e}")
|
||||
log.error(f'update_folder: {e}')
|
||||
return
|
||||
|
||||
def delete_folder_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> list[str]:
|
||||
def delete_folder_by_id_and_user_id(self, id: str, user_id: str, db: Optional[Session] = None) -> list[str]:
|
||||
try:
|
||||
folder_ids = []
|
||||
with get_db_context(db) as db:
|
||||
@@ -306,11 +295,8 @@ class FolderTable:
|
||||
|
||||
# Delete all children folders
|
||||
def delete_children(folder):
|
||||
folder_children = self.get_folders_by_parent_id_and_user_id(
|
||||
folder.id, user_id, db=db
|
||||
)
|
||||
folder_children = self.get_folders_by_parent_id_and_user_id(folder.id, user_id, db=db)
|
||||
for folder_child in folder_children:
|
||||
|
||||
delete_children(folder_child)
|
||||
folder_ids.append(folder_child.id)
|
||||
|
||||
@@ -323,12 +309,12 @@ class FolderTable:
|
||||
db.commit()
|
||||
return folder_ids
|
||||
except Exception as e:
|
||||
log.error(f"delete_folder: {e}")
|
||||
log.error(f'delete_folder: {e}')
|
||||
return []
|
||||
|
||||
def normalize_folder_name(self, name: str) -> str:
|
||||
# Replace _ and space with a single space, lower case, collapse multiple spaces
|
||||
name = re.sub(r"[\s_]+", " ", name)
|
||||
name = re.sub(r'[\s_]+', ' ', name)
|
||||
return name.strip().lower()
|
||||
|
||||
def search_folders_by_names(
|
||||
@@ -349,9 +335,7 @@ class FolderTable:
|
||||
results[folder.id] = FolderModel.model_validate(folder)
|
||||
|
||||
# get children folders
|
||||
children = self.get_children_folders_by_id_and_user_id(
|
||||
folder.id, user_id, db=db
|
||||
)
|
||||
children = self.get_children_folders_by_id_and_user_id(folder.id, user_id, db=db)
|
||||
for child in children:
|
||||
results[child.id] = child
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Function(Base):
|
||||
__tablename__ = "function"
|
||||
__tablename__ = 'function'
|
||||
|
||||
id = Column(String, primary_key=True, unique=True)
|
||||
user_id = Column(String)
|
||||
@@ -30,13 +30,13 @@ class Function(Base):
|
||||
updated_at = Column(BigInteger)
|
||||
created_at = Column(BigInteger)
|
||||
|
||||
__table_args__ = (Index("is_global_idx", "is_global"),)
|
||||
__table_args__ = (Index('is_global_idx', 'is_global'),)
|
||||
|
||||
|
||||
class FunctionMeta(BaseModel):
|
||||
description: Optional[str] = None
|
||||
manifest: Optional[dict] = {}
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
class FunctionModel(BaseModel):
|
||||
@@ -113,10 +113,10 @@ class FunctionsTable:
|
||||
function = FunctionModel(
|
||||
**{
|
||||
**form_data.model_dump(),
|
||||
"user_id": user_id,
|
||||
"type": type,
|
||||
"updated_at": int(time.time()),
|
||||
"created_at": int(time.time()),
|
||||
'user_id': user_id,
|
||||
'type': type,
|
||||
'updated_at': int(time.time()),
|
||||
'created_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -131,7 +131,7 @@ class FunctionsTable:
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.exception(f"Error creating a new function: {e}")
|
||||
log.exception(f'Error creating a new function: {e}')
|
||||
return None
|
||||
|
||||
def sync_functions(
|
||||
@@ -156,16 +156,16 @@ class FunctionsTable:
|
||||
db.query(Function).filter_by(id=func.id).update(
|
||||
{
|
||||
**func.model_dump(),
|
||||
"user_id": user_id,
|
||||
"updated_at": int(time.time()),
|
||||
'user_id': user_id,
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
else:
|
||||
new_func = Function(
|
||||
**{
|
||||
**func.model_dump(),
|
||||
"user_id": user_id,
|
||||
"updated_at": int(time.time()),
|
||||
'user_id': user_id,
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.add(new_func)
|
||||
@@ -177,17 +177,12 @@ class FunctionsTable:
|
||||
|
||||
db.commit()
|
||||
|
||||
return [
|
||||
FunctionModel.model_validate(func)
|
||||
for func in db.query(Function).all()
|
||||
]
|
||||
return [FunctionModel.model_validate(func) for func in db.query(Function).all()]
|
||||
except Exception as e:
|
||||
log.exception(f"Error syncing functions for user {user_id}: {e}")
|
||||
log.exception(f'Error syncing functions for user {user_id}: {e}')
|
||||
return []
|
||||
|
||||
def get_function_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[FunctionModel]:
|
||||
def get_function_by_id(self, id: str, db: Optional[Session] = None) -> Optional[FunctionModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
function = db.get(Function, id)
|
||||
@@ -195,9 +190,7 @@ class FunctionsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_functions_by_ids(
|
||||
self, ids: list[str], db: Optional[Session] = None
|
||||
) -> list[FunctionModel]:
|
||||
def get_functions_by_ids(self, ids: list[str], db: Optional[Session] = None) -> list[FunctionModel]:
|
||||
"""
|
||||
Batch fetch multiple functions by their IDs in a single query.
|
||||
Returns functions in the same order as the input IDs (None entries filtered out).
|
||||
@@ -225,18 +218,11 @@ class FunctionsTable:
|
||||
functions = db.query(Function).all()
|
||||
|
||||
if include_valves:
|
||||
return [
|
||||
FunctionWithValvesModel.model_validate(function)
|
||||
for function in functions
|
||||
]
|
||||
return [FunctionWithValvesModel.model_validate(function) for function in functions]
|
||||
else:
|
||||
return [
|
||||
FunctionModel.model_validate(function) for function in functions
|
||||
]
|
||||
return [FunctionModel.model_validate(function) for function in functions]
|
||||
|
||||
def get_function_list(
|
||||
self, db: Optional[Session] = None
|
||||
) -> list[FunctionUserResponse]:
|
||||
def get_function_list(self, db: Optional[Session] = None) -> list[FunctionUserResponse]:
|
||||
with get_db_context(db) as db:
|
||||
functions = db.query(Function).order_by(Function.updated_at.desc()).all()
|
||||
user_ids = list(set(func.user_id for func in functions))
|
||||
@@ -248,69 +234,48 @@ class FunctionsTable:
|
||||
FunctionUserResponse.model_validate(
|
||||
{
|
||||
**FunctionModel.model_validate(func).model_dump(),
|
||||
"user": (
|
||||
users_dict.get(func.user_id).model_dump()
|
||||
if func.user_id in users_dict
|
||||
else None
|
||||
),
|
||||
'user': (users_dict.get(func.user_id).model_dump() if func.user_id in users_dict else None),
|
||||
}
|
||||
)
|
||||
for func in functions
|
||||
]
|
||||
|
||||
def get_functions_by_type(
|
||||
self, type: str, active_only=False, db: Optional[Session] = None
|
||||
) -> list[FunctionModel]:
|
||||
def get_functions_by_type(self, type: str, active_only=False, db: Optional[Session] = None) -> list[FunctionModel]:
|
||||
with get_db_context(db) as db:
|
||||
if active_only:
|
||||
return [
|
||||
FunctionModel.model_validate(function)
|
||||
for function in db.query(Function)
|
||||
.filter_by(type=type, is_active=True)
|
||||
.all()
|
||||
for function in db.query(Function).filter_by(type=type, is_active=True).all()
|
||||
]
|
||||
else:
|
||||
return [
|
||||
FunctionModel.model_validate(function)
|
||||
for function in db.query(Function).filter_by(type=type).all()
|
||||
FunctionModel.model_validate(function) for function in db.query(Function).filter_by(type=type).all()
|
||||
]
|
||||
|
||||
def get_global_filter_functions(
|
||||
self, db: Optional[Session] = None
|
||||
) -> list[FunctionModel]:
|
||||
def get_global_filter_functions(self, db: Optional[Session] = None) -> list[FunctionModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FunctionModel.model_validate(function)
|
||||
for function in db.query(Function)
|
||||
.filter_by(type="filter", is_active=True, is_global=True)
|
||||
.all()
|
||||
for function in db.query(Function).filter_by(type='filter', is_active=True, is_global=True).all()
|
||||
]
|
||||
|
||||
def get_global_action_functions(
|
||||
self, db: Optional[Session] = None
|
||||
) -> list[FunctionModel]:
|
||||
def get_global_action_functions(self, db: Optional[Session] = None) -> list[FunctionModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
FunctionModel.model_validate(function)
|
||||
for function in db.query(Function)
|
||||
.filter_by(type="action", is_active=True, is_global=True)
|
||||
.all()
|
||||
for function in db.query(Function).filter_by(type='action', is_active=True, is_global=True).all()
|
||||
]
|
||||
|
||||
def get_function_valves_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[dict]:
|
||||
def get_function_valves_by_id(self, id: str, db: Optional[Session] = None) -> Optional[dict]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
function = db.get(Function, id)
|
||||
return function.valves if function.valves else {}
|
||||
except Exception as e:
|
||||
log.exception(f"Error getting function valves by id {id}: {e}")
|
||||
log.exception(f'Error getting function valves by id {id}: {e}')
|
||||
return None
|
||||
|
||||
def get_function_valves_by_ids(
|
||||
self, ids: list[str], db: Optional[Session] = None
|
||||
) -> dict[str, dict]:
|
||||
def get_function_valves_by_ids(self, ids: list[str], db: Optional[Session] = None) -> dict[str, dict]:
|
||||
"""
|
||||
Batch fetch valves for multiple functions in a single query.
|
||||
Returns a dict mapping function_id -> valves dict.
|
||||
@@ -320,14 +285,10 @@ class FunctionsTable:
|
||||
return {}
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
functions = (
|
||||
db.query(Function.id, Function.valves)
|
||||
.filter(Function.id.in_(ids))
|
||||
.all()
|
||||
)
|
||||
functions = db.query(Function.id, Function.valves).filter(Function.id.in_(ids)).all()
|
||||
return {f.id: (f.valves if f.valves else {}) for f in functions}
|
||||
except Exception as e:
|
||||
log.exception(f"Error batch-fetching function valves: {e}")
|
||||
log.exception(f'Error batch-fetching function valves: {e}')
|
||||
return {}
|
||||
|
||||
def update_function_valves_by_id(
|
||||
@@ -364,25 +325,23 @@ class FunctionsTable:
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.exception(f"Error updating function metadata by id {id}: {e}")
|
||||
log.exception(f'Error updating function metadata by id {id}: {e}')
|
||||
return None
|
||||
|
||||
def get_user_valves_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[dict]:
|
||||
def get_user_valves_by_id_and_user_id(self, id: str, user_id: str, db: Optional[Session] = None) -> Optional[dict]:
|
||||
try:
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
user_settings = user.settings.model_dump() if user.settings else {}
|
||||
|
||||
# Check if user has "functions" and "valves" settings
|
||||
if "functions" not in user_settings:
|
||||
user_settings["functions"] = {}
|
||||
if "valves" not in user_settings["functions"]:
|
||||
user_settings["functions"]["valves"] = {}
|
||||
if 'functions' not in user_settings:
|
||||
user_settings['functions'] = {}
|
||||
if 'valves' not in user_settings['functions']:
|
||||
user_settings['functions']['valves'] = {}
|
||||
|
||||
return user_settings["functions"]["valves"].get(id, {})
|
||||
return user_settings['functions']['valves'].get(id, {})
|
||||
except Exception as e:
|
||||
log.exception(f"Error getting user values by id {id} and user id {user_id}")
|
||||
log.exception(f'Error getting user values by id {id} and user id {user_id}')
|
||||
return None
|
||||
|
||||
def update_user_valves_by_id_and_user_id(
|
||||
@@ -393,32 +352,28 @@ class FunctionsTable:
|
||||
user_settings = user.settings.model_dump() if user.settings else {}
|
||||
|
||||
# Check if user has "functions" and "valves" settings
|
||||
if "functions" not in user_settings:
|
||||
user_settings["functions"] = {}
|
||||
if "valves" not in user_settings["functions"]:
|
||||
user_settings["functions"]["valves"] = {}
|
||||
if 'functions' not in user_settings:
|
||||
user_settings['functions'] = {}
|
||||
if 'valves' not in user_settings['functions']:
|
||||
user_settings['functions']['valves'] = {}
|
||||
|
||||
user_settings["functions"]["valves"][id] = valves
|
||||
user_settings['functions']['valves'][id] = valves
|
||||
|
||||
# Update the user settings in the database
|
||||
Users.update_user_by_id(user_id, {"settings": user_settings}, db=db)
|
||||
Users.update_user_by_id(user_id, {'settings': user_settings}, db=db)
|
||||
|
||||
return user_settings["functions"]["valves"][id]
|
||||
return user_settings['functions']['valves'][id]
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
f"Error updating user valves by id {id} and user_id {user_id}: {e}"
|
||||
)
|
||||
log.exception(f'Error updating user valves by id {id} and user_id {user_id}: {e}')
|
||||
return None
|
||||
|
||||
def update_function_by_id(
|
||||
self, id: str, updated: dict, db: Optional[Session] = None
|
||||
) -> Optional[FunctionModel]:
|
||||
def update_function_by_id(self, id: str, updated: dict, db: Optional[Session] = None) -> Optional[FunctionModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(Function).filter_by(id=id).update(
|
||||
{
|
||||
**updated,
|
||||
"updated_at": int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
@@ -432,8 +387,8 @@ class FunctionsTable:
|
||||
try:
|
||||
db.query(Function).update(
|
||||
{
|
||||
"is_active": False,
|
||||
"updated_at": int(time.time()),
|
||||
'is_active': False,
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
|
||||
@@ -34,7 +34,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Group(Base):
|
||||
__tablename__ = "group"
|
||||
__tablename__ = 'group'
|
||||
|
||||
id = Column(Text, unique=True, primary_key=True)
|
||||
user_id = Column(Text)
|
||||
@@ -70,12 +70,12 @@ class GroupModel(BaseModel):
|
||||
|
||||
|
||||
class GroupMember(Base):
|
||||
__tablename__ = "group_member"
|
||||
__tablename__ = 'group_member'
|
||||
|
||||
id = Column(Text, unique=True, primary_key=True)
|
||||
group_id = Column(
|
||||
Text,
|
||||
ForeignKey("group.id", ondelete="CASCADE"),
|
||||
ForeignKey('group.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
)
|
||||
user_id = Column(Text, nullable=False)
|
||||
@@ -133,28 +133,26 @@ class GroupListResponse(BaseModel):
|
||||
class GroupTable:
|
||||
def _ensure_default_share_config(self, group_data: dict) -> dict:
|
||||
"""Ensure the group data dict has a default share config if not already set."""
|
||||
if "data" not in group_data or group_data["data"] is None:
|
||||
group_data["data"] = {}
|
||||
if "config" not in group_data["data"]:
|
||||
group_data["data"]["config"] = {}
|
||||
if "share" not in group_data["data"]["config"]:
|
||||
group_data["data"]["config"]["share"] = DEFAULT_GROUP_SHARE_PERMISSION
|
||||
if 'data' not in group_data or group_data['data'] is None:
|
||||
group_data['data'] = {}
|
||||
if 'config' not in group_data['data']:
|
||||
group_data['data']['config'] = {}
|
||||
if 'share' not in group_data['data']['config']:
|
||||
group_data['data']['config']['share'] = DEFAULT_GROUP_SHARE_PERMISSION
|
||||
return group_data
|
||||
|
||||
def insert_new_group(
|
||||
self, user_id: str, form_data: GroupForm, db: Optional[Session] = None
|
||||
) -> Optional[GroupModel]:
|
||||
with get_db_context(db) as db:
|
||||
group_data = self._ensure_default_share_config(
|
||||
form_data.model_dump(exclude_none=True)
|
||||
)
|
||||
group_data = self._ensure_default_share_config(form_data.model_dump(exclude_none=True))
|
||||
group = GroupModel(
|
||||
**{
|
||||
**group_data,
|
||||
"id": str(uuid.uuid4()),
|
||||
"user_id": user_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
'id': str(uuid.uuid4()),
|
||||
'user_id': user_id,
|
||||
'created_at': int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -183,19 +181,19 @@ class GroupTable:
|
||||
.where(GroupMember.group_id == Group.id)
|
||||
.correlate(Group)
|
||||
.scalar_subquery()
|
||||
.label("member_count")
|
||||
.label('member_count')
|
||||
)
|
||||
query = db.query(Group, member_count)
|
||||
|
||||
if filter:
|
||||
if "query" in filter:
|
||||
query = query.filter(Group.name.ilike(f"%{filter['query']}%"))
|
||||
if 'query' in filter:
|
||||
query = query.filter(Group.name.ilike(f'%{filter["query"]}%'))
|
||||
|
||||
# When share filter is present, member check is handled in the share logic
|
||||
if "share" in filter:
|
||||
share_value = filter["share"]
|
||||
member_id = filter.get("member_id")
|
||||
json_share = Group.data["config"]["share"]
|
||||
if 'share' in filter:
|
||||
share_value = filter['share']
|
||||
member_id = filter.get('member_id')
|
||||
json_share = Group.data['config']['share']
|
||||
json_share_str = json_share.as_string()
|
||||
json_share_lower = func.lower(json_share_str)
|
||||
|
||||
@@ -203,37 +201,27 @@ class GroupTable:
|
||||
anyone_can_share = or_(
|
||||
Group.data.is_(None),
|
||||
json_share_str.is_(None),
|
||||
json_share_lower == "true",
|
||||
json_share_lower == "1", # Handle SQLite boolean true
|
||||
json_share_lower == 'true',
|
||||
json_share_lower == '1', # Handle SQLite boolean true
|
||||
)
|
||||
|
||||
if member_id:
|
||||
member_groups_select = select(GroupMember.group_id).where(
|
||||
GroupMember.user_id == member_id
|
||||
)
|
||||
member_groups_select = select(GroupMember.group_id).where(GroupMember.user_id == member_id)
|
||||
members_only_and_is_member = and_(
|
||||
json_share_lower == "members",
|
||||
json_share_lower == 'members',
|
||||
Group.id.in_(member_groups_select),
|
||||
)
|
||||
query = query.filter(
|
||||
or_(anyone_can_share, members_only_and_is_member)
|
||||
)
|
||||
query = query.filter(or_(anyone_can_share, members_only_and_is_member))
|
||||
else:
|
||||
query = query.filter(anyone_can_share)
|
||||
else:
|
||||
query = query.filter(
|
||||
and_(Group.data.isnot(None), json_share_lower == "false")
|
||||
)
|
||||
query = query.filter(and_(Group.data.isnot(None), json_share_lower == 'false'))
|
||||
|
||||
else:
|
||||
# Only apply member_id filter when share filter is NOT present
|
||||
if "member_id" in filter:
|
||||
if 'member_id' in filter:
|
||||
query = query.filter(
|
||||
Group.id.in_(
|
||||
select(GroupMember.group_id).where(
|
||||
GroupMember.user_id == filter["member_id"]
|
||||
)
|
||||
)
|
||||
Group.id.in_(select(GroupMember.group_id).where(GroupMember.user_id == filter['member_id']))
|
||||
)
|
||||
|
||||
results = query.order_by(Group.updated_at.desc()).all()
|
||||
@@ -242,7 +230,7 @@ class GroupTable:
|
||||
GroupResponse.model_validate(
|
||||
{
|
||||
**GroupModel.model_validate(group).model_dump(),
|
||||
"member_count": count or 0,
|
||||
'member_count': count or 0,
|
||||
}
|
||||
)
|
||||
for group, count in results
|
||||
@@ -259,22 +247,16 @@ class GroupTable:
|
||||
query = db.query(Group)
|
||||
|
||||
if filter:
|
||||
if "query" in filter:
|
||||
query = query.filter(Group.name.ilike(f"%{filter['query']}%"))
|
||||
if "member_id" in filter:
|
||||
if 'query' in filter:
|
||||
query = query.filter(Group.name.ilike(f'%{filter["query"]}%'))
|
||||
if 'member_id' in filter:
|
||||
query = query.filter(
|
||||
Group.id.in_(
|
||||
select(GroupMember.group_id).where(
|
||||
GroupMember.user_id == filter["member_id"]
|
||||
)
|
||||
)
|
||||
Group.id.in_(select(GroupMember.group_id).where(GroupMember.user_id == filter['member_id']))
|
||||
)
|
||||
|
||||
if "share" in filter:
|
||||
share_value = filter["share"]
|
||||
query = query.filter(
|
||||
Group.data.op("->>")("share") == str(share_value)
|
||||
)
|
||||
if 'share' in filter:
|
||||
share_value = filter['share']
|
||||
query = query.filter(Group.data.op('->>')('share') == str(share_value))
|
||||
|
||||
total = query.count()
|
||||
|
||||
@@ -283,32 +265,24 @@ class GroupTable:
|
||||
.where(GroupMember.group_id == Group.id)
|
||||
.correlate(Group)
|
||||
.scalar_subquery()
|
||||
.label("member_count")
|
||||
)
|
||||
results = (
|
||||
query.add_columns(member_count)
|
||||
.order_by(Group.updated_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
.label('member_count')
|
||||
)
|
||||
results = query.add_columns(member_count).order_by(Group.updated_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
return {
|
||||
"items": [
|
||||
'items': [
|
||||
GroupResponse.model_validate(
|
||||
{
|
||||
**GroupModel.model_validate(group).model_dump(),
|
||||
"member_count": count or 0,
|
||||
'member_count': count or 0,
|
||||
}
|
||||
)
|
||||
for group, count in results
|
||||
],
|
||||
"total": total,
|
||||
'total': total,
|
||||
}
|
||||
|
||||
def get_groups_by_member_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[GroupModel]:
|
||||
def get_groups_by_member_id(self, user_id: str, db: Optional[Session] = None) -> list[GroupModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
GroupModel.model_validate(group)
|
||||
@@ -340,9 +314,7 @@ class GroupTable:
|
||||
|
||||
return user_groups
|
||||
|
||||
def get_group_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[GroupModel]:
|
||||
def get_group_by_id(self, id: str, db: Optional[Session] = None) -> Optional[GroupModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
group = db.query(Group).filter_by(id=id).first()
|
||||
@@ -350,41 +322,29 @@ class GroupTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_group_user_ids_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> list[str]:
|
||||
def get_group_user_ids_by_id(self, id: str, db: Optional[Session] = None) -> list[str]:
|
||||
with get_db_context(db) as db:
|
||||
members = (
|
||||
db.query(GroupMember.user_id).filter(GroupMember.group_id == id).all()
|
||||
)
|
||||
members = db.query(GroupMember.user_id).filter(GroupMember.group_id == id).all()
|
||||
|
||||
if not members:
|
||||
return []
|
||||
|
||||
return [m[0] for m in members]
|
||||
|
||||
def get_group_user_ids_by_ids(
|
||||
self, group_ids: list[str], db: Optional[Session] = None
|
||||
) -> dict[str, list[str]]:
|
||||
def get_group_user_ids_by_ids(self, group_ids: list[str], db: Optional[Session] = None) -> dict[str, list[str]]:
|
||||
with get_db_context(db) as db:
|
||||
members = (
|
||||
db.query(GroupMember.group_id, GroupMember.user_id)
|
||||
.filter(GroupMember.group_id.in_(group_ids))
|
||||
.all()
|
||||
db.query(GroupMember.group_id, GroupMember.user_id).filter(GroupMember.group_id.in_(group_ids)).all()
|
||||
)
|
||||
|
||||
group_user_ids: dict[str, list[str]] = {
|
||||
group_id: [] for group_id in group_ids
|
||||
}
|
||||
group_user_ids: dict[str, list[str]] = {group_id: [] for group_id in group_ids}
|
||||
|
||||
for group_id, user_id in members:
|
||||
group_user_ids[group_id].append(user_id)
|
||||
|
||||
return group_user_ids
|
||||
|
||||
def set_group_user_ids_by_id(
|
||||
self, group_id: str, user_ids: list[str], db: Optional[Session] = None
|
||||
) -> None:
|
||||
def set_group_user_ids_by_id(self, group_id: str, user_ids: list[str], db: Optional[Session] = None) -> None:
|
||||
with get_db_context(db) as db:
|
||||
# Delete existing members
|
||||
db.query(GroupMember).filter(GroupMember.group_id == group_id).delete()
|
||||
@@ -405,20 +365,12 @@ class GroupTable:
|
||||
db.add_all(new_members)
|
||||
db.commit()
|
||||
|
||||
def get_group_member_count_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> int:
|
||||
def get_group_member_count_by_id(self, id: str, db: Optional[Session] = None) -> int:
|
||||
with get_db_context(db) as db:
|
||||
count = (
|
||||
db.query(func.count(GroupMember.user_id))
|
||||
.filter(GroupMember.group_id == id)
|
||||
.scalar()
|
||||
)
|
||||
count = db.query(func.count(GroupMember.user_id)).filter(GroupMember.group_id == id).scalar()
|
||||
return count if count else 0
|
||||
|
||||
def get_group_member_counts_by_ids(
|
||||
self, ids: list[str], db: Optional[Session] = None
|
||||
) -> dict[str, int]:
|
||||
def get_group_member_counts_by_ids(self, ids: list[str], db: Optional[Session] = None) -> dict[str, int]:
|
||||
if not ids:
|
||||
return {}
|
||||
with get_db_context(db) as db:
|
||||
@@ -442,7 +394,7 @@ class GroupTable:
|
||||
db.query(Group).filter_by(id=id).update(
|
||||
{
|
||||
**form_data.model_dump(exclude_none=True),
|
||||
"updated_at": int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
@@ -470,9 +422,7 @@ class GroupTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def remove_user_from_all_groups(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def remove_user_from_all_groups(self, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
# Find all groups the user belongs to
|
||||
@@ -489,9 +439,7 @@ class GroupTable:
|
||||
GroupMember.group_id == group.id, GroupMember.user_id == user_id
|
||||
).delete()
|
||||
|
||||
db.query(Group).filter_by(id=group.id).update(
|
||||
{"updated_at": int(time.time())}
|
||||
)
|
||||
db.query(Group).filter_by(id=group.id).update({'updated_at': int(time.time())})
|
||||
|
||||
db.commit()
|
||||
return True
|
||||
@@ -503,7 +451,6 @@ class GroupTable:
|
||||
def create_groups_by_group_names(
|
||||
self, user_id: str, group_names: list[str], db: Optional[Session] = None
|
||||
) -> list[GroupModel]:
|
||||
|
||||
# check for existing groups
|
||||
existing_groups = self.get_all_groups(db=db)
|
||||
existing_group_names = {group.name for group in existing_groups}
|
||||
@@ -517,10 +464,10 @@ class GroupTable:
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
name=group_name,
|
||||
description="",
|
||||
description='',
|
||||
data={
|
||||
"config": {
|
||||
"share": DEFAULT_GROUP_SHARE_PERMISSION,
|
||||
'config': {
|
||||
'share': DEFAULT_GROUP_SHARE_PERMISSION,
|
||||
}
|
||||
},
|
||||
created_at=int(time.time()),
|
||||
@@ -537,17 +484,13 @@ class GroupTable:
|
||||
continue
|
||||
return new_groups
|
||||
|
||||
def sync_groups_by_group_names(
|
||||
self, user_id: str, group_names: list[str], db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def sync_groups_by_group_names(self, user_id: str, group_names: list[str], db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
now = int(time.time())
|
||||
|
||||
# 1. Groups that SHOULD contain the user
|
||||
target_groups = (
|
||||
db.query(Group).filter(Group.name.in_(group_names)).all()
|
||||
)
|
||||
target_groups = db.query(Group).filter(Group.name.in_(group_names)).all()
|
||||
target_group_ids = {g.id for g in target_groups}
|
||||
|
||||
# 2. Groups the user is CURRENTLY in
|
||||
@@ -571,7 +514,7 @@ class GroupTable:
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
db.query(Group).filter(Group.id.in_(groups_to_remove)).update(
|
||||
{"updated_at": now}, synchronize_session=False
|
||||
{'updated_at': now}, synchronize_session=False
|
||||
)
|
||||
|
||||
# 5. Bulk insert missing memberships
|
||||
@@ -588,7 +531,7 @@ class GroupTable:
|
||||
|
||||
if groups_to_add:
|
||||
db.query(Group).filter(Group.id.in_(groups_to_add)).update(
|
||||
{"updated_at": now}, synchronize_session=False
|
||||
{'updated_at': now}, synchronize_session=False
|
||||
)
|
||||
|
||||
db.commit()
|
||||
@@ -656,9 +599,9 @@ class GroupTable:
|
||||
return GroupModel.model_validate(group)
|
||||
|
||||
# Remove users from group_member in batch
|
||||
db.query(GroupMember).filter(
|
||||
GroupMember.group_id == id, GroupMember.user_id.in_(user_ids)
|
||||
).delete(synchronize_session=False)
|
||||
db.query(GroupMember).filter(GroupMember.group_id == id, GroupMember.user_id.in_(user_ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
# Update group timestamp
|
||||
group.updated_at = int(time.time())
|
||||
|
||||
@@ -38,7 +38,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Knowledge(Base):
|
||||
__tablename__ = "knowledge"
|
||||
__tablename__ = 'knowledge'
|
||||
|
||||
id = Column(Text, unique=True, primary_key=True)
|
||||
user_id = Column(Text)
|
||||
@@ -70,24 +70,18 @@ class KnowledgeModel(BaseModel):
|
||||
|
||||
|
||||
class KnowledgeFile(Base):
|
||||
__tablename__ = "knowledge_file"
|
||||
__tablename__ = 'knowledge_file'
|
||||
|
||||
id = Column(Text, unique=True, primary_key=True)
|
||||
|
||||
knowledge_id = Column(
|
||||
Text, ForeignKey("knowledge.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
file_id = Column(Text, ForeignKey("file.id", ondelete="CASCADE"), nullable=False)
|
||||
knowledge_id = Column(Text, ForeignKey('knowledge.id', ondelete='CASCADE'), nullable=False)
|
||||
file_id = Column(Text, ForeignKey('file.id', ondelete='CASCADE'), nullable=False)
|
||||
user_id = Column(Text, nullable=False)
|
||||
|
||||
created_at = Column(BigInteger, nullable=False)
|
||||
updated_at = Column(BigInteger, nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"knowledge_id", "file_id", name="uq_knowledge_file_knowledge_file"
|
||||
),
|
||||
)
|
||||
__table_args__ = (UniqueConstraint('knowledge_id', 'file_id', name='uq_knowledge_file_knowledge_file'),)
|
||||
|
||||
|
||||
class KnowledgeFileModel(BaseModel):
|
||||
@@ -138,10 +132,8 @@ class KnowledgeFileListResponse(BaseModel):
|
||||
|
||||
|
||||
class KnowledgeTable:
|
||||
def _get_access_grants(
|
||||
self, knowledge_id: str, db: Optional[Session] = None
|
||||
) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource("knowledge", knowledge_id, db=db)
|
||||
def _get_access_grants(self, knowledge_id: str, db: Optional[Session] = None) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource('knowledge', knowledge_id, db=db)
|
||||
|
||||
def _to_knowledge_model(
|
||||
self,
|
||||
@@ -149,13 +141,9 @@ class KnowledgeTable:
|
||||
access_grants: Optional[list[AccessGrantModel]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> KnowledgeModel:
|
||||
knowledge_data = KnowledgeModel.model_validate(knowledge).model_dump(
|
||||
exclude={"access_grants"}
|
||||
)
|
||||
knowledge_data["access_grants"] = (
|
||||
access_grants
|
||||
if access_grants is not None
|
||||
else self._get_access_grants(knowledge_data["id"], db=db)
|
||||
knowledge_data = KnowledgeModel.model_validate(knowledge).model_dump(exclude={'access_grants'})
|
||||
knowledge_data['access_grants'] = (
|
||||
access_grants if access_grants is not None else self._get_access_grants(knowledge_data['id'], db=db)
|
||||
)
|
||||
return KnowledgeModel.model_validate(knowledge_data)
|
||||
|
||||
@@ -165,23 +153,21 @@ class KnowledgeTable:
|
||||
with get_db_context(db) as db:
|
||||
knowledge = KnowledgeModel(
|
||||
**{
|
||||
**form_data.model_dump(exclude={"access_grants"}),
|
||||
"id": str(uuid.uuid4()),
|
||||
"user_id": user_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
"access_grants": [],
|
||||
**form_data.model_dump(exclude={'access_grants'}),
|
||||
'id': str(uuid.uuid4()),
|
||||
'user_id': user_id,
|
||||
'created_at': int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
'access_grants': [],
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
result = Knowledge(**knowledge.model_dump(exclude={"access_grants"}))
|
||||
result = Knowledge(**knowledge.model_dump(exclude={'access_grants'}))
|
||||
db.add(result)
|
||||
db.commit()
|
||||
db.refresh(result)
|
||||
AccessGrants.set_access_grants(
|
||||
"knowledge", result.id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('knowledge', result.id, form_data.access_grants, db=db)
|
||||
if result:
|
||||
return self._to_knowledge_model(result, db=db)
|
||||
else:
|
||||
@@ -193,17 +179,13 @@ class KnowledgeTable:
|
||||
self, skip: int = 0, limit: int = 30, db: Optional[Session] = None
|
||||
) -> list[KnowledgeUserModel]:
|
||||
with get_db_context(db) as db:
|
||||
all_knowledge = (
|
||||
db.query(Knowledge).order_by(Knowledge.updated_at.desc()).all()
|
||||
)
|
||||
all_knowledge = db.query(Knowledge).order_by(Knowledge.updated_at.desc()).all()
|
||||
user_ids = list(set(knowledge.user_id for knowledge in all_knowledge))
|
||||
knowledge_ids = [knowledge.id for knowledge in all_knowledge]
|
||||
|
||||
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
|
||||
users_dict = {user.id: user for user in users}
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"knowledge", knowledge_ids, db=db
|
||||
)
|
||||
grants_map = AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db)
|
||||
|
||||
knowledge_bases = []
|
||||
for knowledge in all_knowledge:
|
||||
@@ -216,7 +198,7 @@ class KnowledgeTable:
|
||||
access_grants=grants_map.get(knowledge.id, []),
|
||||
db=db,
|
||||
).model_dump(),
|
||||
"user": user.model_dump() if user else None,
|
||||
'user': user.model_dump() if user else None,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -232,27 +214,25 @@ class KnowledgeTable:
|
||||
) -> KnowledgeListResponse:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Knowledge, User).outerjoin(
|
||||
User, User.id == Knowledge.user_id
|
||||
)
|
||||
query = db.query(Knowledge, User).outerjoin(User, User.id == Knowledge.user_id)
|
||||
|
||||
if filter:
|
||||
query_key = filter.get("query")
|
||||
query_key = filter.get('query')
|
||||
if query_key:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Knowledge.name.ilike(f"%{query_key}%"),
|
||||
Knowledge.description.ilike(f"%{query_key}%"),
|
||||
User.name.ilike(f"%{query_key}%"),
|
||||
User.email.ilike(f"%{query_key}%"),
|
||||
User.username.ilike(f"%{query_key}%"),
|
||||
Knowledge.name.ilike(f'%{query_key}%'),
|
||||
Knowledge.description.ilike(f'%{query_key}%'),
|
||||
User.name.ilike(f'%{query_key}%'),
|
||||
User.email.ilike(f'%{query_key}%'),
|
||||
User.username.ilike(f'%{query_key}%'),
|
||||
)
|
||||
)
|
||||
|
||||
view_option = filter.get("view_option")
|
||||
if view_option == "created":
|
||||
view_option = filter.get('view_option')
|
||||
if view_option == 'created':
|
||||
query = query.filter(Knowledge.user_id == user_id)
|
||||
elif view_option == "shared":
|
||||
elif view_option == 'shared':
|
||||
query = query.filter(Knowledge.user_id != user_id)
|
||||
|
||||
query = AccessGrants.has_permission_filter(
|
||||
@@ -260,8 +240,8 @@ class KnowledgeTable:
|
||||
query=query,
|
||||
DocumentModel=Knowledge,
|
||||
filter=filter,
|
||||
resource_type="knowledge",
|
||||
permission="read",
|
||||
resource_type='knowledge',
|
||||
permission='read',
|
||||
)
|
||||
|
||||
query = query.order_by(Knowledge.updated_at.desc(), Knowledge.id.asc())
|
||||
@@ -275,9 +255,7 @@ class KnowledgeTable:
|
||||
items = query.all()
|
||||
|
||||
knowledge_ids = [kb.id for kb, _ in items]
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"knowledge", knowledge_ids, db=db
|
||||
)
|
||||
grants_map = AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db)
|
||||
|
||||
knowledge_bases = []
|
||||
for knowledge_base, user in items:
|
||||
@@ -289,11 +267,7 @@ class KnowledgeTable:
|
||||
access_grants=grants_map.get(knowledge_base.id, []),
|
||||
db=db,
|
||||
).model_dump(),
|
||||
"user": (
|
||||
UserModel.model_validate(user).model_dump()
|
||||
if user
|
||||
else None
|
||||
),
|
||||
'user': (UserModel.model_validate(user).model_dump() if user else None),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -327,15 +301,15 @@ class KnowledgeTable:
|
||||
query=query,
|
||||
DocumentModel=Knowledge,
|
||||
filter=filter,
|
||||
resource_type="knowledge",
|
||||
permission="read",
|
||||
resource_type='knowledge',
|
||||
permission='read',
|
||||
)
|
||||
|
||||
# Apply filename search
|
||||
if filter:
|
||||
q = filter.get("query")
|
||||
q = filter.get('query')
|
||||
if q:
|
||||
query = query.filter(File.filename.ilike(f"%{q}%"))
|
||||
query = query.filter(File.filename.ilike(f'%{q}%'))
|
||||
|
||||
# Order by file changes
|
||||
query = query.order_by(File.updated_at.desc(), File.id.asc())
|
||||
@@ -355,39 +329,27 @@ class KnowledgeTable:
|
||||
items.append(
|
||||
FileUserResponse(
|
||||
**FileModel.model_validate(file).model_dump(),
|
||||
user=(
|
||||
UserResponse(
|
||||
**UserModel.model_validate(user).model_dump()
|
||||
)
|
||||
if user
|
||||
else None
|
||||
),
|
||||
collection=self._to_knowledge_model(
|
||||
knowledge, db=db
|
||||
).model_dump(),
|
||||
user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None),
|
||||
collection=self._to_knowledge_model(knowledge, db=db).model_dump(),
|
||||
)
|
||||
)
|
||||
|
||||
return KnowledgeFileListResponse(items=items, total=total)
|
||||
|
||||
except Exception as e:
|
||||
print("search_knowledge_files error:", e)
|
||||
print('search_knowledge_files error:', e)
|
||||
return KnowledgeFileListResponse(items=[], total=0)
|
||||
|
||||
def check_access_by_user_id(
|
||||
self, id, user_id, permission="write", db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def check_access_by_user_id(self, id, user_id, permission='write', db: Optional[Session] = None) -> bool:
|
||||
knowledge = self.get_knowledge_by_id(id, db=db)
|
||||
if not knowledge:
|
||||
return False
|
||||
if knowledge.user_id == user_id:
|
||||
return True
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id, db=db)}
|
||||
return AccessGrants.has_access(
|
||||
user_id=user_id,
|
||||
resource_type="knowledge",
|
||||
resource_type='knowledge',
|
||||
resource_id=knowledge.id,
|
||||
permission=permission,
|
||||
user_group_ids=user_group_ids,
|
||||
@@ -395,19 +357,17 @@ class KnowledgeTable:
|
||||
)
|
||||
|
||||
def get_knowledge_bases_by_user_id(
|
||||
self, user_id: str, permission: str = "write", db: Optional[Session] = None
|
||||
self, user_id: str, permission: str = 'write', db: Optional[Session] = None
|
||||
) -> list[KnowledgeUserModel]:
|
||||
knowledge_bases = self.get_knowledge_bases(db=db)
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id, db=db)}
|
||||
return [
|
||||
knowledge_base
|
||||
for knowledge_base in knowledge_bases
|
||||
if knowledge_base.user_id == user_id
|
||||
or AccessGrants.has_access(
|
||||
user_id=user_id,
|
||||
resource_type="knowledge",
|
||||
resource_type='knowledge',
|
||||
resource_id=knowledge_base.id,
|
||||
permission=permission,
|
||||
user_group_ids=user_group_ids,
|
||||
@@ -415,9 +375,7 @@ class KnowledgeTable:
|
||||
)
|
||||
]
|
||||
|
||||
def get_knowledge_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[KnowledgeModel]:
|
||||
def get_knowledge_by_id(self, id: str, db: Optional[Session] = None) -> Optional[KnowledgeModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
knowledge = db.query(Knowledge).filter_by(id=id).first()
|
||||
@@ -435,23 +393,19 @@ class KnowledgeTable:
|
||||
if knowledge.user_id == user_id:
|
||||
return knowledge
|
||||
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id, db=db)}
|
||||
if AccessGrants.has_access(
|
||||
user_id=user_id,
|
||||
resource_type="knowledge",
|
||||
resource_type='knowledge',
|
||||
resource_id=knowledge.id,
|
||||
permission="write",
|
||||
permission='write',
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
):
|
||||
return knowledge
|
||||
return None
|
||||
|
||||
def get_knowledges_by_file_id(
|
||||
self, file_id: str, db: Optional[Session] = None
|
||||
) -> list[KnowledgeModel]:
|
||||
def get_knowledges_by_file_id(self, file_id: str, db: Optional[Session] = None) -> list[KnowledgeModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
knowledges = (
|
||||
@@ -461,9 +415,7 @@ class KnowledgeTable:
|
||||
.all()
|
||||
)
|
||||
knowledge_ids = [k.id for k in knowledges]
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"knowledge", knowledge_ids, db=db
|
||||
)
|
||||
grants_map = AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db)
|
||||
return [
|
||||
self._to_knowledge_model(
|
||||
knowledge,
|
||||
@@ -497,32 +449,26 @@ class KnowledgeTable:
|
||||
primary_sort = File.updated_at.desc()
|
||||
|
||||
if filter:
|
||||
query_key = filter.get("query")
|
||||
query_key = filter.get('query')
|
||||
if query_key:
|
||||
query = query.filter(or_(File.filename.ilike(f"%{query_key}%")))
|
||||
query = query.filter(or_(File.filename.ilike(f'%{query_key}%')))
|
||||
|
||||
view_option = filter.get("view_option")
|
||||
if view_option == "created":
|
||||
view_option = filter.get('view_option')
|
||||
if view_option == 'created':
|
||||
query = query.filter(KnowledgeFile.user_id == user_id)
|
||||
elif view_option == "shared":
|
||||
elif view_option == 'shared':
|
||||
query = query.filter(KnowledgeFile.user_id != user_id)
|
||||
|
||||
order_by = filter.get("order_by")
|
||||
direction = filter.get("direction")
|
||||
is_asc = direction == "asc"
|
||||
order_by = filter.get('order_by')
|
||||
direction = filter.get('direction')
|
||||
is_asc = direction == 'asc'
|
||||
|
||||
if order_by == "name":
|
||||
primary_sort = (
|
||||
File.filename.asc() if is_asc else File.filename.desc()
|
||||
)
|
||||
elif order_by == "created_at":
|
||||
primary_sort = (
|
||||
File.created_at.asc() if is_asc else File.created_at.desc()
|
||||
)
|
||||
elif order_by == "updated_at":
|
||||
primary_sort = (
|
||||
File.updated_at.asc() if is_asc else File.updated_at.desc()
|
||||
)
|
||||
if order_by == 'name':
|
||||
primary_sort = File.filename.asc() if is_asc else File.filename.desc()
|
||||
elif order_by == 'created_at':
|
||||
primary_sort = File.created_at.asc() if is_asc else File.created_at.desc()
|
||||
elif order_by == 'updated_at':
|
||||
primary_sort = File.updated_at.asc() if is_asc else File.updated_at.desc()
|
||||
|
||||
# Apply sort with secondary key for deterministic pagination
|
||||
query = query.order_by(primary_sort, File.id.asc())
|
||||
@@ -542,13 +488,7 @@ class KnowledgeTable:
|
||||
files.append(
|
||||
FileUserResponse(
|
||||
**FileModel.model_validate(file).model_dump(),
|
||||
user=(
|
||||
UserResponse(
|
||||
**UserModel.model_validate(user).model_dump()
|
||||
)
|
||||
if user
|
||||
else None
|
||||
),
|
||||
user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -557,9 +497,7 @@ class KnowledgeTable:
|
||||
print(e)
|
||||
return KnowledgeFileListResponse(items=[], total=0)
|
||||
|
||||
def get_files_by_id(
|
||||
self, knowledge_id: str, db: Optional[Session] = None
|
||||
) -> list[FileModel]:
|
||||
def get_files_by_id(self, knowledge_id: str, db: Optional[Session] = None) -> list[FileModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
files = (
|
||||
@@ -572,9 +510,7 @@ class KnowledgeTable:
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def get_file_metadatas_by_id(
|
||||
self, knowledge_id: str, db: Optional[Session] = None
|
||||
) -> list[FileMetadataResponse]:
|
||||
def get_file_metadatas_by_id(self, knowledge_id: str, db: Optional[Session] = None) -> list[FileMetadataResponse]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
files = self.get_files_by_id(knowledge_id, db=db)
|
||||
@@ -592,12 +528,12 @@ class KnowledgeTable:
|
||||
with get_db_context(db) as db:
|
||||
knowledge_file = KnowledgeFileModel(
|
||||
**{
|
||||
"id": str(uuid.uuid4()),
|
||||
"knowledge_id": knowledge_id,
|
||||
"file_id": file_id,
|
||||
"user_id": user_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
'id': str(uuid.uuid4()),
|
||||
'knowledge_id': knowledge_id,
|
||||
'file_id': file_id,
|
||||
'user_id': user_id,
|
||||
'created_at': int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -613,37 +549,24 @@ class KnowledgeTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def has_file(
|
||||
self, knowledge_id: str, file_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def has_file(self, knowledge_id: str, file_id: str, db: Optional[Session] = None) -> bool:
|
||||
"""Check whether a file belongs to a knowledge base."""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
return (
|
||||
db.query(KnowledgeFile)
|
||||
.filter_by(knowledge_id=knowledge_id, file_id=file_id)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
return db.query(KnowledgeFile).filter_by(knowledge_id=knowledge_id, file_id=file_id).first() is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def remove_file_from_knowledge_by_id(
|
||||
self, knowledge_id: str, file_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def remove_file_from_knowledge_by_id(self, knowledge_id: str, file_id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
db.query(KnowledgeFile).filter_by(
|
||||
knowledge_id=knowledge_id, file_id=file_id
|
||||
).delete()
|
||||
db.query(KnowledgeFile).filter_by(knowledge_id=knowledge_id, file_id=file_id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def reset_knowledge_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[KnowledgeModel]:
|
||||
def reset_knowledge_by_id(self, id: str, db: Optional[Session] = None) -> Optional[KnowledgeModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
# Delete all knowledge_file entries for this knowledge_id
|
||||
@@ -653,7 +576,7 @@ class KnowledgeTable:
|
||||
# Update the knowledge entry's updated_at timestamp
|
||||
db.query(Knowledge).filter_by(id=id).update(
|
||||
{
|
||||
"updated_at": int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
@@ -675,15 +598,13 @@ class KnowledgeTable:
|
||||
knowledge = self.get_knowledge_by_id(id=id, db=db)
|
||||
db.query(Knowledge).filter_by(id=id).update(
|
||||
{
|
||||
**form_data.model_dump(exclude={"access_grants"}),
|
||||
"updated_at": int(time.time()),
|
||||
**form_data.model_dump(exclude={'access_grants'}),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
if form_data.access_grants is not None:
|
||||
AccessGrants.set_access_grants(
|
||||
"knowledge", id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('knowledge', id, form_data.access_grants, db=db)
|
||||
return self.get_knowledge_by_id(id=id, db=db)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
@@ -697,8 +618,8 @@ class KnowledgeTable:
|
||||
knowledge = self.get_knowledge_by_id(id=id, db=db)
|
||||
db.query(Knowledge).filter_by(id=id).update(
|
||||
{
|
||||
"data": data,
|
||||
"updated_at": int(time.time()),
|
||||
'data': data,
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
@@ -710,7 +631,7 @@ class KnowledgeTable:
|
||||
def delete_knowledge_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
AccessGrants.revoke_all_access("knowledge", id, db=db)
|
||||
AccessGrants.revoke_all_access('knowledge', id, db=db)
|
||||
db.query(Knowledge).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
@@ -722,7 +643,7 @@ class KnowledgeTable:
|
||||
try:
|
||||
knowledge_ids = [row[0] for row in db.query(Knowledge.id).all()]
|
||||
for knowledge_id in knowledge_ids:
|
||||
AccessGrants.revoke_all_access("knowledge", knowledge_id, db=db)
|
||||
AccessGrants.revoke_all_access('knowledge', knowledge_id, db=db)
|
||||
db.query(Knowledge).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from sqlalchemy import BigInteger, Column, String, Text
|
||||
|
||||
|
||||
class Memory(Base):
|
||||
__tablename__ = "memory"
|
||||
__tablename__ = 'memory'
|
||||
|
||||
id = Column(String, primary_key=True, unique=True)
|
||||
user_id = Column(String)
|
||||
@@ -49,11 +49,11 @@ class MemoriesTable:
|
||||
|
||||
memory = MemoryModel(
|
||||
**{
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
"content": content,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
'id': id,
|
||||
'user_id': user_id,
|
||||
'content': content,
|
||||
'created_at': int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
result = Memory(**memory.model_dump())
|
||||
@@ -95,9 +95,7 @@ class MemoriesTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_memories_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[MemoryModel]:
|
||||
def get_memories_by_user_id(self, user_id: str, db: Optional[Session] = None) -> list[MemoryModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
memories = db.query(Memory).filter_by(user_id=user_id).all()
|
||||
@@ -105,9 +103,7 @@ class MemoriesTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_memory_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[MemoryModel]:
|
||||
def get_memory_by_id(self, id: str, db: Optional[Session] = None) -> Optional[MemoryModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
memory = db.get(Memory, id)
|
||||
@@ -126,9 +122,7 @@ class MemoriesTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_memories_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_memories_by_user_id(self, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
db.query(Memory).filter_by(user_id=user_id).delete()
|
||||
@@ -138,9 +132,7 @@ class MemoriesTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_memory_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_memory_by_id_and_user_id(self, id: str, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
memory = db.get(Memory, id)
|
||||
|
||||
@@ -21,7 +21,7 @@ from sqlalchemy.sql import exists
|
||||
|
||||
|
||||
class MessageReaction(Base):
|
||||
__tablename__ = "message_reaction"
|
||||
__tablename__ = 'message_reaction'
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
user_id = Column(Text)
|
||||
message_id = Column(Text)
|
||||
@@ -40,7 +40,7 @@ class MessageReactionModel(BaseModel):
|
||||
|
||||
|
||||
class Message(Base):
|
||||
__tablename__ = "message"
|
||||
__tablename__ = 'message'
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
|
||||
user_id = Column(Text)
|
||||
@@ -112,7 +112,7 @@ class MessageUserResponse(MessageModel):
|
||||
class MessageUserSlimResponse(MessageUserResponse):
|
||||
data: bool | None = None
|
||||
|
||||
@field_validator("data", mode="before")
|
||||
@field_validator('data', mode='before')
|
||||
def convert_data_to_bool(cls, v):
|
||||
# No data or not a dict → False
|
||||
if not isinstance(v, dict):
|
||||
@@ -152,19 +152,19 @@ class MessageTable:
|
||||
|
||||
message = MessageModel(
|
||||
**{
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
"channel_id": channel_id,
|
||||
"reply_to_id": form_data.reply_to_id,
|
||||
"parent_id": form_data.parent_id,
|
||||
"is_pinned": False,
|
||||
"pinned_at": None,
|
||||
"pinned_by": None,
|
||||
"content": form_data.content,
|
||||
"data": form_data.data,
|
||||
"meta": form_data.meta,
|
||||
"created_at": ts,
|
||||
"updated_at": ts,
|
||||
'id': id,
|
||||
'user_id': user_id,
|
||||
'channel_id': channel_id,
|
||||
'reply_to_id': form_data.reply_to_id,
|
||||
'parent_id': form_data.parent_id,
|
||||
'is_pinned': False,
|
||||
'pinned_at': None,
|
||||
'pinned_by': None,
|
||||
'content': form_data.content,
|
||||
'data': form_data.data,
|
||||
'meta': form_data.meta,
|
||||
'created_at': ts,
|
||||
'updated_at': ts,
|
||||
}
|
||||
)
|
||||
result = Message(**message.model_dump())
|
||||
@@ -186,9 +186,7 @@ class MessageTable:
|
||||
return None
|
||||
|
||||
reply_to_message = (
|
||||
self.get_message_by_id(
|
||||
message.reply_to_id, include_thread_replies=False, db=db
|
||||
)
|
||||
self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db)
|
||||
if message.reply_to_id
|
||||
else None
|
||||
)
|
||||
@@ -200,22 +198,22 @@ class MessageTable:
|
||||
thread_replies = self.get_thread_replies_by_message_id(id, db=db)
|
||||
|
||||
# Check if message was sent by webhook (webhook info in meta takes precedence)
|
||||
webhook_info = message.meta.get("webhook") if message.meta else None
|
||||
if webhook_info and webhook_info.get("id"):
|
||||
webhook_info = message.meta.get('webhook') if message.meta else None
|
||||
if webhook_info and webhook_info.get('id'):
|
||||
# Look up webhook by ID to get current name
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get('id'), db=db)
|
||||
if webhook:
|
||||
user_info = {
|
||||
"id": webhook.id,
|
||||
"name": webhook.name,
|
||||
"role": "webhook",
|
||||
'id': webhook.id,
|
||||
'name': webhook.name,
|
||||
'role': 'webhook',
|
||||
}
|
||||
else:
|
||||
# Webhook was deleted, use placeholder
|
||||
user_info = {
|
||||
"id": webhook_info.get("id"),
|
||||
"name": "Deleted Webhook",
|
||||
"role": "webhook",
|
||||
'id': webhook_info.get('id'),
|
||||
'name': 'Deleted Webhook',
|
||||
'role': 'webhook',
|
||||
}
|
||||
else:
|
||||
user = Users.get_user_by_id(message.user_id, db=db)
|
||||
@@ -224,79 +222,57 @@ class MessageTable:
|
||||
return MessageResponse.model_validate(
|
||||
{
|
||||
**MessageModel.model_validate(message).model_dump(),
|
||||
"user": user_info,
|
||||
"reply_to_message": (
|
||||
reply_to_message.model_dump() if reply_to_message else None
|
||||
),
|
||||
"latest_reply_at": (
|
||||
thread_replies[0].created_at if thread_replies else None
|
||||
),
|
||||
"reply_count": len(thread_replies),
|
||||
"reactions": reactions,
|
||||
'user': user_info,
|
||||
'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None),
|
||||
'latest_reply_at': (thread_replies[0].created_at if thread_replies else None),
|
||||
'reply_count': len(thread_replies),
|
||||
'reactions': reactions,
|
||||
}
|
||||
)
|
||||
|
||||
def get_thread_replies_by_message_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> list[MessageReplyToResponse]:
|
||||
def get_thread_replies_by_message_id(self, id: str, db: Optional[Session] = None) -> list[MessageReplyToResponse]:
|
||||
with get_db_context(db) as db:
|
||||
all_messages = (
|
||||
db.query(Message)
|
||||
.filter_by(parent_id=id)
|
||||
.order_by(Message.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
all_messages = db.query(Message).filter_by(parent_id=id).order_by(Message.created_at.desc()).all()
|
||||
|
||||
messages = []
|
||||
for message in all_messages:
|
||||
reply_to_message = (
|
||||
self.get_message_by_id(
|
||||
message.reply_to_id, include_thread_replies=False, db=db
|
||||
)
|
||||
self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db)
|
||||
if message.reply_to_id
|
||||
else None
|
||||
)
|
||||
|
||||
webhook_info = message.meta.get("webhook") if message.meta else None
|
||||
webhook_info = message.meta.get('webhook') if message.meta else None
|
||||
user_info = None
|
||||
if webhook_info and webhook_info.get("id"):
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
|
||||
if webhook_info and webhook_info.get('id'):
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get('id'), db=db)
|
||||
if webhook:
|
||||
user_info = {
|
||||
"id": webhook.id,
|
||||
"name": webhook.name,
|
||||
"role": "webhook",
|
||||
'id': webhook.id,
|
||||
'name': webhook.name,
|
||||
'role': 'webhook',
|
||||
}
|
||||
else:
|
||||
user_info = {
|
||||
"id": webhook_info.get("id"),
|
||||
"name": "Deleted Webhook",
|
||||
"role": "webhook",
|
||||
'id': webhook_info.get('id'),
|
||||
'name': 'Deleted Webhook',
|
||||
'role': 'webhook',
|
||||
}
|
||||
|
||||
messages.append(
|
||||
MessageReplyToResponse.model_validate(
|
||||
{
|
||||
**MessageModel.model_validate(message).model_dump(),
|
||||
"user": user_info,
|
||||
"reply_to_message": (
|
||||
reply_to_message.model_dump()
|
||||
if reply_to_message
|
||||
else None
|
||||
),
|
||||
'user': user_info,
|
||||
'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None),
|
||||
}
|
||||
)
|
||||
)
|
||||
return messages
|
||||
|
||||
def get_reply_user_ids_by_message_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> list[str]:
|
||||
def get_reply_user_ids_by_message_id(self, id: str, db: Optional[Session] = None) -> list[str]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
message.user_id
|
||||
for message in db.query(Message).filter_by(parent_id=id).all()
|
||||
]
|
||||
return [message.user_id for message in db.query(Message).filter_by(parent_id=id).all()]
|
||||
|
||||
def get_messages_by_channel_id(
|
||||
self,
|
||||
@@ -318,40 +294,34 @@ class MessageTable:
|
||||
messages = []
|
||||
for message in all_messages:
|
||||
reply_to_message = (
|
||||
self.get_message_by_id(
|
||||
message.reply_to_id, include_thread_replies=False, db=db
|
||||
)
|
||||
self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db)
|
||||
if message.reply_to_id
|
||||
else None
|
||||
)
|
||||
|
||||
webhook_info = message.meta.get("webhook") if message.meta else None
|
||||
webhook_info = message.meta.get('webhook') if message.meta else None
|
||||
user_info = None
|
||||
if webhook_info and webhook_info.get("id"):
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
|
||||
if webhook_info and webhook_info.get('id'):
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get('id'), db=db)
|
||||
if webhook:
|
||||
user_info = {
|
||||
"id": webhook.id,
|
||||
"name": webhook.name,
|
||||
"role": "webhook",
|
||||
'id': webhook.id,
|
||||
'name': webhook.name,
|
||||
'role': 'webhook',
|
||||
}
|
||||
else:
|
||||
user_info = {
|
||||
"id": webhook_info.get("id"),
|
||||
"name": "Deleted Webhook",
|
||||
"role": "webhook",
|
||||
'id': webhook_info.get('id'),
|
||||
'name': 'Deleted Webhook',
|
||||
'role': 'webhook',
|
||||
}
|
||||
|
||||
messages.append(
|
||||
MessageReplyToResponse.model_validate(
|
||||
{
|
||||
**MessageModel.model_validate(message).model_dump(),
|
||||
"user": user_info,
|
||||
"reply_to_message": (
|
||||
reply_to_message.model_dump()
|
||||
if reply_to_message
|
||||
else None
|
||||
),
|
||||
'user': user_info,
|
||||
'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -387,55 +357,42 @@ class MessageTable:
|
||||
messages = []
|
||||
for message in all_messages:
|
||||
reply_to_message = (
|
||||
self.get_message_by_id(
|
||||
message.reply_to_id, include_thread_replies=False, db=db
|
||||
)
|
||||
self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db)
|
||||
if message.reply_to_id
|
||||
else None
|
||||
)
|
||||
|
||||
webhook_info = message.meta.get("webhook") if message.meta else None
|
||||
webhook_info = message.meta.get('webhook') if message.meta else None
|
||||
user_info = None
|
||||
if webhook_info and webhook_info.get("id"):
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db)
|
||||
if webhook_info and webhook_info.get('id'):
|
||||
webhook = Channels.get_webhook_by_id(webhook_info.get('id'), db=db)
|
||||
if webhook:
|
||||
user_info = {
|
||||
"id": webhook.id,
|
||||
"name": webhook.name,
|
||||
"role": "webhook",
|
||||
'id': webhook.id,
|
||||
'name': webhook.name,
|
||||
'role': 'webhook',
|
||||
}
|
||||
else:
|
||||
user_info = {
|
||||
"id": webhook_info.get("id"),
|
||||
"name": "Deleted Webhook",
|
||||
"role": "webhook",
|
||||
'id': webhook_info.get('id'),
|
||||
'name': 'Deleted Webhook',
|
||||
'role': 'webhook',
|
||||
}
|
||||
|
||||
messages.append(
|
||||
MessageReplyToResponse.model_validate(
|
||||
{
|
||||
**MessageModel.model_validate(message).model_dump(),
|
||||
"user": user_info,
|
||||
"reply_to_message": (
|
||||
reply_to_message.model_dump()
|
||||
if reply_to_message
|
||||
else None
|
||||
),
|
||||
'user': user_info,
|
||||
'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None),
|
||||
}
|
||||
)
|
||||
)
|
||||
return messages
|
||||
|
||||
def get_last_message_by_channel_id(
|
||||
self, channel_id: str, db: Optional[Session] = None
|
||||
) -> Optional[MessageModel]:
|
||||
def get_last_message_by_channel_id(self, channel_id: str, db: Optional[Session] = None) -> Optional[MessageModel]:
|
||||
with get_db_context(db) as db:
|
||||
message = (
|
||||
db.query(Message)
|
||||
.filter_by(channel_id=channel_id)
|
||||
.order_by(Message.created_at.desc())
|
||||
.first()
|
||||
)
|
||||
message = db.query(Message).filter_by(channel_id=channel_id).order_by(Message.created_at.desc()).first()
|
||||
return MessageModel.model_validate(message) if message else None
|
||||
|
||||
def get_pinned_messages_by_channel_id(
|
||||
@@ -513,11 +470,7 @@ class MessageTable:
|
||||
) -> Optional[MessageReactionModel]:
|
||||
with get_db_context(db) as db:
|
||||
# check for existing reaction
|
||||
existing_reaction = (
|
||||
db.query(MessageReaction)
|
||||
.filter_by(message_id=id, user_id=user_id, name=name)
|
||||
.first()
|
||||
)
|
||||
existing_reaction = db.query(MessageReaction).filter_by(message_id=id, user_id=user_id, name=name).first()
|
||||
if existing_reaction:
|
||||
return MessageReactionModel.model_validate(existing_reaction)
|
||||
|
||||
@@ -535,9 +488,7 @@ class MessageTable:
|
||||
db.refresh(result)
|
||||
return MessageReactionModel.model_validate(result) if result else None
|
||||
|
||||
def get_reactions_by_message_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> list[Reactions]:
|
||||
def get_reactions_by_message_id(self, id: str, db: Optional[Session] = None) -> list[Reactions]:
|
||||
with get_db_context(db) as db:
|
||||
# JOIN User so all user info is fetched in one query
|
||||
results = (
|
||||
@@ -552,18 +503,18 @@ class MessageTable:
|
||||
for reaction, user in results:
|
||||
if reaction.name not in reactions:
|
||||
reactions[reaction.name] = {
|
||||
"name": reaction.name,
|
||||
"users": [],
|
||||
"count": 0,
|
||||
'name': reaction.name,
|
||||
'users': [],
|
||||
'count': 0,
|
||||
}
|
||||
|
||||
reactions[reaction.name]["users"].append(
|
||||
reactions[reaction.name]['users'].append(
|
||||
{
|
||||
"id": user.id,
|
||||
"name": user.name,
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
}
|
||||
)
|
||||
reactions[reaction.name]["count"] += 1
|
||||
reactions[reaction.name]['count'] += 1
|
||||
|
||||
return [Reactions(**reaction) for reaction in reactions.values()]
|
||||
|
||||
@@ -571,9 +522,7 @@ class MessageTable:
|
||||
self, id: str, user_id: str, name: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
db.query(MessageReaction).filter_by(
|
||||
message_id=id, user_id=user_id, name=name
|
||||
).delete()
|
||||
db.query(MessageReaction).filter_by(message_id=id, user_id=user_id, name=name).delete()
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@@ -612,21 +561,15 @@ class MessageTable:
|
||||
with get_db_context(db) as db:
|
||||
query_builder = db.query(Message).filter(
|
||||
Message.channel_id.in_(channel_ids),
|
||||
Message.content.ilike(f"%{query}%"),
|
||||
Message.content.ilike(f'%{query}%'),
|
||||
)
|
||||
|
||||
if start_timestamp:
|
||||
query_builder = query_builder.filter(
|
||||
Message.created_at >= start_timestamp
|
||||
)
|
||||
query_builder = query_builder.filter(Message.created_at >= start_timestamp)
|
||||
if end_timestamp:
|
||||
query_builder = query_builder.filter(
|
||||
Message.created_at <= end_timestamp
|
||||
)
|
||||
query_builder = query_builder.filter(Message.created_at <= end_timestamp)
|
||||
|
||||
messages = (
|
||||
query_builder.order_by(Message.created_at.desc()).limit(limit).all()
|
||||
)
|
||||
messages = query_builder.order_by(Message.created_at.desc()).limit(limit).all()
|
||||
return [MessageModel.model_validate(msg) for msg in messages]
|
||||
|
||||
|
||||
|
||||
@@ -28,13 +28,13 @@ log = logging.getLogger(__name__)
|
||||
|
||||
# ModelParams is a model for the data stored in the params field of the Model table
|
||||
class ModelParams(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
pass
|
||||
|
||||
|
||||
# ModelMeta is a model for the data stored in the meta field of the Model table
|
||||
class ModelMeta(BaseModel):
|
||||
profile_image_url: Optional[str] = "/static/favicon.png"
|
||||
profile_image_url: Optional[str] = '/static/favicon.png'
|
||||
|
||||
description: Optional[str] = None
|
||||
"""
|
||||
@@ -43,13 +43,13 @@ class ModelMeta(BaseModel):
|
||||
|
||||
capabilities: Optional[dict] = None
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Model(Base):
|
||||
__tablename__ = "model"
|
||||
__tablename__ = 'model'
|
||||
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
"""
|
||||
@@ -139,10 +139,8 @@ class ModelForm(BaseModel):
|
||||
|
||||
|
||||
class ModelsTable:
|
||||
def _get_access_grants(
|
||||
self, model_id: str, db: Optional[Session] = None
|
||||
) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource("model", model_id, db=db)
|
||||
def _get_access_grants(self, model_id: str, db: Optional[Session] = None) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource('model', model_id, db=db)
|
||||
|
||||
def _to_model_model(
|
||||
self,
|
||||
@@ -150,13 +148,9 @@ class ModelsTable:
|
||||
access_grants: Optional[list[AccessGrantModel]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> ModelModel:
|
||||
model_data = ModelModel.model_validate(model).model_dump(
|
||||
exclude={"access_grants"}
|
||||
)
|
||||
model_data["access_grants"] = (
|
||||
access_grants
|
||||
if access_grants is not None
|
||||
else self._get_access_grants(model_data["id"], db=db)
|
||||
model_data = ModelModel.model_validate(model).model_dump(exclude={'access_grants'})
|
||||
model_data['access_grants'] = (
|
||||
access_grants if access_grants is not None else self._get_access_grants(model_data['id'], db=db)
|
||||
)
|
||||
return ModelModel.model_validate(model_data)
|
||||
|
||||
@@ -167,37 +161,32 @@ class ModelsTable:
|
||||
with get_db_context(db) as db:
|
||||
result = Model(
|
||||
**{
|
||||
**form_data.model_dump(exclude={"access_grants"}),
|
||||
"user_id": user_id,
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
**form_data.model_dump(exclude={'access_grants'}),
|
||||
'user_id': user_id,
|
||||
'created_at': int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.add(result)
|
||||
db.commit()
|
||||
db.refresh(result)
|
||||
AccessGrants.set_access_grants(
|
||||
"model", result.id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('model', result.id, form_data.access_grants, db=db)
|
||||
|
||||
if result:
|
||||
return self._to_model_model(result, db=db)
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.exception(f"Failed to insert a new model: {e}")
|
||||
log.exception(f'Failed to insert a new model: {e}')
|
||||
return None
|
||||
|
||||
def get_all_models(self, db: Optional[Session] = None) -> list[ModelModel]:
|
||||
with get_db_context(db) as db:
|
||||
all_models = db.query(Model).all()
|
||||
model_ids = [model.id for model in all_models]
|
||||
grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db)
|
||||
grants_map = AccessGrants.get_grants_by_resources('model', model_ids, db=db)
|
||||
return [
|
||||
self._to_model_model(
|
||||
model, access_grants=grants_map.get(model.id, []), db=db
|
||||
)
|
||||
for model in all_models
|
||||
self._to_model_model(model, access_grants=grants_map.get(model.id, []), db=db) for model in all_models
|
||||
]
|
||||
|
||||
def get_models(self, db: Optional[Session] = None) -> list[ModelUserResponse]:
|
||||
@@ -209,7 +198,7 @@ class ModelsTable:
|
||||
|
||||
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
|
||||
users_dict = {user.id: user for user in users}
|
||||
grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db)
|
||||
grants_map = AccessGrants.get_grants_by_resources('model', model_ids, db=db)
|
||||
|
||||
models = []
|
||||
for model in all_models:
|
||||
@@ -222,7 +211,7 @@ class ModelsTable:
|
||||
access_grants=grants_map.get(model.id, []),
|
||||
db=db,
|
||||
).model_dump(),
|
||||
"user": user.model_dump() if user else None,
|
||||
'user': user.model_dump() if user else None,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -232,28 +221,23 @@ class ModelsTable:
|
||||
with get_db_context(db) as db:
|
||||
all_models = db.query(Model).filter(Model.base_model_id == None).all()
|
||||
model_ids = [model.id for model in all_models]
|
||||
grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db)
|
||||
grants_map = AccessGrants.get_grants_by_resources('model', model_ids, db=db)
|
||||
return [
|
||||
self._to_model_model(
|
||||
model, access_grants=grants_map.get(model.id, []), db=db
|
||||
)
|
||||
for model in all_models
|
||||
self._to_model_model(model, access_grants=grants_map.get(model.id, []), db=db) for model in all_models
|
||||
]
|
||||
|
||||
def get_models_by_user_id(
|
||||
self, user_id: str, permission: str = "write", db: Optional[Session] = None
|
||||
self, user_id: str, permission: str = 'write', db: Optional[Session] = None
|
||||
) -> list[ModelUserResponse]:
|
||||
models = self.get_models(db=db)
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id, db=db)}
|
||||
return [
|
||||
model
|
||||
for model in models
|
||||
if model.user_id == user_id
|
||||
or AccessGrants.has_access(
|
||||
user_id=user_id,
|
||||
resource_type="model",
|
||||
resource_type='model',
|
||||
resource_id=model.id,
|
||||
permission=permission,
|
||||
user_group_ids=user_group_ids,
|
||||
@@ -261,13 +245,13 @@ class ModelsTable:
|
||||
)
|
||||
]
|
||||
|
||||
def _has_permission(self, db, query, filter: dict, permission: str = "read"):
|
||||
def _has_permission(self, db, query, filter: dict, permission: str = 'read'):
|
||||
return AccessGrants.has_permission_filter(
|
||||
db=db,
|
||||
query=query,
|
||||
DocumentModel=Model,
|
||||
filter=filter,
|
||||
resource_type="model",
|
||||
resource_type='model',
|
||||
permission=permission,
|
||||
)
|
||||
|
||||
@@ -285,22 +269,22 @@ class ModelsTable:
|
||||
query = query.filter(Model.base_model_id != None)
|
||||
|
||||
if filter:
|
||||
query_key = filter.get("query")
|
||||
query_key = filter.get('query')
|
||||
if query_key:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Model.name.ilike(f"%{query_key}%"),
|
||||
Model.base_model_id.ilike(f"%{query_key}%"),
|
||||
User.name.ilike(f"%{query_key}%"),
|
||||
User.email.ilike(f"%{query_key}%"),
|
||||
User.username.ilike(f"%{query_key}%"),
|
||||
Model.name.ilike(f'%{query_key}%'),
|
||||
Model.base_model_id.ilike(f'%{query_key}%'),
|
||||
User.name.ilike(f'%{query_key}%'),
|
||||
User.email.ilike(f'%{query_key}%'),
|
||||
User.username.ilike(f'%{query_key}%'),
|
||||
)
|
||||
)
|
||||
|
||||
view_option = filter.get("view_option")
|
||||
if view_option == "created":
|
||||
view_option = filter.get('view_option')
|
||||
if view_option == 'created':
|
||||
query = query.filter(Model.user_id == user_id)
|
||||
elif view_option == "shared":
|
||||
elif view_option == 'shared':
|
||||
query = query.filter(Model.user_id != user_id)
|
||||
|
||||
# Apply access control filtering
|
||||
@@ -308,10 +292,10 @@ class ModelsTable:
|
||||
db,
|
||||
query,
|
||||
filter,
|
||||
permission="read",
|
||||
permission='read',
|
||||
)
|
||||
|
||||
tag = filter.get("tag")
|
||||
tag = filter.get('tag')
|
||||
if tag:
|
||||
# TODO: This is a simple implementation and should be improved for performance
|
||||
like_pattern = f'%"{tag.lower()}"%' # `"tag"` inside JSON array
|
||||
@@ -319,21 +303,21 @@ class ModelsTable:
|
||||
|
||||
query = query.filter(meta_text.like(like_pattern))
|
||||
|
||||
order_by = filter.get("order_by")
|
||||
direction = filter.get("direction")
|
||||
order_by = filter.get('order_by')
|
||||
direction = filter.get('direction')
|
||||
|
||||
if order_by == "name":
|
||||
if direction == "asc":
|
||||
if order_by == 'name':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Model.name.asc())
|
||||
else:
|
||||
query = query.order_by(Model.name.desc())
|
||||
elif order_by == "created_at":
|
||||
if direction == "asc":
|
||||
elif order_by == 'created_at':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Model.created_at.asc())
|
||||
else:
|
||||
query = query.order_by(Model.created_at.desc())
|
||||
elif order_by == "updated_at":
|
||||
if direction == "asc":
|
||||
elif order_by == 'updated_at':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Model.updated_at.asc())
|
||||
else:
|
||||
query = query.order_by(Model.updated_at.desc())
|
||||
@@ -352,7 +336,7 @@ class ModelsTable:
|
||||
items = query.all()
|
||||
|
||||
model_ids = [model.id for model, _ in items]
|
||||
grants_map = AccessGrants.get_grants_by_resources("model", model_ids, db=db)
|
||||
grants_map = AccessGrants.get_grants_by_resources('model', model_ids, db=db)
|
||||
|
||||
models = []
|
||||
for model, user in items:
|
||||
@@ -363,19 +347,13 @@ class ModelsTable:
|
||||
access_grants=grants_map.get(model.id, []),
|
||||
db=db,
|
||||
).model_dump(),
|
||||
user=(
|
||||
UserResponse(**UserModel.model_validate(user).model_dump())
|
||||
if user
|
||||
else None
|
||||
),
|
||||
user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None),
|
||||
)
|
||||
)
|
||||
|
||||
return ModelListResponse(items=models, total=total)
|
||||
|
||||
def get_model_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ModelModel]:
|
||||
def get_model_by_id(self, id: str, db: Optional[Session] = None) -> Optional[ModelModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
model = db.get(Model, id)
|
||||
@@ -383,16 +361,12 @@ class ModelsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_models_by_ids(
|
||||
self, ids: list[str], db: Optional[Session] = None
|
||||
) -> list[ModelModel]:
|
||||
def get_models_by_ids(self, ids: list[str], db: Optional[Session] = None) -> list[ModelModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
models = db.query(Model).filter(Model.id.in_(ids)).all()
|
||||
model_ids = [model.id for model in models]
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"model", model_ids, db=db
|
||||
)
|
||||
grants_map = AccessGrants.get_grants_by_resources('model', model_ids, db=db)
|
||||
return [
|
||||
self._to_model_model(
|
||||
model,
|
||||
@@ -404,9 +378,7 @@ class ModelsTable:
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def toggle_model_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ModelModel]:
|
||||
def toggle_model_by_id(self, id: str, db: Optional[Session] = None) -> Optional[ModelModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
model = db.query(Model).filter_by(id=id).first()
|
||||
@@ -422,30 +394,26 @@ class ModelsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_model_by_id(
|
||||
self, id: str, model: ModelForm, db: Optional[Session] = None
|
||||
) -> Optional[ModelModel]:
|
||||
def update_model_by_id(self, id: str, model: ModelForm, db: Optional[Session] = None) -> Optional[ModelModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
# update only the fields that are present in the model
|
||||
data = model.model_dump(exclude={"id", "access_grants"})
|
||||
data = model.model_dump(exclude={'id', 'access_grants'})
|
||||
result = db.query(Model).filter_by(id=id).update(data)
|
||||
|
||||
db.commit()
|
||||
if model.access_grants is not None:
|
||||
AccessGrants.set_access_grants(
|
||||
"model", id, model.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('model', id, model.access_grants, db=db)
|
||||
|
||||
return self.get_model_by_id(id, db=db)
|
||||
except Exception as e:
|
||||
log.exception(f"Failed to update the model by id {id}: {e}")
|
||||
log.exception(f'Failed to update the model by id {id}: {e}')
|
||||
return None
|
||||
|
||||
def delete_model_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
AccessGrants.revoke_all_access("model", id, db=db)
|
||||
AccessGrants.revoke_all_access('model', id, db=db)
|
||||
db.query(Model).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
||||
@@ -458,7 +426,7 @@ class ModelsTable:
|
||||
with get_db_context(db) as db:
|
||||
model_ids = [row[0] for row in db.query(Model.id).all()]
|
||||
for model_id in model_ids:
|
||||
AccessGrants.revoke_all_access("model", model_id, db=db)
|
||||
AccessGrants.revoke_all_access('model', model_id, db=db)
|
||||
db.query(Model).delete()
|
||||
db.commit()
|
||||
|
||||
@@ -466,9 +434,7 @@ class ModelsTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def sync_models(
|
||||
self, user_id: str, models: list[ModelModel], db: Optional[Session] = None
|
||||
) -> list[ModelModel]:
|
||||
def sync_models(self, user_id: str, models: list[ModelModel], db: Optional[Session] = None) -> list[ModelModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
# Get existing models
|
||||
@@ -483,37 +449,33 @@ class ModelsTable:
|
||||
if model.id in existing_ids:
|
||||
db.query(Model).filter_by(id=model.id).update(
|
||||
{
|
||||
**model.model_dump(exclude={"access_grants"}),
|
||||
"user_id": user_id,
|
||||
"updated_at": int(time.time()),
|
||||
**model.model_dump(exclude={'access_grants'}),
|
||||
'user_id': user_id,
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
else:
|
||||
new_model = Model(
|
||||
**{
|
||||
**model.model_dump(exclude={"access_grants"}),
|
||||
"user_id": user_id,
|
||||
"updated_at": int(time.time()),
|
||||
**model.model_dump(exclude={'access_grants'}),
|
||||
'user_id': user_id,
|
||||
'updated_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.add(new_model)
|
||||
AccessGrants.set_access_grants(
|
||||
"model", model.id, model.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('model', model.id, model.access_grants, db=db)
|
||||
|
||||
# Remove models that are no longer present
|
||||
for model in existing_models:
|
||||
if model.id not in new_model_ids:
|
||||
AccessGrants.revoke_all_access("model", model.id, db=db)
|
||||
AccessGrants.revoke_all_access('model', model.id, db=db)
|
||||
db.delete(model)
|
||||
|
||||
db.commit()
|
||||
|
||||
all_models = db.query(Model).all()
|
||||
model_ids = [model.id for model in all_models]
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"model", model_ids, db=db
|
||||
)
|
||||
grants_map = AccessGrants.get_grants_by_resources('model', model_ids, db=db)
|
||||
return [
|
||||
self._to_model_model(
|
||||
model,
|
||||
@@ -523,7 +485,7 @@ class ModelsTable:
|
||||
for model in all_models
|
||||
]
|
||||
except Exception as e:
|
||||
log.exception(f"Error syncing models for user {user_id}: {e}")
|
||||
log.exception(f'Error syncing models for user {user_id}: {e}')
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from sqlalchemy import or_, func, cast
|
||||
|
||||
|
||||
class Note(Base):
|
||||
__tablename__ = "note"
|
||||
__tablename__ = 'note'
|
||||
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
user_id = Column(Text)
|
||||
@@ -88,10 +88,8 @@ class NoteListResponse(BaseModel):
|
||||
|
||||
|
||||
class NoteTable:
|
||||
def _get_access_grants(
|
||||
self, note_id: str, db: Optional[Session] = None
|
||||
) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource("note", note_id, db=db)
|
||||
def _get_access_grants(self, note_id: str, db: Optional[Session] = None) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource('note', note_id, db=db)
|
||||
|
||||
def _to_note_model(
|
||||
self,
|
||||
@@ -99,51 +97,43 @@ class NoteTable:
|
||||
access_grants: Optional[list[AccessGrantModel]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> NoteModel:
|
||||
note_data = NoteModel.model_validate(note).model_dump(exclude={"access_grants"})
|
||||
note_data["access_grants"] = (
|
||||
access_grants
|
||||
if access_grants is not None
|
||||
else self._get_access_grants(note_data["id"], db=db)
|
||||
note_data = NoteModel.model_validate(note).model_dump(exclude={'access_grants'})
|
||||
note_data['access_grants'] = (
|
||||
access_grants if access_grants is not None else self._get_access_grants(note_data['id'], db=db)
|
||||
)
|
||||
return NoteModel.model_validate(note_data)
|
||||
|
||||
def _has_permission(self, db, query, filter: dict, permission: str = "read"):
|
||||
def _has_permission(self, db, query, filter: dict, permission: str = 'read'):
|
||||
return AccessGrants.has_permission_filter(
|
||||
db=db,
|
||||
query=query,
|
||||
DocumentModel=Note,
|
||||
filter=filter,
|
||||
resource_type="note",
|
||||
resource_type='note',
|
||||
permission=permission,
|
||||
)
|
||||
|
||||
def insert_new_note(
|
||||
self, user_id: str, form_data: NoteForm, db: Optional[Session] = None
|
||||
) -> Optional[NoteModel]:
|
||||
def insert_new_note(self, user_id: str, form_data: NoteForm, db: Optional[Session] = None) -> Optional[NoteModel]:
|
||||
with get_db_context(db) as db:
|
||||
note = NoteModel(
|
||||
**{
|
||||
"id": str(uuid.uuid4()),
|
||||
"user_id": user_id,
|
||||
**form_data.model_dump(exclude={"access_grants"}),
|
||||
"created_at": int(time.time_ns()),
|
||||
"updated_at": int(time.time_ns()),
|
||||
"access_grants": [],
|
||||
'id': str(uuid.uuid4()),
|
||||
'user_id': user_id,
|
||||
**form_data.model_dump(exclude={'access_grants'}),
|
||||
'created_at': int(time.time_ns()),
|
||||
'updated_at': int(time.time_ns()),
|
||||
'access_grants': [],
|
||||
}
|
||||
)
|
||||
|
||||
new_note = Note(**note.model_dump(exclude={"access_grants"}))
|
||||
new_note = Note(**note.model_dump(exclude={'access_grants'}))
|
||||
|
||||
db.add(new_note)
|
||||
db.commit()
|
||||
AccessGrants.set_access_grants(
|
||||
"note", note.id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('note', note.id, form_data.access_grants, db=db)
|
||||
return self._to_note_model(new_note, db=db)
|
||||
|
||||
def get_notes(
|
||||
self, skip: int = 0, limit: int = 50, db: Optional[Session] = None
|
||||
) -> list[NoteModel]:
|
||||
def get_notes(self, skip: int = 0, limit: int = 50, db: Optional[Session] = None) -> list[NoteModel]:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Note).order_by(Note.updated_at.desc())
|
||||
if skip is not None:
|
||||
@@ -152,13 +142,8 @@ class NoteTable:
|
||||
query = query.limit(limit)
|
||||
notes = query.all()
|
||||
note_ids = [note.id for note in notes]
|
||||
grants_map = AccessGrants.get_grants_by_resources("note", note_ids, db=db)
|
||||
return [
|
||||
self._to_note_model(
|
||||
note, access_grants=grants_map.get(note.id, []), db=db
|
||||
)
|
||||
for note in notes
|
||||
]
|
||||
grants_map = AccessGrants.get_grants_by_resources('note', note_ids, db=db)
|
||||
return [self._to_note_model(note, access_grants=grants_map.get(note.id, []), db=db) for note in notes]
|
||||
|
||||
def search_notes(
|
||||
self,
|
||||
@@ -171,36 +156,32 @@ class NoteTable:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Note, User).outerjoin(User, User.id == Note.user_id)
|
||||
if filter:
|
||||
query_key = filter.get("query")
|
||||
query_key = filter.get('query')
|
||||
if query_key:
|
||||
# Normalize search by removing hyphens and spaces (e.g., "todo" matches "to-do" and "to do")
|
||||
normalized_query = query_key.replace("-", "").replace(" ", "")
|
||||
normalized_query = query_key.replace('-', '').replace(' ', '')
|
||||
query = query.filter(
|
||||
or_(
|
||||
func.replace(func.replace(Note.title, '-', ''), ' ', '').ilike(f'%{normalized_query}%'),
|
||||
func.replace(
|
||||
func.replace(Note.title, "-", ""), " ", ""
|
||||
).ilike(f"%{normalized_query}%"),
|
||||
func.replace(
|
||||
func.replace(
|
||||
cast(Note.data["content"]["md"], Text), "-", ""
|
||||
),
|
||||
" ",
|
||||
"",
|
||||
).ilike(f"%{normalized_query}%"),
|
||||
func.replace(cast(Note.data['content']['md'], Text), '-', ''),
|
||||
' ',
|
||||
'',
|
||||
).ilike(f'%{normalized_query}%'),
|
||||
)
|
||||
)
|
||||
|
||||
view_option = filter.get("view_option")
|
||||
if view_option == "created":
|
||||
view_option = filter.get('view_option')
|
||||
if view_option == 'created':
|
||||
query = query.filter(Note.user_id == user_id)
|
||||
elif view_option == "shared":
|
||||
elif view_option == 'shared':
|
||||
query = query.filter(Note.user_id != user_id)
|
||||
|
||||
# Apply access control filtering
|
||||
if "permission" in filter:
|
||||
permission = filter["permission"]
|
||||
if 'permission' in filter:
|
||||
permission = filter['permission']
|
||||
else:
|
||||
permission = "write"
|
||||
permission = 'write'
|
||||
|
||||
query = self._has_permission(
|
||||
db,
|
||||
@@ -209,21 +190,21 @@ class NoteTable:
|
||||
permission=permission,
|
||||
)
|
||||
|
||||
order_by = filter.get("order_by")
|
||||
direction = filter.get("direction")
|
||||
order_by = filter.get('order_by')
|
||||
direction = filter.get('direction')
|
||||
|
||||
if order_by == "name":
|
||||
if direction == "asc":
|
||||
if order_by == 'name':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Note.title.asc())
|
||||
else:
|
||||
query = query.order_by(Note.title.desc())
|
||||
elif order_by == "created_at":
|
||||
if direction == "asc":
|
||||
elif order_by == 'created_at':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Note.created_at.asc())
|
||||
else:
|
||||
query = query.order_by(Note.created_at.desc())
|
||||
elif order_by == "updated_at":
|
||||
if direction == "asc":
|
||||
elif order_by == 'updated_at':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Note.updated_at.asc())
|
||||
else:
|
||||
query = query.order_by(Note.updated_at.desc())
|
||||
@@ -244,7 +225,7 @@ class NoteTable:
|
||||
items = query.all()
|
||||
|
||||
note_ids = [note.id for note, _ in items]
|
||||
grants_map = AccessGrants.get_grants_by_resources("note", note_ids, db=db)
|
||||
grants_map = AccessGrants.get_grants_by_resources('note', note_ids, db=db)
|
||||
|
||||
notes = []
|
||||
for note, user in items:
|
||||
@@ -255,11 +236,7 @@ class NoteTable:
|
||||
access_grants=grants_map.get(note.id, []),
|
||||
db=db,
|
||||
).model_dump(),
|
||||
user=(
|
||||
UserResponse(**UserModel.model_validate(user).model_dump())
|
||||
if user
|
||||
else None
|
||||
),
|
||||
user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -268,20 +245,16 @@ class NoteTable:
|
||||
def get_notes_by_user_id(
|
||||
self,
|
||||
user_id: str,
|
||||
permission: str = "read",
|
||||
permission: str = 'read',
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[NoteModel]:
|
||||
with get_db_context(db) as db:
|
||||
user_group_ids = [
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
]
|
||||
user_group_ids = [group.id for group in Groups.get_groups_by_member_id(user_id, db=db)]
|
||||
|
||||
query = db.query(Note).order_by(Note.updated_at.desc())
|
||||
query = self._has_permission(
|
||||
db, query, {"user_id": user_id, "group_ids": user_group_ids}, permission
|
||||
)
|
||||
query = self._has_permission(db, query, {'user_id': user_id, 'group_ids': user_group_ids}, permission)
|
||||
|
||||
if skip is not None:
|
||||
query = query.offset(skip)
|
||||
@@ -290,17 +263,10 @@ class NoteTable:
|
||||
|
||||
notes = query.all()
|
||||
note_ids = [note.id for note in notes]
|
||||
grants_map = AccessGrants.get_grants_by_resources("note", note_ids, db=db)
|
||||
return [
|
||||
self._to_note_model(
|
||||
note, access_grants=grants_map.get(note.id, []), db=db
|
||||
)
|
||||
for note in notes
|
||||
]
|
||||
grants_map = AccessGrants.get_grants_by_resources('note', note_ids, db=db)
|
||||
return [self._to_note_model(note, access_grants=grants_map.get(note.id, []), db=db) for note in notes]
|
||||
|
||||
def get_note_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[NoteModel]:
|
||||
def get_note_by_id(self, id: str, db: Optional[Session] = None) -> Optional[NoteModel]:
|
||||
with get_db_context(db) as db:
|
||||
note = db.query(Note).filter(Note.id == id).first()
|
||||
return self._to_note_model(note, db=db) if note else None
|
||||
@@ -315,17 +281,15 @@ class NoteTable:
|
||||
|
||||
form_data = form_data.model_dump(exclude_unset=True)
|
||||
|
||||
if "title" in form_data:
|
||||
note.title = form_data["title"]
|
||||
if "data" in form_data:
|
||||
note.data = {**note.data, **form_data["data"]}
|
||||
if "meta" in form_data:
|
||||
note.meta = {**note.meta, **form_data["meta"]}
|
||||
if 'title' in form_data:
|
||||
note.title = form_data['title']
|
||||
if 'data' in form_data:
|
||||
note.data = {**note.data, **form_data['data']}
|
||||
if 'meta' in form_data:
|
||||
note.meta = {**note.meta, **form_data['meta']}
|
||||
|
||||
if "access_grants" in form_data:
|
||||
AccessGrants.set_access_grants(
|
||||
"note", id, form_data["access_grants"], db=db
|
||||
)
|
||||
if 'access_grants' in form_data:
|
||||
AccessGrants.set_access_grants('note', id, form_data['access_grants'], db=db)
|
||||
|
||||
note.updated_at = int(time.time_ns())
|
||||
|
||||
@@ -335,7 +299,7 @@ class NoteTable:
|
||||
def delete_note_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
AccessGrants.revoke_all_access("note", id, db=db)
|
||||
AccessGrants.revoke_all_access('note', id, db=db)
|
||||
db.query(Note).filter(Note.id == id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@@ -23,23 +23,21 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OAuthSession(Base):
|
||||
__tablename__ = "oauth_session"
|
||||
__tablename__ = 'oauth_session'
|
||||
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
user_id = Column(Text, nullable=False)
|
||||
provider = Column(Text, nullable=False)
|
||||
token = Column(
|
||||
Text, nullable=False
|
||||
) # JSON with access_token, id_token, refresh_token
|
||||
token = Column(Text, nullable=False) # JSON with access_token, id_token, refresh_token
|
||||
expires_at = Column(BigInteger, nullable=False)
|
||||
created_at = Column(BigInteger, nullable=False)
|
||||
updated_at = Column(BigInteger, nullable=False)
|
||||
|
||||
# Add indexes for better performance
|
||||
__table_args__ = (
|
||||
Index("idx_oauth_session_user_id", "user_id"),
|
||||
Index("idx_oauth_session_expires_at", "expires_at"),
|
||||
Index("idx_oauth_session_user_provider", "user_id", "provider"),
|
||||
Index('idx_oauth_session_user_id', 'user_id'),
|
||||
Index('idx_oauth_session_expires_at', 'expires_at'),
|
||||
Index('idx_oauth_session_user_provider', 'user_id', 'provider'),
|
||||
)
|
||||
|
||||
|
||||
@@ -71,7 +69,7 @@ class OAuthSessionTable:
|
||||
def __init__(self):
|
||||
self.encryption_key = OAUTH_SESSION_TOKEN_ENCRYPTION_KEY
|
||||
if not self.encryption_key:
|
||||
raise Exception("OAUTH_SESSION_TOKEN_ENCRYPTION_KEY is not set")
|
||||
raise Exception('OAUTH_SESSION_TOKEN_ENCRYPTION_KEY is not set')
|
||||
|
||||
# check if encryption key is in the right format for Fernet (32 url-safe base64-encoded bytes)
|
||||
if len(self.encryption_key) != 44:
|
||||
@@ -83,7 +81,7 @@ class OAuthSessionTable:
|
||||
try:
|
||||
self.fernet = Fernet(self.encryption_key)
|
||||
except Exception as e:
|
||||
log.error(f"Error initializing Fernet with provided key: {e}")
|
||||
log.error(f'Error initializing Fernet with provided key: {e}')
|
||||
raise
|
||||
|
||||
def _encrypt_token(self, token) -> str:
|
||||
@@ -93,7 +91,7 @@ class OAuthSessionTable:
|
||||
encrypted = self.fernet.encrypt(token_json.encode()).decode()
|
||||
return encrypted
|
||||
except Exception as e:
|
||||
log.error(f"Error encrypting tokens: {e}")
|
||||
log.error(f'Error encrypting tokens: {e}')
|
||||
raise
|
||||
|
||||
def _decrypt_token(self, token: str):
|
||||
@@ -102,7 +100,7 @@ class OAuthSessionTable:
|
||||
decrypted = self.fernet.decrypt(token.encode()).decode()
|
||||
return json.loads(decrypted)
|
||||
except Exception as e:
|
||||
log.error(f"Error decrypting tokens: {type(e).__name__}: {e}")
|
||||
log.error(f'Error decrypting tokens: {type(e).__name__}: {e}')
|
||||
raise
|
||||
|
||||
def create_session(
|
||||
@@ -120,13 +118,13 @@ class OAuthSessionTable:
|
||||
|
||||
result = OAuthSession(
|
||||
**{
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
"provider": provider,
|
||||
"token": self._encrypt_token(token),
|
||||
"expires_at": token.get("expires_at"),
|
||||
"created_at": current_time,
|
||||
"updated_at": current_time,
|
||||
'id': id,
|
||||
'user_id': user_id,
|
||||
'provider': provider,
|
||||
'token': self._encrypt_token(token),
|
||||
'expires_at': token.get('expires_at'),
|
||||
'created_at': current_time,
|
||||
'updated_at': current_time,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -141,12 +139,10 @@ class OAuthSessionTable:
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.error(f"Error creating OAuth session: {e}")
|
||||
log.error(f'Error creating OAuth session: {e}')
|
||||
return None
|
||||
|
||||
def get_session_by_id(
|
||||
self, session_id: str, db: Optional[Session] = None
|
||||
) -> Optional[OAuthSessionModel]:
|
||||
def get_session_by_id(self, session_id: str, db: Optional[Session] = None) -> Optional[OAuthSessionModel]:
|
||||
"""Get OAuth session by ID"""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
@@ -158,7 +154,7 @@ class OAuthSessionTable:
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
log.error(f"Error getting OAuth session by ID: {e}")
|
||||
log.error(f'Error getting OAuth session by ID: {e}')
|
||||
return None
|
||||
|
||||
def get_session_by_id_and_user_id(
|
||||
@@ -167,11 +163,7 @@ class OAuthSessionTable:
|
||||
"""Get OAuth session by ID and user ID"""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
session = (
|
||||
db.query(OAuthSession)
|
||||
.filter_by(id=session_id, user_id=user_id)
|
||||
.first()
|
||||
)
|
||||
session = db.query(OAuthSession).filter_by(id=session_id, user_id=user_id).first()
|
||||
if session:
|
||||
db.expunge(session)
|
||||
session.token = self._decrypt_token(session.token)
|
||||
@@ -179,7 +171,7 @@ class OAuthSessionTable:
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
log.error(f"Error getting OAuth session by ID: {e}")
|
||||
log.error(f'Error getting OAuth session by ID: {e}')
|
||||
return None
|
||||
|
||||
def get_session_by_provider_and_user_id(
|
||||
@@ -201,12 +193,10 @@ class OAuthSessionTable:
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
log.error(f"Error getting OAuth session by provider and user ID: {e}")
|
||||
log.error(f'Error getting OAuth session by provider and user ID: {e}')
|
||||
return None
|
||||
|
||||
def get_sessions_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> List[OAuthSessionModel]:
|
||||
def get_sessions_by_user_id(self, user_id: str, db: Optional[Session] = None) -> List[OAuthSessionModel]:
|
||||
"""Get all OAuth sessions for a user"""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
@@ -220,7 +210,7 @@ class OAuthSessionTable:
|
||||
results.append(OAuthSessionModel.model_validate(session))
|
||||
except Exception as e:
|
||||
log.warning(
|
||||
f"Skipping OAuth session {session.id} due to decryption failure, deleting corrupted session: {type(e).__name__}: {e}"
|
||||
f'Skipping OAuth session {session.id} due to decryption failure, deleting corrupted session: {type(e).__name__}: {e}'
|
||||
)
|
||||
db.query(OAuthSession).filter_by(id=session.id).delete()
|
||||
db.commit()
|
||||
@@ -228,7 +218,7 @@ class OAuthSessionTable:
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error getting OAuth sessions by user ID: {e}")
|
||||
log.error(f'Error getting OAuth sessions by user ID: {e}')
|
||||
return []
|
||||
|
||||
def update_session_by_id(
|
||||
@@ -241,9 +231,9 @@ class OAuthSessionTable:
|
||||
|
||||
db.query(OAuthSession).filter_by(id=session_id).update(
|
||||
{
|
||||
"token": self._encrypt_token(token),
|
||||
"expires_at": token.get("expires_at"),
|
||||
"updated_at": current_time,
|
||||
'token': self._encrypt_token(token),
|
||||
'expires_at': token.get('expires_at'),
|
||||
'updated_at': current_time,
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
@@ -256,12 +246,10 @@ class OAuthSessionTable:
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
log.error(f"Error updating OAuth session tokens: {e}")
|
||||
log.error(f'Error updating OAuth session tokens: {e}')
|
||||
return None
|
||||
|
||||
def delete_session_by_id(
|
||||
self, session_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_session_by_id(self, session_id: str, db: Optional[Session] = None) -> bool:
|
||||
"""Delete an OAuth session"""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
@@ -269,12 +257,10 @@ class OAuthSessionTable:
|
||||
db.commit()
|
||||
return result > 0
|
||||
except Exception as e:
|
||||
log.error(f"Error deleting OAuth session: {e}")
|
||||
log.error(f'Error deleting OAuth session: {e}')
|
||||
return False
|
||||
|
||||
def delete_sessions_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_sessions_by_user_id(self, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
"""Delete all OAuth sessions for a user"""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
@@ -282,12 +268,10 @@ class OAuthSessionTable:
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"Error deleting OAuth sessions by user ID: {e}")
|
||||
log.error(f'Error deleting OAuth sessions by user ID: {e}')
|
||||
return False
|
||||
|
||||
def delete_sessions_by_provider(
|
||||
self, provider: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_sessions_by_provider(self, provider: str, db: Optional[Session] = None) -> bool:
|
||||
"""Delete all OAuth sessions for a provider"""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
@@ -295,7 +279,7 @@ class OAuthSessionTable:
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"Error deleting OAuth sessions by provider {provider}: {e}")
|
||||
log.error(f'Error deleting OAuth sessions by provider {provider}: {e}')
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from sqlalchemy import BigInteger, Column, Text, JSON, Index
|
||||
|
||||
|
||||
class PromptHistory(Base):
|
||||
__tablename__ = "prompt_history"
|
||||
__tablename__ = 'prompt_history'
|
||||
|
||||
id = Column(Text, primary_key=True)
|
||||
prompt_id = Column(Text, nullable=False, index=True)
|
||||
@@ -100,11 +100,7 @@ class PromptHistoryTable:
|
||||
return [
|
||||
PromptHistoryResponse(
|
||||
**PromptHistoryModel.model_validate(entry).model_dump(),
|
||||
user=(
|
||||
users_dict.get(entry.user_id).model_dump()
|
||||
if users_dict.get(entry.user_id)
|
||||
else None
|
||||
),
|
||||
user=(users_dict.get(entry.user_id).model_dump() if users_dict.get(entry.user_id) else None),
|
||||
)
|
||||
for entry in entries
|
||||
]
|
||||
@@ -116,9 +112,7 @@ class PromptHistoryTable:
|
||||
) -> Optional[PromptHistoryModel]:
|
||||
"""Get a specific history entry by ID."""
|
||||
with get_db_context(db) as db:
|
||||
entry = (
|
||||
db.query(PromptHistory).filter(PromptHistory.id == history_id).first()
|
||||
)
|
||||
entry = db.query(PromptHistory).filter(PromptHistory.id == history_id).first()
|
||||
if entry:
|
||||
return PromptHistoryModel.model_validate(entry)
|
||||
return None
|
||||
@@ -147,11 +141,7 @@ class PromptHistoryTable:
|
||||
) -> int:
|
||||
"""Get the number of history entries for a prompt."""
|
||||
with get_db_context(db) as db:
|
||||
return (
|
||||
db.query(PromptHistory)
|
||||
.filter(PromptHistory.prompt_id == prompt_id)
|
||||
.count()
|
||||
)
|
||||
return db.query(PromptHistory).filter(PromptHistory.prompt_id == prompt_id).count()
|
||||
|
||||
def compute_diff(
|
||||
self,
|
||||
@@ -161,9 +151,7 @@ class PromptHistoryTable:
|
||||
) -> Optional[dict]:
|
||||
"""Compute diff between two history entries."""
|
||||
with get_db_context(db) as db:
|
||||
from_entry = (
|
||||
db.query(PromptHistory).filter(PromptHistory.id == from_id).first()
|
||||
)
|
||||
from_entry = db.query(PromptHistory).filter(PromptHistory.id == from_id).first()
|
||||
to_entry = db.query(PromptHistory).filter(PromptHistory.id == to_id).first()
|
||||
|
||||
if not from_entry or not to_entry:
|
||||
@@ -173,26 +161,26 @@ class PromptHistoryTable:
|
||||
to_snapshot = to_entry.snapshot
|
||||
|
||||
# Compute diff for content field
|
||||
from_content = from_snapshot.get("content", "")
|
||||
to_content = to_snapshot.get("content", "")
|
||||
from_content = from_snapshot.get('content', '')
|
||||
to_content = to_snapshot.get('content', '')
|
||||
|
||||
diff_lines = list(
|
||||
difflib.unified_diff(
|
||||
from_content.splitlines(keepends=True),
|
||||
to_content.splitlines(keepends=True),
|
||||
fromfile=f"v{from_id[:8]}",
|
||||
tofile=f"v{to_id[:8]}",
|
||||
lineterm="",
|
||||
fromfile=f'v{from_id[:8]}',
|
||||
tofile=f'v{to_id[:8]}',
|
||||
lineterm='',
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"from_id": from_id,
|
||||
"to_id": to_id,
|
||||
"from_snapshot": from_snapshot,
|
||||
"to_snapshot": to_snapshot,
|
||||
"content_diff": diff_lines,
|
||||
"name_changed": from_snapshot.get("name") != to_snapshot.get("name"),
|
||||
'from_id': from_id,
|
||||
'to_id': to_id,
|
||||
'from_snapshot': from_snapshot,
|
||||
'to_snapshot': to_snapshot,
|
||||
'content_diff': diff_lines,
|
||||
'name_changed': from_snapshot.get('name') != to_snapshot.get('name'),
|
||||
}
|
||||
|
||||
def delete_history_by_prompt_id(
|
||||
@@ -202,9 +190,7 @@ class PromptHistoryTable:
|
||||
) -> bool:
|
||||
"""Delete all history entries for a prompt."""
|
||||
with get_db_context(db) as db:
|
||||
db.query(PromptHistory).filter(
|
||||
PromptHistory.prompt_id == prompt_id
|
||||
).delete()
|
||||
db.query(PromptHistory).filter(PromptHistory.prompt_id == prompt_id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, or_, fun
|
||||
|
||||
|
||||
class Prompt(Base):
|
||||
__tablename__ = "prompt"
|
||||
__tablename__ = 'prompt'
|
||||
|
||||
id = Column(Text, primary_key=True)
|
||||
command = Column(String, unique=True, index=True)
|
||||
@@ -77,7 +77,6 @@ class PromptAccessListResponse(BaseModel):
|
||||
|
||||
|
||||
class PromptForm(BaseModel):
|
||||
|
||||
command: str
|
||||
name: str # Changed from title
|
||||
content: str
|
||||
@@ -91,10 +90,8 @@ class PromptForm(BaseModel):
|
||||
|
||||
|
||||
class PromptsTable:
|
||||
def _get_access_grants(
|
||||
self, prompt_id: str, db: Optional[Session] = None
|
||||
) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource("prompt", prompt_id, db=db)
|
||||
def _get_access_grants(self, prompt_id: str, db: Optional[Session] = None) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource('prompt', prompt_id, db=db)
|
||||
|
||||
def _to_prompt_model(
|
||||
self,
|
||||
@@ -102,13 +99,9 @@ class PromptsTable:
|
||||
access_grants: Optional[list[AccessGrantModel]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> PromptModel:
|
||||
prompt_data = PromptModel.model_validate(prompt).model_dump(
|
||||
exclude={"access_grants"}
|
||||
)
|
||||
prompt_data["access_grants"] = (
|
||||
access_grants
|
||||
if access_grants is not None
|
||||
else self._get_access_grants(prompt_data["id"], db=db)
|
||||
prompt_data = PromptModel.model_validate(prompt).model_dump(exclude={'access_grants'})
|
||||
prompt_data['access_grants'] = (
|
||||
access_grants if access_grants is not None else self._get_access_grants(prompt_data['id'], db=db)
|
||||
)
|
||||
return PromptModel.model_validate(prompt_data)
|
||||
|
||||
@@ -135,26 +128,22 @@ class PromptsTable:
|
||||
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
result = Prompt(**prompt.model_dump(exclude={"access_grants"}))
|
||||
result = Prompt(**prompt.model_dump(exclude={'access_grants'}))
|
||||
db.add(result)
|
||||
db.commit()
|
||||
db.refresh(result)
|
||||
AccessGrants.set_access_grants(
|
||||
"prompt", prompt_id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('prompt', prompt_id, form_data.access_grants, db=db)
|
||||
|
||||
if result:
|
||||
current_access_grants = self._get_access_grants(prompt_id, db=db)
|
||||
snapshot = {
|
||||
"name": form_data.name,
|
||||
"content": form_data.content,
|
||||
"command": form_data.command,
|
||||
"data": form_data.data or {},
|
||||
"meta": form_data.meta or {},
|
||||
"tags": form_data.tags or [],
|
||||
"access_grants": [
|
||||
grant.model_dump() for grant in current_access_grants
|
||||
],
|
||||
'name': form_data.name,
|
||||
'content': form_data.content,
|
||||
'command': form_data.command,
|
||||
'data': form_data.data or {},
|
||||
'meta': form_data.meta or {},
|
||||
'tags': form_data.tags or [],
|
||||
'access_grants': [grant.model_dump() for grant in current_access_grants],
|
||||
}
|
||||
|
||||
history_entry = PromptHistories.create_history_entry(
|
||||
@@ -162,7 +151,7 @@ class PromptsTable:
|
||||
snapshot=snapshot,
|
||||
user_id=user_id,
|
||||
parent_id=None, # Initial commit has no parent
|
||||
commit_message=form_data.commit_message or "Initial version",
|
||||
commit_message=form_data.commit_message or 'Initial version',
|
||||
db=db,
|
||||
)
|
||||
|
||||
@@ -178,9 +167,7 @@ class PromptsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_prompt_by_id(
|
||||
self, prompt_id: str, db: Optional[Session] = None
|
||||
) -> Optional[PromptModel]:
|
||||
def get_prompt_by_id(self, prompt_id: str, db: Optional[Session] = None) -> Optional[PromptModel]:
|
||||
"""Get prompt by UUID."""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
@@ -191,9 +178,7 @@ class PromptsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_prompt_by_command(
|
||||
self, command: str, db: Optional[Session] = None
|
||||
) -> Optional[PromptModel]:
|
||||
def get_prompt_by_command(self, command: str, db: Optional[Session] = None) -> Optional[PromptModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
prompt = db.query(Prompt).filter_by(command=command).first()
|
||||
@@ -205,21 +190,14 @@ class PromptsTable:
|
||||
|
||||
def get_prompts(self, db: Optional[Session] = None) -> list[PromptUserResponse]:
|
||||
with get_db_context(db) as db:
|
||||
all_prompts = (
|
||||
db.query(Prompt)
|
||||
.filter(Prompt.is_active == True)
|
||||
.order_by(Prompt.updated_at.desc())
|
||||
.all()
|
||||
)
|
||||
all_prompts = db.query(Prompt).filter(Prompt.is_active == True).order_by(Prompt.updated_at.desc()).all()
|
||||
|
||||
user_ids = list(set(prompt.user_id for prompt in all_prompts))
|
||||
prompt_ids = [prompt.id for prompt in all_prompts]
|
||||
|
||||
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
|
||||
users_dict = {user.id: user for user in users}
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"prompt", prompt_ids, db=db
|
||||
)
|
||||
grants_map = AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=db)
|
||||
|
||||
prompts = []
|
||||
for prompt in all_prompts:
|
||||
@@ -232,7 +210,7 @@ class PromptsTable:
|
||||
access_grants=grants_map.get(prompt.id, []),
|
||||
db=db,
|
||||
).model_dump(),
|
||||
"user": user.model_dump() if user else None,
|
||||
'user': user.model_dump() if user else None,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -240,12 +218,10 @@ class PromptsTable:
|
||||
return prompts
|
||||
|
||||
def get_prompts_by_user_id(
|
||||
self, user_id: str, permission: str = "write", db: Optional[Session] = None
|
||||
self, user_id: str, permission: str = 'write', db: Optional[Session] = None
|
||||
) -> list[PromptUserResponse]:
|
||||
prompts = self.get_prompts(db=db)
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id, db=db)}
|
||||
|
||||
return [
|
||||
prompt
|
||||
@@ -253,7 +229,7 @@ class PromptsTable:
|
||||
if prompt.user_id == user_id
|
||||
or AccessGrants.has_access(
|
||||
user_id=user_id,
|
||||
resource_type="prompt",
|
||||
resource_type='prompt',
|
||||
resource_id=prompt.id,
|
||||
permission=permission,
|
||||
user_group_ids=user_group_ids,
|
||||
@@ -276,22 +252,22 @@ class PromptsTable:
|
||||
query = db.query(Prompt, User).outerjoin(User, User.id == Prompt.user_id)
|
||||
|
||||
if filter:
|
||||
query_key = filter.get("query")
|
||||
query_key = filter.get('query')
|
||||
if query_key:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Prompt.name.ilike(f"%{query_key}%"),
|
||||
Prompt.command.ilike(f"%{query_key}%"),
|
||||
Prompt.content.ilike(f"%{query_key}%"),
|
||||
User.name.ilike(f"%{query_key}%"),
|
||||
User.email.ilike(f"%{query_key}%"),
|
||||
Prompt.name.ilike(f'%{query_key}%'),
|
||||
Prompt.command.ilike(f'%{query_key}%'),
|
||||
Prompt.content.ilike(f'%{query_key}%'),
|
||||
User.name.ilike(f'%{query_key}%'),
|
||||
User.email.ilike(f'%{query_key}%'),
|
||||
)
|
||||
)
|
||||
|
||||
view_option = filter.get("view_option")
|
||||
if view_option == "created":
|
||||
view_option = filter.get('view_option')
|
||||
if view_option == 'created':
|
||||
query = query.filter(Prompt.user_id == user_id)
|
||||
elif view_option == "shared":
|
||||
elif view_option == 'shared':
|
||||
query = query.filter(Prompt.user_id != user_id)
|
||||
|
||||
# Apply access grant filtering
|
||||
@@ -300,32 +276,32 @@ class PromptsTable:
|
||||
query=query,
|
||||
DocumentModel=Prompt,
|
||||
filter=filter,
|
||||
resource_type="prompt",
|
||||
permission="read",
|
||||
resource_type='prompt',
|
||||
permission='read',
|
||||
)
|
||||
|
||||
tag = filter.get("tag")
|
||||
tag = filter.get('tag')
|
||||
if tag:
|
||||
# Search for tag in JSON array field
|
||||
like_pattern = f'%"{tag.lower()}"%'
|
||||
tags_text = func.lower(cast(Prompt.tags, String))
|
||||
query = query.filter(tags_text.like(like_pattern))
|
||||
|
||||
order_by = filter.get("order_by")
|
||||
direction = filter.get("direction")
|
||||
order_by = filter.get('order_by')
|
||||
direction = filter.get('direction')
|
||||
|
||||
if order_by == "name":
|
||||
if direction == "asc":
|
||||
if order_by == 'name':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Prompt.name.asc())
|
||||
else:
|
||||
query = query.order_by(Prompt.name.desc())
|
||||
elif order_by == "created_at":
|
||||
if direction == "asc":
|
||||
elif order_by == 'created_at':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Prompt.created_at.asc())
|
||||
else:
|
||||
query = query.order_by(Prompt.created_at.desc())
|
||||
elif order_by == "updated_at":
|
||||
if direction == "asc":
|
||||
elif order_by == 'updated_at':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(Prompt.updated_at.asc())
|
||||
else:
|
||||
query = query.order_by(Prompt.updated_at.desc())
|
||||
@@ -345,9 +321,7 @@ class PromptsTable:
|
||||
items = query.all()
|
||||
|
||||
prompt_ids = [prompt.id for prompt, _ in items]
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"prompt", prompt_ids, db=db
|
||||
)
|
||||
grants_map = AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=db)
|
||||
|
||||
prompts = []
|
||||
for prompt, user in items:
|
||||
@@ -358,11 +332,7 @@ class PromptsTable:
|
||||
access_grants=grants_map.get(prompt.id, []),
|
||||
db=db,
|
||||
).model_dump(),
|
||||
user=(
|
||||
UserResponse(**UserModel.model_validate(user).model_dump())
|
||||
if user
|
||||
else None
|
||||
),
|
||||
user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -381,9 +351,7 @@ class PromptsTable:
|
||||
if not prompt:
|
||||
return None
|
||||
|
||||
latest_history = PromptHistories.get_latest_history_entry(
|
||||
prompt.id, db=db
|
||||
)
|
||||
latest_history = PromptHistories.get_latest_history_entry(prompt.id, db=db)
|
||||
parent_id = latest_history.id if latest_history else None
|
||||
current_access_grants = self._get_access_grants(prompt.id, db=db)
|
||||
|
||||
@@ -401,9 +369,7 @@ class PromptsTable:
|
||||
prompt.meta = form_data.meta or prompt.meta
|
||||
prompt.updated_at = int(time.time())
|
||||
if form_data.access_grants is not None:
|
||||
AccessGrants.set_access_grants(
|
||||
"prompt", prompt.id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=db)
|
||||
current_access_grants = self._get_access_grants(prompt.id, db=db)
|
||||
|
||||
db.commit()
|
||||
@@ -411,14 +377,12 @@ class PromptsTable:
|
||||
# Create history entry only if content changed
|
||||
if content_changed:
|
||||
snapshot = {
|
||||
"name": form_data.name,
|
||||
"content": form_data.content,
|
||||
"command": command,
|
||||
"data": form_data.data or {},
|
||||
"meta": form_data.meta or {},
|
||||
"access_grants": [
|
||||
grant.model_dump() for grant in current_access_grants
|
||||
],
|
||||
'name': form_data.name,
|
||||
'content': form_data.content,
|
||||
'command': command,
|
||||
'data': form_data.data or {},
|
||||
'meta': form_data.meta or {},
|
||||
'access_grants': [grant.model_dump() for grant in current_access_grants],
|
||||
}
|
||||
|
||||
history_entry = PromptHistories.create_history_entry(
|
||||
@@ -452,9 +416,7 @@ class PromptsTable:
|
||||
if not prompt:
|
||||
return None
|
||||
|
||||
latest_history = PromptHistories.get_latest_history_entry(
|
||||
prompt.id, db=db
|
||||
)
|
||||
latest_history = PromptHistories.get_latest_history_entry(prompt.id, db=db)
|
||||
parent_id = latest_history.id if latest_history else None
|
||||
current_access_grants = self._get_access_grants(prompt.id, db=db)
|
||||
|
||||
@@ -478,9 +440,7 @@ class PromptsTable:
|
||||
prompt.tags = form_data.tags
|
||||
|
||||
if form_data.access_grants is not None:
|
||||
AccessGrants.set_access_grants(
|
||||
"prompt", prompt.id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=db)
|
||||
current_access_grants = self._get_access_grants(prompt.id, db=db)
|
||||
|
||||
prompt.updated_at = int(time.time())
|
||||
@@ -490,15 +450,13 @@ class PromptsTable:
|
||||
# Create history entry only if content changed
|
||||
if content_changed:
|
||||
snapshot = {
|
||||
"name": form_data.name,
|
||||
"content": form_data.content,
|
||||
"command": prompt.command,
|
||||
"data": form_data.data or {},
|
||||
"meta": form_data.meta or {},
|
||||
"tags": prompt.tags or [],
|
||||
"access_grants": [
|
||||
grant.model_dump() for grant in current_access_grants
|
||||
],
|
||||
'name': form_data.name,
|
||||
'content': form_data.content,
|
||||
'command': prompt.command,
|
||||
'data': form_data.data or {},
|
||||
'meta': form_data.meta or {},
|
||||
'tags': prompt.tags or [],
|
||||
'access_grants': [grant.model_dump() for grant in current_access_grants],
|
||||
}
|
||||
|
||||
history_entry = PromptHistories.create_history_entry(
|
||||
@@ -560,9 +518,7 @@ class PromptsTable:
|
||||
if not prompt:
|
||||
return None
|
||||
|
||||
history_entry = PromptHistories.get_history_entry_by_id(
|
||||
version_id, db=db
|
||||
)
|
||||
history_entry = PromptHistories.get_history_entry_by_id(version_id, db=db)
|
||||
|
||||
if not history_entry:
|
||||
return None
|
||||
@@ -570,11 +526,11 @@ class PromptsTable:
|
||||
# Restore prompt content from the snapshot
|
||||
snapshot = history_entry.snapshot
|
||||
if snapshot:
|
||||
prompt.name = snapshot.get("name", prompt.name)
|
||||
prompt.content = snapshot.get("content", prompt.content)
|
||||
prompt.data = snapshot.get("data", prompt.data)
|
||||
prompt.meta = snapshot.get("meta", prompt.meta)
|
||||
prompt.tags = snapshot.get("tags", prompt.tags)
|
||||
prompt.name = snapshot.get('name', prompt.name)
|
||||
prompt.content = snapshot.get('content', prompt.content)
|
||||
prompt.data = snapshot.get('data', prompt.data)
|
||||
prompt.meta = snapshot.get('meta', prompt.meta)
|
||||
prompt.tags = snapshot.get('tags', prompt.tags)
|
||||
# Note: command and access_grants are not restored from snapshot
|
||||
|
||||
prompt.version_id = version_id
|
||||
@@ -585,9 +541,7 @@ class PromptsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def toggle_prompt_active(
|
||||
self, prompt_id: str, db: Optional[Session] = None
|
||||
) -> Optional[PromptModel]:
|
||||
def toggle_prompt_active(self, prompt_id: str, db: Optional[Session] = None) -> Optional[PromptModel]:
|
||||
"""Toggle the is_active flag on a prompt."""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
@@ -602,16 +556,14 @@ class PromptsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_prompt_by_command(
|
||||
self, command: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_prompt_by_command(self, command: str, db: Optional[Session] = None) -> bool:
|
||||
"""Permanently delete a prompt and its history."""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
prompt = db.query(Prompt).filter_by(command=command).first()
|
||||
if prompt:
|
||||
PromptHistories.delete_history_by_prompt_id(prompt.id, db=db)
|
||||
AccessGrants.revoke_all_access("prompt", prompt.id, db=db)
|
||||
AccessGrants.revoke_all_access('prompt', prompt.id, db=db)
|
||||
|
||||
db.delete(prompt)
|
||||
db.commit()
|
||||
@@ -627,7 +579,7 @@ class PromptsTable:
|
||||
prompt = db.query(Prompt).filter_by(id=prompt_id).first()
|
||||
if prompt:
|
||||
PromptHistories.delete_history_by_prompt_id(prompt.id, db=db)
|
||||
AccessGrants.revoke_all_access("prompt", prompt.id, db=db)
|
||||
AccessGrants.revoke_all_access('prompt', prompt.id, db=db)
|
||||
|
||||
db.delete(prompt)
|
||||
db.commit()
|
||||
|
||||
@@ -19,7 +19,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Skill(Base):
|
||||
__tablename__ = "skill"
|
||||
__tablename__ = 'skill'
|
||||
|
||||
id = Column(String, primary_key=True, unique=True)
|
||||
user_id = Column(String)
|
||||
@@ -77,7 +77,7 @@ class SkillResponse(BaseModel):
|
||||
class SkillUserResponse(SkillResponse):
|
||||
user: Optional[UserResponse] = None
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
class SkillAccessResponse(SkillUserResponse):
|
||||
@@ -105,10 +105,8 @@ class SkillAccessListResponse(BaseModel):
|
||||
|
||||
|
||||
class SkillsTable:
|
||||
def _get_access_grants(
|
||||
self, skill_id: str, db: Optional[Session] = None
|
||||
) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource("skill", skill_id, db=db)
|
||||
def _get_access_grants(self, skill_id: str, db: Optional[Session] = None) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource('skill', skill_id, db=db)
|
||||
|
||||
def _to_skill_model(
|
||||
self,
|
||||
@@ -116,13 +114,9 @@ class SkillsTable:
|
||||
access_grants: Optional[list[AccessGrantModel]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> SkillModel:
|
||||
skill_data = SkillModel.model_validate(skill).model_dump(
|
||||
exclude={"access_grants"}
|
||||
)
|
||||
skill_data["access_grants"] = (
|
||||
access_grants
|
||||
if access_grants is not None
|
||||
else self._get_access_grants(skill_data["id"], db=db)
|
||||
skill_data = SkillModel.model_validate(skill).model_dump(exclude={'access_grants'})
|
||||
skill_data['access_grants'] = (
|
||||
access_grants if access_grants is not None else self._get_access_grants(skill_data['id'], db=db)
|
||||
)
|
||||
return SkillModel.model_validate(skill_data)
|
||||
|
||||
@@ -136,29 +130,25 @@ class SkillsTable:
|
||||
try:
|
||||
result = Skill(
|
||||
**{
|
||||
**form_data.model_dump(exclude={"access_grants"}),
|
||||
"user_id": user_id,
|
||||
"updated_at": int(time.time()),
|
||||
"created_at": int(time.time()),
|
||||
**form_data.model_dump(exclude={'access_grants'}),
|
||||
'user_id': user_id,
|
||||
'updated_at': int(time.time()),
|
||||
'created_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.add(result)
|
||||
db.commit()
|
||||
db.refresh(result)
|
||||
AccessGrants.set_access_grants(
|
||||
"skill", result.id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('skill', result.id, form_data.access_grants, db=db)
|
||||
if result:
|
||||
return self._to_skill_model(result, db=db)
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.exception(f"Error creating a new skill: {e}")
|
||||
log.exception(f'Error creating a new skill: {e}')
|
||||
return None
|
||||
|
||||
def get_skill_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[SkillModel]:
|
||||
def get_skill_by_id(self, id: str, db: Optional[Session] = None) -> Optional[SkillModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
skill = db.get(Skill, id)
|
||||
@@ -166,9 +156,7 @@ class SkillsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_skill_by_name(
|
||||
self, name: str, db: Optional[Session] = None
|
||||
) -> Optional[SkillModel]:
|
||||
def get_skill_by_name(self, name: str, db: Optional[Session] = None) -> Optional[SkillModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
skill = db.query(Skill).filter_by(name=name).first()
|
||||
@@ -185,7 +173,7 @@ class SkillsTable:
|
||||
|
||||
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
|
||||
users_dict = {user.id: user for user in users}
|
||||
grants_map = AccessGrants.get_grants_by_resources("skill", skill_ids, db=db)
|
||||
grants_map = AccessGrants.get_grants_by_resources('skill', skill_ids, db=db)
|
||||
|
||||
skills = []
|
||||
for skill in all_skills:
|
||||
@@ -198,19 +186,17 @@ class SkillsTable:
|
||||
access_grants=grants_map.get(skill.id, []),
|
||||
db=db,
|
||||
).model_dump(),
|
||||
"user": user.model_dump() if user else None,
|
||||
'user': user.model_dump() if user else None,
|
||||
}
|
||||
)
|
||||
)
|
||||
return skills
|
||||
|
||||
def get_skills_by_user_id(
|
||||
self, user_id: str, permission: str = "write", db: Optional[Session] = None
|
||||
self, user_id: str, permission: str = 'write', db: Optional[Session] = None
|
||||
) -> list[SkillUserModel]:
|
||||
skills = self.get_skills(db=db)
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id, db=db)}
|
||||
|
||||
return [
|
||||
skill
|
||||
@@ -218,7 +204,7 @@ class SkillsTable:
|
||||
if skill.user_id == user_id
|
||||
or AccessGrants.has_access(
|
||||
user_id=user_id,
|
||||
resource_type="skill",
|
||||
resource_type='skill',
|
||||
resource_id=skill.id,
|
||||
permission=permission,
|
||||
user_group_ids=user_group_ids,
|
||||
@@ -242,22 +228,22 @@ class SkillsTable:
|
||||
query = db.query(Skill, User).outerjoin(User, User.id == Skill.user_id)
|
||||
|
||||
if filter:
|
||||
query_key = filter.get("query")
|
||||
query_key = filter.get('query')
|
||||
if query_key:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Skill.name.ilike(f"%{query_key}%"),
|
||||
Skill.description.ilike(f"%{query_key}%"),
|
||||
Skill.id.ilike(f"%{query_key}%"),
|
||||
User.name.ilike(f"%{query_key}%"),
|
||||
User.email.ilike(f"%{query_key}%"),
|
||||
Skill.name.ilike(f'%{query_key}%'),
|
||||
Skill.description.ilike(f'%{query_key}%'),
|
||||
Skill.id.ilike(f'%{query_key}%'),
|
||||
User.name.ilike(f'%{query_key}%'),
|
||||
User.email.ilike(f'%{query_key}%'),
|
||||
)
|
||||
)
|
||||
|
||||
view_option = filter.get("view_option")
|
||||
if view_option == "created":
|
||||
view_option = filter.get('view_option')
|
||||
if view_option == 'created':
|
||||
query = query.filter(Skill.user_id == user_id)
|
||||
elif view_option == "shared":
|
||||
elif view_option == 'shared':
|
||||
query = query.filter(Skill.user_id != user_id)
|
||||
|
||||
# Apply access grant filtering
|
||||
@@ -266,8 +252,8 @@ class SkillsTable:
|
||||
query=query,
|
||||
DocumentModel=Skill,
|
||||
filter=filter,
|
||||
resource_type="skill",
|
||||
permission="read",
|
||||
resource_type='skill',
|
||||
permission='read',
|
||||
)
|
||||
|
||||
query = query.order_by(Skill.updated_at.desc())
|
||||
@@ -283,9 +269,7 @@ class SkillsTable:
|
||||
items = query.all()
|
||||
|
||||
skill_ids = [skill.id for skill, _ in items]
|
||||
grants_map = AccessGrants.get_grants_by_resources(
|
||||
"skill", skill_ids, db=db
|
||||
)
|
||||
grants_map = AccessGrants.get_grants_by_resources('skill', skill_ids, db=db)
|
||||
|
||||
skills = []
|
||||
for skill, user in items:
|
||||
@@ -296,33 +280,23 @@ class SkillsTable:
|
||||
access_grants=grants_map.get(skill.id, []),
|
||||
db=db,
|
||||
).model_dump(),
|
||||
user=(
|
||||
UserResponse(
|
||||
**UserModel.model_validate(user).model_dump()
|
||||
)
|
||||
if user
|
||||
else None
|
||||
),
|
||||
user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None),
|
||||
)
|
||||
)
|
||||
|
||||
return SkillListResponse(items=skills, total=total)
|
||||
except Exception as e:
|
||||
log.exception(f"Error searching skills: {e}")
|
||||
log.exception(f'Error searching skills: {e}')
|
||||
return SkillListResponse(items=[], total=0)
|
||||
|
||||
def update_skill_by_id(
|
||||
self, id: str, updated: dict, db: Optional[Session] = None
|
||||
) -> Optional[SkillModel]:
|
||||
def update_skill_by_id(self, id: str, updated: dict, db: Optional[Session] = None) -> Optional[SkillModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
access_grants = updated.pop("access_grants", None)
|
||||
db.query(Skill).filter_by(id=id).update(
|
||||
{**updated, "updated_at": int(time.time())}
|
||||
)
|
||||
access_grants = updated.pop('access_grants', None)
|
||||
db.query(Skill).filter_by(id=id).update({**updated, 'updated_at': int(time.time())})
|
||||
db.commit()
|
||||
if access_grants is not None:
|
||||
AccessGrants.set_access_grants("skill", id, access_grants, db=db)
|
||||
AccessGrants.set_access_grants('skill', id, access_grants, db=db)
|
||||
|
||||
skill = db.query(Skill).get(id)
|
||||
db.refresh(skill)
|
||||
@@ -330,9 +304,7 @@ class SkillsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def toggle_skill_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[SkillModel]:
|
||||
def toggle_skill_by_id(self, id: str, db: Optional[Session] = None) -> Optional[SkillModel]:
|
||||
with get_db_context(db) as db:
|
||||
try:
|
||||
skill = db.query(Skill).filter_by(id=id).first()
|
||||
@@ -351,7 +323,7 @@ class SkillsTable:
|
||||
def delete_skill_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
AccessGrants.revoke_all_access("skill", id, db=db)
|
||||
AccessGrants.revoke_all_access('skill', id, db=db)
|
||||
db.query(Skill).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
@@ -17,19 +17,19 @@ log = logging.getLogger(__name__)
|
||||
# Tag DB Schema
|
||||
####################
|
||||
class Tag(Base):
|
||||
__tablename__ = "tag"
|
||||
__tablename__ = 'tag'
|
||||
id = Column(String)
|
||||
name = Column(String)
|
||||
user_id = Column(String)
|
||||
meta = Column(JSON, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", "user_id", name="pk_id_user_id"),
|
||||
Index("user_id_idx", "user_id"),
|
||||
PrimaryKeyConstraint('id', 'user_id', name='pk_id_user_id'),
|
||||
Index('user_id_idx', 'user_id'),
|
||||
)
|
||||
|
||||
# Unique constraint ensuring (id, user_id) is unique, not just the `id` column
|
||||
__table_args__ = (PrimaryKeyConstraint("id", "user_id", name="pk_id_user_id"),)
|
||||
__table_args__ = (PrimaryKeyConstraint('id', 'user_id', name='pk_id_user_id'),)
|
||||
|
||||
|
||||
class TagModel(BaseModel):
|
||||
@@ -51,12 +51,10 @@ class TagChatIdForm(BaseModel):
|
||||
|
||||
|
||||
class TagTable:
|
||||
def insert_new_tag(
|
||||
self, name: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[TagModel]:
|
||||
def insert_new_tag(self, name: str, user_id: str, db: Optional[Session] = None) -> Optional[TagModel]:
|
||||
with get_db_context(db) as db:
|
||||
id = name.replace(" ", "_").lower()
|
||||
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
|
||||
id = name.replace(' ', '_').lower()
|
||||
tag = TagModel(**{'id': id, 'user_id': user_id, 'name': name})
|
||||
try:
|
||||
result = Tag(**tag.model_dump())
|
||||
db.add(result)
|
||||
@@ -67,89 +65,63 @@ class TagTable:
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.exception(f"Error inserting a new tag: {e}")
|
||||
log.exception(f'Error inserting a new tag: {e}')
|
||||
return None
|
||||
|
||||
def get_tag_by_name_and_user_id(
|
||||
self, name: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[TagModel]:
|
||||
def get_tag_by_name_and_user_id(self, name: str, user_id: str, db: Optional[Session] = None) -> Optional[TagModel]:
|
||||
try:
|
||||
id = name.replace(" ", "_").lower()
|
||||
id = name.replace(' ', '_').lower()
|
||||
with get_db_context(db) as db:
|
||||
tag = db.query(Tag).filter_by(id=id, user_id=user_id).first()
|
||||
return TagModel.model_validate(tag)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_tags_by_user_id(
|
||||
self, user_id: str, db: Optional[Session] = None
|
||||
) -> list[TagModel]:
|
||||
def get_tags_by_user_id(self, user_id: str, db: Optional[Session] = None) -> list[TagModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [TagModel.model_validate(tag) for tag in (db.query(Tag).filter_by(user_id=user_id).all())]
|
||||
|
||||
def get_tags_by_ids_and_user_id(self, ids: list[str], user_id: str, db: Optional[Session] = None) -> list[TagModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
TagModel.model_validate(tag)
|
||||
for tag in (db.query(Tag).filter_by(user_id=user_id).all())
|
||||
for tag in (db.query(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id).all())
|
||||
]
|
||||
|
||||
def get_tags_by_ids_and_user_id(
|
||||
self, ids: list[str], user_id: str, db: Optional[Session] = None
|
||||
) -> list[TagModel]:
|
||||
with get_db_context(db) as db:
|
||||
return [
|
||||
TagModel.model_validate(tag)
|
||||
for tag in (
|
||||
db.query(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id).all()
|
||||
)
|
||||
]
|
||||
|
||||
def delete_tag_by_name_and_user_id(
|
||||
self, name: str, user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_tag_by_name_and_user_id(self, name: str, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
id = name.replace(" ", "_").lower()
|
||||
id = name.replace(' ', '_').lower()
|
||||
res = db.query(Tag).filter_by(id=id, user_id=user_id).delete()
|
||||
log.debug(f"res: {res}")
|
||||
log.debug(f'res: {res}')
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"delete_tag: {e}")
|
||||
log.error(f'delete_tag: {e}')
|
||||
return False
|
||||
|
||||
def delete_tags_by_ids_and_user_id(
|
||||
self, ids: list[str], user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def delete_tags_by_ids_and_user_id(self, ids: list[str], user_id: str, db: Optional[Session] = None) -> bool:
|
||||
"""Delete all tags whose id is in *ids* for the given user, in one query."""
|
||||
if not ids:
|
||||
return True
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.query(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"delete_tags_by_ids: {e}")
|
||||
log.error(f'delete_tags_by_ids: {e}')
|
||||
return False
|
||||
|
||||
def ensure_tags_exist(
|
||||
self, names: list[str], user_id: str, db: Optional[Session] = None
|
||||
) -> None:
|
||||
def ensure_tags_exist(self, names: list[str], user_id: str, db: Optional[Session] = None) -> None:
|
||||
"""Create tag rows for any *names* that don't already exist for *user_id*."""
|
||||
if not names:
|
||||
return
|
||||
ids = [n.replace(" ", "_").lower() for n in names]
|
||||
ids = [n.replace(' ', '_').lower() for n in names]
|
||||
with get_db_context(db) as db:
|
||||
existing = {
|
||||
t.id
|
||||
for t in db.query(Tag.id)
|
||||
.filter(Tag.id.in_(ids), Tag.user_id == user_id)
|
||||
.all()
|
||||
}
|
||||
existing = {t.id for t in db.query(Tag.id).filter(Tag.id.in_(ids), Tag.user_id == user_id).all()}
|
||||
new_tags = [
|
||||
Tag(id=tag_id, name=name, user_id=user_id)
|
||||
for tag_id, name in zip(ids, names)
|
||||
if tag_id not in existing
|
||||
Tag(id=tag_id, name=name, user_id=user_id) for tag_id, name in zip(ids, names) if tag_id not in existing
|
||||
]
|
||||
if new_tags:
|
||||
db.add_all(new_tags)
|
||||
|
||||
@@ -19,7 +19,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Tool(Base):
|
||||
__tablename__ = "tool"
|
||||
__tablename__ = 'tool'
|
||||
|
||||
id = Column(String, primary_key=True, unique=True)
|
||||
user_id = Column(String)
|
||||
@@ -75,7 +75,7 @@ class ToolResponse(BaseModel):
|
||||
class ToolUserResponse(ToolResponse):
|
||||
user: Optional[UserResponse] = None
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
class ToolAccessResponse(ToolUserResponse):
|
||||
@@ -95,10 +95,8 @@ class ToolValves(BaseModel):
|
||||
|
||||
|
||||
class ToolsTable:
|
||||
def _get_access_grants(
|
||||
self, tool_id: str, db: Optional[Session] = None
|
||||
) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource("tool", tool_id, db=db)
|
||||
def _get_access_grants(self, tool_id: str, db: Optional[Session] = None) -> list[AccessGrantModel]:
|
||||
return AccessGrants.get_grants_by_resource('tool', tool_id, db=db)
|
||||
|
||||
def _to_tool_model(
|
||||
self,
|
||||
@@ -106,11 +104,9 @@ class ToolsTable:
|
||||
access_grants: Optional[list[AccessGrantModel]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> ToolModel:
|
||||
tool_data = ToolModel.model_validate(tool).model_dump(exclude={"access_grants"})
|
||||
tool_data["access_grants"] = (
|
||||
access_grants
|
||||
if access_grants is not None
|
||||
else self._get_access_grants(tool_data["id"], db=db)
|
||||
tool_data = ToolModel.model_validate(tool).model_dump(exclude={'access_grants'})
|
||||
tool_data['access_grants'] = (
|
||||
access_grants if access_grants is not None else self._get_access_grants(tool_data['id'], db=db)
|
||||
)
|
||||
return ToolModel.model_validate(tool_data)
|
||||
|
||||
@@ -125,30 +121,26 @@ class ToolsTable:
|
||||
try:
|
||||
result = Tool(
|
||||
**{
|
||||
**form_data.model_dump(exclude={"access_grants"}),
|
||||
"specs": specs,
|
||||
"user_id": user_id,
|
||||
"updated_at": int(time.time()),
|
||||
"created_at": int(time.time()),
|
||||
**form_data.model_dump(exclude={'access_grants'}),
|
||||
'specs': specs,
|
||||
'user_id': user_id,
|
||||
'updated_at': int(time.time()),
|
||||
'created_at': int(time.time()),
|
||||
}
|
||||
)
|
||||
db.add(result)
|
||||
db.commit()
|
||||
db.refresh(result)
|
||||
AccessGrants.set_access_grants(
|
||||
"tool", result.id, form_data.access_grants, db=db
|
||||
)
|
||||
AccessGrants.set_access_grants('tool', result.id, form_data.access_grants, db=db)
|
||||
if result:
|
||||
return self._to_tool_model(result, db=db)
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.exception(f"Error creating a new tool: {e}")
|
||||
log.exception(f'Error creating a new tool: {e}')
|
||||
return None
|
||||
|
||||
def get_tool_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[ToolModel]:
|
||||
def get_tool_by_id(self, id: str, db: Optional[Session] = None) -> Optional[ToolModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
tool = db.get(Tool, id)
|
||||
@@ -156,9 +148,7 @@ class ToolsTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_tools(
|
||||
self, defer_content: bool = False, db: Optional[Session] = None
|
||||
) -> list[ToolUserModel]:
|
||||
def get_tools(self, defer_content: bool = False, db: Optional[Session] = None) -> list[ToolUserModel]:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Tool).order_by(Tool.updated_at.desc())
|
||||
if defer_content:
|
||||
@@ -170,7 +160,7 @@ class ToolsTable:
|
||||
|
||||
users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else []
|
||||
users_dict = {user.id: user for user in users}
|
||||
grants_map = AccessGrants.get_grants_by_resources("tool", tool_ids, db=db)
|
||||
grants_map = AccessGrants.get_grants_by_resources('tool', tool_ids, db=db)
|
||||
|
||||
tools = []
|
||||
for tool in all_tools:
|
||||
@@ -183,7 +173,7 @@ class ToolsTable:
|
||||
access_grants=grants_map.get(tool.id, []),
|
||||
db=db,
|
||||
).model_dump(),
|
||||
"user": user.model_dump() if user else None,
|
||||
'user': user.model_dump() if user else None,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -192,14 +182,12 @@ class ToolsTable:
|
||||
def get_tools_by_user_id(
|
||||
self,
|
||||
user_id: str,
|
||||
permission: str = "write",
|
||||
permission: str = 'write',
|
||||
defer_content: bool = False,
|
||||
db: Optional[Session] = None,
|
||||
) -> list[ToolUserModel]:
|
||||
tools = self.get_tools(defer_content=defer_content, db=db)
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user_id, db=db)
|
||||
}
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id, db=db)}
|
||||
|
||||
return [
|
||||
tool
|
||||
@@ -207,7 +195,7 @@ class ToolsTable:
|
||||
if tool.user_id == user_id
|
||||
or AccessGrants.has_access(
|
||||
user_id=user_id,
|
||||
resource_type="tool",
|
||||
resource_type='tool',
|
||||
resource_id=tool.id,
|
||||
permission=permission,
|
||||
user_group_ids=user_group_ids,
|
||||
@@ -215,48 +203,38 @@ class ToolsTable:
|
||||
)
|
||||
]
|
||||
|
||||
def get_tool_valves_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[dict]:
|
||||
def get_tool_valves_by_id(self, id: str, db: Optional[Session] = None) -> Optional[dict]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
tool = db.get(Tool, id)
|
||||
return tool.valves if tool.valves else {}
|
||||
except Exception as e:
|
||||
log.exception(f"Error getting tool valves by id {id}")
|
||||
log.exception(f'Error getting tool valves by id {id}')
|
||||
return None
|
||||
|
||||
def update_tool_valves_by_id(
|
||||
self, id: str, valves: dict, db: Optional[Session] = None
|
||||
) -> Optional[ToolValves]:
|
||||
def update_tool_valves_by_id(self, id: str, valves: dict, db: Optional[Session] = None) -> Optional[ToolValves]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Tool).filter_by(id=id).update(
|
||||
{"valves": valves, "updated_at": int(time.time())}
|
||||
)
|
||||
db.query(Tool).filter_by(id=id).update({'valves': valves, 'updated_at': int(time.time())})
|
||||
db.commit()
|
||||
return self.get_tool_by_id(id, db=db)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_valves_by_id_and_user_id(
|
||||
self, id: str, user_id: str, db: Optional[Session] = None
|
||||
) -> Optional[dict]:
|
||||
def get_user_valves_by_id_and_user_id(self, id: str, user_id: str, db: Optional[Session] = None) -> Optional[dict]:
|
||||
try:
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
user_settings = user.settings.model_dump() if user.settings else {}
|
||||
|
||||
# Check if user has "tools" and "valves" settings
|
||||
if "tools" not in user_settings:
|
||||
user_settings["tools"] = {}
|
||||
if "valves" not in user_settings["tools"]:
|
||||
user_settings["tools"]["valves"] = {}
|
||||
if 'tools' not in user_settings:
|
||||
user_settings['tools'] = {}
|
||||
if 'valves' not in user_settings['tools']:
|
||||
user_settings['tools']['valves'] = {}
|
||||
|
||||
return user_settings["tools"]["valves"].get(id, {})
|
||||
return user_settings['tools']['valves'].get(id, {})
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
f"Error getting user values by id {id} and user_id {user_id}: {e}"
|
||||
)
|
||||
log.exception(f'Error getting user values by id {id} and user_id {user_id}: {e}')
|
||||
return None
|
||||
|
||||
def update_user_valves_by_id_and_user_id(
|
||||
@@ -267,35 +245,29 @@ class ToolsTable:
|
||||
user_settings = user.settings.model_dump() if user.settings else {}
|
||||
|
||||
# Check if user has "tools" and "valves" settings
|
||||
if "tools" not in user_settings:
|
||||
user_settings["tools"] = {}
|
||||
if "valves" not in user_settings["tools"]:
|
||||
user_settings["tools"]["valves"] = {}
|
||||
if 'tools' not in user_settings:
|
||||
user_settings['tools'] = {}
|
||||
if 'valves' not in user_settings['tools']:
|
||||
user_settings['tools']['valves'] = {}
|
||||
|
||||
user_settings["tools"]["valves"][id] = valves
|
||||
user_settings['tools']['valves'][id] = valves
|
||||
|
||||
# Update the user settings in the database
|
||||
Users.update_user_by_id(user_id, {"settings": user_settings}, db=db)
|
||||
Users.update_user_by_id(user_id, {'settings': user_settings}, db=db)
|
||||
|
||||
return user_settings["tools"]["valves"][id]
|
||||
return user_settings['tools']['valves'][id]
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
f"Error updating user valves by id {id} and user_id {user_id}: {e}"
|
||||
)
|
||||
log.exception(f'Error updating user valves by id {id} and user_id {user_id}: {e}')
|
||||
return None
|
||||
|
||||
def update_tool_by_id(
|
||||
self, id: str, updated: dict, db: Optional[Session] = None
|
||||
) -> Optional[ToolModel]:
|
||||
def update_tool_by_id(self, id: str, updated: dict, db: Optional[Session] = None) -> Optional[ToolModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
access_grants = updated.pop("access_grants", None)
|
||||
db.query(Tool).filter_by(id=id).update(
|
||||
{**updated, "updated_at": int(time.time())}
|
||||
)
|
||||
access_grants = updated.pop('access_grants', None)
|
||||
db.query(Tool).filter_by(id=id).update({**updated, 'updated_at': int(time.time())})
|
||||
db.commit()
|
||||
if access_grants is not None:
|
||||
AccessGrants.set_access_grants("tool", id, access_grants, db=db)
|
||||
AccessGrants.set_access_grants('tool', id, access_grants, db=db)
|
||||
|
||||
tool = db.query(Tool).get(id)
|
||||
db.refresh(tool)
|
||||
@@ -306,7 +278,7 @@ class ToolsTable:
|
||||
def delete_tool_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
AccessGrants.revoke_all_access("tool", id, db=db)
|
||||
AccessGrants.revoke_all_access('tool', id, db=db)
|
||||
db.query(Tool).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
@@ -40,12 +40,12 @@ import datetime
|
||||
|
||||
class UserSettings(BaseModel):
|
||||
ui: Optional[dict] = {}
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
pass
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "user"
|
||||
__tablename__ = 'user'
|
||||
|
||||
id = Column(String, primary_key=True, unique=True)
|
||||
email = Column(String)
|
||||
@@ -83,7 +83,7 @@ class UserModel(BaseModel):
|
||||
|
||||
email: str
|
||||
username: Optional[str] = None
|
||||
role: str = "pending"
|
||||
role: str = 'pending'
|
||||
|
||||
name: str
|
||||
|
||||
@@ -112,10 +112,10 @@ class UserModel(BaseModel):
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@model_validator(mode="after")
|
||||
@model_validator(mode='after')
|
||||
def set_profile_image_url(self):
|
||||
if not self.profile_image_url:
|
||||
self.profile_image_url = f"/api/v1/users/{self.id}/profile/image"
|
||||
self.profile_image_url = f'/api/v1/users/{self.id}/profile/image'
|
||||
return self
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ class UserStatusModel(UserModel):
|
||||
|
||||
|
||||
class ApiKey(Base):
|
||||
__tablename__ = "api_key"
|
||||
__tablename__ = 'api_key'
|
||||
|
||||
id = Column(Text, primary_key=True, unique=True)
|
||||
user_id = Column(Text, nullable=False)
|
||||
@@ -163,7 +163,7 @@ class UpdateProfileForm(BaseModel):
|
||||
gender: Optional[str] = None
|
||||
date_of_birth: Optional[datetime.date] = None
|
||||
|
||||
@field_validator("profile_image_url")
|
||||
@field_validator('profile_image_url')
|
||||
@classmethod
|
||||
def check_profile_image_url(cls, v: str) -> str:
|
||||
return validate_profile_image_url(v)
|
||||
@@ -174,7 +174,7 @@ class UserGroupIdsModel(UserModel):
|
||||
|
||||
|
||||
class UserModelResponse(UserModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
class UserListResponse(BaseModel):
|
||||
@@ -251,7 +251,7 @@ class UserUpdateForm(BaseModel):
|
||||
profile_image_url: str
|
||||
password: Optional[str] = None
|
||||
|
||||
@field_validator("profile_image_url")
|
||||
@field_validator('profile_image_url')
|
||||
@classmethod
|
||||
def check_profile_image_url(cls, v: str) -> str:
|
||||
return validate_profile_image_url(v)
|
||||
@@ -263,8 +263,8 @@ class UsersTable:
|
||||
id: str,
|
||||
name: str,
|
||||
email: str,
|
||||
profile_image_url: str = "/user.png",
|
||||
role: str = "pending",
|
||||
profile_image_url: str = '/user.png',
|
||||
role: str = 'pending',
|
||||
username: Optional[str] = None,
|
||||
oauth: Optional[dict] = None,
|
||||
db: Optional[Session] = None,
|
||||
@@ -272,16 +272,16 @@ class UsersTable:
|
||||
with get_db_context(db) as db:
|
||||
user = UserModel(
|
||||
**{
|
||||
"id": id,
|
||||
"email": email,
|
||||
"name": name,
|
||||
"role": role,
|
||||
"profile_image_url": profile_image_url,
|
||||
"last_active_at": int(time.time()),
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
"username": username,
|
||||
"oauth": oauth,
|
||||
'id': id,
|
||||
'email': email,
|
||||
'name': name,
|
||||
'role': role,
|
||||
'profile_image_url': profile_image_url,
|
||||
'last_active_at': int(time.time()),
|
||||
'created_at': int(time.time()),
|
||||
'updated_at': int(time.time()),
|
||||
'username': username,
|
||||
'oauth': oauth,
|
||||
}
|
||||
)
|
||||
result = User(**user.model_dump())
|
||||
@@ -293,9 +293,7 @@ class UsersTable:
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_user_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
def get_user_by_id(self, id: str, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
@@ -303,49 +301,32 @@ class UsersTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_by_api_key(
|
||||
self, api_key: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
def get_user_by_api_key(self, api_key: str, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
user = (
|
||||
db.query(User)
|
||||
.join(ApiKey, User.id == ApiKey.user_id)
|
||||
.filter(ApiKey.key == api_key)
|
||||
.first()
|
||||
)
|
||||
user = db.query(User).join(ApiKey, User.id == ApiKey.user_id).filter(ApiKey.key == api_key).first()
|
||||
return UserModel.model_validate(user) if user else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_by_email(
|
||||
self, email: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
def get_user_by_email(self, email: str, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
user = (
|
||||
db.query(User)
|
||||
.filter(func.lower(User.email) == email.lower())
|
||||
.first()
|
||||
)
|
||||
user = db.query(User).filter(func.lower(User.email) == email.lower()).first()
|
||||
return UserModel.model_validate(user) if user else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_by_oauth_sub(
|
||||
self, provider: str, sub: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
def get_user_by_oauth_sub(self, provider: str, sub: str, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db_context(db) as db: # type: Session
|
||||
dialect_name = db.bind.dialect.name
|
||||
|
||||
query = db.query(User)
|
||||
if dialect_name == "sqlite":
|
||||
query = query.filter(User.oauth.contains({provider: {"sub": sub}}))
|
||||
elif dialect_name == "postgresql":
|
||||
query = query.filter(
|
||||
User.oauth[provider].cast(JSONB)["sub"].astext == sub
|
||||
)
|
||||
if dialect_name == 'sqlite':
|
||||
query = query.filter(User.oauth.contains({provider: {'sub': sub}}))
|
||||
elif dialect_name == 'postgresql':
|
||||
query = query.filter(User.oauth[provider].cast(JSONB)['sub'].astext == sub)
|
||||
|
||||
user = query.first()
|
||||
return UserModel.model_validate(user) if user else None
|
||||
@@ -361,15 +342,10 @@ class UsersTable:
|
||||
dialect_name = db.bind.dialect.name
|
||||
|
||||
query = db.query(User)
|
||||
if dialect_name == "sqlite":
|
||||
query = query.filter(
|
||||
User.scim.contains({provider: {"external_id": external_id}})
|
||||
)
|
||||
elif dialect_name == "postgresql":
|
||||
query = query.filter(
|
||||
User.scim[provider].cast(JSONB)["external_id"].astext
|
||||
== external_id
|
||||
)
|
||||
if dialect_name == 'sqlite':
|
||||
query = query.filter(User.scim.contains({provider: {'external_id': external_id}}))
|
||||
elif dialect_name == 'postgresql':
|
||||
query = query.filter(User.scim[provider].cast(JSONB)['external_id'].astext == external_id)
|
||||
|
||||
user = query.first()
|
||||
return UserModel.model_validate(user) if user else None
|
||||
@@ -388,16 +364,16 @@ class UsersTable:
|
||||
query = db.query(User).options(defer(User.profile_image_url))
|
||||
|
||||
if filter:
|
||||
query_key = filter.get("query")
|
||||
query_key = filter.get('query')
|
||||
if query_key:
|
||||
query = query.filter(
|
||||
or_(
|
||||
User.name.ilike(f"%{query_key}%"),
|
||||
User.email.ilike(f"%{query_key}%"),
|
||||
User.name.ilike(f'%{query_key}%'),
|
||||
User.email.ilike(f'%{query_key}%'),
|
||||
)
|
||||
)
|
||||
|
||||
channel_id = filter.get("channel_id")
|
||||
channel_id = filter.get('channel_id')
|
||||
if channel_id:
|
||||
query = query.filter(
|
||||
exists(
|
||||
@@ -408,13 +384,13 @@ class UsersTable:
|
||||
)
|
||||
)
|
||||
|
||||
user_ids = filter.get("user_ids")
|
||||
group_ids = filter.get("group_ids")
|
||||
user_ids = filter.get('user_ids')
|
||||
group_ids = filter.get('group_ids')
|
||||
|
||||
if isinstance(user_ids, list) and isinstance(group_ids, list):
|
||||
# If both are empty lists, return no users
|
||||
if not user_ids and not group_ids:
|
||||
return {"users": [], "total": 0}
|
||||
return {'users': [], 'total': 0}
|
||||
|
||||
if user_ids:
|
||||
query = query.filter(User.id.in_(user_ids))
|
||||
@@ -429,21 +405,21 @@ class UsersTable:
|
||||
)
|
||||
)
|
||||
|
||||
roles = filter.get("roles")
|
||||
roles = filter.get('roles')
|
||||
if roles:
|
||||
include_roles = [role for role in roles if not role.startswith("!")]
|
||||
exclude_roles = [role[1:] for role in roles if role.startswith("!")]
|
||||
include_roles = [role for role in roles if not role.startswith('!')]
|
||||
exclude_roles = [role[1:] for role in roles if role.startswith('!')]
|
||||
|
||||
if include_roles:
|
||||
query = query.filter(User.role.in_(include_roles))
|
||||
if exclude_roles:
|
||||
query = query.filter(~User.role.in_(exclude_roles))
|
||||
|
||||
order_by = filter.get("order_by")
|
||||
direction = filter.get("direction")
|
||||
order_by = filter.get('order_by')
|
||||
direction = filter.get('direction')
|
||||
|
||||
if order_by and order_by.startswith("group_id:"):
|
||||
group_id = order_by.split(":", 1)[1]
|
||||
if order_by and order_by.startswith('group_id:'):
|
||||
group_id = order_by.split(':', 1)[1]
|
||||
|
||||
# Subquery that checks if the user belongs to the group
|
||||
membership_exists = exists(
|
||||
@@ -456,42 +432,42 @@ class UsersTable:
|
||||
# CASE: user in group → 1, user not in group → 0
|
||||
group_sort = case((membership_exists, 1), else_=0)
|
||||
|
||||
if direction == "asc":
|
||||
if direction == 'asc':
|
||||
query = query.order_by(group_sort.asc(), User.name.asc())
|
||||
else:
|
||||
query = query.order_by(group_sort.desc(), User.name.asc())
|
||||
|
||||
elif order_by == "name":
|
||||
if direction == "asc":
|
||||
elif order_by == 'name':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(User.name.asc())
|
||||
else:
|
||||
query = query.order_by(User.name.desc())
|
||||
|
||||
elif order_by == "email":
|
||||
if direction == "asc":
|
||||
elif order_by == 'email':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(User.email.asc())
|
||||
else:
|
||||
query = query.order_by(User.email.desc())
|
||||
|
||||
elif order_by == "created_at":
|
||||
if direction == "asc":
|
||||
elif order_by == 'created_at':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(User.created_at.asc())
|
||||
else:
|
||||
query = query.order_by(User.created_at.desc())
|
||||
|
||||
elif order_by == "last_active_at":
|
||||
if direction == "asc":
|
||||
elif order_by == 'last_active_at':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(User.last_active_at.asc())
|
||||
else:
|
||||
query = query.order_by(User.last_active_at.desc())
|
||||
|
||||
elif order_by == "updated_at":
|
||||
if direction == "asc":
|
||||
elif order_by == 'updated_at':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(User.updated_at.asc())
|
||||
else:
|
||||
query = query.order_by(User.updated_at.desc())
|
||||
elif order_by == "role":
|
||||
if direction == "asc":
|
||||
elif order_by == 'role':
|
||||
if direction == 'asc':
|
||||
query = query.order_by(User.role.asc())
|
||||
else:
|
||||
query = query.order_by(User.role.desc())
|
||||
@@ -510,13 +486,11 @@ class UsersTable:
|
||||
|
||||
users = query.all()
|
||||
return {
|
||||
"users": [UserModel.model_validate(user) for user in users],
|
||||
"total": total,
|
||||
'users': [UserModel.model_validate(user) for user in users],
|
||||
'total': total,
|
||||
}
|
||||
|
||||
def get_users_by_group_id(
|
||||
self, group_id: str, db: Optional[Session] = None
|
||||
) -> list[UserModel]:
|
||||
def get_users_by_group_id(self, group_id: str, db: Optional[Session] = None) -> list[UserModel]:
|
||||
with get_db_context(db) as db:
|
||||
users = (
|
||||
db.query(User)
|
||||
@@ -527,16 +501,9 @@ class UsersTable:
|
||||
)
|
||||
return [UserModel.model_validate(user) for user in users]
|
||||
|
||||
def get_users_by_user_ids(
|
||||
self, user_ids: list[str], db: Optional[Session] = None
|
||||
) -> list[UserStatusModel]:
|
||||
def get_users_by_user_ids(self, user_ids: list[str], db: Optional[Session] = None) -> list[UserStatusModel]:
|
||||
with get_db_context(db) as db:
|
||||
users = (
|
||||
db.query(User)
|
||||
.options(defer(User.profile_image_url))
|
||||
.filter(User.id.in_(user_ids))
|
||||
.all()
|
||||
)
|
||||
users = db.query(User).options(defer(User.profile_image_url)).filter(User.id.in_(user_ids)).all()
|
||||
return [UserModel.model_validate(user) for user in users]
|
||||
|
||||
def get_num_users(self, db: Optional[Session] = None) -> Optional[int]:
|
||||
@@ -555,9 +522,7 @@ class UsersTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_webhook_url_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[str]:
|
||||
def get_user_webhook_url_by_id(self, id: str, db: Optional[Session] = None) -> Optional[str]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
@@ -565,11 +530,7 @@ class UsersTable:
|
||||
if user.settings is None:
|
||||
return None
|
||||
else:
|
||||
return (
|
||||
user.settings.get("ui", {})
|
||||
.get("notifications", {})
|
||||
.get("webhook_url", None)
|
||||
)
|
||||
return user.settings.get('ui', {}).get('notifications', {}).get('webhook_url', None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -577,14 +538,10 @@ class UsersTable:
|
||||
with get_db_context(db) as db:
|
||||
current_timestamp = int(datetime.datetime.now().timestamp())
|
||||
today_midnight_timestamp = current_timestamp - (current_timestamp % 86400)
|
||||
query = db.query(User).filter(
|
||||
User.last_active_at > today_midnight_timestamp
|
||||
)
|
||||
query = db.query(User).filter(User.last_active_at > today_midnight_timestamp)
|
||||
return query.count()
|
||||
|
||||
def update_user_role_by_id(
|
||||
self, id: str, role: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
def update_user_role_by_id(self, id: str, role: str, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
@@ -629,9 +586,7 @@ class UsersTable:
|
||||
return None
|
||||
|
||||
@throttle(DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL)
|
||||
def update_last_active_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
def update_last_active_by_id(self, id: str, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
@@ -665,10 +620,10 @@ class UsersTable:
|
||||
oauth = user.oauth or {}
|
||||
|
||||
# Update or insert provider entry
|
||||
oauth[provider] = {"sub": sub}
|
||||
oauth[provider] = {'sub': sub}
|
||||
|
||||
# Persist updated JSON
|
||||
db.query(User).filter_by(id=id).update({"oauth": oauth})
|
||||
db.query(User).filter_by(id=id).update({'oauth': oauth})
|
||||
db.commit()
|
||||
|
||||
return UserModel.model_validate(user)
|
||||
@@ -698,9 +653,9 @@ class UsersTable:
|
||||
return None
|
||||
|
||||
scim = user.scim or {}
|
||||
scim[provider] = {"external_id": external_id}
|
||||
scim[provider] = {'external_id': external_id}
|
||||
|
||||
db.query(User).filter_by(id=id).update({"scim": scim})
|
||||
db.query(User).filter_by(id=id).update({'scim': scim})
|
||||
db.commit()
|
||||
|
||||
return UserModel.model_validate(user)
|
||||
@@ -708,9 +663,7 @@ class UsersTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_by_id(
|
||||
self, id: str, updated: dict, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
def update_user_by_id(self, id: str, updated: dict, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
@@ -725,9 +678,7 @@ class UsersTable:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
def update_user_settings_by_id(
|
||||
self, id: str, updated: dict, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
def update_user_settings_by_id(self, id: str, updated: dict, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
@@ -741,7 +692,7 @@ class UsersTable:
|
||||
|
||||
user_settings.update(updated)
|
||||
|
||||
db.query(User).filter_by(id=id).update({"settings": user_settings})
|
||||
db.query(User).filter_by(id=id).update({'settings': user_settings})
|
||||
db.commit()
|
||||
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
@@ -768,9 +719,7 @@ class UsersTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_user_api_key_by_id(
|
||||
self, id: str, db: Optional[Session] = None
|
||||
) -> Optional[str]:
|
||||
def get_user_api_key_by_id(self, id: str, db: Optional[Session] = None) -> Optional[str]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
api_key = db.query(ApiKey).filter_by(user_id=id).first()
|
||||
@@ -778,9 +727,7 @@ class UsersTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_api_key_by_id(
|
||||
self, id: str, api_key: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
def update_user_api_key_by_id(self, id: str, api_key: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
db.query(ApiKey).filter_by(user_id=id).delete()
|
||||
@@ -788,7 +735,7 @@ class UsersTable:
|
||||
|
||||
now = int(time.time())
|
||||
new_api_key = ApiKey(
|
||||
id=f"key_{id}",
|
||||
id=f'key_{id}',
|
||||
user_id=id,
|
||||
key=api_key,
|
||||
created_at=now,
|
||||
@@ -811,16 +758,14 @@ class UsersTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_valid_user_ids(
|
||||
self, user_ids: list[str], db: Optional[Session] = None
|
||||
) -> list[str]:
|
||||
def get_valid_user_ids(self, user_ids: list[str], db: Optional[Session] = None) -> list[str]:
|
||||
with get_db_context(db) as db:
|
||||
users = db.query(User).filter(User.id.in_(user_ids)).all()
|
||||
return [user.id for user in users]
|
||||
|
||||
def get_super_admin_user(self, db: Optional[Session] = None) -> Optional[UserModel]:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(role="admin").first()
|
||||
user = db.query(User).filter_by(role='admin').first()
|
||||
if user:
|
||||
return UserModel.model_validate(user)
|
||||
else:
|
||||
@@ -830,9 +775,7 @@ class UsersTable:
|
||||
with get_db_context(db) as db:
|
||||
# Consider user active if last_active_at within the last 3 minutes
|
||||
three_minutes_ago = int(time.time()) - 180
|
||||
count = (
|
||||
db.query(User).filter(User.last_active_at >= three_minutes_ago).count()
|
||||
)
|
||||
count = db.query(User).filter(User.last_active_at >= three_minutes_ago).count()
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user