Compare commits

..

No commits in common. "#15_user_spaces" and "dev" have entirely different histories.

22 changed files with 87 additions and 308 deletions

View File

@ -120,7 +120,7 @@ class QueryABC(ObjectType):
skip = None
if field.default_filter:
filters.append(await field.default_filter(*args, **kwargs))
filters.append(field.default_filter(*args, **kwargs))
if field.filter_type and "filter" in kwargs:
in_filters = kwargs["filter"]

View File

@ -11,6 +11,3 @@ class GroupFilter(DbModelFilterABC):
self.add_field("name", StringFilter)
self.add_field("description", StringFilter)
self.add_field("isNull", bool)
self.add_field("isNotNull", bool)

View File

@ -39,8 +39,8 @@ input StringFilter {
startsWith: String
endsWith: String
isNull: Boolean
isNotNull: Boolean
isNull: String
isNotNull: String
}
input IntFilter {
@ -51,8 +51,8 @@ input IntFilter {
less: Int
lessOrEqual: Int
isNull: Boolean
isNotNull: Boolean
isNull: Int
isNotNull: Int
in: [Int]
notIn: [Int]
}
@ -61,8 +61,8 @@ input BooleanFilter {
equal: Boolean
notEqual: Int
isNull: Boolean
isNotNull: Boolean
isNull: Int
isNotNull: Int
}
input DateFilter {
@ -78,8 +78,8 @@ input DateFilter {
contains: String
notContains: String
isNull: Boolean
isNotNull: Boolean
isNull: String
isNotNull: String
in: [String]
notIn: [String]

View File

@ -61,9 +61,6 @@ input GroupFilter {
editor: IntFilter
created: DateFilter
updated: DateFilter
isNull: Boolean
isNotNull: Boolean
}
type GroupMutation {

View File

@ -1,11 +1,8 @@
from typing import Optional
from api.route import Route
from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.input.group_create_input import GroupCreateInput
from api_graphql.input.group_update_input import GroupUpdateInput
from core.configuration.feature_flags import FeatureFlags
from core.configuration.feature_flags_enum import FeatureFlagsEnum
from core.logger import APILogger
from data.schemas.public.group import Group
from data.schemas.public.group_dao import groupDao
@ -78,11 +75,6 @@ class GroupMutation(MutationABC):
group = Group(
0,
obj.name,
(
(await Route.get_user()).id
if await FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup)
else None
),
)
gid = await groupDao.create(group)

View File

@ -1,9 +1,6 @@
from api.route import Route
from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.input.short_url_create_input import ShortUrlCreateInput
from api_graphql.input.short_url_update_input import ShortUrlUpdateInput
from core.configuration.feature_flags import FeatureFlags
from core.configuration.feature_flags_enum import FeatureFlagsEnum
from core.logger import APILogger
from data.schemas.public.domain_dao import domainDao
from data.schemas.public.group_dao import groupDao
@ -60,11 +57,6 @@ class ShortUrlMutation(MutationABC):
obj.group_id,
obj.domain_id,
obj.loading_screen,
(
(await Route.get_user()).id
if await FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup)
else None
),
)
nid = await shortUrlDao.create(short_url)
return await shortUrlDao.get_by_id(nid)

View File

@ -1,9 +1,7 @@
from api.route import Route
from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
from api_graphql.input.user_setting_input import UserSettingInput
from core.logger import APILogger
from core.string import first_to_lower
from data.schemas.public.user_setting import UserSetting
from data.schemas.public.user_setting_dao import userSettingsDao
from data.schemas.system.setting_dao import settingsDao
@ -15,20 +13,13 @@ logger = APILogger(__name__)
class UserSettingMutation(MutationABC):
def __init__(self):
MutationABC.__init__(self, "UserSetting")
self.field(
MutationFieldBuilder("change")
.with_resolver(self.resolve_change)
.with_change_broadcast(
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
)
.with_input(UserSettingInput, "input")
.with_require_any([], [self._x])
self.mutation(
"change",
self.resolve_change,
UserSettingInput,
require_any_permission=[Permissions.settings_update],
)
@staticmethod
async def _x(ctx):
return ctx.data.user_id == (await Route.get_user()).id
@staticmethod
async def resolve_change(obj: UserSettingInput, *_):
logger.debug(f"create new setting: {input}")

View File

@ -1,6 +1,6 @@
from api_graphql.abc.db_history_model_query_abc import DbHistoryModelQueryABC
from api_graphql.field.resolver_field_builder import ResolverFieldBuilder
from api_graphql.require_any_resolvers import by_assignment_resolver
from api_graphql.require_any_resolvers import group_by_assignment_resolver
from data.schemas.public.group import Group
from data.schemas.public.group_dao import groupDao
from data.schemas.public.group_role_assignment_dao import groupRoleAssignmentDao
@ -21,7 +21,7 @@ class GroupHistoryQuery(DbHistoryModelQueryABC):
[
Permissions.groups,
],
[by_assignment_resolver],
[group_by_assignment_resolver],
)
)
self.set_field(

View File

@ -1,6 +1,6 @@
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
from api_graphql.field.resolver_field_builder import ResolverFieldBuilder
from api_graphql.require_any_resolvers import by_assignment_resolver
from api_graphql.require_any_resolvers import group_by_assignment_resolver
from data.schemas.public.group import Group
from data.schemas.public.group_dao import groupDao
from data.schemas.public.group_role_assignment_dao import groupRoleAssignmentDao
@ -21,7 +21,7 @@ class GroupQuery(DbModelQueryABC):
[
Permissions.groups,
],
[by_assignment_resolver],
[group_by_assignment_resolver],
)
)
self.set_field("roles", self._get_roles)

View File

@ -11,12 +11,7 @@ from api_graphql.filter.permission_filter import PermissionFilter
from api_graphql.filter.role_filter import RoleFilter
from api_graphql.filter.short_url_filter import ShortUrlFilter
from api_graphql.filter.user_filter import UserFilter
from api_graphql.require_any_resolvers import (
by_assignment_resolver,
by_user_setup_resolver,
)
from core.configuration.feature_flags import FeatureFlags
from core.configuration.feature_flags_enum import FeatureFlagsEnum
from api_graphql.require_any_resolvers import group_by_assignment_resolver
from data.schemas.administration.api_key import ApiKey
from data.schemas.administration.api_key_dao import apiKeyDao
from data.schemas.administration.user import User
@ -114,8 +109,7 @@ class Query(QueryABC):
]
)
)
group_field = (
self.field(
DaoFieldBuilder("groups")
.with_dao(groupDao)
.with_filter(GroupFilter)
@ -126,33 +120,15 @@ class Query(QueryABC):
Permissions.short_urls_create,
Permissions.short_urls_update,
],
[by_assignment_resolver, by_user_setup_resolver],
[group_by_assignment_resolver],
)
)
if FeatureFlags.get_default(FeatureFlagsEnum.per_user_setup):
group_field = group_field.with_default_filter(self._resolve_default_user_filter)
self.field(
group_field
)
short_url_field = (
DaoFieldBuilder("shortUrls")
.with_dao(shortUrlDao)
.with_filter(ShortUrlFilter)
.with_sort(Sort[ShortUrl])
.with_require_any(
[Permissions.short_urls],
[by_assignment_resolver, by_user_setup_resolver],
)
)
if FeatureFlags.get_default(FeatureFlagsEnum.per_user_setup):
short_url_field = short_url_field.with_default_filter(self._resolve_default_user_filter)
self.field(
short_url_field
.with_require_any([Permissions.short_urls], [group_by_assignment_resolver])
)
self.field(
@ -226,7 +202,3 @@ class Query(QueryABC):
if "key" in kwargs:
return [await featureFlagDao.find_by_key(kwargs["key"])]
return await featureFlagDao.get_all()
@staticmethod
async def _resolve_default_user_filter(*args, **kwargs) -> dict:
return {"user": {"id": {"equal": (await Route.get_user()).id}}}

View File

@ -1,12 +1,10 @@
from api_graphql.service.collection_result import CollectionResult
from api_graphql.service.query_context import QueryContext
from core.configuration.feature_flags import FeatureFlags
from core.configuration.feature_flags_enum import FeatureFlagsEnum
from data.schemas.public.group_dao import groupDao
from service.permission.permissions_enum import Permissions
async def by_assignment_resolver(ctx: QueryContext) -> bool:
async def group_by_assignment_resolver(ctx: QueryContext) -> bool:
if not isinstance(ctx.data, CollectionResult):
return False
@ -21,19 +19,12 @@ async def by_assignment_resolver(ctx: QueryContext) -> bool:
and all(r.id in role_ids for r in roles)
]
return all(
(await node.group) is not None and (await node.group).id in filtered_groups
ctx.data.nodes = [
node
for node in ctx.data.nodes
)
if (await node.group) is not None
and (await node.group).id in filtered_groups
]
return True
return False
async def by_user_setup_resolver(ctx: QueryContext) -> bool:
if not isinstance(ctx.data, CollectionResult):
return False
if not FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup):
return False
return all(x.user_setup_id == ctx.user.id for x in ctx.data.nodes)
return True

View File

@ -1,38 +1,20 @@
from typing import Union
from core.configuration.feature_flags_enum import FeatureFlagsEnum
from core.environment import Environment
from data.schemas.system.feature_flag_dao import featureFlagDao
class FeatureFlags:
_flags = {
FeatureFlagsEnum.version_endpoint.value: True, # 15.01.2025
FeatureFlagsEnum.technical_demo_banner.value: False, # 18.04.2025
FeatureFlagsEnum.per_user_setup.value: Environment.get(
"PER_USER_SETUP", bool, False
), # 18.04.2025
}
_overwrite_flags = [
FeatureFlagsEnum.per_user_setup.value,
]
@staticmethod
def overwrite_flag(key: str):
return key in FeatureFlags._overwrite_flags
@staticmethod
def get_default(key: FeatureFlagsEnum) -> bool:
return FeatureFlags._flags[key.value]
@staticmethod
async def has_feature(key: Union[str, FeatureFlagsEnum]) -> bool:
key_value = key.value if isinstance(key, FeatureFlagsEnum) else key
async def has_feature(key: FeatureFlagsEnum) -> bool:
value = await featureFlagDao.find_by_key(key.value)
if value is None:
return False
value = await featureFlagDao.find_by_key(key_value)
return (
value.value
if value
else FeatureFlags.get_default(FeatureFlagsEnum(key_value))
)
return value.value

View File

@ -2,6 +2,5 @@ from enum import Enum
class FeatureFlagsEnum(Enum):
# modules
version_endpoint = "VersionEndpoint"
technical_demo_banner = "TechnicalDemoBanner"
per_user_setup = "PerUserSetup"

View File

@ -18,22 +18,6 @@ T_DBM = TypeVar("T_DBM", bound=DbModelABC)
class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
_external_fields: dict[str, ExternalDataTempTableBuilder] = {}
_operators = [
"equal",
"notEqual",
"greater",
"greaterOrEqual",
"less",
"lessOrEqual",
"isNull",
"isNotNull",
"contains",
"notContains",
"startsWith",
"endsWith",
"in",
"notIn",
]
@abstractmethod
def __init__(self, source: str, model_type: Type[T_DBM], table_name: str):
@ -662,9 +646,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
if attr in self.__foreign_tables:
foreign_table = self.__foreign_tables[attr]
cons, eftd = self._build_foreign_conditions(
attr, foreign_table, values
)
cons, eftd = self._build_foreign_conditions(foreign_table, values)
if eftd:
external_field_table_deps.extend(eftd)
@ -688,8 +670,6 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
isinstance(values, dict) or isinstance(values, list)
) and not attr in self.__foreign_tables:
db_name = f"{self._table_name}.{self.__db_names[attr]}"
elif attr in self._operators:
db_name = f"{self._table_name}.{self.__db_names[attr]}"
else:
db_name = self.__db_names[attr]
@ -711,8 +691,6 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
self._get_value_validation_sql(db_name, value)
)
f_conditions.append(f"({' OR '.join(sub_conditions)})")
elif attr in self._operators:
conditions.append(f"{self._build_condition(db_name, attr, values)}")
else:
f_conditions.append(self._get_value_validation_sql(db_name, values))
@ -733,11 +711,10 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return conditions
def _build_foreign_conditions(
self, base_attr: str, table: str, values: dict
self, table: str, values: dict
) -> (list[str], list[str]):
"""
Build SQL conditions for foreign key references
:param base_attr: Base attribute name
:param table: Foreign table name
:param values: Filter values
:return: List of conditions, List of external field tables
@ -751,7 +728,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
if attr in self.__foreign_tables:
foreign_table = self.__foreign_tables[attr]
sub_conditions, eftd = self._build_foreign_conditions(
attr, foreign_table, sub_values
foreign_table, sub_values
)
if len(eftd) > 0:
external_field_table_deps.extend(eftd)
@ -772,8 +749,6 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
]
db_name = f"{external_fields_table.table_name}.{attr}"
external_field_table_deps.append(external_fields_table.table_name)
elif attr in self._operators:
db_name = f"{self._table_name}.{self.__foreign_table_keys[base_attr]}"
else:
db_name = f"{table}.{attr.lower().replace('_', '')}"
@ -795,8 +770,6 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
self._get_value_validation_sql(db_name, value)
)
conditions.append(f"({' OR '.join(sub_conditions)})")
elif attr in self._operators:
conditions.append(f"{self._build_condition(db_name, attr, sub_values)}")
else:
conditions.append(self._get_value_validation_sql(db_name, sub_values))
@ -842,11 +815,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
def _get_value_validation_sql(self, field: str, value: Any):
value = self._get_value_sql(value)
field_selector = (
f"{self._table_name}.{field}"
if not field.startswith(self._table_name)
else field
)
field_selector = f"{self._table_name}.{field}"
if field in self.__foreign_tables:
field_selector = self.__db_names[field]
@ -881,9 +850,9 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
elif operator == "lessOrEqual":
return f"{db_name} <= {sql_value}"
elif operator == "isNull":
return f"{db_name} IS NULL" if sql_value else f"{db_name} IS NOT NULL"
return f"{db_name} IS NULL"
elif operator == "isNotNull":
return f"{db_name} IS NOT NULL" if sql_value else f"{db_name} IS NULL"
return f"{db_name} IS NOT NULL"
elif operator == "contains":
return f"{db_name} LIKE '%{value}%'"
elif operator == "notContains":

View File

@ -1,8 +1,6 @@
from datetime import datetime
from typing import Optional
from async_property import async_property
from core.database.abc.db_model_abc import DbModelABC
from core.typing import SerialId
@ -12,7 +10,6 @@ class Group(DbModelABC):
self,
id: SerialId,
name: str,
user_id: Optional[SerialId] = None,
deleted: bool = False,
editor_id: Optional[SerialId] = None,
created: Optional[datetime] = None,
@ -20,7 +17,6 @@ class Group(DbModelABC):
):
DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
self._name = name
self._user_id = user_id
@property
def name(self) -> str:
@ -29,17 +25,3 @@ class Group(DbModelABC):
@name.setter
def name(self, value: str):
self._name = value
@property
def user_id(self) -> Optional[SerialId]:
return self._user_id
@async_property
async def user(self):
if self._user_id is None:
return None
from data.schemas.administration.user_dao import userDao
user = await userDao.get_by_id(self.user_id)
return user

View File

@ -11,9 +11,6 @@ class GroupDao(DbModelDaoABC[Group]):
DbModelDaoABC.__init__(self, __name__, Group, "public.groups")
self.attribute(Group.name, str)
self.attribute(Group.user_id, int)
self.reference("user", "id", Group.user_id, "administration.users")
async def get_by_name(self, name: str) -> Group:
result = await self._db.select_map(
f"SELECT * FROM {self._table_name} WHERE Name = '{name}'"

View File

@ -18,7 +18,6 @@ class ShortUrl(DbModelABC):
group_id: Optional[SerialId],
domain_id: Optional[SerialId],
loading_screen: Optional[str] = None,
user_id: Optional[SerialId] = None,
deleted: bool = False,
editor_id: Optional[SerialId] = None,
created: Optional[datetime] = None,
@ -35,8 +34,6 @@ class ShortUrl(DbModelABC):
loading_screen = False
self._loading_screen = loading_screen
self._user_id = user_id
@property
def short_url(self) -> str:
return self._short_url
@ -109,20 +106,6 @@ class ShortUrl(DbModelABC):
def loading_screen(self, value: Optional[str]):
self._loading_screen = value
@property
def user_id(self) -> Optional[SerialId]:
return self._user_id
@async_property
async def user(self):
if self._user_id is None:
return None
from data.schemas.administration.user_dao import userDao
user = await userDao.get_by_id(self.user_id)
return user
def to_dto(self) -> dict:
return {
"id": self.id,

View File

@ -18,8 +18,5 @@ class ShortUrlDao(DbModelDaoABC[ShortUrl]):
self.reference("domain", "id", ShortUrl.domain_id, "public.domains")
self.attribute(ShortUrl.loading_screen, bool)
self.attribute(ShortUrl.user_id, int)
self.reference("user", "id", ShortUrl.user_id, "administration.users")
shortUrlDao = ShortUrlDao()

View File

@ -1,11 +0,0 @@
ALTER TABLE public.groups
ADD COLUMN IF NOT EXISTS UserId INT NULL REFERENCES administration.users (Id);
ALTER TABLE public.groups_history
ADD COLUMN IF NOT EXISTS UserId INT NULL REFERENCES administration.users (Id);
ALTER TABLE public.short_urls
ADD COLUMN IF NOT EXISTS UserId INT NULL REFERENCES administration.users (Id);
ALTER TABLE public.short_urls_history
ADD COLUMN IF NOT EXISTS UserId INT NULL REFERENCES administration.users (Id);

View File

@ -21,7 +21,6 @@ class FeatureFlagsSeeder(DataSeederABC):
x.value: FeatureFlags.get_default(x) for x in FeatureFlagsEnum
}
# Create new feature flags
to_create = [
FeatureFlag(0, x, possible_feature_flags[x])
for x in possible_feature_flags.keys()
@ -32,19 +31,6 @@ class FeatureFlagsSeeder(DataSeederABC):
to_create_dicts = {x.key: x.value for x in to_create}
logger.debug(f"Created feature flags: {to_create_dicts}")
# Update existing feature flags if they can be overwritten and have a different value
to_update = [
FeatureFlag(x.id, x.key, possible_feature_flags[x.key])
for x in feature_flags
if FeatureFlags.overwrite_flag(x.key)
and x.value != possible_feature_flags[x.key]
]
if len(to_update) > 0:
await featureFlagDao.update_many(to_update)
to_update_dicts = {x.key: x.value for x in to_update}
logger.debug(f"Updated feature flags: {to_update_dicts}")
# Delete feature flags that are no longer defined
to_delete = [
x for x in feature_flags if x.key not in possible_feature_flags.keys()
]

View File

@ -1,5 +1,5 @@
import { Injectable, Provider } from '@angular/core';
import { forkJoin, merge, Observable } from 'rxjs';
import { merge, Observable } from 'rxjs';
import {
Create,
Delete,
@ -48,80 +48,50 @@ export class ShortUrlsDataService
skip?: number | undefined,
take?: number | undefined
): Observable<QueryResult<ShortUrl>> {
const query1 = this.apollo.query<{ shortUrls: QueryResult<ShortUrl> }>({
query: gql`
query getShortUrls($filter: [ShortUrlFilter], $sort: [ShortUrlSort]) {
shortUrls(filter: $filter, sort: $sort) {
nodes {
id
shortUrl
targetUrl
description
loadingScreen
visits
group {
return this.apollo
.query<{ shortUrls: QueryResult<ShortUrl> }>({
query: gql`
query getShortUrls($filter: [ShortUrlFilter], $sort: [ShortUrlSort]) {
shortUrls(filter: $filter, sort: $sort) {
count
totalCount
nodes {
id
name
}
domain {
id
name
shortUrl
targetUrl
description
loadingScreen
visits
group {
id
name
}
domain {
id
name
}
...DB_MODEL
}
}
}
}
`,
variables: {
filter: [{ group: { deleted: { equal: false } } }, ...(filter ?? [])],
sort: [{ id: SortOrder.DESC }, ...(sort ?? [])],
skip,
take,
},
});
const query2 = this.apollo.query<{ shortUrls: QueryResult<ShortUrl> }>({
query: gql`
query getShortUrls($filter: [ShortUrlFilter], $sort: [ShortUrlSort]) {
shortUrls(filter: $filter, sort: $sort) {
nodes {
id
shortUrl
targetUrl
description
loadingScreen
visits
group {
id
name
}
domain {
id
name
}
}
}
}
`,
variables: {
filter: [{ group: { isNull: true } }, ...(filter ?? [])],
sort: [{ id: SortOrder.DESC }, ...(sort ?? [])],
skip,
take,
},
});
return forkJoin([query1, query2]).pipe(
map(([result1, result2]) => {
const nodes = [
...result1.data.shortUrls.nodes,
...result2.data.shortUrls.nodes,
];
const uniqueNodes = Array.from(
new Map(nodes.map(node => [node.id, node])).values()
);
return { ...result1.data.shortUrls, nodes: uniqueNodes };
${DB_MODEL_FRAGMENT}
`,
variables: {
filter: [{ group: { deleted: { equal: false } } }, ...(filter ?? [])],
sort: [{ id: SortOrder.DESC }, ...(sort ?? [])],
skip: skip,
take: take,
},
})
);
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data.shortUrls));
}
loadById(id: number): Observable<ShortUrl> {

View File

@ -3,7 +3,6 @@ import { BehaviorSubject } from 'rxjs';
import { MenuElement } from 'src/app/model/view/menu-element';
import { AuthService } from 'src/app/service/auth.service';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { FeatureFlagService } from 'src/app/service/feature-flag.service';
@Injectable({
providedIn: 'root',
@ -12,10 +11,7 @@ export class SidebarService {
visible$ = new BehaviorSubject<boolean>(true);
elements$ = new BehaviorSubject<MenuElement[]>([]);
constructor(
private auth: AuthService,
private featureFlags: FeatureFlagService
) {
constructor(private auth: AuthService) {
this.auth.user$.subscribe(async () => {
await this.setElements();
});
@ -44,19 +40,16 @@ export class SidebarService {
label: 'common.groups',
icon: 'pi pi-tags',
routerLink: ['/admin/groups'],
visible:
(await this.auth.hasAnyPermissionLazy([PermissionsEnum.groups])) ||
(await this.featureFlags.get('PerUserSetup')),
visible: await this.auth.hasAnyPermissionLazy([PermissionsEnum.groups]),
},
{
label: 'common.urls',
icon: 'pi pi-tag',
routerLink: ['/admin/urls'],
visible:
(await this.auth.hasAnyPermissionLazy([
PermissionsEnum.shortUrls,
PermissionsEnum.shortUrlsByAssignment,
])) || (await this.featureFlags.get('PerUserSetup')),
visible: await this.auth.hasAnyPermissionLazy([
PermissionsEnum.shortUrls,
PermissionsEnum.shortUrlsByAssignment,
]),
},
await this.sectionAdmin(),
];