feat: db constistancy

This commit is contained in:
Artem Tsyrulnikov
2025-12-23 10:38:24 +03:00
parent 45a5b59cc8
commit 829cd82bcf
9 changed files with 253 additions and 195 deletions

View File

@@ -138,17 +138,24 @@ class Postgres(DatabaseBase, Database):
)
if scoped_permissions:
await domain.WorkspaceUserPermissionScope.bulk_create(
[
domain.WorkspaceUserPermissionScope(
workspace_user_id=workspace_user_id,
permission=permission,
scope_type=scope_type,
scope_id=scope_id,
)
for permission, scope_type, scope_id in scoped_permissions
]
)
scope_objects = []
for permission, scope_type, scope_id in scoped_permissions:
kwargs = {
'workspace_user_id': workspace_user_id,
'permission': permission,
}
if scope_type == domain.PermissionScopeType.PROJECT:
kwargs['project_id'] = scope_id
elif scope_type == domain.PermissionScopeType.CREATIVE:
kwargs['creative_id'] = scope_id
elif scope_type == domain.PermissionScopeType.PLACEMENT:
kwargs['placement_id'] = scope_id
elif scope_type == domain.PermissionScopeType.CHANNEL:
kwargs['channel_id'] = scope_id
scope_objects.append(domain.WorkspaceUserPermissionScope(**kwargs))
await domain.WorkspaceUserPermissionScope.bulk_create(scope_objects)
async def create_login_token(self, login_token: domain.LoginToken) -> None:
await login_token.save()

View File

@@ -12,7 +12,7 @@ if TYPE_CHECKING:
class LoginToken(TimestampedModel):
token = fields.CharField(max_length=255, unique=True, index=True)
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
'models.User', related_name='login_tokens', on_delete=fields.CASCADE
'models.User', related_name='login_tokens', on_delete=fields.CASCADE, index=True
)
expires_at = fields.DatetimeField()
used_at = fields.DatetimeField(null=True)

View File

@@ -22,3 +22,4 @@ class PostViewsHistory(TimestampedModel):
class Meta:
table = 'post_views_history'
unique_together = (('post_id', 'fetched_at'),)

View File

@@ -9,6 +9,10 @@ from tortoise import fields
from .base import TimestampedModel
if TYPE_CHECKING:
from .channel import Channel
from .creative import Creative
from .placement import Placement
from .project import Project
from .user import User
@@ -117,16 +121,31 @@ class WorkspaceUserPermission(TimestampedModel):
class WorkspaceUserPermissionScope(TimestampedModel):
permission = fields.CharEnumField(PermissionKey)
scope_type = fields.CharEnumField(PermissionScopeType)
scope_id = fields.UUIDField()
workspace_user: fields.ForeignKeyRelation[WorkspaceUser] = fields.ForeignKeyField(
'models.WorkspaceUser', related_name='permission_scopes', on_delete=fields.CASCADE, index=True
)
# Exactly one of these must be set (enforced by CHECK constraint in migration)
project: fields.ForeignKeyRelation[Project] | None = fields.ForeignKeyField(
'models.Project', related_name='permission_scopes', on_delete=fields.CASCADE, null=True, index=True
)
creative: fields.ForeignKeyRelation[Creative] | None = fields.ForeignKeyField(
'models.Creative', related_name='permission_scopes', on_delete=fields.CASCADE, null=True, index=True
)
placement: fields.ForeignKeyRelation[Placement] | None = fields.ForeignKeyField(
'models.Placement', related_name='permission_scopes', on_delete=fields.CASCADE, null=True, index=True
)
channel: fields.ForeignKeyRelation[Channel] | None = fields.ForeignKeyField(
'models.Channel', related_name='permission_scopes', on_delete=fields.CASCADE, null=True, index=True
)
if TYPE_CHECKING:
workspace_user_id: uuid.UUID
project_id: uuid.UUID | None
creative_id: uuid.UUID | None
placement_id: uuid.UUID | None
channel_id: uuid.UUID | None
class Meta:
table = 'workspace_user_permission_scope'
unique_together = (('workspace_user_id', 'permission', 'scope_type', 'scope_id'),)

View File

@@ -23,8 +23,18 @@ class WorkspacePermissions:
scoped_permissions: dict[tuple[PermissionKey, PermissionScopeType], set[UUID]] = {}
for scope in getattr(membership, 'permission_scopes', []) or []:
key = (scope.permission, scope.scope_type)
scoped_permissions.setdefault(key, set()).add(scope.scope_id)
if scope.project_id:
key = (scope.permission, PermissionScopeType.PROJECT)
scoped_permissions.setdefault(key, set()).add(scope.project_id)
elif scope.creative_id:
key = (scope.permission, PermissionScopeType.CREATIVE)
scoped_permissions.setdefault(key, set()).add(scope.creative_id)
elif scope.placement_id:
key = (scope.permission, PermissionScopeType.PLACEMENT)
scoped_permissions.setdefault(key, set()).add(scope.placement_id)
elif scope.channel_id:
key = (scope.permission, PermissionScopeType.CHANNEL)
scoped_permissions.setdefault(key, set()).add(scope.channel_id)
return cls(global_permissions=global_permissions, scoped_permissions=scoped_permissions)

View File

@@ -37,6 +37,17 @@ async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) ->
).first()
if matching_post:
# Validate channel consistency
if matching_post.channel_id != placement.placement_channel_id:
log.error(
'Post %s channel_id mismatch: expected %s, got %s. Skipping placement %s',
matching_post.id,
placement.placement_channel_id,
matching_post.channel_id,
placement.id,
)
continue
placement.post = matching_post
await placement.save()
updated_count += 1