tech_update #23

Merged
edraft merged 10 commits from tech_update into master 2025-04-18 12:14:59 +02:00
43 changed files with 942 additions and 336 deletions
Showing only changes of commit 347f8486af - Show all commits

View File

@ -0,0 +1,60 @@
from datetime import datetime
from typing import Union, Callable, Any
from api_graphql.abc.query_abc import QueryABC
from core.database.abc.data_access_object_abc import DataAccessObjectABC
class DbHistoryModelQueryABC(QueryABC):
def __init__(self, name: str = None):
assert name is not None, f"Name for {__name__} must be provided"
QueryABC.__init__(self, f"{name}History")
self.set_field("id", lambda x, *_: x.id)
self.set_field("deleted", lambda x, *_: x.deleted)
self.set_field("editor", self._resolve_editor)
self.set_field("created", lambda x, *_: x.created)
self.set_field("updated", lambda x, *_: x.updated)
@staticmethod
async def _resolve_editor(x, *_):
editor = await x.editor
return editor.username if editor else None
@staticmethod
async def _resolve_foreign_history(
updated: datetime,
obj_ident: Union[str, int],
join_dao: DataAccessObjectABC,
foreign_dao: DataAccessObjectABC,
foreign_join_key: Callable[[Any], Any],
obj_key="id",
*_,
):
foreign_history = sorted(
[
*await join_dao.find_by(
[
{obj_key: obj_ident},
{"updated": {"lessOrEqual": updated}},
]
),
*await join_dao.get_history(
obj_ident,
by_key=obj_key,
until=updated,
),
],
key=lambda x: x.updated,
)
foreign_ids = set()
for foreign in foreign_history:
if not foreign.deleted:
foreign_ids.add(foreign_join_key(foreign))
continue
foreign_ids.discard(foreign_join_key(foreign))
return [await foreign_dao.get_by_id(x) for x in sorted(foreign_ids)]

View File

@ -23,5 +23,5 @@ class DbModelCollectionFilterABC[T](CollectionFilterABC):
self.add_field("id", IntCollectionFilter)
self.add_field("deleted", BoolCollectionFilter)
self.add_field("editor", IntCollectionFilter)
self.add_field("createdUtc", DateCollectionFilter)
self.add_field("updatedUtc", DateCollectionFilter)
self.add_field("created", DateCollectionFilter)
self.add_field("updated", DateCollectionFilter)

View File

@ -1,10 +1,11 @@
from typing import Optional
from api_graphql.abc.filter.bool_filter import BoolFilter
from api_graphql.abc.filter.date_filter import DateFilter
from api_graphql.abc.filter.fuzzy_filter import FuzzyFilter
from api_graphql.abc.filter.int_filter import IntFilter
from api_graphql.abc.filter.string_filter import StringFilter
from api_graphql.abc.filter_abc import FilterABC
from api_graphql.filter.fuzzy_filter import FuzzyFilter
class DbModelFilterABC[T](FilterABC[T]):
@ -18,7 +19,7 @@ class DbModelFilterABC[T](FilterABC[T]):
self.add_field("id", IntFilter)
self.add_field("deleted", BoolFilter)
self.add_field("editor", UserFilter)
self.add_field("createdUtc", StringFilter, "created")
self.add_field("updatedUtc", StringFilter, "updated")
self.add_field("created", DateFilter)
self.add_field("updated", DateFilter)
self.add_field("fuzzy", FuzzyFilter)

View File

@ -1,18 +1,54 @@
from copy import deepcopy
from typing import Optional
from api_graphql.abc.query_abc import QueryABC
from data.schemas.administration.user import User
from core.database.abc.data_access_object_abc import DataAccessObjectABC
from core.logger import APILogger
logger = APILogger("api.api")
class DbModelQueryABC(QueryABC):
def __init__(self, name: str = __name__):
def __init__(
self,
name: str = __name__,
dao: DataAccessObjectABC = None,
with_history: bool = False,
):
QueryABC.__init__(self, name)
self._dao: Optional[DataAccessObjectABC] = dao
self.set_field("id", lambda x, *_: x.id)
self.set_field("deleted", lambda x, *_: x.deleted)
self.set_field("editor", self.__get_editor)
self.set_field("createdUtc", lambda x, *_: x.created)
self.set_field("updatedUtc", lambda x, *_: x.updated)
self.set_field("editor", lambda x, *_: x.editor)
self.set_field("created", lambda x, *_: x.created)
self.set_field("updated", lambda x, *_: x.updated)
@staticmethod
async def __get_editor(x: User, *_):
return await x.editor
if with_history:
self.set_field("history", self._resolve_history)
self._history_reference_daos: dict[DataAccessObjectABC, str] = {}
async def _resolve_history(self, x, *_):
if self._dao is None:
raise Exception("DAO not set for history query")
history = sorted(
[await self._dao.get_by_id(x.id), *await self._dao.get_history(x.id)],
key=lambda h: h.updated,
reverse=True,
)
return history
def set_history_reference_dao(self, dao: DataAccessObjectABC, key: str = None):
"""
Set the reference DAO for history resolution.
:param dao:
:param key: The key to use for resolving history.
:return:
"""
if key is None:
key = "id"
self._history_reference_daos[dao] = key

View File

@ -0,0 +1,15 @@
from typing import Optional
from api_graphql.abc.filter_abc import FilterABC
class FuzzyFilter(FilterABC):
def __init__(
self,
obj: Optional[dict],
):
FilterABC.__init__(self, obj)
self.add_field("fields", list)
self.add_field("term", str)
self.add_field("threshold", int)

View File

@ -18,3 +18,5 @@ class IntFilter(FilterABC):
self.add_field("lessOrEqual", int)
self.add_field("isNull", int)
self.add_field("isNotNull", int)
self.add_field("in", list)
self.add_field("notIn", list)

View File

@ -18,3 +18,5 @@ class StringFilter(FilterABC):
self.add_field("endsWith", str)
self.add_field("isNull", str)
self.add_field("isNotNull", str)
self.add_field("in", list)
self.add_field("notIn", list)

View File

@ -19,10 +19,12 @@ class FilterABC[T](ABC):
def add_field(
self,
field: str,
filter_type: Union[Type["FilterABC"], Type[Union[int, str, bool, datetime]]],
filter_type: Union[
Type["FilterABC"], Type[Union[int, str, bool, datetime, list]]
],
db_name=None,
):
if field not in self._obj and db_name not in self._obj:
if field not in self._obj:
return
if db_name is None:

View File

@ -1,6 +1,7 @@
from abc import ABC
from typing import Optional, Type, get_origin, get_args
from core.get_value import get_value
from core.typing import T
@ -12,11 +13,15 @@ class InputABC(ABC):
ABC.__init__(self)
self._src = src
self._options = {}
def option(
self, field: str, cast_type: Type[T], default=None, required=False
) -> Optional[T]:
if required and field not in self._src:
raise ValueError(f"{field} is required")
self._options[field] = cast_type
if field not in self._src:
return default
@ -28,4 +33,4 @@ class InputABC(ABC):
return cast_type(value)
def get(self, field: str, default=None) -> Optional[T]:
return self._src.get(field, default)
return get_value(self._src, field, self._options[field], default)

View File

@ -1,7 +1,12 @@
from abc import abstractmethod
from typing import Type, Union
from api_graphql.abc.input_abc import InputABC
from api_graphql.abc.query_abc import QueryABC
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
from core.database.abc.data_access_object_abc import DataAccessObjectABC
from core.database.abc.db_join_model_abc import DbJoinModelABC
from core.typing import T
from service.permission.permissions_enum import Permissions
@ -41,3 +46,79 @@ class MutationABC(QueryABC):
.with_require_any_permission(require_any_permission)
.with_public(public)
)
@staticmethod
async def _resolve_assignments(
foreign_objs: list[int],
resolved_obj: T,
reference_key_own: Union[str, property],
reference_key_foreign: Union[str, property],
source_dao: DataAccessObjectABC[T],
join_dao: DataAccessObjectABC[T],
join_type: Type[DbJoinModelABC],
foreign_dao: DataAccessObjectABC[T],
):
if foreign_objs is None:
return
reference_key_own_attr = reference_key_own
if isinstance(reference_key_own, property):
reference_key_own_attr = reference_key_own.fget.__name__
reference_key_foreign_attr = reference_key_foreign
if isinstance(reference_key_foreign, property):
reference_key_foreign_attr = reference_key_foreign.fget.__name__
foreign_list = await join_dao.find_by(
[{reference_key_own: resolved_obj.id}, {"deleted": False}]
)
to_delete = (
foreign_list
if len(foreign_objs) == 0
else await join_dao.find_by(
[
{reference_key_own: resolved_obj.id},
{reference_key_foreign: {"notIn": foreign_objs}},
]
)
)
foreign_ids = [getattr(x, reference_key_foreign_attr) for x in foreign_list]
deleted_foreign_ids = [
getattr(x, reference_key_foreign_attr)
for x in await join_dao.find_by(
[{reference_key_own: resolved_obj.id}, {"deleted": True}]
)
]
to_create = [
join_type(0, resolved_obj.id, x)
for x in foreign_objs
if x not in foreign_ids and x not in deleted_foreign_ids
]
to_restore = [
await join_dao.get_single_by(
[
{reference_key_own: resolved_obj.id},
{reference_key_foreign: x},
]
)
for x in foreign_objs
if x not in foreign_ids and x in deleted_foreign_ids
]
if len(to_delete) > 0:
await join_dao.delete_many(to_delete)
if len(to_create) > 0:
await join_dao.create_many(to_create)
if len(to_restore) > 0:
await join_dao.restore_many(to_restore)
foreign_changes = [*to_delete, *to_create, *to_restore]
if len(foreign_changes) > 0:
await source_dao.touch(resolved_obj)
await foreign_dao.touch_many_by_id(
[getattr(x, reference_key_foreign_attr) for x in foreign_changes]
)

View File

@ -6,11 +6,12 @@ from typing import Callable, Type, get_args, Any, Union
from ariadne import ObjectType, SubscriptionType
from graphql import GraphQLResolveInfo
from starlette.requests import Request
from typing_extensions import deprecated
from api.middleware.request import get_request
from api.route import Route
from api_graphql.abc.collection_filter_abc import CollectionFilterABC
from api_graphql.abc.field_abc import FieldABC
from api_graphql.abc.input_abc import InputABC
from api_graphql.abc.sort_abc import Sort
from api_graphql.field.collection_field import CollectionField
@ -46,8 +47,8 @@ class QueryABC(ObjectType):
self._subscriptions: dict[str, SubscriptionType] = {}
@staticmethod
async def _authorize():
if not await Route.is_authorized():
async def _authorize(request: Request):
if not await Route.is_authorized(request):
raise UnauthorizedException()
@staticmethod
@ -71,8 +72,6 @@ class QueryABC(ObjectType):
*args,
**kwargs,
):
info = args[0]
if len(permissions) > 0:
user = await Route.get_authenticated_user_or_api_key_or_default()
if user is not None and all(
@ -120,6 +119,9 @@ class QueryABC(ObjectType):
take = None
skip = None
if field.default_filter:
filters.append(field.default_filter(*args, **kwargs))
if field.filter_type and "filter" in kwargs:
in_filters = kwargs["filter"]
if not isinstance(in_filters, list):
@ -227,7 +229,7 @@ class QueryABC(ObjectType):
async def wrapper(*args, **kwargs):
if not field.public:
await self._authorize()
await self._authorize(get_request())
if (
field.require_any is None

View File

@ -2,8 +2,8 @@ from abc import abstractmethod
from asyncio import iscoroutinefunction
from ariadne import SubscriptionType
from graphql import GraphQLResolveInfo
from api.middleware.request import get_request
from api_graphql.abc.query_abc import QueryABC
from api_graphql.field.subscription_field_builder import SubscriptionFieldBuilder
from core.logger import APILogger
@ -20,9 +20,12 @@ class SubscriptionABC(SubscriptionType, QueryABC):
def subscribe(self, builder: SubscriptionFieldBuilder):
field = builder.build()
async def wrapper(*args, **kwargs):
async def wrapper(_, info: GraphQLResolveInfo, *args, **kwargs):
# rebuild args for resolvers
args = [_, info, *args]
if not field.public:
await self._authorize()
r = info.context.get("request")
await self._authorize(r)
if (
field.require_any is None

View File

@ -1,6 +1,7 @@
import importlib
import os
from api_graphql.abc.db_history_model_query_abc import DbHistoryModelQueryABC
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.abc.query_abc import QueryABC
@ -20,7 +21,12 @@ def import_graphql_schema_part(part: str):
import_graphql_schema_part("queries")
import_graphql_schema_part("mutations")
sub_query_classes = [DbModelQueryABC, MutationABC, SubscriptionABC]
sub_query_classes = [
DbModelQueryABC,
DbHistoryModelQueryABC,
MutationABC,
SubscriptionABC,
]
query_classes = [
*[y for x in sub_query_classes for y in x.__subclasses__()],
*[x for x in QueryABC.__subclasses__() if x not in sub_query_classes],

View File

@ -4,16 +4,30 @@ type ApiKeyResult {
nodes: [ApiKey]
}
type ApiKeyHistory implements DbHistoryModel {
id: Int
identifier: String
key: String
permissions: [Permission]
deleted: Boolean
editor: String
created: String
updated: String
}
type ApiKey implements DbModel {
id: ID
id: Int
identifier: String
key: String
permissions: [Permission]
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
history: [ApiKeyHistory]
}
input ApiKeySort {
@ -22,12 +36,18 @@ input ApiKeySort {
deleted: SortOrder
editor: UserSort
createdUtc: SortOrder
updatedUtc: SortOrder
created: SortOrder
updated: SortOrder
}
enum ApiKeyFuzzyFields {
id
identifier
deleted
editor
created
updated
}
input ApiKeyFuzzy {
@ -42,24 +62,24 @@ input ApiKeyFilter {
deleted: BooleanFilter
editorId: IntFilter
createdUtc: DateFilter
updatedUtc: DateFilter
created: DateFilter
updated: DateFilter
}
type ApiKeyMutation {
create(input: ApiKeyCreateInput!): ApiKey
update(input: ApiKeyUpdateInput!): ApiKey
delete(identifier: String!): Boolean
restore(identifier: String!): Boolean
delete(id: Int!): Boolean
restore(id: Int!): Boolean
}
input ApiKeyCreateInput {
identifier: String
permissions: [ID]
permissions: [Int]
}
input ApiKeyUpdateInput {
id: ID!
id: Int!
identifier: String
permissions: [ID]
permissions: [Int]
}

View File

@ -1,12 +1,21 @@
scalar Upload
interface DbModel {
id: ID
id: Int
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
}
interface DbHistoryModel {
id: Int
deleted: Boolean
editor: String
created: String
updated: String
}
enum SortOrder {
@ -44,6 +53,8 @@ input IntFilter {
isNull: Int
isNotNull: Int
in: [Int]
notIn: [Int]
}
input BooleanFilter {
@ -58,9 +69,18 @@ input DateFilter {
equal: String
notEqual: String
greater: String
greaterOrEqual: String
less: String
lessOrEqual: String
contains: String
notContains: String
isNull: String
isNotNull: String
in: [String]
notIn: [String]
}

View File

@ -0,0 +1,12 @@
enum Attendance {
absent
present
delayed
canceled
}
enum Payment {
not_paid
paid
refunded
}

View File

@ -5,15 +5,15 @@ type DomainResult {
}
type Domain implements DbModel {
id: ID
id: Int
name: String
shortUrls: [ShortUrl]
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
}
input DomainSort {
@ -22,8 +22,8 @@ input DomainSort {
deleted: SortOrder
editorId: SortOrder
createdUtc: SortOrder
updatedUtc: SortOrder
created: SortOrder
updated: SortOrder
}
enum DomainFuzzyFields {
@ -44,15 +44,15 @@ input DomainFilter {
deleted: BooleanFilter
editor: IntFilter
createdUtc: DateFilter
updatedUtc: DateFilter
created: DateFilter
updated: DateFilter
}
type DomainMutation {
create(input: DomainCreateInput!): Domain
update(input: DomainUpdateInput!): Domain
delete(id: ID!): Boolean
restore(id: ID!): Boolean
delete(id: Int!): Boolean
restore(id: Int!): Boolean
}
input DomainCreateInput {
@ -60,6 +60,6 @@ input DomainCreateInput {
}
input DomainUpdateInput {
id: ID!
id: Int!
name: String
}

View File

@ -1,12 +1,12 @@
type FeatureFlag implements DbModel {
id: ID
id: Int
key: String
value: Boolean
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
}
type FeatureFlagMutation {

View File

@ -5,7 +5,7 @@ type GroupResult {
}
type Group implements DbModel {
id: ID
id: Int
name: String
shortUrls: [ShortUrl]
@ -13,8 +13,8 @@ type Group implements DbModel {
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
}
input GroupSort {
@ -23,8 +23,8 @@ input GroupSort {
deleted: SortOrder
editorId: SortOrder
createdUtc: SortOrder
updatedUtc: SortOrder
created: SortOrder
updated: SortOrder
}
enum GroupFuzzyFields {
@ -45,24 +45,24 @@ input GroupFilter {
deleted: BooleanFilter
editor: IntFilter
createdUtc: DateFilter
updatedUtc: DateFilter
created: DateFilter
updated: DateFilter
}
type GroupMutation {
create(input: GroupCreateInput!): Group
update(input: GroupUpdateInput!): Group
delete(id: ID!): Boolean
restore(id: ID!): Boolean
delete(id: Int!): Boolean
restore(id: Int!): Boolean
}
input GroupCreateInput {
name: String!
roles: [ID]
roles: [Int]
}
input GroupUpdateInput {
id: ID!
id: Int!
name: String
roles: [ID]
roles: [Int]
}

View File

@ -5,14 +5,14 @@ type PermissionResult {
}
type Permission implements DbModel {
id: ID
id: Int
name: String
description: String
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
}
input PermissionSort {
@ -22,8 +22,8 @@ input PermissionSort {
deleted: SortOrder
editorId: SortOrder
createdUtc: SortOrder
updatedUtc: SortOrder
created: SortOrder
updated: SortOrder
}
input PermissionFilter {
@ -33,12 +33,12 @@ input PermissionFilter {
deleted: BooleanFilter
editor: IntFilter
createdUtc: DateFilter
updatedUtc: DateFilter
created: DateFilter
updated: DateFilter
}
input PermissionInput {
id: ID
id: Int
name: String
description: String
}

View File

@ -9,7 +9,7 @@ type Query {
user: User
userHasPermission(permission: String!): Boolean
userHasAnyPermission(permissions: [String]!): Boolean
notExistingUsersFromKeycloak: KeycloakUserResult
notExistingUsersFromKeycloak: [KeycloakUser]
domains(filter: [DomainFilter], sort: [DomainSort], skip: Int, take: Int): DomainResult
groups(filter: [GroupFilter], sort: [GroupSort], skip: Int, take: Int): GroupResult

View File

@ -4,8 +4,21 @@ type RoleResult {
nodes: [Role]
}
type RoleHistory implements DbHistoryModel {
id: Int
name: String
description: String
permissions: [Permission]
deleted: Boolean
editor: String
created: String
updated: String
}
type Role implements DbModel {
id: ID
id: Int
name: String
description: String
permissions: [Permission]
@ -13,8 +26,10 @@ type Role implements DbModel {
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
history: [RoleHistory]
}
input RoleSort {
@ -24,13 +39,19 @@ input RoleSort {
deleted: SortOrder
editor: UserSort
createdUtc: SortOrder
updatedUtc: SortOrder
created: SortOrder
updated: SortOrder
}
enum RoleFuzzyFields {
id
name
description
deleted
editor
created
updated
}
input RoleFuzzy {
@ -48,26 +69,26 @@ input RoleFilter {
deleted: BooleanFilter
editor_id: IntFilter
createdUtc: DateFilter
updatedUtc: DateFilter
created: DateFilter
updated: DateFilter
}
type RoleMutation {
create(input: RoleCreateInput!): Role
update(input: RoleUpdateInput!): Role
delete(id: ID!): Boolean
restore(id: ID!): Boolean
delete(id: Int!): Boolean
restore(id: Int!): Boolean
}
input RoleCreateInput {
name: String!
description: String
permissions: [ID]
permissions: [Int]
}
input RoleUpdateInput {
id: ID!
id: Int!
name: String
description: String
permissions: [ID]
permissions: [Int]
}

View File

@ -1,12 +1,12 @@
type Setting implements DbModel {
id: ID
id: Int
key: String
value: String
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
}
type SettingMutation {

View File

@ -5,7 +5,7 @@ type ShortUrlResult {
}
type ShortUrl implements DbModel {
id: ID
id: Int
shortUrl: String
targetUrl: String
description: String
@ -16,8 +16,8 @@ type ShortUrl implements DbModel {
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
}
input ShortUrlSort {
@ -28,8 +28,8 @@ input ShortUrlSort {
deleted: SortOrder
editorId: SortOrder
createdUtc: SortOrder
updatedUtc: SortOrder
created: SortOrder
updated: SortOrder
}
enum ShortUrlFuzzyFields {
@ -57,33 +57,33 @@ input ShortUrlFilter {
deleted: BooleanFilter
editor: IntFilter
createdUtc: DateFilter
updatedUtc: DateFilter
created: DateFilter
updated: DateFilter
}
type ShortUrlMutation {
create(input: ShortUrlCreateInput!): ShortUrl
update(input: ShortUrlUpdateInput!): ShortUrl
delete(id: ID!): Boolean
restore(id: ID!): Boolean
trackVisit(id: ID!, agent: String): Boolean
delete(id: Int!): Boolean
restore(id: Int!): Boolean
trackVisit(id: Int!, agent: String): Boolean
}
input ShortUrlCreateInput {
shortUrl: String!
targetUrl: String!
description: String
groupId: ID
domainId: ID
groupId: Int
domainId: Int
loadingScreen: Boolean
}
input ShortUrlUpdateInput {
id: ID!
id: Int!
shortUrl: String
targetUrl: String
description: String
groupId: ID
domainId: ID
groupId: Int
domainId: Int
loadingScreen: Boolean
}

View File

@ -1,9 +1,3 @@
type KeycloakUserResult {
totalCount: Int
count: Int
nodes: [KeycloakUser]
}
type KeycloakUser {
keycloakId: String
username: String
@ -15,8 +9,22 @@ type UserResult {
nodes: [User]
}
type UserHistory implements DbHistoryModel {
id: Int
keycloakId: String
username: String
email: String
roles: [Role]
deleted: Boolean
editor: String
created: String
updated: String
}
type User implements DbModel {
id: ID
id: Int
keycloakId: String
username: String
email: String
@ -24,8 +32,10 @@ type User implements DbModel {
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
history: [UserHistory]
}
input UserSort {
@ -36,14 +46,20 @@ input UserSort {
deleted: SortOrder
editor: UserSort
createdUtc: SortOrder
updatedUtc: SortOrder
created: SortOrder
updated: SortOrder
}
enum UserFuzzyFields {
id
keycloakId
username
email
deleted
editor
created
updated
}
input UserFuzzy {
@ -62,23 +78,23 @@ input UserFilter {
deleted: BooleanFilter
editor: UserFilter
createdUtc: DateFilter
updatedUtc: DateFilter
created: DateFilter
updated: DateFilter
}
type UserMutation {
create(input: UserCreateInput!): User
update(input: UserUpdateInput!): User
delete(id: ID!): Boolean
restore(id: ID!): Boolean
delete(id: Int!): Boolean
restore(id: Int!): Boolean
}
input UserCreateInput {
keycloakId: String
roles: [ID]
roles: [Int]
}
input UserUpdateInput {
id: ID
roles: [ID]
id: Int
roles: [Int]
}

View File

@ -1,12 +1,12 @@
type UserSetting implements DbModel {
id: ID
id: Int
key: String
value: String
deleted: Boolean
editor: User
createdUtc: String
updatedUtc: String
created: String
updated: String
}
type UserSettingMutation {

View File

@ -1,5 +1,3 @@
from uuid import uuid4
from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.input.api_key_create_input import ApiKeyCreateInput
from api_graphql.input.api_key_update_input import ApiKeyUpdateInput
@ -8,6 +6,7 @@ from data.schemas.administration.api_key import ApiKey
from data.schemas.administration.api_key_dao import apiKeyDao
from data.schemas.permission.api_key_permission import ApiKeyPermission
from data.schemas.permission.api_key_permission_dao import apiKeyPermissionDao
from data.schemas.permission.permission_dao import permissionDao
from service.permission.permissions_enum import Permissions
logger = APILogger(__name__)
@ -44,77 +43,28 @@ class APIKeyMutation(MutationABC):
async def resolve_create(obj: ApiKeyCreateInput, *_):
logger.debug(f"create api key: {obj.__dict__}")
api_key = ApiKey(
0,
obj.identifier,
str(uuid4()),
)
api_key = ApiKey.new(obj.identifier)
await apiKeyDao.create(api_key)
api_key = await apiKeyDao.get_by_identifier(api_key.identifier)
api_key = await apiKeyDao.get_single_by([{ApiKey.identifier: obj.identifier}])
await apiKeyPermissionDao.create_many(
[ApiKeyPermission(0, api_key.id, x) for x in obj.permissions]
)
return api_key
@staticmethod
async def resolve_update(obj: ApiKeyUpdateInput, *_):
async def resolve_update(self, obj: ApiKeyUpdateInput, *_):
logger.debug(f"update api key: {input}")
api_key = await apiKeyDao.get_by_id(obj.id)
if obj.permissions is not None:
permissions = [
x for x in await apiKeyPermissionDao.find_by_api_key_id(api_key.id)
]
to_delete = (
permissions
if len(obj.permissions) == 0
else await apiKeyPermissionDao.find_by(
[
{ApiKeyPermission.api_key_id: api_key.id},
{
ApiKeyPermission.permission_id: {
"notIn": obj.get("permissions", [])
}
},
]
)
)
permission_ids = [x.permission_id for x in permissions]
deleted_permission_ids = [
x.permission_id
for x in await apiKeyPermissionDao.find_by(
[
{ApiKeyPermission.api_key_id: api_key.id},
{ApiKeyPermission.deleted: True},
]
)
]
to_create = [
ApiKeyPermission(0, api_key.id, x)
for x in obj.permissions
if x not in permission_ids and x not in deleted_permission_ids
]
to_restore = [
await apiKeyPermissionDao.get_single_by(
[
{ApiKeyPermission.api_key_id: api_key.id},
{ApiKeyPermission.permission_id: x},
]
)
for x in obj.permissions
if x not in permission_ids and x in deleted_permission_ids
]
if len(to_delete) > 0:
await apiKeyPermissionDao.delete_many(to_delete)
if len(to_create) > 0:
await apiKeyPermissionDao.create_many(to_create)
if len(to_restore) > 0:
await apiKeyPermissionDao.restore_many(to_restore)
await self._resolve_assignments(
obj.get("permissions", []),
api_key,
ApiKeyPermission.api_key_id,
ApiKeyPermission.permission_id,
apiKeyDao,
apiKeyPermissionDao,
ApiKeyPermission,
permissionDao,
)
return api_key

View File

@ -2,6 +2,7 @@ from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.input.role_create_input import RoleCreateInput
from api_graphql.input.role_update_input import RoleUpdateInput
from core.logger import APILogger
from data.schemas.permission.permission_dao import permissionDao
from data.schemas.permission.role import Role
from data.schemas.permission.role_dao import roleDao
from data.schemas.permission.role_permission import RolePermission
@ -54,63 +55,23 @@ class RoleMutation(MutationABC):
return role
@staticmethod
async def resolve_update(obj: RoleUpdateInput, *_):
async def resolve_update(self, obj: RoleUpdateInput, *_):
logger.debug(f"update role: {obj.__dict__}")
role = await roleDao.get_by_id(obj.id)
role.name = obj.get("name", role.name)
role.description = obj.get("description", role.description)
await roleDao.update(role)
if obj.permissions is not None:
permissions = [x for x in await rolePermissionDao.get_by_role_id(role.id)]
to_delete = (
permissions
if len(obj.permissions) == 0
else await rolePermissionDao.find_by(
[
{RolePermission.role_id: role.id},
{
RolePermission.permission_id: {
"notIn": obj.get("permissions", [])
}
},
]
)
)
permission_ids = [x.permission_id for x in permissions]
deleted_permission_ids = [
x.permission_id
for x in await rolePermissionDao.find_by(
[{RolePermission.role_id: role.id}, {RolePermission.deleted: True}]
)
]
to_create = [
RolePermission(0, role.id, x)
for x in obj.permissions
if x not in permission_ids and x not in deleted_permission_ids
]
to_restore = [
await rolePermissionDao.get_single_by(
[
{RolePermission.role_id: role.id},
{RolePermission.permission_id: x},
]
)
for x in obj.permissions
if x not in permission_ids and x in deleted_permission_ids
]
if len(to_delete) > 0:
await rolePermissionDao.delete_many(to_delete)
if len(to_create) > 0:
await rolePermissionDao.create_many(to_create)
if len(to_restore) > 0:
await rolePermissionDao.restore_many(to_restore)
await self._resolve_assignments(
obj.get("permissions", []),
role,
RolePermission.role_id,
RolePermission.permission_id,
roleDao,
rolePermissionDao,
RolePermission,
permissionDao,
)
return role

View File

@ -1,10 +1,13 @@
from api.auth.keycloak_client import Keycloak
from api.broadcast import broadcast
from api.route import Route
from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.input.user_create_input import UserCreateInput
from api_graphql.input.user_update_input import UserUpdateInput
from core.logger import APILogger
from data.schemas.administration.user import User
from data.schemas.administration.user_dao import userDao
from data.schemas.permission.role_dao import roleDao
from data.schemas.permission.role_user import RoleUser
from data.schemas.permission.role_user_dao import roleUserDao
from service.permission.permissions_enum import Permissions
@ -49,62 +52,26 @@ class UserMutation(MutationABC):
raise ValueError(f"Keycloak user with id {obj.keycloak_id} does not exist")
user = User(0, obj.keycloak_id)
await userDao.create(user)
user = await userDao.get_by_keycloak_id(user.keycloak_id)
await roleUserDao.create_many([RoleUser(0, user.id, x) for x in obj.roles])
user_id = await userDao.create(user)
user = await userDao.get_by_id(user_id)
await roleUserDao.create_many([RoleUser(0, user.id, x) for x in set(obj.roles)])
return user
@staticmethod
async def resolve_update(obj: UserUpdateInput, *_):
async def resolve_update(self, obj: UserUpdateInput, *_):
logger.debug(f"update user: {obj.__dict__}")
user = await userDao.get_by_id(obj.id)
if obj.roles is not None:
roles = await roleUserDao.get_by_user_id(user.id)
to_delete = (
roles
if len(obj.roles) == 0
else await roleUserDao.find_by(
[
{RoleUser.user_id: user.id},
{RoleUser.role_id: {"notIn": obj.get("roles", [])}},
]
)
)
role_ids = [x.role_id for x in roles]
deleted_role_ids = [
x.role_id
for x in await roleUserDao.find_by(
[{RoleUser.user_id: user.id}, {RoleUser.deleted: True}]
)
]
to_create = [
RoleUser(0, x, user.id)
for x in obj.roles
if x not in role_ids and x not in deleted_role_ids
]
to_restore = [
await roleUserDao.get_single_by(
[
{RoleUser.user_id: user.id},
{RoleUser.role_id: x},
]
)
for x in obj.roles
if x not in role_ids and x in deleted_role_ids
]
if len(to_delete) > 0:
await roleUserDao.delete_many(to_delete)
if len(to_create) > 0:
await roleUserDao.create_many(to_create)
if len(to_restore) > 0:
await roleUserDao.restore_many(to_restore)
await self._resolve_assignments(
obj.get("roles", []),
user,
RoleUser.user_id,
RoleUser.role_id,
userDao,
roleUserDao,
RoleUser,
roleDao,
)
return user
@ -113,6 +80,13 @@ class UserMutation(MutationABC):
logger.debug(f"delete user: {id}")
user = await userDao.get_by_id(id)
await userDao.delete(user)
try:
active_user = await Route.get_user_or_default()
if active_user is not None and active_user.id == user.id:
await broadcast.publish("userLogout", user.id)
Keycloak.admin.user_logout(user_id=user.keycloak_id)
except Exception as e:
logger.error(f"Failed to logout user from Keycloak", e)
return True
@staticmethod

View File

@ -0,0 +1,22 @@
from api_graphql.abc.db_history_model_query_abc import DbHistoryModelQueryABC
from data.schemas.permission.api_key_permission_dao import apiKeyPermissionDao
from data.schemas.permission.permission_dao import permissionDao
class ApiKeyHistoryQuery(DbHistoryModelQueryABC):
def __init__(self):
DbHistoryModelQueryABC.__init__(self, "ApiKey")
self.set_field("identifier", lambda x, *_: x.identifier)
self.set_field("key", lambda x, *_: x.key)
self.set_field(
"permissions",
lambda x, *_: self._resolve_foreign_history(
x.updated,
x.id,
apiKeyPermissionDao,
permissionDao,
lambda y: y.permission_id,
obj_key="apikeyid",
),
)

View File

@ -1,9 +1,14 @@
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
from data.schemas.administration.api_key_dao import apiKeyDao
from data.schemas.permission.role_permission_dao import rolePermissionDao
class ApiKeyQuery(DbModelQueryABC):
def __init__(self):
DbModelQueryABC.__init__(self, "ApiKey")
DbModelQueryABC.__init__(self, "ApiKey", apiKeyDao, with_history=True)
self.set_field("identifier", lambda x, *_: x.identifier)
self.set_field("key", lambda x, *_: x.key)
self.set_field("permissions", lambda x, *_: x.permissions)
self.set_history_reference_dao(rolePermissionDao, "apikeyid")

View File

@ -0,0 +1,24 @@
from api_graphql.abc.db_history_model_query_abc import DbHistoryModelQueryABC
from data.schemas.administration.user_dao import userDao
from data.schemas.permission.permission_dao import permissionDao
from data.schemas.permission.role_permission_dao import rolePermissionDao
from data.schemas.permission.role_user_dao import roleUserDao
class RoleHistoryQuery(DbHistoryModelQueryABC):
def __init__(self):
DbHistoryModelQueryABC.__init__(self, "Role")
self.set_field("name", lambda x, *_: x.name)
self.set_field("description", lambda x, *_: x.description)
self.set_field(
"permissions",
lambda x, *_: self._resolve_foreign_history(
x.updated,
x.id,
rolePermissionDao,
permissionDao,
lambda y: y.permission_id,
obj_key="roleid",
),
)

View File

@ -1,11 +1,17 @@
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
from data.schemas.permission.role_dao import roleDao
from data.schemas.permission.role_permission_dao import rolePermissionDao
from data.schemas.permission.role_user_dao import roleUserDao
class RoleQuery(DbModelQueryABC):
def __init__(self):
DbModelQueryABC.__init__(self, "Role")
DbModelQueryABC.__init__(self, "Role", roleDao, with_history=True)
self.set_field("name", lambda x, *_: x.name)
self.set_field("description", lambda x, *_: x.description)
self.set_field("permissions", lambda x, *_: x.permissions)
self.set_field("users", lambda x, *_: x.users)
self.set_history_reference_dao(rolePermissionDao, "roleid")
self.set_history_reference_dao(roleUserDao, "roleid")

View File

@ -0,0 +1,23 @@
from api_graphql.abc.db_history_model_query_abc import DbHistoryModelQueryABC
from data.schemas.permission.role_dao import roleDao
from data.schemas.permission.role_user_dao import roleUserDao
class UserHistoryQuery(DbHistoryModelQueryABC):
def __init__(self):
DbHistoryModelQueryABC.__init__(self, "User")
self.set_field("keycloakId", lambda x, *_: x.keycloak_id)
self.set_field("username", lambda x, *_: x.username)
self.set_field("email", lambda x, *_: x.email)
self.set_field(
"roles",
lambda x, *_: self._resolve_foreign_history(
x.updated,
x.id,
roleUserDao,
roleDao,
lambda y: y.role_id,
obj_key="userid",
),
)

View File

@ -1,11 +1,15 @@
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
from data.schemas.administration.user_dao import userDao
from data.schemas.permission.role_user_dao import roleUserDao
class UserQuery(DbModelQueryABC):
def __init__(self):
DbModelQueryABC.__init__(self, "User")
DbModelQueryABC.__init__(self, "User", userDao, with_history=True)
self.set_field("keycloakId", lambda x, *_: x.keycloak_id)
self.set_field("username", lambda x, *_: x.username)
self.set_field("email", lambda x, *_: x.email)
self.set_field("roles", lambda x, *_: x.roles)
self.set_history_reference_dao(roleUserDao, "userid")

View File

@ -11,7 +11,7 @@ from core.database.external_data_temp_table_builder import ExternalDataTempTable
from core.get_value import get_value
from core.logger import DBLogger
from core.string import camel_to_snake
from core.typing import T, Attribute, AttributeFilters, AttributeSorts
from core.typing import T, Attribute, AttributeFilters, AttributeSorts, Id
T_DBM = TypeVar("T_DBM", bound=DbModelABC)
@ -51,6 +51,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
db_name: str = None,
ignore=False,
primary_key=False,
aliases: list[str] = None,
):
"""
Add an attribute for db and object mapping to the data access object
@ -59,6 +60,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
:param str db_name: Name of the field in the database, if None the attribute lowered attr_name without "_" is used
:param bool ignore: Defines if field is ignored for create and update (for e.g. auto increment fields or created/updated fields)
:param bool primary_key: Defines if field is the primary key
:param list[str] aliases: List of aliases for the attribute name
:return:
"""
if isinstance(attr_name, property):
@ -72,11 +74,20 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
db_name = attr_name.lower().replace("_", "")
self.__db_names[attr_name] = db_name
self.__db_names[db_name] = db_name
if aliases is not None:
for alias in aliases:
if alias in self.__db_names:
raise ValueError(f"Alias {alias} already exists")
self.__db_names[alias] = db_name
if primary_key:
self.__primary_key = db_name
self.__primary_key_type = attr_type
if attr_type in [datetime, datetime.datetime]:
self.__date_attributes.add(attr_name)
self.__date_attributes.add(db_name)
def reference(
@ -156,9 +167,42 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return 0
return result[0]["count"]
async def get_history(
self,
entry_id: int,
by_key: str = None,
when: datetime = None,
until: datetime = None,
without_deleted=False,
) -> list[T_DBM]:
query = f"SELECT {self._table_name}_history.* FROM {self._table_name}_history"
for join in self.__joins:
query += f" {self.__joins[join].replace(self._table_name, f'{self._table_name}_history')}"
query += f" WHERE {f'{self._table_name}_history.{self.__primary_key}' if by_key is None else f'{self._table_name}_history.{by_key}'} = {entry_id}"
if self._default_filter_condition is not None:
query += f" AND {self._default_filter_condition}"
if without_deleted:
query += f" AND {self._table_name}_history.deleted = false"
if when is not None:
query += f" AND {self._attr_from_date_to_char(f'{self._table_name}_history.updated')} = '{when.strftime(DATETIME_FORMAT)}'"
if until is not None:
query += f" AND {self._attr_from_date_to_char(f'{self._table_name}_history.updated')} <= '{until.strftime(DATETIME_FORMAT)}'"
query += f" ORDER BY {self._table_name}_history.updated DESC;"
result = await self._db.select_map(query)
if result is None:
return []
return [self.to_object(x) for x in result]
async def get_all(self) -> list[T_DBM]:
result = await self._db.select_map(
f"SELECT * FROM {self._table_name}{f" WHERE {self._default_filter_condition}" if self._default_filter_condition is not None else ''}"
f"SELECT * FROM {self._table_name}{f" WHERE {self._default_filter_condition}" if self._default_filter_condition is not None else ''} ORDER BY {self.__primary_key};"
)
if result is None:
return []
@ -278,6 +322,35 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
raise ValueError("More than one result found")
return result[0]
async def touch(self, obj: T_DBM):
"""
Touch the entry to update the last updated date
:return:
"""
await self._db.execute(
f"""
UPDATE {self._table_name}
SET updated = NOW()
WHERE {self.__primary_key} = {self._get_primary_key_value_sql(obj)};
"""
)
async def touch_many_by_id(self, ids: list[Id]):
"""
Touch the entries to update the last updated date
:return:
"""
if len(ids) == 0:
return
await self._db.execute(
f"""
UPDATE {self._table_name}
SET updated = NOW()
WHERE {self.__primary_key} IN ({", ".join([str(x) for x in ids])});
"""
)
async def _build_create_statement(self, obj: T_DBM, skip_editor=False) -> str:
allowed_fields = [
x for x in self.__attributes.keys() if x not in self.__ignored_attributes
@ -499,20 +572,41 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
take: int = None,
skip: int = None,
) -> str:
filter_conditions = []
sort_conditions = []
external_table_deps = []
query = f"SELECT {self._table_name}.* FROM {self._table_name}"
for join in self.__joins:
query += f" {self.__joins[join]}"
# Collect dependencies from filters
if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
conditions, external_table_deps = await self._build_conditions(filters)
filter_conditions, filter_deps = await self._build_conditions(filters)
external_table_deps.extend(filter_deps)
# Collect dependencies from sorts
if sorts is not None and (not isinstance(sorts, list) or len(sorts) > 0):
sort_conditions, sort_deps = self._build_order_by(sorts)
external_table_deps.extend(sort_deps)
# Handle external table dependencies before WHERE and ORDER BY
if external_table_deps:
query = await self._handle_query_external_temp_tables(
query, external_table_deps
)
query += f" WHERE {conditions}"
# Add WHERE clause
if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
query += f" WHERE {filter_conditions}"
# Add ORDER BY clause
if sorts is not None and (not isinstance(sorts, list) or len(sorts) > 0):
query += f" ORDER BY {self._build_order_by(sorts)}"
query += f" ORDER BY {sort_conditions}"
if take is not None:
query += f" LIMIT {take}"
if skip is not None:
query += f" OFFSET {skip}"
@ -737,8 +831,10 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
:param value:
:return:
"""
if db_name in self.__date_attributes:
db_name = f"TO_CHAR({db_name}, 'DD.MM.YYYY HH24:MI:SS.US')"
attr = db_name.split(".")[-1]
if attr in self.__date_attributes:
db_name = self._attr_from_date_to_char(db_name)
sql_value = self._get_value_sql(value)
if operator == "equal":
@ -774,12 +870,17 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
else:
raise ValueError(f"Unsupported operator: {operator}")
def _build_order_by(self, sorts: AttributeSorts) -> str:
@staticmethod
def _attr_from_date_to_char(attr: str) -> str:
return f"TO_CHAR({attr}, 'YYYY-MM-DD HH24:MI:SS.US TZ')"
def _build_order_by(self, sorts: AttributeSorts) -> (str, list[str]):
"""
Build SQL order by clause from the given sorts
:param sorts:
:return:
"""
external_field_table_deps = []
if not isinstance(sorts, list):
sorts = [sorts]
@ -791,35 +892,38 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
if attr in self.__foreign_tables:
foreign_table = self.__foreign_tables[attr]
sort_clauses.extend(
self._build_foreign_order_by(foreign_table, direction)
f_sorts, eftd = self._build_foreign_order_by(
foreign_table, direction
)
if eftd:
external_field_table_deps.extend(eftd)
sort_clauses.extend(f_sorts)
continue
match attr:
case "createdUtc":
attr = "created"
case "updatedUtc":
attr = "updated"
if attr.endswith("Utc") and attr.split("Utc")[0].lower() in [
"created",
"updated",
]:
attr = attr.replace("Utc", "")
db_name = self.__db_names[attr]
external_fields_table_name = self._get_external_field_key(attr)
if external_fields_table_name is not None:
external_fields_table = self._external_fields[
external_fields_table_name
]
db_name = f"{external_fields_table.table_name}.{attr}"
external_field_table_deps.append(external_fields_table.table_name)
else:
db_name = self.__db_names[attr]
sort_clauses.append(f"{db_name} {direction.upper()}")
return ", ".join(sort_clauses)
return ", ".join(sort_clauses), external_field_table_deps
def _build_foreign_order_by(self, table: str, direction: str) -> list[str]:
def _build_foreign_order_by(
self, table: str, direction: dict
) -> (list[str], list[str]):
"""
Build SQL order by clause for foreign key references
:param table: Foreign table name
:param direction: Sort direction
:return: List of order by clauses
"""
external_field_table_deps = []
sort_clauses = []
for attr, sub_direction in direction.items():
if isinstance(attr, property):
@ -827,15 +931,25 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
if attr in self.__foreign_tables:
foreign_table = self.__foreign_tables[attr]
sort_clauses.extend(
self._build_foreign_order_by(foreign_table, sub_direction)
)
f_sorts, eftd = self._build_foreign_order_by(foreign_table, direction)
if eftd:
external_field_table_deps.extend(eftd)
sort_clauses.extend(f_sorts)
continue
db_name = f"{table}.{attr.lower().replace('_', '')}"
external_fields_table_name = self._get_external_field_key(attr)
if external_fields_table_name is not None:
external_fields_table = self._external_fields[
external_fields_table_name
]
db_name = f"{external_fields_table.table_name}.{attr}"
external_field_table_deps.append(external_fields_table.table_name)
else:
db_name = f"{table}.{attr.lower().replace('_', '')}"
sort_clauses.append(f"{db_name} {sub_direction.upper()}")
return sort_clauses
return sort_clauses, external_field_table_deps
@staticmethod
async def _get_editor_id(obj: T_DBM):

View File

@ -0,0 +1,19 @@
from datetime import datetime
from typing import Optional
from core.database.abc.db_model_abc import DbModelABC
from core.typing import Id, SerialId
class DbJoinModelABC(DbModelABC):
def __init__(
self,
id: Id,
source_id: Id,
foreign_id: Id,
deleted: bool = False,
editor_id: Optional[SerialId] = None,
created: Optional[datetime] = None,
updated: Optional[datetime] = None,
):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated)

View File

@ -15,5 +15,5 @@ class DbModelDaoABC[T_DBM](DataAccessObjectABC[T_DBM]):
self.attribute(DbModelABC.id, int, ignore=True)
self.attribute(DbModelABC.deleted, bool)
self.attribute(DbModelABC.editor_id, int, ignore=True)
self.attribute(DbModelABC.created, datetime, "createdutc", ignore=True)
self.attribute(DbModelABC.updated, datetime, "updatedutc", ignore=True)
self.attribute(DbModelABC.created, datetime, "created", ignore=True)
self.attribute(DbModelABC.updated, datetime, "updated", ignore=True)

View File

@ -4,11 +4,11 @@ from core.typing import T
def get_value(
source: dict,
key: str,
cast_type: Type[T],
default: Optional[T] = None,
list_delimiter: str = ",",
source: dict,
key: str,
cast_type: Type[T],
default: Optional[T] = None,
list_delimiter: str = ",",
) -> Optional[T]:
"""
Get value from source dictionary and cast it to a specified type.
@ -26,9 +26,14 @@ def get_value(
value = source[key]
if isinstance(
value,
cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__,
value,
cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__,
):
# Handle list[int] case explicitly
if hasattr(cast_type, "__origin__") and cast_type.__origin__ == list:
subtype = cast_type.__args__[0] if hasattr(cast_type, "__args__") else None
if subtype is not None:
return [subtype(item) for item in value]
return value
try:
@ -36,11 +41,11 @@ def get_value(
return value.lower() in ["true", "1"]
if (
cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__
cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__
) == list:
if (
not (value.startswith("[") and value.endswith("]"))
and list_delimiter not in value
not (value.startswith("[") and value.endswith("]"))
and list_delimiter not in value
):
raise ValueError(
"List values must be enclosed in square brackets or use a delimiter."

View File

@ -0,0 +1,2 @@
DROP EXTENSION IF EXISTS fuzzystrmatch;
CREATE EXTENSION fuzzystrmatch SCHEMA public;

View File

@ -0,0 +1,133 @@
ALTER TABLE system._executed_migrations
RENAME COLUMN createdutc TO created;
ALTER TABLE system.files
RENAME COLUMN createdutc TO created;
ALTER TABLE system.files_history
RENAME COLUMN createdutc TO created;
ALTER TABLE public.short_url_visits
RENAME COLUMN createdutc TO created;
ALTER TABLE public.short_url_visits_history
RENAME COLUMN createdutc TO created;
ALTER TABLE system.feature_flags_history
RENAME COLUMN createdutc TO created;
ALTER TABLE public.user_settings_history
RENAME COLUMN createdutc TO created;
ALTER TABLE administration.users
RENAME COLUMN createdutc TO created;
ALTER TABLE administration.users_history
RENAME COLUMN createdutc TO created;
ALTER TABLE public.groups
RENAME COLUMN createdutc TO created;
ALTER TABLE public.groups_history
RENAME COLUMN createdutc TO created;
ALTER TABLE public.short_urls
RENAME COLUMN createdutc TO created;
ALTER TABLE administration.api_keys
RENAME COLUMN createdutc TO created;
ALTER TABLE administration.api_keys_history
RENAME COLUMN createdutc TO created;
ALTER TABLE public.domains
RENAME COLUMN createdutc TO created;
ALTER TABLE public.domains_history
RENAME COLUMN createdutc TO created;
ALTER TABLE public.short_urls_history
RENAME COLUMN createdutc TO created;
ALTER TABLE system.settings
RENAME COLUMN createdutc TO created;
ALTER TABLE public.group_role_assignments
RENAME COLUMN createdutc TO created;
ALTER TABLE public.group_role_assignments_history
RENAME COLUMN createdutc TO created;
ALTER TABLE system.settings_history
RENAME COLUMN createdutc TO created;
ALTER TABLE public.user_settings
RENAME COLUMN createdutc TO created;
ALTER TABLE permission.permissions
RENAME COLUMN createdutc TO created;
ALTER TABLE permission.permissions_history
RENAME COLUMN createdutc TO created;
ALTER TABLE permission.roles
RENAME COLUMN createdutc TO created;
ALTER TABLE permission.roles_history
RENAME COLUMN createdutc TO created;
ALTER TABLE permission.role_permissions
RENAME COLUMN createdutc TO created;
ALTER TABLE permission.role_permissions_history
RENAME COLUMN createdutc TO created;
ALTER TABLE permission.role_users
RENAME COLUMN createdutc TO created;
ALTER TABLE permission.role_users_history
RENAME COLUMN createdutc TO created;
ALTER TABLE permission.api_key_permissions
RENAME COLUMN createdutc TO created;
ALTER TABLE permission.api_key_permissions_history
RENAME COLUMN createdutc TO created;
ALTER TABLE system.feature_flags
RENAME COLUMN createdutc TO created;
ALTER TABLE system._executed_migrations
RENAME COLUMN updatedutc TO updated;
ALTER TABLE system.files
RENAME COLUMN updatedutc TO updated;
ALTER TABLE system.files_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.short_url_visits
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.short_url_visits_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE system.feature_flags_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.user_settings_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE administration.users
RENAME COLUMN updatedutc TO updated;
ALTER TABLE administration.users_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.groups
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.groups_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.short_urls
RENAME COLUMN updatedutc TO updated;
ALTER TABLE administration.api_keys
RENAME COLUMN updatedutc TO updated;
ALTER TABLE administration.api_keys_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.domains
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.domains_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.short_urls_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE system.settings
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.group_role_assignments
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.group_role_assignments_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE system.settings_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE public.user_settings
RENAME COLUMN updatedutc TO updated;
ALTER TABLE permission.permissions
RENAME COLUMN updatedutc TO updated;
ALTER TABLE permission.permissions_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE permission.roles
RENAME COLUMN updatedutc TO updated;
ALTER TABLE permission.roles_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE permission.role_permissions
RENAME COLUMN updatedutc TO updated;
ALTER TABLE permission.role_permissions_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE permission.role_users
RENAME COLUMN updatedutc TO updated;
ALTER TABLE permission.role_users_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE permission.api_key_permissions
RENAME COLUMN updatedutc TO updated;
ALTER TABLE permission.api_key_permissions_history
RENAME COLUMN updatedutc TO updated;
ALTER TABLE system.feature_flags
RENAME COLUMN updatedutc TO updated;

View File

@ -0,0 +1,37 @@
CREATE OR REPLACE FUNCTION public.history_trigger_function()
RETURNS TRIGGER AS
$$
DECLARE
schema_name TEXT;
history_table_name TEXT;
BEGIN
-- Construct the name of the history table based on the current table
schema_name := TG_TABLE_SCHEMA;
history_table_name := TG_TABLE_NAME || '_history';
IF (TG_OP = 'INSERT') THEN
RETURN NEW;
END IF;
-- Insert the old row into the history table on UPDATE or DELETE
IF (TG_OP = 'UPDATE' OR TG_OP = 'DELETE') THEN
EXECUTE format(
'INSERT INTO %I.%I SELECT ($1).*',
schema_name,
history_table_name
)
USING OLD;
END IF;
-- For UPDATE, update the UpdatedUtc column and return the new row
IF (TG_OP = 'UPDATE') THEN
NEW.updated := NOW(); -- Update the UpdatedUtc column
RETURN NEW;
END IF;
-- For DELETE, return OLD to allow the deletion
IF (TG_OP = 'DELETE') THEN
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1,23 @@
ALTER TABLE permission.role_permissions
ADD CONSTRAINT unique_role_permission
UNIQUE (roleid, permissionid);
ALTER TABLE permission.api_key_permissions
ADD CONSTRAINT unique_api_key_permission
UNIQUE (apikeyid, permissionid);
ALTER TABLE permission.role_users
ADD CONSTRAINT unique_role_user
UNIQUE (roleid, userid);
ALTER TABLE public.user_settings
ADD CONSTRAINT unique_user_setting
UNIQUE (userid, key);
ALTER TABLE system.settings
ADD CONSTRAINT unique_system_setting
UNIQUE (key);
ALTER TABLE system.feature_flags
ADD CONSTRAINT unique_feature_flag
UNIQUE (key);