From d190d2f2181761cf14e0415a7aa499200db31926 Mon Sep 17 00:00:00 2001 From: edraft Date: Fri, 18 Apr 2025 13:56:15 +0200 Subject: [PATCH] Added user spaces --- api/src/api_graphql/abc/query_abc.py | 2 +- api/src/api_graphql/filter/group_filter.py | 3 + api/src/api_graphql/graphql/base.gql | 16 +-- api/src/api_graphql/graphql/group.gql | 3 + .../api_graphql/mutations/group_mutation.py | 8 ++ .../mutations/short_url_mutation.py | 8 ++ .../queries/group_history_query.py | 4 +- api/src/api_graphql/queries/group_query.py | 4 +- api/src/api_graphql/query.py | 36 +++++- api/src/api_graphql/require_any_resolvers.py | 25 ++-- api/src/core/configuration/feature_flags.py | 6 +- .../core/configuration/feature_flags_enum.py | 2 +- .../database/abc/data_access_object_abc.py | 43 ++++++- api/src/data/schemas/public/group.py | 18 +++ api/src/data/schemas/public/group_dao.py | 3 + api/src/data/schemas/public/short_url.py | 17 +++ api/src/data/schemas/public/short_url_dao.py | 3 + .../scripts/2025-04-18-12-15-user-spaces.sql | 11 ++ .../short-urls/short-urls.data.service.ts | 108 +++++++++++------- 19 files changed, 248 insertions(+), 72 deletions(-) create mode 100644 api/src/data/scripts/2025-04-18-12-15-user-spaces.sql 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/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..ce259a5 100644 --- a/api/src/core/configuration/feature_flags.py +++ b/api/src/core/configuration/feature_flags.py @@ -1,10 +1,14 @@ 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.per_user_setup.value: Environment.get( + "PER_USER_SETUP", bool, False + ), # 18.04.2025 } @staticmethod @@ -15,6 +19,6 @@ class FeatureFlags: async def has_feature(key: FeatureFlagsEnum) -> bool: value = await featureFlagDao.find_by_key(key.value) if value is None: - return False + return FeatureFlags.get_default(key) return value.value diff --git a/api/src/core/configuration/feature_flags_enum.py b/api/src/core/configuration/feature_flags_enum.py index c5f48c7..d4475ca 100644 --- a/api/src/core/configuration/feature_flags_enum.py +++ b/api/src/core/configuration/feature_flags_enum.py @@ -2,5 +2,5 @@ from enum import Enum class FeatureFlagsEnum(Enum): - # modules version_endpoint = "VersionEndpoint" + 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/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 {