diff --git a/api/src/api_graphql/abc/query_abc.py b/api/src/api_graphql/abc/query_abc.py index 4b947b6..f9865b9 100644 --- a/api/src/api_graphql/abc/query_abc.py +++ b/api/src/api_graphql/abc/query_abc.py @@ -120,7 +120,7 @@ class QueryABC(ObjectType): skip = None if field.default_filter: - filters.append(field.default_filter(*args, **kwargs)) + filters.append(await field.default_filter(*args, **kwargs)) if field.filter_type and "filter" in kwargs: in_filters = kwargs["filter"] diff --git a/api/src/api_graphql/filter/group_filter.py b/api/src/api_graphql/filter/group_filter.py index 0ebad4e..f71148d 100644 --- a/api/src/api_graphql/filter/group_filter.py +++ b/api/src/api_graphql/filter/group_filter.py @@ -11,3 +11,6 @@ class GroupFilter(DbModelFilterABC): self.add_field("name", StringFilter) self.add_field("description", StringFilter) + + self.add_field("isNull", bool) + self.add_field("isNotNull", bool) diff --git a/api/src/api_graphql/graphql/base.gql b/api/src/api_graphql/graphql/base.gql index f389136..359cbcf 100644 --- a/api/src/api_graphql/graphql/base.gql +++ b/api/src/api_graphql/graphql/base.gql @@ -39,8 +39,8 @@ input StringFilter { startsWith: String endsWith: String - isNull: String - isNotNull: String + isNull: Boolean + isNotNull: Boolean } input IntFilter { @@ -51,8 +51,8 @@ input IntFilter { less: Int lessOrEqual: Int - isNull: Int - isNotNull: Int + isNull: Boolean + isNotNull: Boolean in: [Int] notIn: [Int] } @@ -61,8 +61,8 @@ input BooleanFilter { equal: Boolean notEqual: Int - isNull: Int - isNotNull: Int + isNull: Boolean + isNotNull: Boolean } input DateFilter { @@ -78,8 +78,8 @@ input DateFilter { contains: String notContains: String - isNull: String - isNotNull: String + isNull: Boolean + isNotNull: Boolean in: [String] notIn: [String] diff --git a/api/src/api_graphql/graphql/group.gql b/api/src/api_graphql/graphql/group.gql index 2979c4d..f7f9315 100644 --- a/api/src/api_graphql/graphql/group.gql +++ b/api/src/api_graphql/graphql/group.gql @@ -61,6 +61,9 @@ input GroupFilter { editor: IntFilter created: DateFilter updated: DateFilter + + isNull: Boolean + isNotNull: Boolean } type GroupMutation { diff --git a/api/src/api_graphql/mutations/group_mutation.py b/api/src/api_graphql/mutations/group_mutation.py index 4372cb1..6f0b1dd 100644 --- a/api/src/api_graphql/mutations/group_mutation.py +++ b/api/src/api_graphql/mutations/group_mutation.py @@ -1,8 +1,11 @@ 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 @@ -75,6 +78,11 @@ 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) diff --git a/api/src/api_graphql/mutations/short_url_mutation.py b/api/src/api_graphql/mutations/short_url_mutation.py index d07478d..d229eaf 100644 --- a/api/src/api_graphql/mutations/short_url_mutation.py +++ b/api/src/api_graphql/mutations/short_url_mutation.py @@ -1,6 +1,9 @@ +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 @@ -57,6 +60,11 @@ 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) diff --git a/api/src/api_graphql/mutations/user_setting_mutation.py b/api/src/api_graphql/mutations/user_setting_mutation.py index 1fb012d..bca1a9b 100644 --- a/api/src/api_graphql/mutations/user_setting_mutation.py +++ b/api/src/api_graphql/mutations/user_setting_mutation.py @@ -1,7 +1,9 @@ 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 @@ -13,13 +15,20 @@ logger = APILogger(__name__) class UserSettingMutation(MutationABC): def __init__(self): MutationABC.__init__(self, "UserSetting") - self.mutation( - "change", - self.resolve_change, - UserSettingInput, - require_any_permission=[Permissions.settings_update], + 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]) ) + @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}") diff --git a/api/src/api_graphql/queries/group_history_query.py b/api/src/api_graphql/queries/group_history_query.py index 2c062c6..dbfe220 100644 --- a/api/src/api_graphql/queries/group_history_query.py +++ b/api/src/api_graphql/queries/group_history_query.py @@ -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 group_by_assignment_resolver +from api_graphql.require_any_resolvers import 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, ], - [group_by_assignment_resolver], + [by_assignment_resolver], ) ) self.set_field( diff --git a/api/src/api_graphql/queries/group_query.py b/api/src/api_graphql/queries/group_query.py index 7effacc..8d5b559 100644 --- a/api/src/api_graphql/queries/group_query.py +++ b/api/src/api_graphql/queries/group_query.py @@ -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 group_by_assignment_resolver +from api_graphql.require_any_resolvers import 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, ], - [group_by_assignment_resolver], + [by_assignment_resolver], ) ) self.set_field("roles", self._get_roles) diff --git a/api/src/api_graphql/query.py b/api/src/api_graphql/query.py index 6e999f6..0b9908e 100644 --- a/api/src/api_graphql/query.py +++ b/api/src/api_graphql/query.py @@ -11,7 +11,12 @@ 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 group_by_assignment_resolver +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 data.schemas.administration.api_key import ApiKey from data.schemas.administration.api_key_dao import apiKeyDao from data.schemas.administration.user import User @@ -109,7 +114,8 @@ class Query(QueryABC): ] ) ) - self.field( + + group_field = ( DaoFieldBuilder("groups") .with_dao(groupDao) .with_filter(GroupFilter) @@ -120,15 +126,33 @@ class Query(QueryABC): Permissions.short_urls_create, Permissions.short_urls_update, ], - [group_by_assignment_resolver], + [by_assignment_resolver, by_user_setup_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], [group_by_assignment_resolver]) + .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 ) self.field( @@ -202,3 +226,7 @@ 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}}} diff --git a/api/src/api_graphql/require_any_resolvers.py b/api/src/api_graphql/require_any_resolvers.py index 0d31a90..2032dcf 100644 --- a/api/src/api_graphql/require_any_resolvers.py +++ b/api/src/api_graphql/require_any_resolvers.py @@ -1,10 +1,12 @@ 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 group_by_assignment_resolver(ctx: QueryContext) -> bool: +async def by_assignment_resolver(ctx: QueryContext) -> bool: if not isinstance(ctx.data, CollectionResult): return False @@ -19,12 +21,19 @@ async def group_by_assignment_resolver(ctx: QueryContext) -> bool: and all(r.id in role_ids for r in roles) ] - ctx.data.nodes = [ - node + return all( + (await node.group) is not None and (await node.group).id in filtered_groups for node in ctx.data.nodes - if (await node.group) is not None - and (await node.group).id in filtered_groups - ] - return True + ) - 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) diff --git a/api/src/core/configuration/feature_flags.py b/api/src/core/configuration/feature_flags.py index 46b83d0..8eaa333 100644 --- a/api/src/core/configuration/feature_flags.py +++ b/api/src/core/configuration/feature_flags.py @@ -1,20 +1,38 @@ +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: FeatureFlagsEnum) -> bool: - value = await featureFlagDao.find_by_key(key.value) - if value is None: - return False + async def has_feature(key: Union[str, FeatureFlagsEnum]) -> bool: + key_value = key.value if isinstance(key, FeatureFlagsEnum) else key - return value.value + value = await featureFlagDao.find_by_key(key_value) + return ( + value.value + if value + else FeatureFlags.get_default(FeatureFlagsEnum(key_value)) + ) diff --git a/api/src/core/configuration/feature_flags_enum.py b/api/src/core/configuration/feature_flags_enum.py index c5f48c7..c5af428 100644 --- a/api/src/core/configuration/feature_flags_enum.py +++ b/api/src/core/configuration/feature_flags_enum.py @@ -2,5 +2,6 @@ from enum import Enum class FeatureFlagsEnum(Enum): - # modules version_endpoint = "VersionEndpoint" + technical_demo_banner = "TechnicalDemoBanner" + per_user_setup = "PerUserSetup" diff --git a/api/src/core/database/abc/data_access_object_abc.py b/api/src/core/database/abc/data_access_object_abc.py index e83b684..4a8092f 100644 --- a/api/src/core/database/abc/data_access_object_abc.py +++ b/api/src/core/database/abc/data_access_object_abc.py @@ -18,6 +18,22 @@ 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): @@ -646,7 +662,9 @@ 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(foreign_table, values) + cons, eftd = self._build_foreign_conditions( + attr, foreign_table, values + ) if eftd: external_field_table_deps.extend(eftd) @@ -670,6 +688,8 @@ 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] @@ -691,6 +711,8 @@ 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)) @@ -711,10 +733,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]): return conditions def _build_foreign_conditions( - self, table: str, values: dict + self, base_attr: str, 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 @@ -728,7 +751,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( - foreign_table, sub_values + attr, foreign_table, sub_values ) if len(eftd) > 0: external_field_table_deps.extend(eftd) @@ -749,6 +772,8 @@ 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('_', '')}" @@ -770,6 +795,8 @@ 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)) @@ -815,7 +842,11 @@ 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}" + field_selector = ( + f"{self._table_name}.{field}" + if not field.startswith(self._table_name) + else field + ) if field in self.__foreign_tables: field_selector = self.__db_names[field] @@ -850,9 +881,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" + return f"{db_name} IS NULL" if sql_value else f"{db_name} IS NOT NULL" elif operator == "isNotNull": - return f"{db_name} IS NOT NULL" + return f"{db_name} IS NOT NULL" if sql_value else f"{db_name} IS NULL" elif operator == "contains": return f"{db_name} LIKE '%{value}%'" elif operator == "notContains": diff --git a/api/src/data/schemas/public/group.py b/api/src/data/schemas/public/group.py index ddd1c16..da11936 100644 --- a/api/src/data/schemas/public/group.py +++ b/api/src/data/schemas/public/group.py @@ -1,6 +1,8 @@ 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 @@ -10,6 +12,7 @@ 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, @@ -17,6 +20,7 @@ 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: @@ -25,3 +29,17 @@ 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 diff --git a/api/src/data/schemas/public/group_dao.py b/api/src/data/schemas/public/group_dao.py index 52603f8..e856f04 100644 --- a/api/src/data/schemas/public/group_dao.py +++ b/api/src/data/schemas/public/group_dao.py @@ -11,6 +11,9 @@ 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}'" diff --git a/api/src/data/schemas/public/short_url.py b/api/src/data/schemas/public/short_url.py index 5b04e65..8d23653 100644 --- a/api/src/data/schemas/public/short_url.py +++ b/api/src/data/schemas/public/short_url.py @@ -18,6 +18,7 @@ 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, @@ -34,6 +35,8 @@ 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 @@ -106,6 +109,20 @@ 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, diff --git a/api/src/data/schemas/public/short_url_dao.py b/api/src/data/schemas/public/short_url_dao.py index 6866d5c..66e0003 100644 --- a/api/src/data/schemas/public/short_url_dao.py +++ b/api/src/data/schemas/public/short_url_dao.py @@ -18,5 +18,8 @@ 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() diff --git a/api/src/data/scripts/2025-04-18-12-15-user-spaces.sql b/api/src/data/scripts/2025-04-18-12-15-user-spaces.sql new file mode 100644 index 0000000..a01734d --- /dev/null +++ b/api/src/data/scripts/2025-04-18-12-15-user-spaces.sql @@ -0,0 +1,11 @@ +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); \ No newline at end of file diff --git a/api/src/data/seeder/feature_flags_seeder.py b/api/src/data/seeder/feature_flags_seeder.py index 185312c..373e556 100644 --- a/api/src/data/seeder/feature_flags_seeder.py +++ b/api/src/data/seeder/feature_flags_seeder.py @@ -21,6 +21,7 @@ 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() @@ -31,6 +32,19 @@ 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() ] diff --git a/web/src/app/modules/admin/short-urls/short-urls.data.service.ts b/web/src/app/modules/admin/short-urls/short-urls.data.service.ts index 6fa766a..d3d754b 100644 --- a/web/src/app/modules/admin/short-urls/short-urls.data.service.ts +++ b/web/src/app/modules/admin/short-urls/short-urls.data.service.ts @@ -1,5 +1,5 @@ import { Injectable, Provider } from '@angular/core'; -import { merge, Observable } from 'rxjs'; +import { forkJoin, merge, Observable } from 'rxjs'; import { Create, Delete, @@ -48,50 +48,80 @@ export class ShortUrlsDataService skip?: number | undefined, take?: number | undefined ): Observable> { - return this.apollo - .query<{ shortUrls: QueryResult }>({ - query: gql` - query getShortUrls($filter: [ShortUrlFilter], $sort: [ShortUrlSort]) { - shortUrls(filter: $filter, sort: $sort) { - count - totalCount - nodes { + const query1 = this.apollo.query<{ shortUrls: QueryResult }>({ + query: gql` + query getShortUrls($filter: [ShortUrlFilter], $sort: [ShortUrlSort]) { + shortUrls(filter: $filter, sort: $sort) { + nodes { + id + shortUrl + targetUrl + description + loadingScreen + visits + group { id - shortUrl - targetUrl - description - loadingScreen - visits - group { - id - name - } - domain { - id - name - } - - ...DB_MODEL + name + } + domain { + id + name } } } + } + `, + variables: { + filter: [{ group: { deleted: { equal: false } } }, ...(filter ?? [])], + sort: [{ id: SortOrder.DESC }, ...(sort ?? [])], + skip, + take, + }, + }); - ${DB_MODEL_FRAGMENT} - `, - variables: { - filter: [{ group: { deleted: { equal: false } } }, ...(filter ?? [])], - sort: [{ id: SortOrder.DESC }, ...(sort ?? [])], - skip: skip, - take: take, - }, + const query2 = this.apollo.query<{ shortUrls: QueryResult }>({ + 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 }; }) - .pipe( - catchError(err => { - this.spinner.hide(); - throw err; - }) - ) - .pipe(map(result => result.data.shortUrls)); + ); } loadById(id: number): Observable { diff --git a/web/src/app/service/sidebar.service.ts b/web/src/app/service/sidebar.service.ts index b6f3a2d..49bff52 100644 --- a/web/src/app/service/sidebar.service.ts +++ b/web/src/app/service/sidebar.service.ts @@ -3,6 +3,7 @@ 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', @@ -11,7 +12,10 @@ export class SidebarService { visible$ = new BehaviorSubject(true); elements$ = new BehaviorSubject([]); - constructor(private auth: AuthService) { + constructor( + private auth: AuthService, + private featureFlags: FeatureFlagService + ) { this.auth.user$.subscribe(async () => { await this.setElements(); }); @@ -40,16 +44,19 @@ export class SidebarService { label: 'common.groups', icon: 'pi pi-tags', routerLink: ['/admin/groups'], - visible: await this.auth.hasAnyPermissionLazy([PermissionsEnum.groups]), + visible: + (await this.auth.hasAnyPermissionLazy([PermissionsEnum.groups])) || + (await this.featureFlags.get('PerUserSetup')), }, { label: 'common.urls', icon: 'pi pi-tag', routerLink: ['/admin/urls'], - visible: await this.auth.hasAnyPermissionLazy([ - PermissionsEnum.shortUrls, - PermissionsEnum.shortUrlsByAssignment, - ]), + visible: + (await this.auth.hasAnyPermissionLazy([ + PermissionsEnum.shortUrls, + PermissionsEnum.shortUrlsByAssignment, + ])) || (await this.featureFlags.get('PerUserSetup')), }, await this.sectionAdmin(), ];